Protocols

Swift/Language Guide 2018. 9. 18. 00:06
반응형

[최종 수정일 : 2018.09.05]

원문 : 애플 개발 문서 Swift 4.2 Language Guide - Protocols

프로토콜(Protocols)

프로토콜(protocol)은 특정 작업이나 기능적인 부분에 맞는 메소드, 프로퍼티, 다른 요구사항을 상세히(blueprint) 정의합니다. 프로토콜은 이러한 요구사항들의 실제 구현을 제공하기 위해 클래스, 구조체 열거형에 의해 채택될(adopted) 수 있습니다. 프로토콜의 요구사항을 만족하는(satisfies) 모든 타입은 프로토콜을 준수(conform)한다고 말합니다.

요구사항을 지정하는 것 외에도 타입을 준수하도록 반드시 구현해야 하며, 이러한 요구사항을 구현하거나 추가적으로 준수하는 타입의 함수를 구현하기 위해 프로토콜을 확장할 수 있습니다.

프로토콜 문법(Protocol Syntax)

클래스, 구조체, 열겨형과 매우 비슷한 방법으로 프로토콜을 정의 합니다.

protocol SomeProtocol {
    // protocol definition goes here
}

사용자정의 타입은 정의하는 곳에서 타입의 이름 뒤에 콜론(:)으로 구분해서 프로토콜의 이름을 위치시켜 특정 프로토콜을 채택한다고 명시합니다. 여러개의 프로토콜을 목록화 할수 있고 콤마(,)로 구분됩니다.

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

클래스가 상위클래스를 가지고 있는 경우, 상위클래스 이름을 채택한 프로토콜 앞에 콤마(,)를 붙여서 나열합니다.

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

프로토콜 요구사항(Property Requirements)

프로토콜은 특정 이름과 타입으로 인스턴스 프로퍼티나 타입 프로퍼티를 제공하기 위해 이를 준수하는 모든 타입에 요구할수 있습니다. 프로토콜은 프로퍼티가 저장 프로퍼티(storead property)인지 게산 프로퍼티(computed property)인지 지정하지 않습니다. - 단지 필수 프로퍼티 이름과 타입만 지정합니다. 또한, 프로토콜은 각 프로퍼티가 gettable 또는 gettabel과 settable인지를 반드시 지정해야합니다.

프로토콜 프로퍼티가 gettable과 settable이 필요한 경우, 그 프로퍼티는 상수 저장 프로퍼티나 읽기전용(read-only) 계산 프로퍼티로는 요구사항을 충족시킬수 없습니다. 프로토콜이 gettable이 되는 프로퍼티만 요구되는 경우, 모든 종류의 프로퍼티에 대해 그 요구사항을 만족할수 있고, 코드에서 유용한 경우에 settable이 되는 프로퍼티에 대해서도 유효합니다.

프로퍼티 요구사항은 항상 변수 프로퍼티로 선언되며, var키워드를 앞에 붙입니다. gettable과 settable 프로퍼티는 타입 선언 뒤에 { get set }으로 작성하고, gettable프로퍼티는 { get }으로 작성합니다.

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

타입 프로퍼티는 프로토콜을 정의할때 항상 static키워드를 앞에 붙여야 합니다. 이 규칙은 클래스가 구현될때 class나 static 키워드를 접두사(prefixed)로 사용할수 있는 타입 프로퍼티 요구사항과 관련(pertains)있습니다.

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

다음은 하나의 인스턴스 프로퍼티를 요구하는 프로토콜의 예제입니다.

protocol FullyNamed {
    var fullName: String { get }
}

FullyName프로토콜을 준수하는 타입이 완전한 이름을 제공하는 것을 요구합니다. 프로토콜은 준수(conforming)하는 타입에 대해 다른 아무것도 지정하지 않습니다. - 이것은 타입이 자신의 전체 이름을 제공해야 합니다. 프로토콜은 anyFullyName타입에 String타입의 fullName이라는gettable 인스턴스 프로퍼티를 반드시 가져야 합니다.

다음은 FullyNamed프로포콜을 채택하고 준수하는 간단한 구조체 예제 입니다.

struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

이 예제는 특정 이름을 가진 사람을 표현하는 구조체 Person을 정의합니다. 그것을 정의하는 첫번째 줄에서 FullyNamed 프로토콜을 채택합니다.

Person의 각 인스턴스는 String타입인 하나의 저장 프로퍼티 fullName을 가지고 있습니다. FullyNamed 프로토콜의 한개의 요구사항과 일치하고, Person이 그 프로토콜을 정확히 준수한다는 것을 의미합니다. (Swift는 프로토콜 요구사항이 완전하지 않은 경우에, 컴파일시 오류가 납니다.)

다음은 FullyNamed 프로토콜을 채택하고 준수하는 좀 더 복잡한 클래스 입니다.

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"

이 클래스는 fullName 프로퍼티 요구사항을 우주선(starship)의 계산 읽기 전용 프로퍼티로 구현합니다. 각 우주선(Starship) 클래스 인스턴스는 필수적인(mandatory) name와 선택적인(optional) prefix를 저장합니다. fullName프로퍼티는 prefix값이 있은 경우에 사용하고, 우주선에 대한 전체 이름을 만들기 위해 이름의 시작부분에 추가합니다.

메소드 요구사항(Method Requirements)

프로토콜은 준수하는 타입 구현을 위해 특정 인스턴스 메소드와 타입메소드를 요구할 수 있습니다. 이러한 메소드는 중괄호({})나 메소드 본문 없이, 일반 인스턴스와 타입 메소드와 정확히 같은 방식으로, 프로토콜의 정의에 작성합니다. 가변 매개변수는 허용되며, 서브스크립트는 일반 메소드와 동일한 규칙이 적용됩니다. 하지만, 기본 값은 프로토콜의 정의에서 메소드 매개변수에 대해 지정할 수 없습니다.

