Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

프로토콜 #58

Open
simoniful opened this issue Nov 2, 2022 · 0 comments
Open

프로토콜 #58

simoniful opened this issue Nov 2, 2022 · 0 comments

Comments

@simoniful
Copy link
Owner

simoniful commented Nov 2, 2022

프로토콜(protocol)은 특정 작업이나 기능에 적합한 메서드, 프로퍼티 및 기타 요구 사항의 청사진을 정의한다
그런 다음 프로토콜은 클래스, 구조체 또는 열거형에 의해 채택되어 그러한 요구 사항의 실제 구현을 제공할 수 있다
프로토콜의 요구 사항을 충족하는 모든 타입은 해당 프로토콜에 부합한다고 한다

프로토콜을 따르는 타입이 구현해야 하는 요구 사항을 지정하는 것 외에도
프로토콜을 확장하여 이러한 요구 사항 중 일부를 구현하거나
적합한 타입이 활용할 수 있는 추가 기능을 구현할 수 있다

프로토콜 문법

프로토콜은 클래스, 구조체 및 열거향과 매우 유사한 방식으로 정의한다

protocol SomeProtocol {
    // protocol definition goes here
}

커스텀 타입은 정의의 일부로서
프로토콜 이름을 타입 이름 뒤에 콜론으로 구분하여 배치하여 특정 프로토콜을 채택한다고 명시한다
여러 프로토콜을 나열할 수 있으며 쉼표로 구분된다

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

클래스에 부모 클래스가 있는 경우 클래스 이름을 선택한 프로토콜 앞에 나열한 후 쉼표를 붙인다

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

프로퍼티 요구 사항

프로토콜은 인스턴스 프로퍼티 또는 타입 프로퍼티를
특정 이름과 형식으로 제공하기 위해 모든 적합한 타입을 요구할 수 있다
프로토콜은 프로퍼티가 저장 프로퍼티인지 계산 프로퍼티인지 지정하지 않고
필요한 프로퍼티 이름 및 타입만 지정한다
프로토콜은 각 프로퍼티가 gettable이어야 하는지, gettable 및 settable이어야 하는지 여부를 지정한다

프로토콜에서 프로퍼티가 gettable 및 settable 하는 경우,
해당 프로퍼티 요구 사항은 상수로 저장된 프로퍼티나 읽기 전용 계산 프로퍼티로는 만족시킬 수 없다

프로토콜에서 프로퍼티가 gettable 하는 경우,
해당 프로퍼티 요구 사항은 모든 종류의 프로퍼티로 충족될 수 있으며,
자신의 코드에 유용하다면, settable에도 유효하다

프로퍼티 요구 사항은 항상 변수 속성으로 선언되고 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 }
}

FullyNamed 프로토콜은 풀네임을 제공하기 위해 준수해야하는 타입을 요구한다
프로토콜은 다른 준수하는 타입의 특성에 대한 다른 것을 지정하지는 않는다
프로토콜은 타입에게 fullName을 제공할 수 있어야 한다는 것만 정해준다
프로토콜을 준수하는 모든 FullyNamed 타입에는 String 타입의 fullName이라는 gettable 인스턴스 프로퍼티가 존재해야한다

다음은 FullyNamed 프로토콜을 채택하고 준수하는 struct에 대한 예시다

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

위 코드는 특정 이름을 가진 사람을 나타내는 Person이라는 구조체를 정의한다
정의 첫 줄에서 FullyNamed 프로토콜을 채택한다고 작성했다

각각의 Person 인스턴스는 String 타입의 fullName이라는 단일 저장 프로퍼티를 갖는다
이는 FullyNamed 프로토콜의 단일 요구 사항과 일치하며
Person이 프로토콜을 올바르게 준수한다는 의미다
만약 프로토콜의 요구 사항을 충족하지 않는다면 컴파일 에러가 발생한다

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 클래스 인스턴스는 의무적인 name과 옵션인 prefix를 저장한다
prefix 값이 존재하면, fullName 프로퍼티가, name앞에 이를 붙여 우주선 전체 이름을 생성한다

메서드 요구 사항

프로토콜은 자신을 채택한 타입에게 특정 인스턴스나 타입 메서드를 요구할 수도 있다
이러한 메서드는 일반적인 인스턴스 메서드, 타입 메서드와 같은 방식으로 프로토콜 내부에 작성되지만 중괄호나 메서드 본문은 없다
일반적인 메서드와 동일한 규칙에 따라 가변 매개변수는 허용되지만,
프로토콜 정의 내에서는 메서드 매개변수에 기본 값을 지정할 수는 없다

