Protocols

Groot·2022년 10월 10일
0

Swift Language Guide

목록 보기
21/24
post-thumbnail

Protocols

  • 프로토콜은 특정 작업이나 기능에 적합한 메서드, 속성 및 기타 요구 사항의 청사진을 정의합니다.
  • 그런 다음 프로토콜은 해당 요구 사항의 실제 구현을 제공하기 위해 클래스, 구조 또는 열거에 의해 채택될 수 있습니다.
  • 프로토콜의 요구 사항을 충족하는 모든 유형은 해당 프로토콜을 준수한다고 합니다.
  • 준수 유형이 구현해야 하는 요구 사항을 지정하는 것 외에도 프로토콜을 extension하여 이러한 요구 사항 중 일부를 구현하거나 준수 유형이 활용할 수 있는 추가 기능을 구현할 수 있습니다.

📌 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

  • 프로토콜은 특정 이름과 유형을 가진 인스턴스 속성 또는 유형 속성을 제공하기 위해 준수 유형을 요구할 수 있습니다.
  • 프로토콜은 속성이 저장 속성이어야 하는지 계산 속성이어야 하는지를 지정하지 않으며 필요한 속성 이름과 유형만 지정합니다.
  • 프로토콜은 또한 각 속성이 gettable 또는 gettable 및 settable이어야 하는지 여부를 지정합니다.
  • 프로토콜에서 속성을 가져오고 설정할 수 있어야 하는 경우 해당 속성 요구 사항은 상수 저장 속성이나 읽기 전용 계산 속성으로 충족될 수 없습니다.
  • 프로토콜이 gettable 속성만 요구하는 경우 요구 사항은 모든 종류의 속성으로 충족될 수 있으며 이것이 자신의 코드에 유용하다면 속성 설정도 유효합니다.
  • 속성 요구 사항은 항상 var 키워드가 접두사로 붙는 변수 속성으로 선언됩니다.
  • Gettable 및 settable 속성은 유형 선언 뒤에 { get set }을 써 표시하고, gettable 속성은 { get }을 써서 표시합니다.
    protocol SomeProtocol {
        var mustBeSettable: Int { get set }
        var doesNotNeedToBeSettable: Int { get }
    }
  • 프로토콜에서 정의할 때 항상 유형 속성 요구 사항에 static 키워드를 접두사로 붙입니다.
  • 이 규칙은 유형 속성 요구 사항이 클래스로 구현될 때 class 또는 static 키워드를 접두사로 사용할 수 있는 경우에도 적용됩니다.
    protocol AnotherProtocol {
        static var someTypeProperty: Int { get set }
    }
  • 다음은 단일 인스턴스 속성 요구 사항이 있는 프로토콜의 예입니다.
    protocol FullyNamed {
        var fullName: String { get }
    }
  • FullNamed 프로토콜은 정규화된 이름을 제공하기 위해 준수 유형이 필요합니다.
  • 프로토콜은 일치하는 유형의 특성에 대해 다른 것을 지정하지 않습니다.
  • 유형이 자체에 대한 전체 이름을 제공할 수 있어야 함만 지정합니다.
  • 프로토콜에 따르면 모든 FullNamed 유형에는 문자열 유형의 fullName이라는 gettable 인스턴스 속성이 있어야 합니다.
  • 다음은 FullNamed 프로토콜을 채택하고 준수하는 간단한 구조의 예입니다.
    struct Person: FullyNamed {
        var fullName: String
    }
    let john = Person(fullName: "John Appleseed")
    // john.fullName is "John Appleseed"
  • 이 예는 특정 명명된 사람을 나타내는 Person이라는 구조를 정의합니다.
  • 정의의 첫 번째 줄의 일부로 FullNamed 프로토콜을 채택한다고 명시되어 있습니다.
  • Person의 각 인스턴스에는 문자열 유형의 fullName이라는 단일 저장 속성이 있습니다.
  • 이는 FullNamed 프로토콜의 단일 요구 사항과 일치하며 Person이 프로토콜을 올바르게 준수했음을 의미합니다. (Swift는 프로토콜 요구 사항이 충족되지 않으면 컴파일 타임에 오류를 보고합니다.)
  • 다음은 FullNamed 프로토콜을 채택하고 준수하는 더 복잡한 클래스입니다.
    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 클래스 인스턴스는 필수 이름과 선택적 접두사를 저장합니다.
  • fullName 속성은 접두사 값이 있는 경우 이를 사용하고, 이를 이름 앞에 추가하여 우주선의 전체 이름을 만듭니다.