타입 프로퍼티 요구사항처럼, 프로토콜을 정의할 때 타입 메소드 요구사항에 항상 static 키둬드를 접두사로 사용합니다. 이것은 클래스를 구현할때 타입 메소드 요구사항 앞에 class나 static 키워드를 붙일 때에도 마찬가지 입니다.

protocol SomeProtocol {
    static func someTypeMethod()
}

다음은 하나의 인스턴스 메소드를 요구하는 프로토콜을 정의한 예제입니다.

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator 프로토콜은 준수하는 모든 타입에 대해 호출될때마다 Double 값을 반환하는 인스턴스 메소드 random를 요구합니다. 비록 프로토콜로 지정되지 않았지만, 이 값이 0.0부터 1.0 까지의 숫자가 될 것이라 가정합니다.

RandomNumberGenerator프로토콜을 어떻게 무작위(random) 숫자를 만들어 내는지에 관여하지 않습니다 - 단지 새로운 무작위 숫자를 생성하는 기본 방법을 제공하도록 요구만 할 뿐입니다.

다음은 RandomNumberGenerator 프로토콜을 채택(adopts)하고 준수(conforms)하는 클래스 구현입니다. 이 클래스는 선형 합동 생성기(linar congruential generator)으로 의사난수(pseudorandom) 숫자 생성 알고리즘을 구현합니다.

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"

변경하는 메소드 요구사항 (Mutating Method Requirements)

가끔씩 인스턴스에 속한 메소드에 대해 수정(또는 변경)하는게 필요합니다. 값 타입에서 인스턴스 메소드에 대해 이 메소드가 인스턴스와 해당 인스턴스의 모든 프로퍼티를 수정하는 것이 허용된다는 것을 가리키기 위해 mutating키워드를 메소드의 func 키워드 앞에 위치시킵니다. 이 처리과정은 인스턴스 메소드에서 값 타입 수정하기(Modifying Value Types from Within Instance Methods)에 설명되어 있습니다.

프로토콜을 채택한 모든 타입의 인스턴스를 변경하기 위해 프로토콜 인스턴스 메소드 요구사항을 정의 하는 경우에, 그 메소드는 프로토콜의 정의에서 mutating 키워드로 표시합니다. 이것은 구조체와 열거형이 프로토콜을 채택하고 메소드 요구사항을 만족(satisfy)하는 것을 가능하게 합니다.

주의
프로토콜 인스턴스 메소드 요구사항을 mutating으로 표시하는 경우, 클래스에 대한 메소드의 구현에 mutating 키워드를 작성할 필요가 없습니다. mutating키워드는 구조체나 열거형에서만 사용됩니다.

아래 예제는 하나의 인스턴스 메소드 toggle 요구사항이 있는 프로토콜 Togglable을 정의합니다. 이름에서 알수있듯이, toggle() 메소드는 모든 준수하는 타입의 상태를 토글(toggle)하거나 상태를 반전(invertd) 을 의도하며, 일반적으로 타입의 프로퍼티를 수정합니다.

toggle() 메소드는 Toggleable프로토콜 정의에서 mutating키워드로 표시되며, 그 메소드는 호출될때 준수하는 인스턴스의 상태를 변경할수 있는게 예상됩니다.

protocol Togglable {
    mutating func toggle()
}

구조체나 열거형에 대한 Togglable 프로토콜을 구현하는 경우, 그 구조체나 열거형은 mutating으로 표시된 toggle() 메소드 구현을 제공하여 프로콜을 준수 할 수 있습니다.

아래 예제는 열거형 OnOffSwitch를 정의합니다. 이 열거형은 두가지 상태를 토글하며, 열거형 케이스 on과 off를 가리킵니다. 열거형의 toggle 구현은 mutating으로 구현되고, Toggleable프로토콜의 요구사항과 일치합니다.

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on

초기화 요구사항

프로토콜은 준수하는 타입에게 특정 초기화 구현을 요구할 수 있습니다. 일반적인 초기화와 정확히 같은 방법으로 프로토콜의 정의에서 이러한 초기화를 작성 할 수 있지만, 중괄호와 초기화 본문은 없습니다.

protocol SomeProtocol {
    init(someParameter: Int)
}

프로토콜 초기화 요구사항의 클래스 구현(Class Implementations of Protocol Initializer Requirements)

지정된 초기화나 편리한 초기화 처럼, 이를 준수하는 클래스에서 프로토콜 초기화 요구사항을 구현할 수 있습니다. 두 경우 모두, 반드시 초기화 구현에 required 수식어를 표시해야 합니다.

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // initializer implementation goes here
    }
}

required 수식어를 사용하는 것은 이를 준수하는 클래스의 모든 하위 클래스에서 명시적으로 제공하거나 초기화 요구사항의 구현하는 것을 보장합니다.

필요한 초기화에 대한 자세한 정보는, 필요한 초기화(Required Initializers)를 보세요.

주의
final 클래스는 하위클래스를 사용할수 없기 때문에, final 수식어로 표시된 클래스에서는 프로토콜 초기화 구현에 required 수식어를 표시하는게 필요하지 않습니다. final 수식어에 대한 자세한 정보는 오버라이드 막기(Preventing Overrides)를 보세요.

하위클래스가 상위클래스의 지정된 초기화를 오버라이딩 하는 경우에, 프로토콜의 초기화 요구사항과 일치하도록 구현하며, required와 override 수식어 모두 초기화 구현에 표시합니다.

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // initializer implementation goes here
    }
}

실패할수 있는 초기화 요구사항(Failable Initializer Requirements)

실패할 수 있는 초기화(Failable Initializers)에 정의된 것 처럼, 프로토콜은 준수하는 타입에 대해 실패할 수 있는 초기화 요구사항을 정의할 수 있습니다.

실패할 수 있는 초기화 요구사항은 준수하는 타입에서 실패할수 있거나(faiable) 실패하지 않는(nonfailable) 초기화를 만족할 수 있습니다. 실패하지 않는(nonfailable) 초기화 요구사항은 실패하지 않는 초기화 또는 암시적으로 언래핑된(unwrapped) 실패할 수 있는(failable) 초기화로 만족할 수 있습니다.