타입 프로퍼티와 마찬가지로, 프로토콜에서 타입 메서드 요구 사항을 정의할 때는 항상 static 키워드 접두사를 붙인다
이러한 규칙은 클래스에서 프로토콜을 채택한 경우, 타입 메서드 요구 사항으로
class 또는 static 키워드를 사용하는 경우에도 적용된다

protocol SomeProtocol {
    static func someTypeMethod()
}

다음 예제는 단일 인스턴스 메서드 요구 사항을 가진 프로토콜을 정의한다

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator 프로토콜은 해당 프로토콜을 준수하는 어떤 타입이든
random이라는 인스턴스 메서드를 가지기를 요구하는데,
이는 호출할 때마다 double 값을 반환한다
프로토콜에서 지정하지 않긴 하지만, 이 값은 0.0에서 1.0까지의 값이라 가정한다 (1.0 미포함)

RandomNumberGenerator 프로토콜은 각각의 난수 발생 방법에 대해선 어떤 가정도 하지 않는다
생성자가 새로운 난수를 생성하는 표준 방법을 제공하기만 하면 된다

다음으로는 RandomNumberGenerator 프로토콜을 채택하고 준수하는 클래스를 구현한 예제다
해당 클래스는 linear congruential generator로 알려진 pseudorandom number generator 알고리즘을 구현한다

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 메서드 요구 사항

메서드가 자신이 속한 인스턴스를 수정(변경)하는 것이 필요할 때가 있다
값 타입(구조체와 열거형)의 인스턴스 메서드이면,
메서드의 func 키워드 앞에 mutating 키워드를 작성하여
메서드가 자신이 속한 인스턴스 및 그 인스턴스에 있는 어떤 프로퍼티의 수정이라도 허용한다고 지시할 수 있다

프로토콜을 채택한 어떤 타입의 인스턴스든 변경할 의도로 프로토콜 인스턴스 메서드 요구 사항을 정의하는 거라면,
프로토콜 정의 부분에서 mutating 키워드로 메서드를 표시한다
이는 구조체 및 열거형이 프로토콜을 채택해서 해당 메서드 요구 사항을 만족할 수 있게 한다

프로토콜이 요구하는 인스턴스 메서드에 mutating 을 표시하더라도
클래스는 해당 메서드를 구현할 때 mutating 키워드를 사용할 필요가 없다
mutating은 구조체, 열거형의 경우에만 사용한다

다음은 하나의 인스턴스 메서드를 요구하는 Togglable 프로토콜이다
이름에서 알 수 있듯, toggle() 메서드는 일반적으로 해당 타입의 프로퍼티를 수정해서 해당 타입의 상태를 전환하기 위한 메서드다

toggle() 메서드는 Togglable 프로토콜 정의의 일부로 mutating 키워드로 표시되어
메서드가 호출될 때 해당 인스턴스의 상태를 변경할 수 있다는 것을 알려준다

protocol Togglable {
    mutating func toggle()
}

구조체나 열거형이 Togglable 프로토콜을 구현하면,
mutating으로도 표시한 toggle()메서드를 구현함으로써 그 구조체나 열거형이 프로토콜을 준수할 수 있다

아래 예제는 OnOffSwitch 라는 열거형을 정의한다
해당 열거형은 on 과 off 열거형 case 로 표시한 두 상태 사이를 전환한다
Togglable 프로토콜의 요구 사항과 일치하도록, 열거형의 toggle 구현에 mutating을 표시했다

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)
}

1) 프로토콜 이니셜라이저 요구 사항의 클래스 구현 (required 키워드)

프로토콜 이니셜라이저 요구 사항은 해당 프로토콜을 준수하는 클래스에서
지정 이니셜라이저나 편리한 이니셜라이저 어느 것으로든 구현할 수 있다
두 경우 모두 required 키워드를 표시 해야한다

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

required 이니셜라이저를 사용하면 클래스의 모든 자식 클래스도 해당 이니셜라이저를 명시적으로 혹은 상속된 구현을 통해 구현하므로,
부모 클래스가 채택 중인 프로토콜을 준수하도록 할 수 있다

reqiured 이니셜라이저에 대한 더 많은 정보는 Required Initializers페이지에서 확인 가능하다

final 클래스는 서브클래싱 할 수 없기 때문에
final 클래스는 프로토콜이 요구하는 이니셜라이저에 required를 필요할 표시가 없다

하위 클래스가 상위 클래스의 지정 이니셜라이저를 오버라이드 하면서
프로토콜과 일치하는 이니셜라이저 요구 사항도 구현한다면,
이니셜라이저 구현에 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
    }
}

2) 실패가능한 이니셜라이저 요구 사항

