본문 바로가기
개발/iOS

[Swift 4] 프로토콜 (Protocol) #1

by 마동쿠 2017. 10. 15.

이 글은 Swift4 Document 의 Protocol 부분을 공부하고 번역하여 올린 글입니다. 오역 및 의역이 있을수 있으므로 원문을 볼것을 추천합니다.

오역이 있을시 댓글에 달아주신다면 감사하겠습니다.




프로토콜은 특정한 일이나 기능의 일부에 대한 함수, 속성 그리고 요구사항들의 청사진을 정의합니다. 그런다음 프로토콜을 클래스나 구조체 그리고 열거형에서 채택하여 실제로 구현할 수 있습니다. 프로토콜의 요구사항들을 어떤 타입으로라도 만족한다면 이를 프로토콜에 일치(Conform)한다고 말합니다.


준수유형에서 구현해야하는 요구사항을 지정하는것 이외에도 프로토콜을 확장하여 요구사항중 일부를 구현하거나 준수유형에서 활용할 수 있는 추가적인 기능들을 구현할 수 도 있습니다.



프로토콜 문법


클래스, 구조체 그리고 열거형과 굉장히 비슷하게 프로토콜을 정의할 수 있습니다.

1
2
3
protocol SomeProtocol {
    // 프로토콜의 정의가 이곳에 온다.
}
cs

사용자 정의 유형은 프로토콜의 이름을 콜론으로 분리하여 유형 이름 다음에 배치하여 특정한 프로토콜을 정의의 일부로 채택한다고 명시한다. 여러 프로토콜을 쉼표로 구분하여 나눌 수 있습니다.

1
2
3
struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 구조체 정의가 이곳에 온다.
}
cs

클래스에 수퍼클래스가 있는 경우,  채택한 프로토콜 앞에 쉼표로 구분하여 수퍼클래스의 이름을 나열합니다.

1
2
3
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // 클래스 정의가 이곳에 온다.
}
cs



속성 요구사항


프로토콜은 특정한 이름이나 유형과 함께 인스턴스 속성 또는 유형 속성을 제공하기 위해 어떠한 준수 유형도 요구할 수 있습니다. 프로토콜은 속성이 저장된 속성인지 계산된 속성인지 지정하지 않습니다. 프로토콜은 오직 요구된 속성의 이름과 유형만 지정할 수 있습니다. 프로토콜은 또한 속성이 gettable 인지 gettable 과 settable인지 지정할 수 있습니다.

프로토콜이 gettable 및 settable 속성을 요구한다면, 해당 속성 요구사항은 상수 저장 속성이나 읽기전용 계산 속성으로 충족될 수 없습니다. 만약 프로토콜이 오직 gettable 속성만을 요구한다면, 요구사항은 어떤 종류의 속성으로도 만족할 수 있고, 코드가 자신의 코드에 유용하다면 속성을 settable이 될 수도 있습니다.

속성 요구사항은 언제나  var  키워드를 접두어로 사용하는 변수 속성으로 정의합니다. gettable 및 settable 속성은 { get set } 을 유형 정의 다음에 씀으로 표시할 수 있고, gettable 속성은 { get } 을 씀으로 표시할 수 있습니다. 
  1. protocol SomeProtocol {
  2. var mustBeSettable: Int { get set }
  3. var doesNotNeedToBeSettable: Int { get }
  4. }
프로토콜을 정의할 때 항상 접두사 유형 속성 요구사항에 static 키워드를 접두사로 붙입니다. 이 규칙은 클래스가 구현할 때 유형 속성 요구사항에 class 또는 static 키워드를 접두어로 구현될 수 있는 경우에도 적용됩니다.
  1. protocol AnotherProtocol {
  2. static var someTypeProperty: Int { get set }
  3. }
다음은 단일 인스턴스 등록 정보 요구사항이 있는 프로토콜의 예시입니다.
  1. protocol FullyNamed {
  2. var fullName: String { get }
  3. }