타입 같은 프로토콜(Protocols as Types)

프로토콜은 실제 어떤 함수도 구현하지 않습니다. 그럼에도 불구하고, 모든 프로토콜은 코드에서 사용하는 완벽한 타입이 될것 입니다.

프로토콜도 타입이기 때문에, 프로토콜을 다른 타입이 허용되는 많은 곳에서 사용할 수 있습니다.

  • 함수, 메소드, 초기화에서 매개변수 타입이나 반환 타입
  • 상수, 변수, 프로퍼티의 타입
  • 배열, 딕셔너리, 다른 컨테이너에서 항목들의 타입

주의
프로토콜은 타입이기 때문에, Swift에서의 다른 타입의 이름과 같이(Int, String, Double) 이름을 대문자로(FullyNamed, RandomNumberGenerator) 시작합니다.

다음은 프로토콜을 타입으로 사용하는 예제입니다.

class Dice {
    let sides: Int
    let generator: RandomNumberGenerator
    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

이 예제는 보드게임에서 사용하는 n면의 주사위를 표현하는 새로운 클래스 Dice를 정의합니다. Dice 인스턴스는 얼마나 많은 면을 가지는지를 나타내는 정수형 프로퍼티 sides와 생성한 주사위를 던진 값을 무작위로 생성하는 프로퍼티 generator를 가집니다.

generator프로퍼티는 RandomNumberGenerator 타입입니다. 그러므로, RandomNumberGenerator프로토콜을 채택한 모든(any) 타입의 인스턴스에 설정 할 수 있습니다. 이 프로퍼피에 할당된 인스턴스에는 RandomNumberGenerator 프로토콜을 채택해야 하는 것을 빼고 다른 것은 필요하지 않습니다.

또한, Dice는 초기 상태를 설정하기 위한 초기화를 가지고 있습니다. 이 초기화는 RandomNumberGenerator 타입인 매개변수 generator를 가집니다. 새로운 Dice인스턴스를 초기화 할때 이 매개변수에 모든 준수하는 타입의 값을 전달 할수 있습니다.

Dice는 1과 주사위 면의 숫자 사이의 정수형 값을 반환하는 인스턴스 메소드 roll를 제공합니다. 이 메소드는 0.0과 1.0사이의 새로운 무작위 숫자를 생성하고 이 무작위 숫자를 주사위를 던지는 정확한 범위의 값으로 생성하기 위해 생성기(generator)의 random() 메소드를 호출합니다. generator은 RandomNumberGenerator을 채택한 것으로 알려져 있기 때문에, 호출할 random() 메소드가 있어야 합니다.

다음은 Dice 클래스가 무작위 숫자를 생성하는 LinearCongruentialGenerator 인스턴스로 6면의 주사위를 만들어 사용하는 방법입니다.

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

위임(Delegation)

위임(delegation)은 클래스나 구조체가 다른 타입의 인스턴스에게 책임을 넘겨주는(hand off 또는 delegate)것이 가능한 디자인 패턴입니다. 이 디자인 패턴은 준수하는 타입이 위임된 기능을 제공하는 것을 보장하는것 처럼, 위임된 책임을 캡슐화하는 프로토콜을 정의해서 구현됩니다. 위임은 특정 동작의 응답하는데 사용될 수 있거나 소스의 기본 타입과 상관 없이 외부 소스로부터 데이터를 검색하는데 사용할 수 있습니다.

아래 예제는 주사위 기반의 보드게임에서 사용하는 프로토콜 2개를 정의합니다

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

DiceGame프로토콜은 주사위와 관련있는 모든 게임으로 채택(adopted)될 수 있는 프로토콜 입니다.

DiceGameDelegate 프로토콜은 DiceGame의 진행을 추적하기 위해 채택될 수 있습니다. 강한 순환참조(strong reference cycles)를 막기 위해, 위임자(delegates)는 약한 참조(weak references)로 선언됩니다. 약한 참조에 대한 자세한 정보는 클래스 인스턴스간에 강한 순한참조(Strong Reference Cycles Between Class Instances)를 보세요. 프로토콜을 클래스 전용으로 만들어 이 챕터의 마지막에 있는 SnakesAndLadders 클래스에서 해당 위임자(delegate)가 반드시 약한참조를 사용하도록 선언할 수 있습니다. 클래스 전용 프로토콜(Class-Only Protocols)에서 논의된 것처럼 클래스 전용 프로토콜은 AnyObject로 부터의 상속을 표시합니다.

다음은 흐름제어(Control Flow)에서 처음(orignally) 소개된 뱀과 사다리(Snakes and Ladders)게임의 버젼입니다. 이 버젼은 주사위를 던지기(dice-rolls)위해, DiceGame프로토콜을 적용하기 위해, 그리고 DiceGameDelegate 에 진행상황을 알리기 위해, Dice인스턴스가 채택되었습니다.

class SnakesAndLadders: DiceGame {
    let finalSquare = 25
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    var square = 0
    var board: [Int]
    init() {
        board = Array(repeating: 0, count: finalSquare + 1)
        board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
        board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
    }
    weak var delegate: DiceGameDelegate?
    func play() {
        square = 0
        delegate?.gameDidStart(self)
        gameLoop: while square != finalSquare {
            let diceRoll = dice.roll()
            delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
            switch square + diceRoll {
            case finalSquare:
                break gameLoop
            case let newSquare where newSquare > finalSquare:
                continue gameLoop
            default:
                square += diceRoll
                square += board[square]
            }
        }
        delegate?.gameDidEnd(self)
    }
}

뱀과 사다리(Snakes and Ladders) 게임 진행에 대해서는 , Break를 보세요.

이 게임의 버젼은 DiceGame프로토콜을 채택한 SnakesAndLadders으로 래핑(wrapped)되었습니다. 프로토콜을 준수하기 위해 gettable dice프로퍼티와 play() 메소드를 제공합니다. (dice프로퍼티는 초기화 이후에 변경할 필요가 없기 때문에, 상수 프로퍼티로 선언되고, 프로토콜은 gettable만 요구합니다)

뱀과 사다리(Snakes and Ladders) 보드 게임은 클래스의 init() 초기화에서 설정합니다. 모든 게임 로직은 주사위 던지는 값을 제공하기 위해 프로토콜의 요구사항인 dice프로퍼티를 사용하는, 프로토콜의 play 메소드로 이동됩니다.

delegate이 게임 플레이에서 필수사항이 아니기 때문에, delegate프로퍼티가 옵셔널(optional) DiceGameDelegate로 정의된 것을 주의합니다. 옵셔널 타입이기 때문에, delegate프로퍼티는 자동으로 초기 값으로 nil을 설정합니다. 그후에, 게임 인스턴스는 프로퍼티를 적절한 위임자(delegate)로 설정 할수 있습니다. DiceGameDelegate프로토콜은 클래스전용(class-only)이기 때문에, 위임자(delegate)를 순환참조를 막기 위해 weak가 되도록 선언 할수 있습니다.

DiceGameDelegate는 게임의 진행을 추적하는 3개의 메소드를 제공합니다. 이러한 3개의 메소드는 위의 play()메소드에 게임 로직 안으로 통합(incorporated)되고, 새 게임을 시작할때 호출되며, 새로운 턴(turn)이 시작하거나 게임이 끝납니다.

delegate프로퍼티는 옵셔널(optional) DiceGameDelegate이기 때문에, play() 메소드는 위임자(delegate)에서 메소드가 호출될때마다 옵셔널 체이닝을 사용합니다. delegate프로퍼티가 nil인 경우에, 이 위임자(delegate) 호출은 오류 없이 우아하게 실패할 것입니다. delegate프로퍼티가 non-nil인 경우에, 위임자(delegate) 메소드는 호출되고, 매개변수로 SnakesAndLadders인스턴스가 전달될 것입니다.

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

class DiceGameTracker: DiceGameDelegate {
    var numberOfTurns = 0
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        if game is SnakesAndLadders {
            print("Started a new game of Snakes and Ladders")
        }
        print("The game is using a \(game.dice.sides)-sided dice")
    }
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("Rolled a \(diceRoll)")
    }
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns) turns")
    }
}