Failable Initializers 페이지에서 정의한 것 처럼, 프로토콜은 실패 가능한 이니셜라이저 요구 사항으로 정의할 수 있다

프로토콜의 실패 가능한 이니셜라이저 요구 사항은 자신을 채택한 타입이 실패 가능, 실패 불가능한 이니셜라이저가 있을 때 만족된다
실패할 수 없는 이니셜라이저를 요구하는 프로토콜을 채택한 경우,
실패할 수 없는 이니셜라이저 또는 암시적으로 래핑되지 않는 실패 가능한 이니셜라이저로 만족시킬 수 있다

타입으로써의 프로토콜

프로토콜은 자체적으로 어떤 기능을 구현하지는 않는다
하지만, 코딩을 할 때 프로토콜은 하나의 타입처럼 사용할 수 있다
프로토콜을 타입처럼 사용하는 것을 existential type 이라고 부르는데
이는 ‘there exists a type T such that T conforms to the protocol’ 라는 문구에서 유래되었다

다음을 포함하여 다른 타입이 허용되는 여러 위치에서 프로토콜을 사용할 수 있다

  • 함수, 메서드, 생성자의 매개변수 타입 또는 반환 타입
  • 상수, 변수, 프로퍼티의 타입
  • 배열, 딕셔너리 등의 항목 타입

프로토콜이 타입이므로 (앞에서 봤던 FullyNamed, RandomNumberGenerator 처럼),
Swift의 다른 타입들 (Int, String 등) 처럼 이름이 대문자로 시작한다

프로토콜을 타입을 사용하는 예제를 살펴보자

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 프로토콜을 채택하는 모든 타입의 인스턴스를 사용할 수 있다
인스턴스가 RandomNumberGenerator 프로토콜을 채택한다는 점을 제외하고 해당 프로퍼티에 적용될 인스턴스 요구 사항은 없다

generator의 타입이 RandomNumberGenerator이므로,
Dice 클래스 내부 코드는 프로토콜이 준수하는 모든 인스턴스에 적용되는 방식으로만 generator을 사용할 수 있다
즉, generator의 기본 타입에 정의된 메서드나 프로퍼티를 사용할 수 없다
하지만, 부모 클래스를 자식 클래스로 다운 캐스팅하는 것처럼,
프로토콜 타입을 기본 타입으로 다운 캐스팅해서 사용할 수는 있다

Dice의 초기상태를 설정하기 위한 이니셜라이저가 있다
이니셜라이저에는 RandomNumberGenerator 타입의 generator 라는 매개변수가 존재하며
새로운 Dice 인스턴스를 생성할 때 generator 매개변수에는 RandomNumberGenerator를 채택한 모든 인스턴스 값을 전달할 수 있다

Dice는 1과 sided 사이의 정수값을 반환하는 인스턴스 메서드 roll()이 있다
해당 메서드는 generator의 random() 메서드를 호출해서
0.0~1.0 사이의 새로운 난수를 생성해주고, 난수를 사용해서 범위 내의 값을 만들어준다
generator는 RandomNumberGenerator를 채택했기 때문에 random() 메서드가 반드시 존재하게 된다

위의 Dice 클래스를 사용하여 LinearCongruentialGenerator 인스턴스를 generator로 구성해서 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은 클래스나 구조체가 일부 책임을 다른 타입의 인스턴스에 넘길 수 있도록 하는 디자인 패턴이다
Delegation 패턴은 위임된 책임을 캡슐화하는 프로토콜을 정의하여 구현된다
따라서, 프로토콜을 채택한 타입이 위임된 기능을 제공하도록 보장된다
Delegation을 사용하여 특정 작업에 응답하거나, 해당 소스의 기본 타입을 알 필요 없이 외부 소스에서 데이터를 검색할 수 있다

아래 예시는 주사위 기반 보드게임에서 사용하기 위한 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 프로토콜은 주사위와 엮인 어떤 게임이든 채택할 수 있는 프로토콜이다.

DiceGameDelegate프로토콜을 채택하면 DiceGame의 진행상황을 추적할 수 있다
강한 참조 사이클을 방지하기 위해서 delegate는 weak 참조로 선언된다
weak 참조에 대한 설명은 Strong Reference Cycles Between Class Instances 페이지에서 볼 수 있다

잠시 뒤에 나오는 class-only 프로토콜 섹션에서 나올 내용을 미리 보면,
클래스 전용 프로토콜은 AnyObject를 상속하는 것으로 표시하면 된다
프로토콜을 클래스 전용으로 표시하면 이번 챕터의 뒷 부분에 나오는 SnakesAndLadders 클래스의 delegate처럼 weak 참조로 선언할 수 있다