📌 Method Requirements

  • 프로토콜은 준수하는 유형에 의해 구현되는 특정 인스턴스 메소드 및 유형 메소드를 요구할 수 있습니다.
  • 이러한 메소드는 일반 인스턴스 및 유형 메소드와 정확히 같은 방식으로 프로토콜 정의의 일부로 작성되지만 중괄호 또는 메소드 본문이 없습니다.
  • 일반적인 방법과 동일한 규칙에 따라 가변 매개변수가 허용됩니다.
  • 그러나 프로토콜 정의 내에서 메소드 매개변수에 대해 기본값을 지정할 수 없습니다.
  • 유형 속성 요구 사항과 마찬가지로 프로토콜에 정의될 때 유형 메서드 요구 사항에 항상 static 키워드를 접두사로 붙입니다.
  • 이는 유형 메소드 요구사항이 클래스에 의해 구현될 때 class 또는 static 키워드가 접두어로 붙는 경우에도 마찬가지입니다.
    protocol SomeProtocol {
        static func someTypeMethod()
    }
  • 다음 예제에서는 단일 인스턴스 메서드 요구 사항이 있는 프로토콜을 정의합니다.
    protocol RandomNumberGenerator {
        func random() -> Double
    }
  • 이 프로토콜인 RandomNumberGenerator는 모든 준수 유형이 호출될 때마다 Double 값을 반환하는 random이라는 인스턴스 메서드를 갖도록 요구합니다.
  • 프로토콜의 일부로 지정되지는 않았지만 이 값은 0.0에서 1.0까지(포함하지 않음)의 숫자로 가정합니다.
  • RandomNumberGenerator 프로토콜은 각 난수가 생성되는 방식에 대해 어떠한 가정도 하지 않습니다.
  • 생성자가 새로운 난수를 생성하는 표준 방법을 제공하기만 하면 됩니다.
  • 다음은 RandomNumberGenerator 프로토콜을 채택하고 준수하는 클래스의 구현입니다.
  • 이 클래스는 선형 합동 생성기로 알려진 의사 난수 생성기 알고리즘을 구현합니다.
    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

  • 메서드가 속한 인스턴스를 수정(또는 변경)해야 하는 경우가 있습니다.
  • 값 유형(즉, 구조 및 열거형)에 대한 인스턴스 메서드의 경우 메서드의 func 키워드 앞에 mutating 키워드를 배치하여 메서드가 속한 인스턴스와 해당 인스턴스의 속성을 수정할 수 있음을 나타냅니다.
  • 이 프로세스는 인스턴스 메서드 내에서 값 유형 수정에 설명되어 있습니다.
  • 프로토콜을 채택하는 모든 유형의 인스턴스를 변경하기 위한 프로토콜 인스턴스 메서드 요구 사항을 정의하는 경우 프로토콜 정의의 일부로 mutating 키워드를 사용하여 메서드를 표시합니다.
  • 이를 통해 구조와 열거형이 프로토콜을 채택하고 해당 메서드 요구 사항을 충족할 수 있습니다.

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

  • 아래 예제는 토글이라는 단일 인스턴스 메서드 요구 사항을 정의하는 Togglable이라는 프로토콜을 정의합니다.
  • 이름에서 알 수 있듯이 toggle() 메서드는 일반적으로 해당 유형의 속성을 수정하여 해당 유형의 상태를 전환하거나 반전하기 위한 것입니다.
  • 토글() 메서드는 Togglable 프로토콜 정의의 일부로 mutating 키워드로 표시되어 메서드가 호출될 때 해당 인스턴스의 상태를 변경할 것으로 예상됨을 나타냅니다.
    protocol Togglable {
        mutating func toggle()
    }
  • 구조 또는 열거형에 대해 Togglable 프로토콜을 구현하는 경우 해당 구조 또는 열거형은 변경 중인 것으로 표시된 toggle() 메서드 구현을 제공하여 프로토콜을 준수할 수 있습니다.
  • 아래 예제는 OnOffSwitch라는 열거를 정의합니다.
  • 이 열거는 열거 케이스 켜짐 및 꺼짐으로 표시되는 두 상태 사이를 토글합니다.
  • 열거형의 토글 구현은 Togglable 프로토콜의 요구 사항과 일치하도록 변경되는 것으로 표시됩니다.
    num 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

📌 Initializer Requirements

  • 프로토콜은 준수하는 유형으로 구현되는 특정 초기화 프로그램을 요구할 수 있습니다.
  • 이 이니셜라이저는 일반 이니셜라이저와 정확히 같은 방식으로 프로토콜 정의의 일부로 작성하지만 중괄호나 이니셜라이저 본문은 사용하지 않습니다.
    protocol SomeProtocol {
        init(someParameter: Int)
    }

📍 Class Implementations of Protocol Initializer Requirements

  • 지정된 이니셜라이저 또는 편의 이니셜라이저로 준수 클래스에 대한 프로토콜 이니셜라이저 요구 사항을 구현할 수 있습니다.

  • 두 경우 모두 이니셜라이저 구현을 필수 수정자로 표시해야 합니다.

    class SomeClass: SomeProtocol {
        required init(someParameter: Int) {
            // initializer implementation goes here
        }
    }
  • 필수 수정자를 사용하면 준수하는 클래스의 모든 하위 클래스에 대한 초기화 요구 사항의 명시적 또는 상속된 구현을 제공하여 해당 클래스도 프로토콜을 준수하도록 합니다.

    최종 클래스는 서브클래싱할 수 없기 때문에 final 수정자로 표시된 클래스에 필요한 수정자로 프로토콜 이니셜라이저 구현을 표시할 필요가 없습니다.
    최종 수정자에 대한 자세한 내용은 재정의 방지를 참조하십시오.

  • 서브클래스가 슈퍼클래스의 지정된 이니셜라이저를 재정의하고 프로토콜에서 일치하는 이니셜라이저 요구 사항도 구현하는 경우 필수 및 재정의 수정자를 모두 사용하여 이니셜라이저 구현을 표시합니다.

    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

  • 프로토콜은 실패 가능한 이니셜라이저에 정의된 대로 준수 유형에 대한 실패 가능한 이니셜라이저 요구 사항을 정의할 수 있습니다.
  • 실패 가능한 이니셜라이저 요구 사항은 일치하는 유형의 실패 가능 또는 실패 불가능한 이니셜라이저에 의해 충족될 수 있습니다.
  • 실패할 수 없는 이니셜라이저 요구 사항은 실패할 수 없는 이니셜라이저 또는 암시적으로 래핑되지 않은 실패 가능한 이니셜라이저에 의해 충족될 수 있습니다.