FullyNamed 프로토콜은 정규화된 이름을 제공하기 위해 규격 유형이 필요합니다. 프로토콜은 준수 유형의 본질에 대해 아무것도 지정하지 않습니다. 프로토콜은 유형이 자체에 대해 전체 이름을 제공할 수 있다는것을 정의합니다. 프로토콜은 모든 FullyNamed 유형이 fullName 이라 불리는 gettable 인스턴스 속성을 가져야 한다고 명시하며, 이는 String 유형 입니다.

다음은 FullyNamed 프로토콜은 채택하고 준수하는 간단한 구조의 예 입니다 :
  1. struct Person: FullyNamed {
  2. var fullName: String
  3. }
  4. let john = Person(fullName: "John Appleseed")
  5. // john.fullName is "John Appleseed"
이 예제는 특정 이름을 가진 Person 이라는 구조체를 정의합니다. 이는 FullyNamed 프로토콜을 이 정의의 첫번째 라인의 부분에서 채택했음을 명시합니다. 

각 Person 의 인스턴스는 fullName 이라는 단일 저장 속성을 갖고있고, 타입은 String 입니다.  이는 FullyNamed 프로토콜의 단일 요구사항과 일치하며, Person 이 프로토콜을 올바르게 충족했음을 의미합니다. (Swift는 프로토콜이 요구사항을 충종시키지 못했을 경우 컴파일타임에 오류를 보고합니다.)

여기 더 복잡한 클래스 예제가 있습니다. 이는 또한 FullyNamed 프로토콜을 채택하고 준수합니다.
  1. class Starship: FullyNamed {
  2. var prefix: String?
  3. var name: String
  4. init(name: String, prefix: String? = nil) {
  5. self.name = name
  6. self.prefix = prefix
  7. }
  8. var fullName: String {
  9. return (prefix != nil ? prefix! + " " : "") + name
  10. }
  11. }
  12. var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
  13. // ncc1701.fullName is "USS Enterprise"
이 클래스는 fullName 속성 요구사항을 starship의 계산된 읽기전용 속성으로 구현합니다. 각 Starship 클래스 인스턴스는 필수 name 과 선택적(optional) prefix 를 저장합니다. fullName 속성은 prefix 값이 존재하면 이를 name 앞에 붙여 전체 이름을 만듭니다.


메소드 요구사항


프로토콜은 준수유형들을 통해 구현하기 위해 특정한 인스턴스 메소드와 타입을 요구할 수 있습니다. 이 메소드들은 일반적인 인스턴스와 타입 메소드들과 동일한 방식으로, 하지만 중괄호 또는 메소드 내용 없이 프로토콜 정의의 일부로 작성됩니다. 매개변수들은 일반 메소드와 동일한 규칙에 따라 허용됩니다. 하지만 프로토콜 정의 내의 매개변수엔 기본값을 정의할 수 없습니다.

타입 속성 요구사항과 마찬가지로, 프로토콜에 정의된 경우 타입 메소드 요구사항 앞에 항상 static 키워드를 접두사로 붙입니다. 클래스로 구현할때 타입 메소드 요구사항 앞에 class 나 static 키워드가 접두어로 붙어있는 경우에도 마찬가지입니다.
  1. protocol SomeProtocol {
  2. static func someTypeMethod()
  3. }
다음 예제는 단일 인스턴스 메소드 요구사항이 있는 프로토콜을 정의합니다.
  1. protocol RandomNumberGenerator {
  2. func random() -> Double
  3. }
RandomNumberGenerator 프로토콜은 random 이라 불리는 인스턴스 메소드를 소유하는 모든 준수 타입을 요구하고, 호출될 때 마다 Double 값을 반환합니다. 프로토콜의 부분으로 정의되지는 않았지만, 이 값은 0.0이상 1.0 미만의 숫자로 간주됩니다. 

RandomNumberGenerator 프로토콜은 각 난수 생성 방법에 대한 가정을 하지 않습니다. 단순히 난수를 생성하는 표준 방법만 제공하면 됩니다.