다음은 원래 Control Flow에서 소개된 snake and ladders 게임 버전이다
이 버전은 DiceGame 프로토콜을 채택하고 DiceGameDeleagate에게 진행상황을 알리기 위해 dice-roll에 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)
    }
}

SnakesAndLadders 게임에 대한 설명은 Break 페이지를 참조하면 된다

위 버전의 게임은 DiceGame 프로토콜을 채택한 SnakesAndLadders 클래스로 래핑된다
이는 프로토콜 준수를 위해서 gettable dice 프로퍼티와 play() 메서드를 제공한다
dice 프로퍼티는 생성 후 변경할 필요가 없기 때문에 let으로 선언되며, 프로토콜은 gettable만 요구하므로 문제가 없다

SnakesAndLadders 게임의 보드의 준비는 클래스의 init()에서 이루어진다
모든 게임 로직은 프로토콜의 요구 사항인 dice 프로퍼티를 사용해서
주사위 역할을 제공하는 프로토콜의 play() 메서드로 진행된다

delegate 프로퍼티는 옵셔널 DiceGameDelegate 타입으로 정의되어 있다
옵셔널인 이유는 게임을 플레이하기 위해 delegate는 필요하지 않기 때문이다
옵셔널 타입이기 때문에 delegate 프로퍼티의 초기값은 nil 이다
그런 뒤 게임을 생성한 곳에서 적절한 delegate값을 설정할 수 있다
DiceGameDelegate 프로토콜은 클래스 전용이므로 참조 주기를 방지하기 위해 weak 으로 선언할 수 있는 것도 볼 수 있다

DiceGameDelegatge는 게임 진행상황을 추적하는 세 가지 메서드를 제공한다
이러한 세 가지 메서드는 위의 play() 메서드 내에서 게임 논리에 통합되어
새로운 게임이 시작되거나 새 차례가 오거나 게임이 종료될 때 호출된다

delegate프로퍼티는 옵셔널 DiceGameDelegate이므로,
play() 메서드는 delegate 메서드를 호출할 때마다 옵셔널 체이닝을 사용한다
delegate 프로퍼티가 nil 이라면 이러한 호출은 정상적으로 실패하게 되고,
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")
    }
}

DiceGagmeTracker는 DiceGameDelegate에 필요한 세 가지 메서드를 모두 구현해뒀다
이런 메서드를 사용해서 게임에서 수행한 차례의 횟수를 알아낸다
게임이 시작되면 numberOfTurns 프로퍼티를 0으로 재설정하고,
새로운 턴이 시작되면 값을 증가시키며 게임이 끝나면 총 턴 수를 출력한다

위 코드에 있는 gameDidStart(: ) 메서드는 game 매개변수를 사용하여
플레이하려는 게임에 대한 일부 소개 정보를 print한다
game 매개변수는 SnakesAndLadders가 아닌 DiceGame타입이므로,
gameDidStart(
: )는 DiceGame 프로토콜의 일부로 구현된 메서드와 프로퍼티에만 접근하고 사용할 수 있다
그러나, 메서드는 여전히 타입 캐스팅을 사용하여 기본 인스턴스 타입을 쿼리할 수도 있다
예제에서는 back 단에서 game 매개변수가 SnakeAndLadders 인스턴스인지 확인하고 그렇다면 적절한 메시지를 print한다

gameDidSet(: )메서드는 전달된 game 매개변수의 dice 프로퍼티에도 접근한다
game 은 DiceGame 프로토콜을 준수하므로 dice 프로퍼티가 반드시 존재하기 때문에
gameDidStart(_: ) 메서드는 어떤 종류의 게임이 플레이되는지 상관 없이 dice의 sides 프로퍼티에 접근하여 print할 수 있다

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

익스텐션으로 프로토콜 준수 추가하기

기존 타입의 소스 코드에서 접근할 수 없는 경우, 기존 타입의 익스텐션을 통해 새로운 protocol을 채택할 수 있다
익스텐션은 기존 타입에 새로운 프로퍼티, 메서드, 서브 스크립트를 추가할 수 있으므로 프로토콜이 요구하는 것도 만족시킬 수 있다

타입의 기존 인스턴스에도 해당 타입의 익스텐션에 의해 프로토콜이 채택 되면 자동으로 반영된다

예를들어 아래에서 예시로 보여준 TextRepresentable이라는 프로토콜은
텍스트로 표현되는 방법이 있는 모든 타입을 구현할 수 있는 프로토콜이다
이는 자신에 대한 설명 혹은 현재 상태에 대한 텍스트 일 수 있다

protocol TextRepresentable {
    var textualDescription: String { get }
}

위에 있는 Dice 클래스를 확장하여 TextRepresentable 을 채택해보자

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