📌 Protocols as Types

  • 프로토콜은 실제로 기능 자체를 구현하지 않습니다. 그럼에도 불구하고 코드에서 프로토콜을 완전한 유형으로 사용할 수 있습니다.
  • 프로토콜을 유형으로 사용하는 것을 실존적 유형이라고 하는 경우가 있는데, 이는 "T가 프로토콜을 준수하도록 유형 T가 존재합니다"라는 문구에서 유래했습니다.
  • 다음을 포함하여 다른 유형이 허용되는 여러 위치에서 프로토콜을 사용할 수 있습니다.
    • 함수, 메서드 또는 이니셜라이저의 매개변수 유형 또는 반환 유형 상수
    • 변수 또는 속성의 유형으로
    • 배열, 사전 또는 기타 컨테이너의 항목 유형

      프로토콜은 유형이기 때문에 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 인스턴스에는 측면 수를 나타내는 정수 속성과 주사위 굴림 값을 생성하는 난수 생성기를 제공하는 생성기 속성이 있습니다.
  • 생성기 속성은 RandomNumberGenerator 유형입니다.
  • 따라서 RandomNumberGenerator 프로토콜을 채택하는 모든 유형의 인스턴스로 설정할 수 있습니다.
  • 인스턴스가 RandomNumberGenerator 프로토콜을 채택해야 한다는 점을 제외하고 이 속성에 할당하는 인스턴스에는 다른 것이 필요하지 않습니다.
  • 유형이 RandomNumberGenerator이기 때문에 Dice 클래스 내부의 코드는 이 프로토콜을 준수하는 모든 생성기에 적용되는 방식으로만 생성기와 상호 작용할 수 있습니다.
  • 즉, 생성기의 기본 유형으로 정의된 메서드나 속성을 사용할 수 없습니다.
  • 그러나 다운캐스팅에서 설명한 것처럼 수퍼클래스에서 서브클래스로 다운캐스트하는 것과 같은 방식으로 프로토콜 유형에서 기본 유형으로 다운캐스트할 수 있습니다.
  • Dice에는 초기 상태를 설정하는 초기화 프로그램도 있습니다. 이 이니셜라이저에는 RandomNumberGenerator 유형의 generator라는 매개변수가 있습니다.
  • 새 Dice 인스턴스를 초기화할 때 일치하는 모든 유형의 값을 이 매개변수에 전달할 수 있습니다.
  • Dice는 1과 주사위의 면 수 사이의 정수 값을 반환하는 하나의 인스턴스 메서드인 roll을 제공합니다.
  • 이 메서드는 생성기의 random() 메서드를 호출하여 0.0과 1.0 사이의 새로운 난수를 만들고 이 난수를 사용하여 올바른 범위 내에서 주사위 굴림 값을 만듭니다.
  • 제너레이터는 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

  • 위임은 클래스 또는 구조가 일부 책임을 다른 유형의 인스턴스에 넘길 수 있도록 하는 디자인 패턴입니다.
  • 이 디자인 패턴은 위임된 책임을 캡슐화하는 프로토콜을 정의하여 구현됩니다.
  • 따라서 준수하는 형식(대리자라고 함)이 위임된 기능을 제공하도록 보장됩니다.
  • 위임을 사용하여 특정 작업에 응답하거나 해당 소스의 기본 유형을 알 필요 없이 외부 소스에서 데이터를 검색할 수 있습니다.
  • 아래 예는 주사위 기반 보드 게임에 사용하기 위한 두 가지 프로토콜을 정의합니다.
    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 프로토콜은 주사위와 관련된 모든 게임에서 채택할 수 있는 프로토콜입니다.
  • DiceGameDelegate 프로토콜을 채택하여 DiceGame의 진행 상황을 추적할 수 있습니다.
  • 강한 참조 순환을 방지하기 위해 대리자는 약한 참조로 선언됩니다.
  • 약한 참조에 대한 자세한 내용은 클래스 인스턴스 간의 강력한 참조 주기를 참조하세요.
  • 프로토콜을 클래스 전용으로 표시하면 이 장의 뒷부분에서 SnakesAndLadders 클래스가 대리자가 약한 참조를 사용해야 한다고 선언할 수 있습니다.
  • 클래스 전용 프로토콜은 클래스 전용 프로토콜에서 설명한 대로 AnyObject로부터의 상속으로 표시됩니다.
  • 다음은 원래 Control Flow에 도입된 Snakes and Ladders 게임 버전입니다. 이 버전은 주사위 굴림에 Dice 인스턴스를 사용하도록 조정되었습니다.(DiceGame 프로토콜을 채택하기 위해,DiceGameDelegate에 진행 상황을 알리기위해)
    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)
        }
    }
  • 이 버전의 게임은 DiceGame 프로토콜을 채택한 SnakesAndLadders라는 클래스로 래핑됩니다.
  • 프로토콜을 준수하기 위해 gettable dice 속성과 play() 메서드를 제공합니다.
  • (dice 속성은 초기화 후 변경할 필요가 없기 때문에 상수 속성으로 선언되며 프로토콜은 gettable이어야만 합니다.)
  • Snakes and Ladders 게임 보드 설정은 클래스의 init() 이니셜라이저 내에서 이루어집니다.
  • 모든 게임 로직은 프로토콜의 필수 주사위 속성을 사용하여 주사위 굴림 값을 제공하는 프로토콜의 플레이 방법으로 이동됩니다.
  • 대리자는 게임을 플레이하는 데 필요하지 않기 때문에 대리자 속성은 선택적 DiceGameDelegate로 정의됩니다.
  • 옵셔널 타입이기 때문에 delegate 속성은 자동으로 초기값 nil로 설정됩니다.
  • 그후에 게임 인스턴스화에는 속성을 적절한 대리자로 설정할 수 있는 옵션이 있습니다. -
  • DiceGameDelegate 프로토콜은 클래스 전용이므로 참조 주기를 방지하기 위해 대리자를 약한 것으로 선언할 수 있습니다.
  • DiceGameDelegate는 게임 진행 상황을 추적하는 세 가지 방법을 제공합니다. 이 세 가지 메서드는 위의 play() 메서드 내에서 게임 논리에 통합되었으며 새 게임이 시작되거나 새 차례가 시작되거나 게임이 종료될 때 호출됩니다.
  • 대리자 속성은 선택적 DiceGameDelegate이므로 play() 메서드는 대리자에서 메서드를 호출할 때마다 선택적 연결을 사용합니다. 대리자 속성이 nil이면 이러한 대리자 호출은 오류 없이 정상적으로 실패합니다.대리자 속성이 nil이 아닌 경우 대리자 메서드가 호출되고 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")
        }
    }
  • DiceGameTracker는 DiceGameDelegate에 필요한 세 가지 방법을 모두 구현합니다. 게임의 회전 수를 추적하기 위해 이러한 방법을 사용합니다.
  • 게임이 시작되면 numberOfTurns 속성을 0으로 재설정하고, 새로운 턴이 시작될 때마다 값을 증가시키며, 게임이 끝나면 총 턴 수를 출력합니다.
  • 위에 표시된 gameDidStart(_:) 구현은 game 매개변수를 사용하여 플레이하려는 게임에 대한 몇 가지 소개 정보를 인쇄합니다.
  • 게임 매개변수에는 SnakesAndLadders가 아닌 DiceGame 유형이 있으므로 gameDidStart(_:)는 DiceGame 프로토콜의 일부로 구현된 메서드와 속성에만 액세스하고 사용할 수 있습니다.
  • 그러나 메서드는 여전히 형식 캐스팅을 사용하여 기본 인스턴스의 형식을 쿼리할 수 있습니다. 이 예에서 게임이 실제로 무대 뒤에서 SnakesAndLadders의 인스턴스인지 확인하고 그렇다면 적절한 메시지를 인쇄합니다.
  • gameDidStart(_:) 메서드는 전달된 게임 매개변수의 dice 속성에도 액세스합니다.
  • 게임은 DiceGame 프로토콜을 준수하는 것으로 알려져 있기 때문에 주사위 속성이 보장되므로 gameDidStart(_:) 메서드는 어떤 종류의 게임이 재생되는지에 관계없이 주사위의 측면 속성에 액세스하여 인쇄할 수 있습니다.
  • 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

  • 기존 유형의 소스 코드에 대한 액세스 권한이 없더라도 기존 유형을 extension하여 새 프로토콜을 채택하고 준수할 수 있습니다.
  • extension은 기존 유형에 새 속성, 메서드 및 첨자를 추가할 수 있으므로 프로토콜이 요구할 수 있는 요구 사항을 추가할 수 있습니다.

    유형의 기존 인스턴스는 해당 준수가 extension의 인스턴스 유형에 추가될 때 프로토콜을 자동으로 채택하고 준수합니다.

  • 예를 들어, TextRepresentable이라고 하는 이 프로토콜은 텍스트로 표현되는 방법이 있는 모든 유형으로 구현될 수 있습니다. 이것은 자신에 대한 설명이거나 현재 상태의 텍스트 버전일 수 있습니다.
    protocol TextRepresentable {
        var textualDescription: String { get }
    }
  • 위의 Dice 클래스는 TextRepresentable을 채택하고 준수하도록 extension될 수 있습니다.
    extension Dice: TextRepresentable {
        var textualDescription: String {
            return "A \(sides)-sided dice"
        }
    }
  • 이 extension은 Dice가 원래 구현에서 제공한 것과 똑같은 방식으로 새 프로토콜을 채택합니다.
  • 프로토콜 이름은 콜론으로 구분된 유형 이름 뒤에 제공되며 프로토콜의 모든 요구 사항 구현은 extension의 중괄호 안에 제공됩니다.
  • 이제 모든 Dice 인스턴스를 TextRepresentable로 처리할 수 있습니다.
    let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
    print(d12.textualDescription)
    // Prints "A 12-sided dice"
  • 마찬가지로 SnakesAndLadders 게임 클래스는 TextRepresentable 프로토콜을 채택하고 준수하도록 extension할 수 있습니다.
    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

  • 제네릭 형식은 형식의 제네릭 매개 변수가 프로토콜을 준수하는 경우와 같은 특정 조건에서만 프로토콜의 요구 사항을 충족할 수 있습니다.
  • 형식을 extension할 때 제약 조건을 나열하여 제네릭 형식을 조건부로 프로토콜을 준수하도록 만들 수 있습니다.
  • 일반 where 절을 작성하여 채택하는 프로토콜의 이름 뒤에 이러한 제약 조건을 작성하십시오. 일반 Where 절에 대한 자세한 내용은 일반 Where 절을 참조하십시오.
    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

  • 유형이 이미 프로토콜의 모든 요구 사항을 준수하지만 해당 프로토콜을 채택한다고 아직 명시하지 않은 경우 빈 extension자를 사용하여 프로토콜을 채택하도록 할 수 있습니다.
    struct Hamster {
        var name: String
        var textualDescription: String {
            return "A hamster named \(name)"
        }
    }
    extension Hamster: TextRepresentable {}
  • 이제 TextRepresentable이 필수 유형인 모든 곳에서 Hamster의 인스턴스를 사용할 수 있습니다.
    let simonTheHamster = Hamster(name: "Simon")
    let somethingTextRepresentable: TextRepresentable = simonTheHamster
    print(somethingTextRepresentable.textualDescription)
    // Prints "A hamster named Simon"

    유형은 요구 사항을 충족한다고 해서 프로토콜을 자동으로 채택하지 않습니다. 그들은 항상 프로토콜의 채택을 명시적으로 선언해야 합니다.