RandomNumberGenerator 프로토콜을 채택하고 준수한 클래스의 구현이 여기 있습니다. 이 클래스는 linear congruential generator 로 알려진 의사난수 생성 알고리즘을 구현합니다.
  1. class LinearCongruentialGenerator: RandomNumberGenerator {
  2. var lastRandom = 42.0
  3. let m = 139968.0
  4. let a = 3877.0
  5. let c = 29573.0
  6. func random() -> Double {
  7. lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
  8. return lastRandom / m
  9. }
  10. }
  11. let generator = LinearCongruentialGenerator()
  12. print("Here's a random number: \(generator.random())")
  13. // Prints "Here's a random number: 0.37464991998171"
  14. print("And another one: \(generator.random())")
  15. // Prints "And another one: 0.729023776863283"
변형중인 메소드 요구사항


메소드가 속한 인스턴스를 수정 (또는 변경)해야할 때 가 가끔 있습니다. 예를들어 메소드가 func 키워드  앞에 mutating 키워드를 넣어 메소드가 속한 인스턴스와 인스턴스의 모든 속성이 변경될 수 있다는 것을 나타냅니다.  이 과정은 인스턴스 메소드 내에서 값 타입 변경 에서 설명하고 있습니다.

프로토콜을 채택한 모든 타입의 인스턴스를 변경하려는 프로토콜 인스턴스 메소드 요구사항을 정의하는 경우, 프로토콜 정의의 일부로 mutating 키워드로 메소드를 표시하십시오. 이것은 가능하게합니다. 이를 통해 구조와 열거형을 통해 프로토콜을 채택하고 메소드 요구사항을 만족시킵니다.
아래 예제는 Togglable 이라는 프로토콜을 정의합니다. 이는 toggle 이라는 단일 인스턴스 메소드 요구사항을 정의합니다. 이름에서 알 수 있듯이, toggle() 메소드는 일반적으로 해당 타입의 속성을 변경하여 준수 유형의 상태를 전환하거나 반전시킵니다.

toggle() 메소드는 Togglable 프로토콜 정의의 일부분으로 mutating 키워드로 표시되어, 메소드가 호출될때 일치하는 인스턴스의 상태가 변경될 것을 예상함을 나타냅니다.
  1. protocol Togglable {
  2. mutating func toggle()
  3. }
구조체 또는 열거형에 대해 Togglable 프로토콜을 구현한다면, 그 구조체 또는 열거형은 mutating 으로 표시된 toggle() 메소드 구현을 제공함으로써 프로토콜을 준수할 수 있습니다.

아래 예제는 OnOffSwitch 라는 열거형을 구현합니다. 이 열거형은 열거형으로 표시된 on 케이스와 off 케이스 둘의 상태를 전환합니다. 열거형의 toggle 구현은 Togglable 프로토콜 요구사항에 맞추기 위해 mutating 으로 표시됩니다.
  1. enum OnOffSwitch: Togglable {
  2. case off, on
  3. mutating func toggle() {
  4. switch self {
  5. case .off:
  6. self = .on
  7. case .on:
  8. self = .off
  9. }
  10. }
  11. }
  12. var lightSwitch = OnOffSwitch.off
  13. lightSwitch.toggle()
  14. // lightSwitch is now equal to .on
이니셜라이저 요구사항



프로토콜은 타입을 준수하여 구현된 특정한 이니셜라이저의 요구할 수 있습니다. 이 이니셜라이저는 프로토콜 정의의 일부로 일반 이니셜라이저와 동일한 방법으로 하지만 중괄호와 내용 없이 작성합니다.
  1. protocol SomeProtocol {
  2. init(someParameter: Int)
  3. }

프로토콜 이니셜라이저 요구사항의 클래스 구현


프로토콜 이니셜라이저 요구사항을 준수하는 클래스에 대해 지정된(designated) 이니셜라이저 또는 편의(convenience) 이니셜라이저로 구현할 수 있습니다. 두 경우 모두 이니셜라이저를 required 로 표시해야합니다.
  1. class SomeClass: SomeProtocol {
  2. required init(someParameter: Int) {
  3. // initializer implementation goes here
  4. }
  5. }