해당 익스텐션은 Dice 자신의 원본 구현에서 제공하는 것과 동일한 방법으로 새로운 프로토콜을 채택한다
프로토콜 이름은 타입 뒤에 콜론(: )을 이용하여 작성하면 되고,
프로토콜의 모든 요구 사항은 해당 extension의 중괄호 안에서 정의하면 된다
이제 모든 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"

1) 조건에 따라 프로토콜 준수하기

제네릭 타입은 제네릭 매개변수가 프로토콜을 준수하는 경우와 같은 특정 조건에서만 프로토콜의 요구 사항을 만족시킬 수 있다
타입에 익스텐션을 사용할 때 제약 조건을 나열하여 제네릭 타입이 조건부로 프로토콜을 준수하도록 만들 수 있다
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]"

2) 익스텐션으로 프로토콜 채택 선언하기

타입이 프로토콜의 모든 요구 사항을 이미 준수하고 있지만,
해당 프로토콜을 채택한다고 아직 알리지 않은 경우라면
빈 익스텐션으로 프로토콜을 채택하게 할 수도 있다

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"

프로토콜의 요구 사항을 만족한다고 해서 타입이 이를 자동으로 채택하진 않는다
반드시 자신이 프로토콜을 채택한다는 것을 명시적으로 선언해주어야 한다

통합 구현을 써서 프로토콜 채택하기

Swift는 많은 간단한 경우에 Equatable, Hashable 및 Comparable에 대한 프로토콜 준수를 자동으로 제공할 수 있다
이런 통합 구현을 사용하면 스스로 프로토콜 요구 사항을 구현하고자 보일러 플레이트 코드를 반복 작성하지 않아도 된다는 걸 의미한다

1) Equatable

Swift는 다음과 같은 종류의 커스텀 타입에 대해 Equatable의 통합 구현을 제공한다

  • Equatable 프로토콜을 준수하는 저장 프로퍼티만 있는 구조체
  • Equatable 프로토콜을 준수하는 associated type 만 있는 열거형
  • associated type이 없는 열거형

== 의 통합구현을 사용하려면 == 연산자를 직접 구현하지 말고 코드에 Equatable 프로토콜을 채택한다
Euatalbe 프로토콜은 !=의 기본 구현을 제공한다

다음 코드는 Vector2D 구조와 유사한 3차원 위치 벡터 (x,y,z)에 대한 Vector 3D 구조체를 정의한다
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."

2) Hashable

Swift는 다음 종류의 커스텀 타입에서 Hashable 통합 구현을 제공한다

  • Hashable 프로토콜을 준수하는 저장 프로퍼티만 있는 구조체
  • Hashable 프로토콜을 준수하는 associated type 만 있는 열거형
  • associated type이 없는 열거형

hash(into: )의 통합구현을 사용하려면, hash(into: ) 메서드를 직접 구현하지 않고 코드에 Hashable 프로토콜을 채택하면 된다

3) Hashable

Swift는 raw Value가 없는 열거형에 대해 Comparable의 통합 구현을 제공한다
열거형에 associated 타입이 있다면 이들은 모두 반드시 Comparable 프로토콜을 준수해야한다
<의 통합 구현을 사용하려면, < 연산자를 직접 구현하지 않고
원래 열거형이 선언된 코드에 Comparable 프로토콜을 채택하면 된다
Comparable 프로토콜의 기본구현인 <=, > >=는 나머지 비교연산자를 제공한다

다음 코드는 beginner, intermediate, expert 케이스를 갖는 SkillLevel 열거형을 정의한다
expert는 보유한 별의 수에 따라 추가로 순위가 매겨진다

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)"

프로토콜 타입의 집합체

위에서 Protocols as Types에서 언급한 것처럼, 배열이나 딕셔너리 같은 집합체에 저장할 타입으로 프로토콜을 사용할 수 있다

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

이제 배열의 항목을 반복하고, 각 항목의 textualDescription을 print하는게 가능하다

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나 DiceGame, Hamster 타입인 경우에도 각각의 타입들이 아닌 TextRepresentable로 간주된다
실제로 그런 타입이더라도 모두 TextRepresentable 타입이고,
해당 타입에는 textualDescription 프로퍼티가 있으므로 위와 같이 사용 가능하다

프로토콜 상속

프로토콜은 하나 이상의 다른 프로토콜을 상속(inherit)할 수 있으며
자신이 상속한 요구 사항 위에 요구 사항을 더 추가할 수 있다
프로토콜 상속 구문은 클래스 상속구문과 비슷하지만, 옵션으로 상속할 프로토콜이 여러 개라면 쉼표(,) 로 구분하여 나열할 수 있다

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