📌 Adopting a Protocol Using a Synthesized Implementation

  • Swift는 많은 간단한 경우에 Equatable, Hashable 및 Comparable에 대한 프로토콜 적합성을 자동으로 제공할 수 있습니다.

  • 이 합성된 구현을 사용하면 프로토콜 요구 사항을 직접 구현하기 위해 반복적인 상용구 코드를 작성할 필요가 없습니다.

  • Swift는 다음 종류의 사용자 정의 유형에 대해 Equatable의 합성 구현을 제공합니다.

    • Equatable 프로토콜을 준수하는 stored properties만 있는 구조
    • Equatable 프로토콜을 준수하는 associated types만 있는 열거
    • associated types이 없는 열거
  • ==의 합성된 구현을 수신하려면 == 연산자를 직접 구현하지 않고 원래 선언이 포함된 파일에서 Equatable에 대한 준수를 선언합니다. Equatable 프로토콜은 !=의 기본 구현을 제공합니다.

  • 아래 예제는 Vector2D 구조와 유사한 3차원 위치 벡터(x, y, z)에 대한 Vector3D 구조를 정의합니다.

  • x, y 및 z 속성은 모두 Equatable 유형이므로 Vector3D는 등가 연산자의 합성된 구현을 수신합니다.

    struct Vector3D: Equatable {
        var x = 0.0, y = 0.0, z = 0.0
    }
    
    let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
    let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
    if twoThreeFour == anotherTwoThreeFour {
        print("These two vectors are also equivalent.")
    }
    // Prints "These two vectors are also equivalent."
  • Swift는 다음과 같은 종류의 사용자 정의 유형에 대해 Hashable의 합성 구현을 제공합니다.

    • Hashable 프로토콜을 준수하는 stored properties만 있는 구조
    • Hashable 프로토콜을 준수하는 associated types만 있는 열거형
    • associated types이 없는 열거
  • hash(into:)의 합성된 구현을 수신하려면 hash(into:) 메서드를 직접 구현하지 않고 원래 선언이 포함된 파일에서 Hashable 준수를 선언합니다.

  • Swift는 원시 값이 없는 열거형에 대해 Comparable의 합성된 구현을 제공합니다.

  • 열거형에 연결된 유형이 있는 경우 모두 Comparable 프로토콜을 준수해야 합니다.

  • <의 합성된 구현을 수신하려면 < 연산자를 직접 구현하지 않고 원래 열거 선언이 포함된 파일에서 Comparable에 대한 준수를 선언합니다.

  • Comparable 프로토콜의 기본 구현인 <=, > 및 >=는 나머지 비교 연산자를 제공합니다.

  • 아래 예는 초보자, 중급자 및 전문가를 위한 사례로 SkillLevel 열거를 정의합니다. 전문가는 보유한 별의 수에 따라 추가로 순위가 매겨집니다.

    enum SkillLevel: Comparable {
        case beginner
        case intermediate
        case expert(stars: Int)
    }
    var levels = [SkillLevel.intermediate, SkillLevel.beginner,
                  SkillLevel.expert(stars: 5), SkillLevel.expert(stars: 3)]
    for level in levels.sorted() {
        print(level)
    }
    // Prints "beginner"
    // Prints "intermediate"
    // Prints "expert(stars: 3)"
    // Prints "expert(stars: 5)"