DiceGameTrackerDiceGameDelegate의 요구사항인 3개의 메소드 모두 구현합니다. 이 메소드들은 게임의 턴수를 기록하는데 사용합니다. 게임을 시작할때, numberOfTurns 프로퍼티는 0으로 재설정하며, 새로운 턴이 시작할때마다 증가시키고, 게임이 종료되면 전체 턴의 개수를 출력합니다.

위에서 보여준 gameDidStart(_:)의 구현은 시작된 게임을 소개하는 정보를 출력하기 위해 game 매개변수를 사용합니다. game 매개변수는 SnakesAndLadders가 아닌, DiceGame타입이고 gameDidStart(_:)를 사용할수 있고 DiceGame프로토콜로 구현된 메소드와 프로퍼티만 사용할 수 있습니다. 하지만, 이 메소드는 여전히 기본 인스턴스 타입을 조회하기 위해 타입 변환이 가능합니다. 이 예제에서, game이 실제 SnakesAndLadders의 인스턴스인지 검사하고, 적절한 메시지를 출력합니다.

gameDidStart(_:) 메소드는 game 매개변수로 전달된 dice프로퍼티에 접근합니다. game이 DiceGame프로토콜을 준수하는것으로 알고 있기 때문에, dice프로퍼티가 있는 것을 보장하고, gameDidStart(_:) 메소드는 어떤 게임의 종류인지 상관없이(regardless), 주사위의 sides프로퍼티에 접근하고 출력이 가능합니다.

다음은 DiceGameTracker가 동작하는 방법입니다.

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

확장으로 프로토콜 준수하도록 추가하기(Adding Protocol Conformance with an Extension)

기존 타입에 대한 소스코드 접근이 없는 경우에도, 새로운 프로토콜을 채택(adopt)하고 준수(conform)해서 기존 타입을 확장할 수 있습니다. 확장(Extensions)은 기존 타입에 새로운 프로퍼티, 메소드, 서브스크립트를 추가할 수 있고, 프로토콜이 요구사항을 추가하는 것이 가능합니다. 확장에 대한 자세한 정보는 확장(Extensions)를 보세요.

주의
타입의 기존 인스턴스는 확장에서 인스턴스의 타입에 준수하도록 프로토콜을이 추가될때, 프로토콜은 자동으로 채택되고 준수하게 됩니다.

예를 들어, 프로토콜 TextRepresentable은 텍스트롤 표현할수 있는 모든 타입으로 구현될 수 있습니다. 이는 스스로에 대한 설명이거나 현재 상태의 텍스트 버젼일수 있습니다.

protocol TextRepresentable {
    var textualDescription: String { get }
}

위의 Dice클래스는 TextRepresentable을 채택하고 준수하여 확장될수 있습니다.

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

이 확장은 Dice가 원래 구현에서 제공된 것과 정확히 같은 방법으로 새로운 프로토콜을 채택합니다. 이 프로토콜 이름은 타입 이름 뒤에, 콜론(:)으로 구분되어 제공되고, 프로토콜의 모든 요구사항의 구현은 확장의 중괄호(curly braces {}) 안에서 제공됩니다.

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

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

비슷하게, SnakesAndLadders 게임 클래스는 TextRepresentable프로토콜을 채택하고 준수하도록 확장될 수 있습니다.

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

조건부로 프로토콜 준수하기(Conditionally Conforming to a Protocol)