위에 있는 TextRepresentable 프로토콜을 상속한 프로토콜 예제를 살펴보자

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

위 코드는 PrettyTextRepresentable 이라는 TextRepresentable을 상속한 새 프로토콜을 정의한다
PrettyTextRepresentable를 채택한 어떤 것이든 반드시 TextRepresentable 이 강제로 지정한 모든 요구 사항과 더불어서PrettyTextRepresentable이 추가한 요구 사항까지 모두 만족해야한다
해당 코드에서는 PrettyTextRepresentable는 String을 반환하는 prettyTextualDescription이라는 gettable 프로퍼티를 제공하라는 단일 요구 사항을 추가한다

이렇게 생성한 프로토콜을 SnakeAndLadders 클래스에 익스텐션을 활용하여 프로토콜을 채택해보자

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
    }
}

해당 익스텐션은 SnakesAndLadders 타입이 PrettyTextRepresentable 프로토콜을 채택한다는 걸 알리며
이를 위해 prettyTextualDescription 프로퍼티의 구현을 제공한다고 명시한다
PrettyTextRepresentable인 모든 것은 TextRepresentable이어야 하며,
따라서, 해당 요구 사항을 모두 만족하도록 구현되어야한다

PrettyTextRepresentable 프로토콜에서 가져야 할 prettyTextualDescription 프로퍼티의 구현은
textualDescription 프로퍼티에 접근하며 출력 문자열을 구성하는 것으로 시작된다

콜론(: )과 줄 바꿈(\n)을 추가하고 이를 예쁜 텍스트 표현의 시작으로 사용한다
그런 뒤 board 배열의 square 값을 index 기반으로 접근하여 케이스에 따라 각 사각형의 내용을 나타내는 도형을 추가한다

  • square 값이 0 보다 크면, 사다리의 바닥이므로, ▲ 로 나타낸다
  • square 값이 0 보다 작으면, 뱀의 머리이므로, ▼ 로 나타낸다
  • 그 외의 경우, square 값은 0 이고, “자유 (free)” square라, ○ 로 나타낸다

이제 prettyTextualDescriptoin 프로퍼티를 사용하면 어떤 SnakesAndLadders 클래스 인스턴스의 꾸밈 문장 설명도 print할 수 있다

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

Class-only 프로토콜

프로토콜 상속 목록에 AnyObject 프로토콜을 추가함으로써
프로토콜 채택을 클래스 타입으로만 제한할 수 있다 (구조체 x, 열거형 x)

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)

동시에 여러 개의 프로토콜을 준수하는 타입을 요구하는 것이 유용할 때가 있다
프로토콜 composition을 사용하여 여러 개의 프로토콜을 하나의 프로토콜로 결합할 수 있다
프로토콜 composition은 composition안에 있는 모든 프로토콜의 요구 사항이 결합된 임시 로컬 프로퍼티를 정의한 것 처럼 작동한다
프로토콜 composition은 새로운 프로토콜 타입을 정의하지는 않는다

프로토콜 composition의 형식은 SomeProtocol & AnotherProtocol이다
앤드 기호 (&)로 구분하여 필요한 만큼 많은 프로토콜을 나열할 수 있다
자신의 프로토콜 목록에 더하여, 프로토콜 composition은 클래스 타입도 하나 담을 수 있는데,
이를 사용하여 필수 부모 클래스를 지정할 수도 있다

Named와 Aged라는 두 프로토콜을 함수 매개변수에 대한 단일 프로토콜 composition 요구 사항으로 합성한 예제를 살펴보자

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: ) 함수가 자신의 생일 인사말을 print한다

이전 예제의 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 프로토콜을 준수하는 어떤 타입이라는 뜻이다
이 경우 City는 요구 사항을 둘 다 만족한다

beginConcert(in: ) 함수에 birthdayPerson을 전달하는 것은 무효화 되는데,
Person은 Location의 자식 클래스가 아니기 때문이다
마찬가지로 Location의 자식 클래스를 만들었는데,
Named 프로토콜을 준수하지 않으면 해당 타입의 인스턴스를 가지고 beginConcert(in: )을 호출하는것도 무효화된다

프로토콜 준수 검사하기

Type Casting에서 설명한 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 클래스에서 radius 저장 프로퍼티에 기초한 계산 프로퍼티인 area 프로퍼티를 프로토콜 요구 사항에 따라 구현한다
Country 클래스에서 저장 프로퍼티인 area 프로퍼티를 직접 프로토콜 요구 사항에 따라 구현한다
두 클래스 모두 HasArea 프로토콜을 올바르게 준수한다