📌 Collections of Protocol Types

  • 프로토콜은 유형으로서의 프로토콜에서 언급한 것처럼 array or a dictionary과 같은 컬렉션에 저장할 유형으로 사용할 수 있습니다. 이 예에서는 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
  • 사물 상수는 TextRepresentable 유형입니다. Dice, DiceGame 또는 Hamster 유형이 아닙니다. 이면의 실제 인스턴스가 이러한 유형 중 하나라도 마찬가지입니다.
  • 그럼에도 불구하고 TextRepresentable 유형이고 TextRepresentable인 모든 항목에는 textualDescription 속성이 있는 것으로 알려져 있으므로 루프를 통해 매번 thing.textualDescription에 액세스하는 것이 안전합니다.

📌 Protocol Inheritance

  • 프로토콜은 하나 이상의 다른 프로토콜을 상속할 수 있으며 상속된 요구 사항에 추가 요구 사항을 추가할 수 있습니다.
  • 프로토콜 상속 구문은 클래스 상속 구문과 유사하지만 여러 상속된 프로토콜을 쉼표로 구분하여 나열하는 옵션이 있습니다.
    protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
        // protocol definition goes here
    }
  • 다음은 위에서 TextRepresentable 프로토콜을 상속하는 프로토콜의 예입니다.
    protocol PrettyTextRepresentable: TextRepresentable {
        var prettyTextualDescription: String { get }
    }
  • 이 예에서는 TextRepresentable에서 상속되는 새 프로토콜 PrettyTextRepresentable을 정의합니다.
  • PrettyTextRepresentable을 채택하는 모든 것은 TextRepresentable에 의해 시행되는 모든 요구 사항과 PrettyTextRepresentable에 의해 시행되는 추가 요구 사항을 충족해야 합니다.
  • 이 예에서 PrettyTextRepresentable은 String을 반환하는 prettyTextualDescription이라는 gettable 속성을 제공하기 위한 단일 요구 사항을 추가합니다.
  • SnakesAndLadders 클래스는 PrettyTextRepresentable을 채택하고 준수하도록 extension될 수 있습니다.
    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
        }
    }
  • 이 extension은 PrettyTextRepresentable 프로토콜을 채택하고 SnakesAndLadders 유형에 대한 prettyTextualDescription 속성의 구현을 제공한다고 명시합니다.
  • PrettyTextRepresentable은 무엇이든 TextRepresentable이어야 하므로 PrettyTextualDescription 구현은 TextRepresentable 프로토콜의 textualDescription 속성에 액세스하여 출력 문자열을 시작하는 것으로 시작됩니다.
  • 콜론과 줄 바꿈을 추가하고 이것을 pretty text representation의 시작으로 사용합니다. 그런 다음 보드 정사각형 배열을 반복하고 각 정사각형의 내용을 나타내는 기하학적 모양을 추가합니다.
    • 사각형의 값이 0보다 크면 사다리의 밑변이며 ▲로 표시됩니다.
    • 사각형의 값이 0보다 작으면 뱀의 머리이며 ▼로 표시됩니다.
    • 그렇지 않으면 정사각형의 값은 0이고 ○로 표시되는 "자유로운" 정사각형입니다.
  • 이제 prettyTextualDescription 속성을 사용하여 SnakesAndLadders 인스턴스의 pretty text representation을 인쇄할 수 있습니다.
    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을 채택하려고 하는 구조체 또는 열거형 정의를 작성하는 것은 컴파일 타임 오류입니다.

    해당 프로토콜의 요구 사항에 의해 정의된 동작이 일치하는 유형에 값 의미가 아닌 참조 의미가 있다고 가정하거나 요구할 때 클래스 전용 프로토콜을 사용합니다.
    참조 및 값 의미 체계에 대한 자세한 내용은 구조 및 열거형은 값 형식이고 클래스는 참조 형식입니다.