제네릭 타입은 타입의 제네릭 매개변수가 프로토콜을 준수 할때, 프로토콜의 요구사항을 특정 조건에서만 만족하는게 가능할 것입니다. 타입을 확장할때 제약조건(constraints)을 나열하여(listing), 제네릭 타입이 프로토콜을 조건부 준수하도록 만들수 있습니다. 이러한 제약조건은 제네릭 where 절을 채택(adopting)하는 프로토콜의 이름 뒤에 작성합니다. 제네릭 where절에 대한 자세한 것은 제네릭 Where 절(Generic Where Clauses)를 보세요.

다음에 오는 확장은 TextRepresentable을 준수하는 타입의 요소들을 저장할때마다, Array인스턴스가 TextRepresentable프로토콜을 준수하도록 만듭니다.

extension Array: TextRepresentable where Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// Prints "[A 6-sided dice, A 12-sided dice]"

확장으로 프로토콜 채택 선언하기(Declaring Protocol Adoption with an Extension)

타입이 프로토콜의 모든 요구사항을 이미 준수하지만, 프로토콜을 아직 채택하지 않은 경우, 비어있는 확장으로 프로토콜을 채택하도록 만들 수 있습니다.

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

이제 Hamster의 인스턴스는 TextRepresentable이 요구된 타입 일때마다 사용될 수 있습니다.

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

주의
타입은 요구사항을 만족하더라도 프로토콜을 자동으로 채택하지 않습니다. 프로토콜을 채택하는 것에 대해 항상 명시적으로 선언해줘야 합니다.

프로토콜 타입 모음(Collections of Protocol Type)

타입같은 프로토콜(Protocols as Types)에서 언급했던것처럼, 프로토콜은 배열이나 딕셔너리처럼, 컬렉션(collection)에 저장되도록 타입처럼 사용될 수 있습니다. 이 예제는 TextRepresentable의 배열을 생성합니다.

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

이제 배열내의 항목들을 반복하고, 각 항목들의 텍스트 설명을 출력하는게 가능합니다.

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

thing 상수는 TextRepresentable 타입인 것을 주의합니다. 그것은 Dice나 DiceGameHamster 타입이 아니며, 심지어는, 실제 인스턴스가 이러한 타입 중 하나일지라도 말입니다. 그럼에도 불구하고, TextRepresentable타입이기 때문에, TextRepresentable이 가지고 있는 것으로 알고 있는 textualDescription프로퍼티를 반복문을 통해서 매번 thing.textualDescription을 안전하게 접근 할수 있습니다.

프로토콜 상속(Protocol Inheritance)

프로토콜은 하나 이상의 다른 프로토콜을 상속(inherit)할 수 있고 상속한 요구사항보다 더 많은 요구사항을 추가할 수 있습니다. 프로토콜 상속에 대한 문법은 클래스 상속에 대한 문법과 비슷하지만, 상속된 여러 프로토콜들을 콤마(,)로 구분해서 나열하는 옵션이 있습니다.

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}

다음은 위의 TextRepresentable 프로토콜을 상속하는 프로토콜의 예제 입니다.

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

이 예제는 TextRepresentable로 부터 상속받은 새로운 프로토콜 PrettyTextRepresentable을 정의합니다. PrettyTextRepresentable을 채택한 것들은 반드시 TextRepresentable에 의해 강제된 요구사항 모두를 만족해야 하며, PrettyTextRepresentable에 의해 강요된 요구사항이 추가적인으로 더해집니다(plus). 이 예제에서, PrettyTextRepresentable은 String을 반환하는 gettable 프로퍼티 prettyTextualDescription을 제공하기 위한 요구사항 하나를 추가합니다.

SnakesAndLadders클래스는 PrettyTextRepresentable을 채택하고 준수하도록 확장될수 있습니다.

extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\n"
        for index in 1...finalSquare {
            switch board[index] {
            case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}

이 확장 상태는 PrettyTextRepresentable 프로토콜을 채택하고 SnakesAndLadders타입에 대한 prettyTextualDescription프로퍼티 구현을 제공합니다. 모든 PrettyTextRepresentable은 TextRepresentable이 되어야 하고, 문자열 출력을 시작하기 위한 prettyTextualDescription의 구현은 TextRepresentable 프로토콜로부터 textualDescription프로퍼티를 접근해서 시작합니다. 콜론(:)과 줄바꿈(line break)을 추가하고, 텍스트를 보기 좋게(pretty) 표현하는 것으로 시작합니다. 그런 다음 보드판의 배열을 반복하고, 각 칸(square)의 콘텐츠를 표현하기 위한 특수문자를 추가합니다.

  • 칸(square)의 값이 0보다 큰 경우, 사다리의 시작이고, ▲으로 표현됩니다.
  • 칸(square)의 값이 0보다 작은 경우, 뱀의 머리이고, ▼으로 표현됩니다.
  • 그외에, 칸(square)의 값이 0이고, 비어있는(free)칸이며, ○으로 표현됩니다.

이제 prettyTextualDescription 프로퍼티는 모든SnakesAndLadders 인스턴스의 텍스트 설명을 보기좋게 출력하는데 사용됩니다.

print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

클래스 전용 프로토콜(Class-Only Protocols)

프로토콜의 상속 목록에 AnyObject프로토콜을 추가하여 클래스 타입(구조체나 열거형이 아님)에 프로토콜 채택을 제한할 수 있습니다.

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // class-only protocol definition goes here
}

위의 예제에서, SomeClassOnlyProtocol은 클래스 타입에서만 채택될수 있습니다. 구조체나 열거형 정의에 SomeClassOnlyProtocol을 적용하려고 하면 컴파일시 오류가 발생합니다.

주의
준수하는 타입이 값의 의미보다는 참조의 의미를 가지는 프로토콜의 요구사항을 가정하거나 요구에 의한 동작을 정의할때 클래스 전용(class-only) 프로토콜을 사용합니다. 참조와 값의 의미에 대한 자세한 것은 구조체와 열거형은 값 타입(Structures and Enumerations Are Value Types)과 클래스는 참조 타입(Classes Are Reference Types)를 보세요.

프로토콜 합성(Protocol Composition)

그것은 한번에 여러개의 프로토콜을 준수하도록 타입에 요구할때 유용할 수 있습니다. 여러개의 프로토콜을 프로토콜 합성(protocol composition)으로 하나의 요구사항으로 결합시킬 수 있습니다. 프로토콜 합성(compositions)은 합성 안에 모든 프로토콜의 요구사항이 결합된 임시 로컬 프로토콜로 정의된 것처럼 동작합니다. 프로토콜 합성은 새로운 프로토콜 타입을 정의하지 않습니다.

프로토콜 합성은 SomeProtocol & AnotherProtocol 형식을 가집니다. 필요한 만큼의 프로토콜을 나열할 수 있으며, &로 구분합니다. 프로토콜의 목록외에도, 프로토콜 합성은 필요한 상위클래스를 지정하기 위해, 클래스 타입을 포함할 수 있습니다.

다음은 함수의 매개변수에서 2개의 프로토콜 Named와 Aged를 하나의 프로토콜 합성 요구사항으로 결합한 예제 입니다.

protocol Named {
    var name: String { get }
}
protocol Aged {
    var age: Int { get }
}
struct Person: Named, Aged {
    var name: String
    var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"

이 예제에서, Named프로토콜은 gettable String프로퍼티 name인 하나의 요구사항을 가집니다. Aged 프로토콜은 gettable Int프로퍼티 age에 대한 하나의 요구사항을 가집니다. 두 프로토콜은 구조체 Person에 의해 채택됩니다.

또한, 그 예제는 wishHappyBirthday(to:) 함수를 정의합니다. celebrator 매개변수의 타입은 모든 타입은 Named와 Aged 프로토콜을 준수합니다를 의미하는 Named & Aged 입니다. 필요한 프로토콜 모두를 준수하는 한, 특정 타입이 함수로 전달 되는 것은 중요하지 않습니다.

그런 다음 그 예제는 새로운 Person인스턴스 birthdayPerson을 생성하고 withHappyBirthday(to:) 함수에 새로운 인스턴스를 전달합니다. Person은 두개의 프로토콜을 모두 준수하기 때문에, 이 호출은 유효하고, wishHappyBirthday(to:) 함수는 생일축하를 출력할 수 있습니다.

다음은 이전 예제의 Location클래스를 사용해서 Named프로토콜을 결합한 예제 입니다.

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"

beginConcert(in:) 함수는 모든 타입은 Location의 하위클래스이고 Named 프로토콜을 준수합니다을 의미하는 Location & Named 타입의 매개변수를 가집니다. 이 경우에는, City는 모든 요구사항을 만족합니다.

Person이 Location의 하위클래스가 아니기 때문에, birthdayPerson을 beginConcert(in:) 함수에 전달하는 것은 유효하지 않습니다. 마찬가지로, Named 프로토콜을 준수하지 않는 Location의 하위클래스를 만드는 경우에, 타입의 인스턴스로 beginConcert(in) 호출하는 것 또한 유효하지 않습니다.

프로토콜을 준수하는지 확인하기(Checking for Protocol Conformance)

프로토콜을 준수하는지 검사하기 위해 타입 변환(Type Casting)에서 설명된 is와 as 연산자를 사용할 수 있고, 특정 프로토콜을 변환할 수 있습니다. 프로토콜을 확인하고 변환 하는 것은 타입을 검사하고 변환화는 것과 정확히 같은 문법입니다.

  • 인스턴스가 프로토콜을 준수하는 경우에 is연산자는 true를 반환하고, 그렇지 않으면, false를 반환합니다.
  • 다운캐스트 연산자의 버젼인 as?는 프로토콜 타읩의 옵셔널 값을 반환하고, 그 인스턴스가 프로토콜을 준수하지 않는 경우에, 이 값이 nil입니다.
  • 강제 다운캐스트 연산자 버젼인 as!는 프로토콜 타입을 다운 케스트하고 다운캐스트가 성공하지 않으면 런타임 오류가 발생합니다.

이 예제는하나의 프로퍼티 gettable Double프로퍼티 area를 요구하는 프로토콜 HasArea를 정의합니다.

protocol HasArea {
    var area: Double { get }
}

2개의 클래스 Circle와 Country가 있으며, 둘다 HasArea프로토콜을 준수합니다.

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

Circle클래스는 저장 프로퍼티 radius를 기반으로 계산 프로퍼티 처럼 area프로퍼티 요구사항을 구현합니다. Country 클래스 구현은 저장프로퍼티 처럼 직접 area 요구사항을 구현합니다. 두 클래스 모두 HasArea 프로토콜을 정확히 준수합니다.

다음은 HasArea프로토콜을 준수하지 않는, Animal 클래스 입니다.

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

Circle, Country, Animal 클래스는 기본 클래스를 공유하지 않습니다. 그럼에도 불구하고 모든 클래스와 이러한 모든 타입의 인스턴스는 AnyObject타입의 값을 저장하는 배열을 초기화하는데 사용될 수 있습니다.

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

objects 배열은 반지름이 2인 Circle인스턴스, 영국의 면적을 평방킬로미터로 초기화된 Country인스턴스, 3개의 다리를 가진 Animal인스턴스를 포함하는 배열 리터럴로 초기화 됩니다.

objects 배열은 이제 반복할 수 있고, 배열내의 각 객체(object)를 검사하여 HasArea 프로토콜을 준수하는지 확인할 수 있습니다.

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

배열내의 객체가 HasArea프로토콜을 준수할때마다, as?연산자에 의해 반환되는 옵셔널 값은 옵셔널 바인딩을 사용하여 objectWithArea 상수로 언래핑됩니다. objectWithArea 상수는 HasArea타입이 되고, area 프로퍼티는 타입에 안전하게(type-safe) 접근하고 출력될 수 있습니다.

기본 객체는 변환(casting) 과정에서 변경되지 않는 것을 주의합니다. 그것들은 Circle, Country, animal이 될수 있습니다. 하지만, 이 시점에, 그것들은 objectWithArea상수에 저장되며, HasArea타입이 되는 것을 알고 있고, area프로퍼티에만 접근할 수 있습니다.

옵셔널 프로토콜 요구사항(Optional Protocol Requirements)

프로토콜에 대한 프로콜에 대한 옵셔널 요구사항(optional requirements)을 정의할 수 있습니다. 이러한 요구사항들은 프로토콜을 준수하는 타입에 의해 구현할 필요가 없습니다. 옵셔널 요구사항은 프로토콜 정의에서 optional수식어를 접두사로 붙입니다. 옵셔널 요구사항은 Objective-C와 상호호환(interoperates)되는 코드를 작성하는 것이 가능합니다. 프로토콜과 옵셔널 요구사항은 반드시 @objc속성으로 표시되어야 합니다. @objc 프로토콜은 Objective-C 클래스나 다른 @objc클래스를 상속하는 클래스에서만 채택될수 있는 것을 주의합니다. 구조체나 열거형에서는 채택할 수 없습니다.

옵셔널 요구사항에서 메소드나 프로퍼티를 사용할때, 그 타입은 자동적으로 옵셔널이 됩니다. 예를 들어, (Int) -> String타입의 메소드는 ((Int) -> String)?이 됩니다. 함수 타입은 메소드의 반환 값이 아니라 전체가 옵셔널로 랩핑(wrapped)되는 것을 주의 합니다.

옵셔널 프로토콜 요구사항은 프로토콜을 준수하는 타입으로 요구사항이 구현되지 않을 가능성을 가정하기 위해, 옵셔널 체이닝으로 호출될 수 있습니다. someOptionalMethod?(someArgument)처럼, 메소드가 호출될때 메소드의 이름 뒤에 물음표(?)를 작성하여 옵셔널 메소드의 구현에 대해 검사합니다 옵셔널 체이닝에 대한 자세한 정보는 옵셔널 체이닝(Optional Chaining)을 보세요.

다음에 오는 예제는 외부 데이터 소스로 양을 증가시키는데 사용하는 정수 계산(integer-counting) 클래스 Counter를 정의합니다. 이 데이터 소스는 2개의 옵셔널 요구사항을 가진 CounterDataSource프로토콜에 의해 정의됩니다.

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}

CounterDatasource 프로토콜은 옵셔널 메소드 요구사항 increment(forCount:)와 옵셔널 프로퍼티 요구사항 fixedIncrement를 정의합니다. 이러한 요구사항들은 Counter인스턴스에 대한 양을 적절히 증가하는 것을 제공하기 위해, 데이터소스에 대해 2가지 다른 방법을 정의합니다.

주의
엄밀히 말하면, 어떠한(either) 프로토콜 요구사항 구현없이 CounterDataSource를 준수하는 사용자정의 클래스를 작성할 수 있습니다. 그것들은 결국 모두 옵셔널입니다. 비록 기술적으로 가능하지만, 아주 좋은 데이터 소스를 만들지는 못합니다.

아래에 정의된 Counter클래스는 CounterDataSource?타입의 옵셔널 dataSource 프로퍼티를 가집니다.

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

Counter클래스는 변수 프로퍼티 count에 현재 값을 저장합니다. 또한, Counter클래스는 메소드가 호출될때마다 count프로퍼티를 증가시키는 메소드 increment를 정의합니다.

increment()메소드는 처음에 데이터소스로부터 increment(forCount:)메소드의 구현으로 조회하여 증가한 값을 가져오도록 시도합니다.increment() 메소드는 increment(forCount:)호출하기 위해 옵셔널 체이닝을 사용하고, 메소드의 단일 인자로 현재 count값을 전달합니다.

옵셔널 체이닝의 2(two) 단계가 여기에서 수행되는 것을 주의합니다. 첫번째, dataSource가 nil이 될 가능성이 있고, dataSource가 nil이 아닌경우에만, dataSource가 increment(forCount:)를 호출하는 것을 가리키기 위해 이름 뒤에 물음표를 가집니다. 두번째, dataSource가 있는 경우, 옵셔널 요구사항이기 때문에, increment(forCount:) 구현을 보장하지 않습니다. 여기에서, 옵셔널 체이닝에 의해 처리되어 increment(forCount:)는 구현되지 않을 수 있습니다. increment(forCount:)가 존재하는 경우에만(nil이 아닌 경우), increment(forCount:)가 호출이 발생합니다.. 이것이 increment(forCount:)가 이름 뒤에 물음표(?)로 작성되는 이유 입니다.

increment(forCount:)호출은 두가지 이유로 실패할 수 있기 때문에, 그 호출은 옵셔널(optional) Int값입니다. CounterDataSource의 정의에서 increment(forCount:)이 옵셔널이 아닌 Int값을 반환하는 것으로 정의됩어 있어도 마찬가지 입니다. 2개의 옵셔널 체이닝 동작이 있으며, 하나씩, 그 결과는 여전히 하나의 옵셔널로 랩핑(wrapped)됩니다. 여러개의 옵셔널 체이닝 동작을 사용하는 것에 관한 자세한 정보는 체이닝의 여러 단계 연결하기(Linking Multiple Levels of Chaining)을 보세요.

increment(forCount:)호출한 이후에, 반환하는 옵셔널 Int는 옵셔널 바인딩을 사용하여, 상수 amount로 언래핑됩니다. 옵셔널 Int가 값(위임자(delegate)와 메소드(method) 모두 존재하는 경우, 메소드가 반환한 값)을 포함하는 경우, 언래핑된 amount는 저장 프로퍼티 count에 추가되고, 증가하는 것이 완료됩니다.

increment(forCount:)메소드로부터 값을 가져오는게 가능하지 않은 경우(datasource가 nil이거나 데이터 소스가 increment(forCount:)구현하지 않았기 때문), increment() 메소드는 데이터소스의 fixedIncrement프로퍼티 대신하여 값을 가져오려고 시도합니다. fixedIncrement프로퍼티는 옵셔널 요구사항이며, 그 값은 옵셔널 Int값이며, fixedIncrement는 CounterDataSource프로토콜 정의에서 옵셔널이 아닌 Int프로퍼티로 정의되어 있습니다.

다음은 간단하게 조회할때마다 3의 상수 값을 반환하는 데이터 소스 CounterDataSource를 구현하였습니다. 그것은 옵셔널 fixedIncrement프로퍼티 요구사항을 구현하여 수행합니다.

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

새로운 Counter인스턴스에 대한 데이터소스로 ThreeSource의 인스턴스를 사용할 수 있습니다.

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

위 코드는 새로운 Counter인스턴스를 생성합니다. 데이터 소스에 새로운 ThreeSource인스턴스를 설정하고, 카운터의 increment()메소드를 4번 호출합니다. 예상대로, 카운터의 count프로퍼티는 increment()가 호출될때마다 3씩 증가합니다.

다음은 Counter 인스턴스의 갯수를 현재 count값으로 부터 올리거나 0 방향으로 내리는 좀 더 복잡한 데이터 소스 TowardsZeroSource입니다.

class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

TowardsZeroSource 클래스는 CounterDataSource 프로토콜로 부터 옵셔널 increment(forCount:) 메소드를 구하고 카운트를 할 방향을 결정하기 위해 count인자값을 사용합니다. count가 이미 0인 경우에, 메소드는 더 이상 카운팅하지 않는다는 것을 가리키기 위해 0을 반환 합니다.

-4에서 0으로 계산하는 기존 Counter인스턴스로 TowardsZeroSource의 인스턴스를 사용할 수 있습니다. 한번 0까지 도달하면, 더 이상 계산하지 않습니다.

 counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0

프로토콜 확장

프로토콜은 타입을 준수하기 위해 메소드, 초기화, 서브스크립트, 계산 프로퍼티 구현을 제공하여 확장될수 있습니다. 각 타입의 개별적인으로 적합하게 하거나(conformance)이나 전역 함수에서 처리하기 보다는, 프로토콜 자체에서 동작을 정의합니다.

예를들어, RandomNumberGenerator프로토콜은 무작위 Bool 값을 반환하도록 random() 메소드의 결과를 사용하는, randomBool()메소드를 제공해서 확장할 수 있습니다.

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

프로토콜에서 확장을 생성하여, 추가적인 수정없이 준수하는 모든 타입의 메소드 구현을 자동으로 가져옵니다.

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// Prints "And here's a random Boolean: true"

프로토콜 확장은 준수하는 타입에 구현을 추가할 수 있지만, 다른 프로토콜로부터 프로토콜을 확장하거나 상속할 수는 없습니다. 프로토콜 상송은 항상 프로토콜 자체 선언 에서만 지정됩니다.

기본 구현을 제공하기(Providing Default Implementations)

프로토콜이 요구하는 모든 메소드나 계산 프로퍼티에 기본 구현을 제공하여 프로토콜 확장을 사용할 수 있습니다. 준수하는 타입이 요구하는 메소드나 프로퍼티를 자체적인 구현을 제공하는 경우, 그 구현은 확장에 의해 제공되는 것을 대신해서 사용될 것입니다.

주의
확장으로 기본 구현이 제공된 프로토콜 요구사항(requirements)은 옵셔널 프로토콜 요구사항과는 다릅니다. 준수하는 타입이 자체적으로 구현할 필요는 없지만, 기본 구현이 있는 요구사항은 옵셔널 체이닝 없이 호출될 수 있습니다.

예를 들어, PrettyTextRepresentable프로토콜

TextRepresentable 프로토콜을 상속받아 단순히 textualDescription프로퍼티에 접근하는 결과를 반환하기 위해 , prettyTextualDescription 프로퍼티의 기본 구현을 제공할 수 있습니다.

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

프로토콜 확장에 제약조건 추가하기(Adding Constraints to Protocol Extensions)

프로토콜 확장을 정의할때, 확장의 메소드와 프로퍼티가 사용가능하기 전에 준수하는 타입이 만족해야만 하는 제약조건을 지정할 수 있습니다. 이러한 제약조건을 제네릭 where 절을 작성해서 확장하려는 프로토콜의 이름 뒤에 작성합니다. 제네릭 where절에 대한 자세한 것은 제네릭 Where 절(Generic Where Clauses)을 보세요.

예를 들어, 요소들이 Equatable 프로토콜을 준수하는 모든 컬렉션에 적용하기 위해 Collection 프로토콜 확장을 정의할 수 있습니다. 표준 라이브러리에서 컬렉션의 요소들을 Equatable프로토콜로 제한함으로써, 두개의 요소들간에 동일하고 동일하지 않는지 검사를 위해, ==와 !=연산자를 사용할 수 있습니다.

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}

컬렉션에 잇는 모든 요소들이 같을때에만, allEqual()메소드가 true를 반환합니다.

하나는 모든 요소가 같고, 하나는 그렇지 않는 두개의 정수형 배열을 생각해봅니다.

let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]

배열은 Collection을 준수하고 정수형은 Equatable을 준수하기 때문에, equalNumber와 differentNumbers는 allEqual() 메소드를 사용할 수 있습니다.

print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

주의
준수하는 타입이 같은 메소드나 프로퍼티에 대한 구현을 제공하는, 여러개의 제한된 확장에 대한 요구사항을 만족하는 경우, Swift는 가장 특별한 제약조건을 위해 해당 구현을 사용합니다.


반응형

'Swift > Language Guide' 카테고리의 다른 글

Access Control  (0) 2018.09.18
Memory Safety  (0) 2018.09.18
Automatic Reference Counting  (0) 2018.09.18
Generics  (0) 2018.09.18
Extensions  (0) 2018.09.18
Nested Types  (0) 2018.09.18
Type Casting  (0) 2018.09.18
Error Handling  (0) 2018.09.18
Posted by 까칠코더
,