Animal 이라는 HasArea 프로토콜을 준수하지 않는 클래스를 정의해보자

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 인스턴스, 네 발 달린 Animal 인스턴스를 포함하여 배열 리터럴로 초기화 된다

이제 objects 배열을 반복하고, 배열 안의 각 객체를 검사하여 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하게 접근하고 print할 수 있다

casting(변환)과정에서 실제 객체가 바뀌는 것은 아니다
모두 Circle, Country, Animal 이다
하지만, objectWithArea 상수에 저장한 시점에는 타입이 HasArea라는 것만 알아서, 자신의 area 속성에만 접근 가능하다

옵셔널 프로토콜 요구 사항

프로토콜을 위한 옵셔널 요구 사항을 정의할 수 있다
해당 요구 사항들은 프로토콜을 준수하는 타입이 구현되지 않아도 된다
‘옵셔널 요구 사항'은 프로토콜 정의에서 접두사로 ‘optional’ 키워드를 붙인다
‘옵셔널 요구 사항'은 objective-c 와 상호 호환되는 코드를 작성하기 위하여 사용된다
‘프로토콜'과 ‘옵셔널 요구 사항'은 둘 다 반드시 ‘@objc’ 특성을 표시해야한다
objective-c 클래스나 다른 @objc 클래스를 상속한 클래스만 ‘@objc 프로토콜'을 채택할 수 있다는 것이 중요하다
구조체나 열거형은 이를 채택할 수 없다 - 클래스 전용

‘옵셔널 요구 사항'에서 메서드나 프로퍼티를 사용할 때,
해당 타입은 자동으로 ‘옵셔널’이 된다
ex. (Int) -> String 타입의 메서드는 ((Int) -> String)? 타입이 된다
메서드 반환값이 아니라, ‘전체 함수 타입'을 옵셔널로 포장한다는 것이 중요하다

‘옵셔널 프로토콜 요구 사항'은 프로토콜을 준수하는 타입이
요구 사항을 구현하지 않을 가능성을 고려하여 ‘옵셔널 체이닝'을 가지고 호출할 수 있다
someOptionalMethod?(someArgument)와 같이 호출될 때 메서드 이름 뒤에 물음표를 넣어 옵셔널 메서드의 구현을 확인한다
옵셔널 체이닝에 대한 내용은 Optional Chaining 페이지를 확인하자

다음 예제에서는 외부 데이터 소스를 사용하여
increment(증가량)를 제공하는 Counter라는 정수 계산 class를 정의한다
해당 데이터 소스는 두 가지 선택적 요구 사항이 있는 CounterDataSource 프로토콜에 의해 정의된다

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

CounterDataSource 프로토콜은 incremental(forCount:) 라는 ‘옵셔널 메소드 요구 사항’과
fixedIncrement 라는 ‘옵셔널 프로퍼티 요구 사항’ 을 정의한다
해당 요구 사항들은 데이터 소스가 Count 인스턴스에 적절한 증가량을 제공하기 위한 서로 다른 두 방식을 정의한다

엄밀히 말하면 protocol 요구사항을 구현하지 않아도 CounterDataSource protocol을 준수하는 class를 만들 수 있다
요구사항이 모두 옵셔널이기 때문이다
물론 굳이 그럴 필요는 없다

아래에서 정의한 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 라는 변수에 저장하며,
매 번 메서드를 호출할 때마다 count 속성을 증가시키는 increment 라는 메서드를 정의한다

increment() 메서드는 증가량을 가져오기 위해 먼저 데이터 소스에 대한 incremental(forCount: )메서드가 구현됐는지 찾으려고 한다
increment()메서드는 increment(forCount: )호출을 시도하기 위해 ‘옵셔널 체이닝'을 사용하며
메서드 단일인자로 현재의 count 값을 전달한다

여기에서 사용된 2단계의 optional 체이닝은 dataSource가 nil일 가능성이 있으므로,
dataSource 가 nil이 아닐 때만 incremental(forCount: )가 호출될 수 있도록 dataSource 뒤에 ?를 붙인다
dataSource가 존재할 지라도, 옵셔널 요구 사항이기 때문에 increment(forCount: )를 구현한다는 보장이 없다
따라서, increment(forCount: )가 구현되어 있지 않을 가능성도 옵셔널 체이닝에 의해 처리된다
increment(forCount: )에 대한 호출은 increment(forCount: )가 존재하는 경우에만 발생한다
increment(forCount: )도 이름 뒤에 물음표가 있는 이유다

이러한 두 가지 이유로 인해 increment(forCount: )호출이 실패할 수 있으므로 increment(forCount: )는 옵셔널 Int값을 반환한다
increment(forCount: )가 CounterDataSource의 정의에서 옵셔널이 아닌 Int값을 반환하도록 되어 있더라도 마찬가지이다
두 개의 옵셔널 체이닝이 차례대로 있지만, 결과는 하나의 옵셔널로 래핑된다