📌 Protocol Composition

  • 동시에 여러 프로토콜을 준수하는 형식을 요구하는 것이 유용할 수 있습니다.

  • 프로토콜 구성을 사용하여 여러 프로토콜을 단일 요구 사항으로 결합할 수 있습니다.

  • 프로토콜 구성은 구성에 있는 모든 프로토콜의 요구 사항이 결합된 임시 로컬 프로토콜을 정의한 것처럼 작동합니다.

  • 프로토콜 구성은 새로운 프로토콜 유형을 정의하지 않습니다.

  • 프로토콜 구성은 SomeProtocol 및 AnotherProtocol 형식입니다. 앰퍼샌드(&)로 구분하여 필요한 만큼 프로토콜을 나열할 수 있습니다. 나

  • 프로토콜 목록 외에도 프로토콜 구성에는 필수 수퍼클래스를 지정하는 데 사용할 수 있는 하나의 클래스 유형이 포함될 수 있습니다.

  • 다음은 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 프로토콜에는 name이라는 gettable String 속성에 대한 단일 요구 사항이 있습니다.

  • Aged 프로토콜에는 age라는 gettable Int 속성에 대한 단일 요구 사항이 있습니다. 두 프로토콜 모두 Person이라는 구조에 의해 채택됩니다.

  • 이 예제는 또한 WishHappyBirthday(to:) 함수를 정의합니다. celebrator 매개변수의 유형은 Named & Aged이며, 이는 "Named 및 Aged 프로토콜을 모두 준수하는 모든 유형"을 의미합니다.

  • 두 가지 필수 프로토콜을 모두 준수하는 한 어떤 특정 유형이 함수에 전달되는지는 중요하지 않습니다.

  • 그런 다음 예제에서는 birthdayPerson이라는 새 Person 인스턴스를 만들고 이 새 인스턴스를 WishHappyBirthday(to:) 함수에 전달합니다.

  • Person이 두 프로토콜을 모두 준수하기 때문에 이 호출은 유효하고 WishHappyBirthday(to:) 함수는 생일 인사말을 인쇄할 수 있습니다.

  • 다음은 이전 예제의 Named 프로토콜을 Location 클래스와 결합한 예제입니다.

    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 프로토콜을 준수하는 모든 유형"을 의미합니다. 이 경우 시는 두 가지 요건을 모두 충족합니다.

  • Person이 Location의 하위 클래스가 아니기 때문에 beginConcert(in:) 함수에 birthdayPerson을 전달하는 것은 유효하지 않습니다.

  • 마찬가지로, Named 프로토콜을 따르지 않는 Location 의 하위 클래스를 만든 경우 해당 유형의 인스턴스로 beginConcert(in:) 를 호출하는 것도 유효하지 않습니다.

📌 Checking for Protocol Conformance

  • 형식 캐스팅에 설명된 is 및 as 연산자를 사용하여 프로토콜 적합성을 확인하고 특정 프로토콜로 캐스팅할 수 있습니다.
  • 프로토콜 확인 및 캐스팅은 유형 확인 및 캐스팅과 정확히 동일한 구문을 따릅니다.
    • is 연산자는 인스턴스가 프로토콜을 준수하면 true를 반환하고 그렇지 않으면 false를 반환합니다.
    • as? 다운캐스트 연산자의 버전은 프로토콜 유형의 선택적 값을 반환하고 인스턴스가 해당 프로토콜을 준수하지 않는 경우 이 값은 nil입니다.
    • as! 다운 캐스트 연산자의 버전은 다운 캐스트를 프로토콜 유형으로 강제 실행하고 다운 캐스트가 성공하지 못하면 런타임 오류를 트리거합니다.
  • 이 예는 Area라는 gettable Double 속성의 단일 속성 요구 사항을 사용하여 HasArea라는 프로토콜을 정의합니다.
    protocol HasArea {
        var area: Double { get }
    }
  • 다음은 두 가지 클래스인 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 클래스는 저장된 반경 속성을 기반으로 면적 속성 요구 사항을 계산 속성으로 구현합니다.
  • Country 클래스는 영역 요구 사항을 저장 속성으로 직접 구현합니다.
  • 두 클래스 모두 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)
    ]
  • 개체 배열은 반경이 2단위인 Circle 인스턴스를 포함하는 배열 리터럴로 초기화됩니다. 영국의 표면적(제곱 킬로미터)으로 초기화된 Country 인스턴스. 네 개의 다리가 있는 Animal 인스턴스.
  • 이제 개체 배열을 반복할 수 있으며 배열의 각 개체를 검사하여 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? operator는 objectWithArea라는 상수에 대한 선택적 바인딩으로 래핑되지 않습니다.
  • objectWithArea 상수는 HasArea 유형으로 알려져 있으므로 해당 area 속성에 액세스하고 유형이 안전한 방식으로 인쇄할 수 있습니다.
  • 기본 개체는 캐스팅 프로세스에 의해 변경되지 않습니다. 그들은 계속해서 원, 국가, 동물입니다.
  • 그러나 objectWithArea 상수에 저장되는 시점에서는 HasArea 유형으로만 알려져 있으므로 해당 area 속성만 액세스할 수 있습니다.

📌 Optional Protocol Requirements

  • 프로토콜에 대한 선택적 요구 사항을 정의할 수 있습니다.
  • 이러한 요구 사항은 프로토콜을 준수하는 유형으로 구현될 필요가 없습니다.
  • 선택적 요구 사항은 프로토콜 정의의 일부로 선택적 수정자가 접두사로 붙습니다.
  • Objective-C와 상호 운용되는 코드를 작성할 수 있도록 선택적 요구 사항을 사용할 수 있습니다.
  • 프로토콜과 선택적 요구 사항은 모두 @objc 속성으로 표시되어야 합니다.
  • @objc 프로토콜은 Objective-C 클래스 또는 다른 @objc 클래스에서 상속받은 클래스에서만 채택할 수 있습니다.
  • 그것들은 구조나 열거에 의해 채택될 수 없습니다.
  • 선택적 요구 사항에서 메서드나 속성을 사용하면 해당 유형이 자동으로 선택 사항이 됩니다.
  • 예를 들어 (Int) -> String 유형의 메서드는 ((Int) -> String)?이 됩니다.
  • 전체 함수 유형은 메서드의 반환 값이 아니라 옵셔널로 래핑됩니다.
  • 선택적 프로토콜 요구사항은 요구사항이 프로토콜을 준수하는 유형에 의해 구현되지 않았을 가능성을 설명하기 위해 선택적 연결을 사용하여 호출할 수 있습니다.
  • someOptionalMethod?(someArgument)와 같이 호출될 때 메서드 이름 뒤에 물음표를 작성하여 선택적 메서드의 구현을 확인합니다. 선택적 연결에 대한 자세한 내용은 선택적 연결을 참조하십시오.
  • 다음 예제에서는 외부 데이터 소스를 사용하여 증가량을 제공하는 Counter라는 정수 계산 클래스를 정의합니다.
  • 이 데이터 소스는 두 가지 선택적 요구 사항이 있는 CounterDataSource 프로토콜에 의해 정의됩니다.
    @objc protocol CounterDataSource {
        @objc optional func increment(forCount count: Int) -> Int
        @objc optional var fixedIncrement: Int { get }
    }
  • CounterDataSource 프로토콜은 increment(forCount:)라는 선택적 메서드 요구 사항과 fixedIncrement라는 선택적 속성 요구 사항을 정의합니다.
  • 이러한 요구 사항은 데이터 소스가 Counter 인스턴스에 적절한 증분량을 제공하는 두 가지 다른 방법을 정의합니다.

    엄밀히 말하면 프로토콜 요구 사항을 구현하지 않고도 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:) 호출을 시도하고 현재 카운트 값을 메서드의 단일 인수로 전달합니다.
  • 여기에서는 두 가지 수준의 선택적 연결이 작동하고 있습니다.
  • 첫째, dataSource가 nil일 수 있으므로 dataSource의 이름 뒤에 물음표가 있어 dataSource가 nil이 아닌 경우에만 increment(forCount:)를 호출해야 함을 나타냅니다.
  • 둘째, dataSource가 존재하더라도 선택적 요구 사항이기 때문에 increment(forCount:)를 구현한다는 보장이 없습니다.
  • 여기서 increment(forCount:) 가 구현되지 않을 가능성도 선택적 연결로 처리됩니다.
  • increment(forCount:) 호출은 increment(forCount:)가 존재하는 경우, 즉 nil이 아닌 경우에만 발생합니다.
  • 이것이 increment(forCount:) 가 이름 뒤에 물음표와 함께 쓰여지는 이유입니다.
  • 이 두 가지 이유로 인해 increment(forCount:) 호출이 실패할 수 있으므로 호출은 선택적 Int 값을 반환합니다.
  • increment(forCount:)가 CounterDataSource의 정의에서 선택 사항이 아닌 Int 값을 반환하는 것으로 정의된 경우에도 마찬가지입니다.
  • 두 개의 선택적 연결 작업이 차례로 있지만 결과는 여전히 하나의 선택 사항으로 래핑됩니다.
  • increment(forCount:)를 호출한 후 반환되는 선택적 Int는 선택적 바인딩을 사용하여 amount라는 상수로 래핑 해제됩니다.
  • 선택적 Int에 값이 포함되어 있는 경우(즉, 대리자와 메서드가 모두 존재하고 메서드가 값을 반환한 경우) 래핑되지 않은 금액이 저장된 count 속성에 추가되고 증가가 완료됩니다.
  • dataSource가 nil이거나 데이터 소스가 increment(forCount:)를 구현하지 않기 때문에 increment(forCount:) 메서드에서 값을 검색할 수 없는 경우 increment() 메서드는 다음에서 값 검색을 시도합니다. 대신 데이터 소스의 fixedIncrement 속성을 사용합니다.
  • fixedIncrement 속성은 선택적 요구 사항이기도 하므로 fixedIncrement가 CounterDataSource 프로토콜 정의의 일부로 선택적이 아닌 Int 속성으로 정의되더라도 해당 값은 선택적 Int 값입니다.
  • 다음은 데이터 소스가 쿼리될 때마다 상수 값 3을 반환하는 간단한 CounterDataSource 구현입니다. 이는 선택적 fixedIncrement 속성 요구 사항을 구현하여 수행합니다.
    class ThreeSource: NSObject, CounterDataSource {
        let fixedIncrement = 3
    }
  • ThreeSource의 인스턴스를 새 Counter 인스턴스의 데이터 소스로 사용할 수 있습니다.
    var counter = Counter()
    counter.dataSource = ThreeSource()
    for _ in 1...4 {
        counter.increment()
        print(counter.count)
    }
    // 3
    // 6
    // 9
    // 12
  • 위의 코드는 새로운 Counter 인스턴스를 생성합니다. 데이터 소스를 새 ThreeSource 인스턴스로 설정합니다. 카운터의 increment() 메서드를 네 번 호출합니다.
  • 예상대로 카운터의 count 속성은 increment()가 호출될 때마다 3씩 증가합니다.
  • 다음은 Counter 인스턴스가 현재 카운트 값에서 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을 반환하여 더 이상 계산이 수행되지 않아야 함을 나타냅니다.
  • 기존 Counter 인스턴스와 함께 TowardsZeroSource의 인스턴스를 사용하여 -4에서 0까지 계산할 수 있습니다. 카운터가 0에 도달하면 더 이상 계산이 수행되지 않습니다.
    counter.count = -4
    counter.dataSource = TowardsZeroSource()
    for _ in 1...5 {
        counter.increment()
        print(counter.count)
    }
    // -3
    // -2
    // -1
    // 0
    // 0

📌 Protocol Extensions

  • 프로토콜은 메소드, 이니셜라이저, 첨자 및 계산된 속성 구현을 준수 유형에 제공하도록 extension될 수 있습니다.
  • 이를 통해 각 유형의 개별 적합성 또는 전역 기능이 아닌 프로토콜 자체에 대한 동작을 정의할 수 있습니다.
  • 예를 들어 RandomNumberGenerator 프로토콜은 randomBool() 메서드를 제공하도록 extension될 수 있습니다. 이 메서드는 필수 random() 메서드의 결과를 사용하여 임의의 Bool 값을 반환합니다.
    extension RandomNumberGenerator {
        func randomBool() -> Bool {
            return random() > 0.5
        }
    }
  • 프로토콜에 extension을 생성함으로써 모든 준수 유형은 추가 수정 없이 자동으로 이 메소드 구현을 얻습니다.
    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"
  • 프로토콜 extension은 준수하는 유형에 구현을 추가할 수 있지만 프로토콜을 extension하거나 다른 프로토콜에서 상속할 수는 없습니다.
  • 프로토콜 상속은 항상 프로토콜 선언 자체에 지정됩니다.

📍 Providing Default Implementations

  • 프로토콜 extension을 사용하여 해당 프로토콜의 모든 메서드 또는 계산된 속성 요구 사항에 대한 기본 구현을 제공할 수 있습니다.
  • 준수 형식이 필수 메서드 또는 속성의 자체 구현을 제공하는 경우 extension에서 제공하는 구현 대신 해당 구현이 사용됩니다.

    extension에 의해 제공되는 기본 구현의 프로토콜 요구 사항은 선택적 프로토콜 요구 사항과 다릅니다.
    준수하는 유형이 자체 구현을 제공할 필요는 없지만 기본 구현이 있는 요구 사항은 선택적 연결 없이 호출할 수 있습니다.

  • 예를 들어, TextRepresentable 프로토콜을 상속하는 PrettyTextRepresentable 프로토콜은 필수 prettyTextualDescription 속성의 기본 구현을 제공하여 단순히 textualDescription 속성에 액세스한 결과를 반환할 수 있습니다.
    extension PrettyTextRepresentable  {
        var prettyTextualDescription: String {
            return textualDescription
        }
    }

📍 Adding Constraints to Protocol Extensions

  • 프로토콜 extension을 정의할 때 해당 유형이 extension의 메서드와 속성을 사용할 수 있기 전에 충족해야 하는 제약 조건을 지정할 수 있습니다.
  • 일반 where 절을 작성하여 extension하려는 프로토콜의 이름 뒤에 이러한 제약 조건을 작성합니다.
profile
I Am Groot

0개의 댓글