required 의 사용은 이니셜라이저 요구사항을 준수한 클래스의 모든 서브클래스의 명시적으로 또는 상속된 방식으로 구현할 수 있으며, 그것들은 프로토콜에도 부합합니다.

요구된 이니셜라이저에 대한 더 많은 정보는 Required Initializers 를 보십시오.

만약 서브클래스가 수퍼클래스로부터 명시된 이니셜라이저를 override한 경우 그리고 또한 프로토콜로부터 이니셜라이저 요구사항을 충족한 경우 required 표시와 override 표시를 둘 다 해주어야 합니다.

  1. protocol SomeProtocol {
  2. init()
  3. }
  4. class SomeSuperClass {
  5. init() {
  6. // initializer implementation goes here
  7. }
  8. }
  9. class SomeSubClass: SomeSuperClass, SomeProtocol {
  10. // "required" from SomeProtocol conformance; "override" from SomeSuperClass
  11. required override init() {
  12. // initializer implementation goes here
  13. }
  14. }

Failable 이니셜라이저 요구사항


프로토클은 Failable Initializer 에 구현된대로, 타입을 준수하기 위해 failable 이니셜라이저 요구사항을 정의할 수 있습니다.


failable 이니셜라이저 요구사항은 준수하는 타입의 failable 이나 nonfailable 이니셜라이저로 만족시킬 수 있습니다. nonfailable 이니셜라이저 요구사항은 nonfailable 이니셜라이저 또는 implicitly unwrapped failable 이니셜라이저로 충족될 수 있습니다.




타입으로서 프로토콜




프로토콜은 실제로 기능 자체를 구현하지 않습니다. 그럼에도 불구하고 작성한 모든 프로토콜은 코드에서 사용할 수 있는 완전한 타입이 됩니다.


타입이기 때문에, 다음 항목을 포함한 다른 타입이 허용되는 많은 곳에서 프로토콜을 사용할 수 있습니다 :


 - 함수, 메서드 또는 이니셜라이저의 매개변수 타입 혹은 반환 타입으로

 

 - 상수, 변수 또는 속성의 타입으로


 - 배열, 딕셔너리 또는 다른 컨테이너의 항목 타입으로

타입으로 사용되는 프로토콜의 예제입니다 : 

  1. class Dice {
  2. let sides: Int
  3. let generator: RandomNumberGenerator
  4. init(sides: Int, generator: RandomNumberGenerator) {
  5. self.sides = sides
  6. self.generator = generator
  7. }
  8. func roll() -> Int {
  9. return Int(generator.random() * Double(sides)) + 1
  10. }
  11. }

이 예제는 보드게임에서 사용하기 위해 n-sided dice를 나타내는 Dice 라는 새로운 클래스를 정의합니다. Dice 인스턴스에는 sides 라는 정수 속성이 있습니다. 이 속성은 sides의 개수를 나타냅니다. generator 라는 이름의 속성은 dice roll 값을 생성하는 난수 생성기를 제공합니다.


generator 속성은 RandomNumberGenerator 타입입니다. 따라서 RandomNumberGenerator 프로토콜을 채택한 모든 타입의 인스턴스로 설정할 수 있습니다. 인스턴스가 RandomNumberGenerator 프로토콜을 채택해야한다는 것을 제외하고는 이 속성에 할당한 인스턴스가 그 밖의 다른것이 필요하지 않습니다.


Dice 에는 초기 상태를 설정하는 이니셜라이저가 있습니다. 이 이니셜라이저에는 generator 라는 매개변수가 있으며 RandomNumberGenerator 타입입니다. 새로운 Dice 인스턴스를 초기화할때 이 매개변수에 적합한 유형의 값을 전달할 수 있습니다.


Dice 는 1과 주사위의 측면 수 사이의 정수값을 반환하는 하나의 인스턴스 메서드인 roll 을 제공합니다. 이 메서드는 생성자의 random() 메서드를 호출하여 0.0과 1.0 사이의 새로운 난수를 만들고 이 난수를 사용하여 올바른 범위 내에서 주사위 롤 값을 만듭니다. generator 가 RandomNumberGenerator 를 채택한 것으로 알려져 있기 때문에 random() 메서드를 호출해야 합니다.


다음은 Dice 클래스를 사용하여 LinearCongruentialGenerator 인스턴스가 난수생성기로된 6면체 주사위를 만드는 방법입니다.

  1. var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
  2. for _ in 1...5 {
  3. print("Random dice roll is \(d6.roll())")
  4. }
  5. // Random dice roll is 3
  6. // Random dice roll is 5
  7. // Random dice roll is 4
  8. // Random dice roll is 5
  9. // Random dice roll is 4




Delegation (위임)




Delegation은 클래스나 구조체가 책임을 일부 다른 유형의 인스턴스로 전달(또는 Delegation(위임))할 수 있게하는 디자인 패턴입니다. 이 디자인 패턴은 타입을 준수한 위임된 책임을 캡슐화한 프로토콜을 정의하여 구현됩니다. 그 준수 유형(delegate로 알려진)은 위임된 기능을 제공할 수 있습니다.(의역) Delegate를 사용하면 특정 동작에 응답하거나 외부 소스에서 해당 소스의 기본 타입을 알 필요 없이 데이터를 검색할 수 있습니다.


아래 예제는 주사위 기반 보드 게임에 사용할 두가지 프로토콜을 정의합니다.

  1. protocol DiceGame {
  2. var dice: Dice { get }
  3. func play()
  4. }
  5. protocol DiceGameDelegate {
  6. func gameDidStart(_ game: DiceGame)
  7. func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
  8. func gameDidEnd(_ game: DiceGame)
  9. }

DiceGame 프로토콜은 주사위를 포함하는 모든 게임에서 채택할 수 있는 프로토콜입니다. DiceGameDelegate 프로토콜은 DiceGame 의 진행상황을 추적하기위해 모든 유형에서 채택할 수 있습니다.


여기 Control Flow 에서 소개된 Snakes and Ladders 라는 게임버전이 있습니다. 이 버전은 주사위 롤을 위해 그리고 DiceGameDelegate 에게 진행상황을 알리기 위해 DiceGame 프로토콜을 채택하여 Dice 인스턴스를 사용하도록 조정되었습니다.


  1. class
    SnakesAndLadders: DiceGame {
  2. let finalSquare = 25
  3. let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
  4. var square = 0
  5. var board: [Int]
  6. init() {
  7. board = Array(repeating: 0, count: finalSquare + 1)
  8. board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
  9. board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
  10. }
  11. var delegate: DiceGameDelegate?
  12. func play() {
  13. square = 0
  14. delegate?.gameDidStart(self)
  15. gameLoop: while square != finalSquare {
  16. let diceRoll = dice.roll()
  17. delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
  18. switch square + diceRoll {
  19. case finalSquare:
  20. break gameLoop
  21. case let newSquare where newSquare > finalSquare:
  22. continue gameLoop
  23. default:
  24. square += diceRoll
  25. square += board[square]
  26. }
  27. }
  28. delegate?.gameDidEnd(self)
  29. }
  30. }

Snakes and Ladders 게임 플레이에 대한 설명은 Control FlowBreak 섹션을 참조하십시오.


이 게임 버전은 DiceGame 프로토콜을 채택한 SnakesAndLadders 라는 클래스로 싸여있습니다 그것은 프로토콜에 부합하기 위해 gettable dice 속성과 play() 메소드를 제공합니다. (dice 속성은 초기화 후에 변경할 필요가 없기 때문에 상수 속성으로 선언되며 프로토콜은 gettable 만 필요로합니다.)


Snakes and Ladders 게임 보드 설정은 클래스의 init() 이니셜라이저 내에서 수행됩니다. 모든 게임 로직은 프로토콜의 play 메서드로 이동합니다. 이 메서드는 프로토콜의 필수 dice 속성을 사용하여 주사위 롤 값을 제공합니다.


delegate 속성은 옵셔널 DiceGameDelegate 로 정의됩니다. 왜냐하면 delegate가 게임을 하기 위해 필요하지 않기 때문입니다. 옵셔널 유형이기 때문에 대리자 속성은 자동으로 nil의 초기값으로 설정됩니다. 그런 다음 게임 인스턴스 생성자는 속성을 적합한 delegate로 설정할 수 있는 옵션을 제공합니다.


DiceGameDelegate 는 게임 진행상황을 추적하는 세가지 방법을 제공합니다. 이 세가지 메서드는 위의 play() 메서드 내에서 게임 논리에 통합되었으며, 새 게임이 시작되거나 새 차례가 시작되거나 게임이 종료될 때 호출됩니다.


delegate 속성은 옵셔널 DiceGameDelegate 이므로 play() 메서드는 대리자의 메서드를 호출할 때 마다 옵셔널 체이닝을 사용합니다. delegate 속성이 nil이면 이러한 대리자 호출이 오류없이 정상적으로 실패합니다. delegate 속성이 nil이 아닌 경우 delegate 메서드가 호출되고, SnakesAndLadders 인스턴스가 매개변수로 전달됩니다. 


이 다음 예제는 DiceGameTracker 라는 클래스를 보여줍니다. 이 클래스는 DiceGameDelegate 프로토콜을 채택합니다.

  1. class DiceGameTracker: DiceGameDelegate {
  2. var numberOfTurns = 0
  3. func gameDidStart(_ game: DiceGame) {
  4. numberOfTurns = 0
  5. if game is SnakesAndLadders {
  6. print("Started a new game of Snakes and Ladders")
  7. }
  8. print("The game is using a \(game.dice.sides)-sided dice")
  9. }
  10. func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
  11. numberOfTurns += 1
  12. print("Rolled a \(diceRoll)")
  13. }
  14. func gameDidEnd(_ game: DiceGame) {
  15. print("The game lasted for \(numberOfTurns) turns")
  16. }
  17. }

DiceGameTracker 는 DiceGameDelegate 가 요구하는 세가지 메서드를 모두 구현합니다. 게임이 수행한 턴 수를 추적하기 위해 이 방법을 사용합니다. 게임이 시작될 때 numberOfTurns 속성을 0으로 재설정하고, 새로운 차례가 시작될 때 마다 이 값이 증가합니다. 그리고 게임이 종료되면 총 턴 수를 출력합니다.


위에 표시된 gameDidStart(_:) 의 구현은 게임 매개변수를 사용하여 재생하려고 하는 게임에 대한 소개 정보를 출력합니다. game 매개변수에는 SnakesAndLadders 타입이 아닌 DiceGame 타입이 있으므로 gameDidStart(_:) 는 DiceGame 프로토콜의 일부로 구현된 메서드와 속성만 액세스하고 사용할 수 있습니다. 그러나 이 메서드는 여전히 기본 인스턴스의 타입을 query하기 위해 타입 캐스팅을 사용할 수 있습니다. 이 예제에서는 게임이 실제로 게임이 SnakesAndLadders 의 인스턴스인지 여부를 확인하고 적절한 경우 메시지를 출력합니다.


gameDidStart(_:) 메서드는 전달된 game 매개변수의 dice 속성에도 액세스합니다. game 은 DiceGame 프로토콜을 따르는 것으로 알려져 있기 때문에 주사위 속성이 있어야하므로 gameDidStart(_:) 메서드는 어떤 종류의 게임을 실행하든 관계없이 주사위의 sides 속성을 액세스하고 출력할 수 있습니다.


DiceGameTracker 가 어떻게 작동하는지 보여줍니다 : 

  1. let tracker = DiceGameTracker()
  2. let game = SnakesAndLadders()
  3. game.delegate = tracker
  4. game.play()
  5. // Started a new game of Snakes and Ladders
  6. // The game is using a 6-sided dice
  7. // Rolled a 3
  8. // Rolled a 5
  9. // Rolled a 4
  10. // Rolled a 5
  11. // The game lasted for 4 turns




확장 기능으로 프로토콜 적합성 추가하기




기존 유형의 소스코드에 액세스할 수 없는 경우에도 기존 프로토콜을 확장하여 새 프로토콜을 채택하고 준수할 수 있습니다. 확장기능은 기존 유형에 새로운 속성, 메서드 및 하위 스크립트를 추가할 수 있으므로 프로토콜에서 요구할 수 있는 모든 요구사항을 추가할 수 있습니다. 확장에 대한 자세한 내용은 Extensions를 참조하세요.

예를들어, TextRepresentable 라는 프로토콜은 텍스트로 표시할 수 있는 모든 유형으로 구현할 수 있습니다. 이것은 자체에 대한 설명이거나, 현재 상태의 텍스트 버전일 수 있습니다.

  1. protocol TextRepresentable {
  2. var textualDescription: String { get }
  3. }

이전의 Dice 클래스를 확장하여 TextRepresentable 을 채택하고 준수할 수 있습니다.

  1. extension Dice: TextRepresentable {
  2. var textualDescription: String {
  3. return "A \(sides)-sided dice"
  4. }
  5. }

이 확장은 Dice 가 원래의 구현에서 제공한것과 동일한 방식으로 새로운 프로토콜을 채택합니다. 프로토콜 이름은 콜론으로 구분된 타입 이름 다음에 제공되며 프로토콜의 모든 요구사항 구현은 확장의 중괄호 안에 제공됩니다.


이제 모든 Dice 인스턴스를 TextRepresentable 로 처리할 수 있습니다.

  1. let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
  2. print(d12.textualDescription)
  3. // Prints "A 12-sided dice"

마찬가지로 SnakesAndLadders 게임 클래스를 확장하여 TextRepresentable 프로토콜을 채택하고 준수할 수 있습니다.

  1. extension SnakesAndLadders: TextRepresentable {
  2. var textualDescription: String {
  3. return "A game of Snakes and Ladders with \(finalSquare) squares"
  4. }
  5. }
  6. print(game.textualDescription)
  7. // Prints "A game of Snakes and Ladders with 25 squares"

확장으로 프로토콜 채택 선언




타입이 이미 프로토콜의 모든 요구사항을 준수하지만 아직 해당 프로토콜을 채택한다고 명시하지 않은 경우 타입이 빈 확장자를 갖는 프로토콜을 채택하도록 할 수 있습니다.

  1. struct Hamster {
  2. var name: String
  3. var textualDescription: String {
  4. return "A hamster named \(name)"
  5. }
  6. }
  7. extension Hamster: TextRepresentable {}

TextRepresentable 이 필수유형인 경우 Hamster 의 인스턴스를 사용할 수 있습니다. (?)

  1. let simonTheHamster = Hamster(name: "Simon")
  2. let somethingTextRepresentable: TextRepresentable = simonTheHamster
  3. print(somethingTextRepresentable.textualDescription)
  4. // Prints "A hamster named Simon"

프로토콜 타입 모음




Protocols as Types에서 언급한 것 처럼 배열이나 dictionary과 같은 콜렉션에 저장될 유형으로 프로토콜을 사용할 수 있습니다. 이 예제는 TextRepresentable 객체의 배열을 만듭니다.

  1. let things: [TextRepresentable] = [game, d12, simonTheHamster]

이제 배열의 항목을 반복하고 각 항목의 텍스트 설명을 출력할 수 있습니다.

  1. for thing in things {
  2. print(thing.textualDescription)
  3. }
  4. // A game of Snakes and Ladders with 25 squares
  5. // A 12-sided dice
  6. // A hamster named Simon

thing 상수는 TextRepresentable 유형입니다. 실제 인스턴스가 그 타입중 하나인 경우에도 Dice , DiceGame 또는 Hamster 타입이 아닙니다. 그럼에도 불구하고 TextRepresentable 타입이고 TextRepresentable 인 모든 요소는 textualDescription 속성을 가진것으로 알려져 있기 때문에 루프를 통해 매번 thing.textualDescription 에 액세스 하는것이 안전합니다.




#2에서 계속..

댓글