increment(forCount: )를 호출한 뒤 반환하는 옵셔널 Int는,
옵셔널 바인딩을 사용하여 amount라는 상수로 래핑 해제 된다
옵셔널 Int 값이 있으면 래핑 되지 않은 금액이 count 프로퍼티에 추가되고 증가가 완료된다

dataSource가 nil이거나 increment(forCount:)를 구현하지 않아 increment(forCount:)의 값을 얻을 수 없는 경우
increment() 메서드는 dataSource의 fixedIncrement 프로퍼티에서 값을 가져오려 시도한다
fixedIncrement 프로퍼티도 옵셔널 요구 사항이므로 해당 값이 CounterDataSource protocol에서 Int 타입으로 정의되어 있더라도 해당 값은 옵셔널 Int 타입이다

다음 코드는 dataSource가 쿼리 될 때마다 상수 값 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 인스턴스를 설정하며,
counter의 increment() 메서드를 네 번 순회 호출한다
Counter의 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의 인스턴스를 같이 사용할 수 있다
Counter가 ‘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 프로토콜에 extension을 사용하여 randomBool() 메서드를 제공하도록 할 수 있다
해당 메서드는 필수적인 random() 메서드의 결과를 사용하여 임의의 Bool 값을 반환한다.

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

프로토콜에 대한 익스텐션을 생성함으로써,
모든 프로토콜을 준수하는 타입은 어떤 추가적인 수정 없이도 randomBool() 메서드를 사용할 수 있게 된다

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"

프로토콜 익스텐션은 프로토콜을 준수하는 타입에 구현을 추가할 순 있지만,
프로토콜 자체를 확장하도록 만들거나, 다른 프로토콜을 상속하게 할 수는 없다
프로토콜 상속은 프로토콜 선언 그 자체에서만 가능하다

1) 기본 구현 제공하기

프로토콜 익스텐션을 사용해서 해당 프로토콜이 요구하는 모든 메서드 또는 계산 프로퍼티에 대한 기본 구현을 제공할 수 있다
자신을 채택한 타입이 요구 사항을 이미 구현한 경우에는,
프로토콜 익스텐션에서 구현한 것이 아닌 해당 타입 내에서 구현한 것이 사용된다

익스텐션에서 프로토콜의 요구 사항을 구현하는 것은 옵셔널 프로토콜 요구사항과는 다르다
프로토콜을 준수하는 타입이 자체적으로 요구 사항을 구현할 필요는 없지만,
이미 프로토콜 익스텐션에 구현이 되어있으므로 옵셔널 체이닝 없이 바로 사용할 수 있다

예를 들어, TextRepresentable 프로토콜을 상속하는 PrettyTextRepresentable 프로토콜은
prettyTextualDescription 프로퍼티의 default 구현을 제공하여
단순하게 textualDescription 프로퍼티에 접근한 결과를 반환할 수 있다

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

2) 프로토콜 익스텐션에 Constraints (제약 조건) 추가하기

프로토콜 익스텐션을 정의할 때, 해당 타입이 익스텐션의 메서드와 프로퍼티를 사용하기 전에 충족해야하는 제약 조건을 지정할 수 있다
제네릭 where절을 사용하여 익스텐션을 사용할 프로퍼티 이름 뒤에 제약 조건을 작성할 수있다
generic where절에 대한 자세한 내용은 바로 다음 챕터인, Generic Where Clauses에서 알아보자

예를 들어 Element가 Equatable 프로토콜을 준수하는 모든 컬렉션에만 적용되는 프로토콜 익스텐션을 정의할 수 있다
컬렉션의 element를 표준 라이브러리의 일부인 Equatable 프로토콜로 제한하면
==, != 연산자를 사용하여 두 Element간의 비교를 할 수 있다

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

allEqual() 메서드는 컬렉션의 모든 Element가 동일한 경우에만 true를 반환한다
두 개의 Int Array에 직접 사용해보자
하나는 다른 모든 Element가 동일하지만 다른 하나는 그렇지 않다

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

Int 타입은 Equatable을 준수하기 때문에 equalNumbers, differentNumbers는 allEqual() 메서드를 사용할 수 있다

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

프로토콜을 준수하는 어떤 타입이 동일한 메서드, 프로퍼티에 대한 구현을 제공하는 여러 개의 제약된 익스텐션의 요구 사항을 만족하는 경우
Swift는 가장 specialezed 한 제약 조건에 해당하는 구현을 사용한다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant