티스토리 뷰

반응형

이번 글에서는 Swift 공식 문서의 21번째 단원인 Protocols을 읽고 정리한 글을 쓰려고 합니다.

Apple Swift 공식 문서 21단원 - Protocol

Protocol

Protocol은 특정 작업이나 기능에 적합한 메서드, 프로퍼티, 요구사항의 청사진을 정의합니다. 그런 뒤 이러한 요구사항의 실제 구현을 위해 Class, Struct, Enum에서 Protocol을 채택할 수 있습니다. 이때 Protocol이 요구하는 사항을 모두 충족하면 해당 타입은 Protocol을 준수한다고 합니다.

준수해야 하는 타입의 요구사항을 정의하는 것 외에도 요구사항의 일부를 구현하거나, 준수하는 타입에 추가 기능을 구현하기 위해 Protocol을 확장할 수도 있습니다.

Protocol Syntax

Protocol을 정의하는 방법은 Class, Struct, Enum과 매우 비슷합니다.

protocol SomeProtocol {
    // protocol definition goes here
}

Class, Struct, Enum을 직접 구현할 때 Protocol을 채택하고 싶다면 타입의 이름 뒤에 채택하고 싶은 Protocol의 이름을 쓰면 됩니다. Protocol은 콤마를 사용해서 여러 개를 채택할 수도 있습니다.

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

Class의 경우 상속의 문법도 Protocol 채택과 동일한데요, 부모 Class를 갖는 경우에는 부모 Class의 이름을 가장 먼저 쓰고 그 뒤에 Protocol의 이름들을 나열하면 됩니다.

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

Property Requirements

Protocol은 특정 이름과 타입을 갖는 인스턴스 프로퍼티, 타입 프로퍼티를 사용해서 요구하는 타입을 정의합니다. Protocol은 프로퍼티가 저장 프로퍼티인지 계산 프로퍼티인지는 지정하지 않고 필요한 프로퍼티의 이름과 타입만 지정합니다. Protocol은 각각의 프로퍼티가 gettable, settable 한지도 지정해줘야 합니다.

Protocol에서 gettable, settable인 프로퍼티를 요구할 경우 해당 프로퍼티는 상수 저장 프로퍼티 혹은 읽기 전용 계산 프로퍼티로는 요구사항을 만족시킬 수 없습니다. Protocol이 gettable 프로퍼티만 요구할 경우에는 모든 종류의 프로퍼티로 요구사항을 만족시킬 수 있고, 이런 프로퍼티는 settable에도 유효합니다.

프로퍼티의 요구사항은 항상 var 키워드와 함께 선언됩니다. gettable, settable 프로퍼티는 타입 선언 뒤에 { get set }으로 작성해서 나타내며 gattable 프로퍼티는 { get }으로 작성하면 됩니다.

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

Protocol에서 타입 프로퍼티를 정의할 땐 항상 static 키워드를 사용해야 합니다. 이러한 규칙은 Class에 Protocol을 채택한 경우에 class, static 키워드를 쓰는 경우에도 적용됩니다.

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

다음은 하나의 프로퍼티를 요구하는 protocol의 예제입니다.

protocol FullyNamed {
    var fullName: String { get }
}

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

다음은 FullyNamed protocol을 채택하고 준수하는 Struct에 대한 예입니다.

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

위 예는 특정한 이름을 갖는 사람을 나타내는 Person struct를 정의합니다. 첫 줄에 보면 FullNamed protocol을 채택한 것도 볼 수 있습니다.

Person의 각 인스턴스에는 String 타입의 fullName이라는 단일 저장 프로퍼티가 있습니다. 이는 FullyNamed protocol의 요구 사항을 만족시키기 때문에 PersonFullyNamed protocol을 올바르게 준수했음을 의미합니다. (Swift는 protocol의 요구사항을 만족시키지 않으면 컴파일 타임에 오류를 발생시킵니다.)

다음은 FullyNamed protocol을 채택하고 준수하는 좀 더 복잡한 class 예제를 보겠습니다.

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"

위 예제의 Starship class는 FullyName protocol의 요구사항인 fullName 프로퍼티를 계산 프로퍼티로 구현했습니다. Starship class의 인스턴스는 name, prefix를 저장 프로퍼티로 갖고 있습니다. fullName 프로퍼티는 prefix값이 있는 경우 이를 name의 앞부분에 붙여서 StarshipfullName을 만들게 됩니다.

Method Requirements

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

요구사항으로 타입 프로퍼티를 정의했을 때와 마찬가지로 타입 메서드를 요구할 때는 항상 static 키워드를 사용해야 합니다. 이는 protocol이 요구하는 메서드를 class가 정의할 때 class, static 키워드를 사용하는 경우에도 동일합니다.

protocol SomeProtocol {
    static func comeTypeMethod()
}

다음은 하나의 인스턴스 메서드를 요구하는 protocol을 정의하는 예입니다.

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator protocol은 자신을 채택한 타입에게 Double 타입을 반환하는 random이라는 인스턴스 메서드를 정의하라고 요구합니다. Protocol의 일부로 지정하지는 않았지만 이 값은 0.0 ~ 1.0 사이의 숫자를 반환한다고 해보겠습니다.

RandomNumberGenerator Protocol은 각각의 난수가 생성되는 방식에 대한 어떠한 것도 만들지 않았습니다. Generator가 난수를 생성하는 방법만 제공하면 되죠.

다음으로 RandomNumberGenerator protocol을 채택하고 준수하는 class를 구현해보겠습니다. 이 class는 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 Method Requirements

메서드가 인스턴스를 수정해야 하는 경우가 있습니다. 값 타입(struct, enum)에 대한 인스턴스 메서드의 경우 func 키워드 앞에 mutating 키워드를 배치해서 해당 메서드가 인스턴스의 값을 수정할 수 있음을 나타냅니다. 여기에 대한 자세한 설명은 Modifying Value Types from Within Instance Methods을 참고해주세요.

Protocol을 채택하는 모든 타입의 인스턴스를 변경하기 위한 인스턴스 메서드 요구사항을 정의하는 경우 mutating 키워드로 메서드에 표시해주면 됩니다. 이를 통해 struct, enum이 protocol이 요구하는 메서드를 인스턴스를 수정할 수 있는 메서드로 만들 수 있습니다.

Note: Protocol이 요구하는 인스턴스 메서드에 mutating을 표시하더라도 class는 해당 메서드를 구현할 때 mutating 키워드를 사용할 필요가 없습니다. mutating은 struct, enum의 경우에만 사용합니다.

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

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

protocol Togglable {
    mutating func toggle()
}

Struct, Enum이 Togglable protocol을 채택하는 경우 mutating 키워드를 사용해서 toggle() 메서드를 구현하면 됩니다.

아래 예제는 OnOffSwitch라는 Enum을 정의한 코드입니다. OnOffSwitchon, off로 표시되는 두 상태를 toggle합니다. Enum의 toggle() 구현에는 mutating 키워드를 사용해서 Toggleable protocol의 요구사항과 일치하도록 해줍니다.

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

Initilaizer Requirements

Protocol은 자신을 채택한 타입에 특정 생성자를 요구할 수도 있습니다. 이러한 생성자는 일반 생성자와 정확히 같은 방식으로 protocol 정의의 일부로 만들지만 중괄호나 생성자 본문은 작성하지 않습니다.

protocol SomeProtocol {
    init(someParameter: Int)
}

Class Implementations of Protocol Initializer Requirements

designated 생성자, convenience 생성자로 생성자를 요구하는 protocol을 채택한 class를 구현할 수 있습니다. 두 경우 모두 생성자에 required를 표시해야 합니다.

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

required 생성자를 사용하면 class의 모든 서브 클래스도 해당 생성자를 명시적으로 혹은 상속된 구현을 통해 구현하므로, 부모 class가 채택 중인 protocol을 준수하도록 할 수 있습니다.

required 생성자에 대한 자세한 정보는 Required Initializers을 참고해주세요.

Note: final 클래스는 서브 클래싱 할 수 없기 때문에 final 클래스는 protocol이 요구하는 생성자에 required를 표시할 필요가 없습니다. final에 대한 자세한 정보는 Preventing Overrides를 참고해주세요.

서브클래스가 슈퍼클래스의 designated 생성자를 override 하고 protocol에서 요구하는 생성자도 구현하는 경우 required, override 수정자를 모두 사용해서 생성자를 구현합니다. 즉 슈퍼클래스는 protocol을 채택하지 않았는데, 서브클래스에서는 protocol을 채택한 경우는 아래와 같이 구현하면 됩니다.

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

Protocol은 Failable Initializers에 정의된 대로 실패 가능한 생성자를 요구사항으로 정의할 수 있습니다.

Protocol의 실패 가능한 생성자 요구사항은 자신을 채택한 타입이 실패 가능, 실패 불가능한 생성자가 있을 때 만족됩니다. 실패할 수 없는 생성자를 요구하는 protocol을 채택한 경우, 실패할 수 없는 생성자 또는 암시적으로 래핑 되지 않은 실패 가능한 생성자로 만족시킬 수 있습니다.

Protocol as Types

Protocol은 자체적으로 기능을 구현하지 않습니다. 하지만 코딩을 할 때 Protocol은 하나의 타입처럼 사용할 수 있습니다. Protocol을 타입처럼 사용하는 것을 existential type이라고 하는 경우가 있는데 이는 "T가 protocol을 준수하도록 타입 T가 존재합니다."라는 문구에서 유래되었습니다.

다음을 포함해서 다른 타입이 허용되는 여러 위치에서 Protocol을 사용할 수 있습니다.

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

Note: Protocol이 타입이므로 (앞서 봤던 FullyNamed, RandomNumberGenerator와 같이..) Swift의 다른 타입들(예를 들어 Int, String, Double)처럼 이름이 대문자로 시작합니다.

다음은 protocol을 타입처럼 사용한 예입니다.

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라는 새로운 class를 정의한 코드입니다. Dice인스턴스에는 면의 수를 나타내는 Int 타입인 sides 프로퍼티와, 주사위를 굴린 값을 생성하는 RandomNumberGenerator타입의 generator 프로퍼티가 있습니다.

generator 프로퍼티는 RandomNumberGenerator 타입입니다. 따라서 RandomNumberGenerator protocol을 채택하는 모든 타입의 인스턴스를 사용할 수 있습니다. 인스턴스가 RandomNumberGenerator protocol을 채택한다는 점을 제외하고 이 속성에 할당될 인스턴스에게 요구하는 것은 없습니다. generator의 타입이 RandomNumberGenerator 이므로 Dice class의 내부 코드는 이 protocol이 준수하는 모든 인스턴스에 적용되는 방식으로만 generator를 사용할 수 있습니다. 즉 generator의 기본 타입에 정의된 메서드나 프로퍼티를 사용할 수 없습니다. 하지만 DownCasting에서 설명한 슈퍼클래스를 서브클래스로 다운 캐스트 하는 것처럼, protocol 타입을 기본 타입으로 다운 캐스트 해서 사용할 수는 있습니다.

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

Dice는 1과 sided 사이의 정수 값을 반환하는 인스턴스 메서드인 roll을 제공합니다. 이 메서드는 generatorrandom()메서드를 호출해서 0.0 ~ 1.0 사이의 새로운 난수를 만들고 이 난수를 사용해서 범위 내의 값을 만듭니다. generatorRandomNumberGenerator를 채택했기 때문에 random() 메서드가 반드시 존재합니다.

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

아래 예는 주사위 기반 보드게임에 사용하기 위한 2개의 protocol을 정의한 코드입니다.

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 프로토콜은 주사위와 관련된 모든 게임에서 채택할 수 있는 Protocol입니다.

DiceGameDelegate protocol을 채택해서 DiceGame의 진행 상황을 추적할 수 있습니다. 강한 참조 사이클을 방지하기 위해 delegate는 weak 참조로 선언됩니다. weak 참조에 대한 자세한 내용은 Strong Reference Cycles BetweenClass Instances를 참고해주세요! 잠시 뒤에 나오는 Class-Only Protocols 섹션에서 나올 내용을 미리 말씀드리면, class 전용 protocol은 AnyObject를 상속하는 것으로 표시하면 됩니다. Protocol을 class 전용으로 표시하면 이번 챕터의 뒷부분에 나오는 SnakesAndLadders class의 delegate처럼 weak참조로 선언할 수 있습니다.

다음은 원래 Control Flow에서 소개된 Snake and Ladders 게임 버전입니다. 이 버전은 DiceGame protocol 채택하고 DiceGameDelegate에게 진행상황을 알리기 위해 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)
    }
}

Snakes and Ladders의 게임 설명을 보고 싶으시면 Control Flow - Break를 참고해주세요.

이번 게임 버전은 DiceGame protocol을 채택한 SnakesAndLadders라는 class로 래핑 됩니다. Protocol을 준수하기 위해 gattable dice 프로퍼티와 play() 메서드를 제공합니다. (dice 프로퍼티는 생성 후 변경할 필요가 없기 때문에 let으로 선언되며 protocol은 gettable만 요구하므로 문제가 없습니다.)

Snakes and Ladders 게임 보드 설정은 class의 init() 생성자에서 이뤄집니다. 모든 게임 논리는 Protocol의 필수 요소인 dice 프로퍼티를 사용해서 주사위 역할을 제공하는 protocol의 play() 메서드로 진행됩니다.

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

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

delegate 프로퍼티는 옵셔널 DiceGameDelegate이므로 play() 메서드는 delegate에서 메서드를 호출할 때마다 옵셔널 체이닝을 사용합니다. delegate 프로퍼티가 nil이라면 이러한 호출은 정상적으로 실패하게 되고, nil이 아니라면 delegate 메서드가 호출되고 SnakesAndLadders 인스턴스가 매개변수로 전달됩니다.

다음 예는 DiceGameDelegate protocol을 채택한 DiceGameTracker라는 class를 정의한 코드입니다.

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에 필요한 세 가지 메서드를 모두 구현해뒀습니다. 이런 메서드를 사용해서 게임에서 수행한 차례의 횟수를 알아냅니다. 게임이 시작되면 numberOfTurns 프로퍼티를 0으로 재설정하고 새로운 턴이 시작될 때마다 값을 증가시키며, 게임이 끝나면 총 턴 수를 출력합니다.

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

gameDidStart(_:) 메서드는 전달된 game 매개변수의 dice 프로퍼티에도 접근합니다. gameDiceGame protocol을 준수하므로 dice 프로퍼티가 반드시 존재하기 때문에 gameDidStart(_:) 메서드는 어떤 종류의 게임이 플레이되는지 상관없이 diceside 프로퍼티에 접근하여 출력할 수 있습니다.

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을 통해 새로운 protocol을 채택할 수도 있습니다. Extension은 기존 타입에 새로운 프로퍼티, 메서드, 서브 스크립트를 추가할 수 있으므로 프로토콜이 요구하는 것도 만족시킬 수 있습니다. Extension에 대한 자세한 내용은 Extension을 참고해주세요.

Note: 타입의 기존 인스턴스에도 해당 타입의 extension에 의해 protocol이 채택되면 자동으로 반영됩니다.

예를 들어 TextRepresentable이라는 텍스트로 표현되는 방법이 있는 모든 타입을 구현할 수 있는 protocol입니다. 이는 자신에 대한 설명 혹은 현재 상태에 대한 텍스트 일 수 있겠죠.

protocol TextRepresentable {
    var textualDescription: String { get }  
}

아까 본 Dice 클래스도 extension을 사용하여 TextRepresentable를 채택하고 준수하도록 할 수 있습니다.

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

위 extension은 Dice가 원래 구현에서 제공하는 것과 동일한 방법으로 새로운 protocol을 채택합니다. Protocol 이름은 타입 이름 뒤에 콜론을 사용하여 작성하면 되고, Protocol의 모든 요구사항은 해당 extension의 중괄호 안에서 정의하면 됩니다.

이제 모든 Dice 인스턴스를 TextRepresentable로도 처리할 수 있게 되었습니다!

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

비슷하게 SnakesAndLadders class도 TextRepresentable protocol을 채택하고 준수하게 만들 수 있습니다.

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

제네릭 타입은 제네릭 매개변수가 protocol을 준수하는 경우와 같은 특정 조건에서만 protocol의 요구사항을 만족시킬 수 있습니다. 타입에 extension을 사용할 때 제약조건을 나열하여 제네릭 타입이 조건부로 protocol을 준수하도록 만들 수 있습니다. where을 사용하여 채택하려는 protocol 이름 뒤에 제약조건을 작성하면 되는데요, where 절에 대한 자세한 내용은 Generic Where Clauses를 참고하세요!

다음 코드는 Array 인스턴스가 TextRepresentable을 채택한 타입의 요소를 저장할 때만 TextRepresentable protocol의 요구사항을 준수하도록 해줍니다.

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

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

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"

Note: 타입이 요구 사항을 충족한다고 해서 protocol을 자동으로 채택하지는 않습니다. 언제나 protocol은 명시적으로 선언해야 채택할 수 있습니다.

Adopting a Protocol Using a Synthesized Implementation

Swift는 많은 간단한 경우에 Equtable, Hashable, Comparable에 대한 protocol의 요구사항을 자동으로 준수할 수 있습니다. 이러한 synthesized implementation(합성 구현)을 사용하면 protocol의 요구 사항을 직접 구현하기 위해 반복적인 코드를 작성할 필요가 없습니다.

Swift는 다음 종류의 custom 타입에 대해 Equatable synthesized implementation을 사용할 수 있습니다.

  • Equatable protocol을 준수하는 저장 프로퍼티만 있는 struct
  • Equatable protocol을 준수하는 associated type만 있는 enum
  • associated type이 없는 enum

==의 synthesized implementation을 사용하려면 == 연산자를 직접 구현하지 말고 코드에 Equatable protocol을 채택합니다. Equatable protocol은 !=의 기본 구현을 제공합니다.

다음 코드는 Vector2D 구조와 유사한 3차원 위치 벡터 (x, y, z)에 대한 Vector3D struct를 정의한 코드입니다. x, y, z 프로퍼티 모두 Equatable 타입이므로 Vector3D는 == 연산자의 synthesized implementation을 사용할 수 있습니다.

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는 다음 종류의 custom type에 대해 Hashable의 synthesized implementation을 사용할 수 있습니다.

  • Hashable protocol을 준수하는 저장 프로퍼티만 있는 struct
  • Hashable protocol을 준수하는 associated type만 있는 enum
  • associated type이 없는 enum

hash(into:)의 synthesized implementation을 사용하려면 hash(into:) 메서드를 직접 구현하지 않고 코드에 Hashable protocol을 채택합니다.

Swift는 raw Value가 없는 enum에 대해 Comparable의 synthesized implementation를 제공합니다. Enum에 associated type이 있는 경우 모두 Comparable protocol을 준수해야 합니다. <의 synthesized implementation을 수신하려면 < 연산자를 직접 구현하지 않고 원래 enum이 선언된 코드에 Comparable protocol을 채택합니다. Comparable protocol의 기본 구현인 <=, >, >=는 나머지 비교 연산자를 제공합니다.

다음 코드는 beginner, intermediate, expert 케이스를 갖는 SkillLevel enum을 정의한 코드입니다. 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)"

Collections of Protocol Types

Protocol은 아까 Protocols as Types 섹션에서 알아본 대로 Array, Dictionary와 같은 컬렉션에 저장할 수도 있습니다. 다음 코드는 TextRepresentable을 저장할 Array를 만드는 코드입니다.

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

이제 Array의 항목을 for문을 통해 처리하면 각 항목에 대한 textualDesciption을 출력할 수 있습니다.

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

thingsDice, DiceGame, Hamster타입이 아닌 TextRepresentable 타입입니다. 실제로는 그런 타입이라도 모두 TextRepresentable 타입이고 해당 타입에는 textualDesciprion 프로퍼티가 있으므로 위와 같이 사용이 가능합니다.

Protocol Inheritance

Protocol은 하나 이상의 다른 protocol을 상속할 수 있으며 상속된 요구사항에 요구사항을 추가할 수 있습니다. Protocol 상속 문법은 class의 상속과 유사하지만 상속할 여러 protocol을 쉼표로 구분하여 나열할 수 있습니다.

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

다음은 위에서 나온 TextRepresentable protocol을 상속하는 protocol의 예입니다.

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

위 코드에서는 TextRepresntable을 상속하는 새로운 protocol인 PrettyTextRepresentable을 정의합니다. PrettyTextRepresentable을 채택하는 모든 타입은 TextRepresntable의 요구사항과 PrettyTextRepresntable의 요구사항을 모두 만족시켜야 합니다. 위 코드에서는 PrettyTextRepresntable는 String을 반환하는 prettyTextualDescription이라는 gettable 프로퍼티를 제공하기 위한 하나의 요구사항을 추가했습니다.

SnakesAndLadders class는 PrettyTextRepresntable를 채택하고 준수하도록 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
    }
}

위 코드는 PrettyTextRepresntable protocol을 채택하고 SnakesAndLadders 타입에 대한 prettyTextualDescription 프로퍼티의 구현을 제공한다고 명시합니다. PrettyTextRepresntable은 무엇이든 TextRepresntable protocol을 채택해야 하며 따라서 prettyTextRepresntable의 구현은 TextRepresntable protocol에서 textualDescription 프로퍼티에 접근하며 시작됩니다. 콜론과 줄 바꿈을 추가하고 이를 예쁜 텍스트 표현의 시작으로 사용합니다. 그런 뒤 board 배열의 square 값을 통해 도형을 추가합니다.

  • square의 값이 0보다 크면 사다리의 밑면이고 ▲로 표시합니다.
  • square의 값이 0보다 작으면 뱀의 머리이고 ▼로 표시합니다.
  • 둘 다 아니면 square의 값은 0이고 ○로 표시합니다.

이제 prettyTextualDescription 프로퍼티를 사용하여 SnakesAndLadders 인스턴스의 예쁜 텍스트를 출력할 수 있습니다.

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

Class-Only Protocol

Protocol의 상속 목록에 AnyObject 프로토콜을 추가해서 Protocol을 class만 채택할 수 있도록 제한할 수 있습니다.

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

위 코드에서 SomeClassOnlyProtocol은 class 타입에서만 채택할 수 있습니다. SomeClassOnlyProtocol을 채택하려고 하는 struct, enum은 컴파일 오류를 발생시킵니다.

Note: 해당 protocol의 요구사항에 의해 이미 정의된 동작이 참조 및 값 타입의 차이에 대한 설명은 Structures and Classes를 참고하세요.

Protocol Composition

동시에 여러 protocol을 준수하는 타입을 요구하는 것이 유용할 수 있습니다. protocol composition을 사용하여 여러 개의 protocol을 하나의 protocol로 결합할 수 있습니다. protocol composition은 composition에 있는 모든 protocol의 요구사항이 결합된 임시 로컬 protocol을 정의한 것처럼 작동합니다. protocol composition은 새로운 protocol 타입을 정의하지는 않습니다.

protocol composition은 SomeProtocol 및 AnotherProtocol 형식입니다. &로 구분하여 필요한 만큼 protocol을 나열할 수 있습니다. Protocol 목록 외에도 protocol composition에는 필수 슈퍼 클래스를 지정하는 데 사용할 수 있는 하나의 클래스 타입이 포함될 수도 있습니다.

다음은 Named, Aged라는 2개의 protocol을 함수 매개변수에 대한 단일 protocol 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 Protocol에는 name이라는 gettable String 프로퍼티를 요구합니다. Aged protocol에는 age라는 gettable Int 프로퍼티를 요구합니다. 두 개의 protocol 모두 Person이라는 struct에 채택되었네요.

위 예에서는 wishHappyBirthday(to:) 함수를 정의합니다. celebrator 매개변수 타입은 Named & Aged 이며, 이는 Named, Aged protocol을 모두 준수하는 타입을 의미합니다. 두 개의 필수 protocol을 모두 준수한다면 어떤 타입이건 함수의 매개변수로 사용될 수 있습니다.

그런 뒤 birthdayPerson이라는 새로운 Person인스턴스를 만들고 새로운 인스턴스를 wishHappyBirthday(to:) 함수에 전달합니다. Person이 두 개의 protocol을 모두 준수하기 때문에 문제없이 wishHappyBirthdat(to:) 함수는 작동됩니다.

다음은 방금 예의 Named protocol을 Location class와 결합한 예제입니다.

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 protocol을 준수하는 모든 타입을 뜻합니다. 위 코드에서 City는 두 조건을 모두 충족합니다.

PersonLocation의 하위 클래스가 아니므로beginConcert(in:)함수에birthdayPerson을 전달하는 것은 옳지 않습니다. 마찬가지로Namedprotocol을 준수하지 않는Location의 서브 클래스를 만든 경우에도beginConcert(in:)`에 전달하는 것이 옳지 않습니다.

Checking for Protocol Conformance

타입 캐스팅에 설명된 is, as 연산자를 사용하여 protocol 적합성을 확인하고 특정 protocol로 캐스팅할 수 있습니다. protocol을 확인하고 캐스팅하는 것은 타입을 확인하고 캐스팅하는 것과 동일한 문법을 사용합니다.

  • is 연산자는 인스턴스가 protocol을 준수하면 true를 반환하고 그렇지 않으면 false를 반환합니다.
  • as? 연산자는 protocol 타입의 옵셔널 값을 반환하고 인스턴스가 해당 protocol을 준수하지 않으면 nil을 반환합니다.
  • as! 연산자는 다운 캐스트를 protocol 타입으로 강제하고 성공하지 못하면 런타임 에러를 발생시킵니다.

다음 예는 area라는 gettable Dobule 프로퍼티를 요구하는 HasArea protocol을 정의한 코드입니다.

protocol HasArea {
    var area: Double { get }
}

그리고 HasArea protocol을 준수하는 Circle, Country 클래스를 정의합니다.

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 class는 계산 프로퍼티로 선언된 area 프로퍼티를 기반으로 radius라는 저장 프로퍼티를 구현합니다. 두 개의 class모두 HasArea protocol을 준수하고 있네요.

다음은 HasArea protocol을 준수하지 않는 Animal이라는 class입니다.

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

Circle, Country, Animal class는 함께 상속하는 class가 없습니다. 하지만 모두 class이므로 AnyObject 타입을 저장하는 Array에 저장할 수 있습니다.

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

Objects array는 radius가 2.0인 Circle 인스턴스, area가 243610인 Country 인스턴스, legs가 4인 Animal 인스턴스로 초기화됩니다.

이제 Objects array를 for문을 사용하여 각각의 element를 검사해서 HasArea protocol을 준수하는지 확인해봅시다.

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 protocol을 준수할 때마다 as? 연산자가 반환하는 옵셔널 값은 objectWithArea라는 상수로 래핑 해제됩니다. objectWithArea 상수는 HasArea 타입으로 알려져 있으므로 해당 area 프로퍼티에 접근하고 타입이 안전한 방식으로 print 될 수 있습니다.

각각의 객체는 캐스팅 프로세스에 의해 변경되지는 않고 계속해서 Circle, Country, Animal 하지만 objectWithArea에 저장되는 시점에는 HasArea타입이 되므로 area 프로퍼티에만 접근할 수 있습니다.

Optional Protocol Requirements

Protocol은 선택적 요구사항을 정의할 수도 있습니다. 이러한 요구사항은 protocol을 채택한 타입이 반드시 구현할 필요가 없습니다. 선택적 요구사항은 protocol 정의의 일부로 optional이라는 접두사가 붙습니다. Objective-C와 함께 사용되는 코드를 작성할 수 있도록 선택적 요구사항을 사용할 수 있는데요, 이 경우 Protocol과 선택적 요구사항은 모든 @objc 프로퍼티로 표시되어야 합니다. @objc 프로토콜은 Objective-C class 또는 @objc class에서 상속받은 class에서만 채택할 수 있습니다. 따라서 struct, enum에서는 채택될 수 없죠.

메서드나 프로퍼티를 선택적 요구 사항으로 선언하면 해당 타입은 자동으로 옵셔널 타입이 됩니다. 예를 들어 (Int) -> String 타입의 메서드는 ((Int) -> String)? 타입이 됩니다. 전체 함수 타입은 메서드의 반환 값이 아니라 옵셔널로 래핑 됩니다.

선택적 요구사항은 요구사항이 protocol을 준수하는 타입에 의해 구현되지 않았을 수 있기 때문에 옵셔널 체이닝으로 호출할 수 있습니다. someOptionalMethod?(someArgument)와 같이 호출될 때 메서드 이름 뒤에 물음표를 넣어 옵셔널 메서드의 구현을 확인합니다. 옵셔널 체이닝에 대한 자세한 내용은 Optional Chaining를 참고해주세요.

다음 예에서는 외부 데이터 소스를 사용하여 increment를 제공하는 Counter라는 정수 계산 class를 정의할 겁니다. 이 data source는 두 가지 선택적 요구사항이 있는 CounterDataSource protocol에 의해 정의됩니다.

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

CounterDataSource protocol은 increment(forCount:)라는 옵셔널 메서드 요구사항과 fixedIncrement라는 옵셔널 프로퍼티 요구사항을 정의합니다. 이러한 요구사항은 data Source가 Counter 인스턴스에 적절한 증분량을 제공하는 두 가지 다른 방법을 정의합니다.

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

아래 구현된 Counter class에는 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 class는 현재 값을 count라는 변수 프로퍼티에 저장합니다. Counter class는 또한 메서드가 호출될 때마다 count 프로퍼티를 증가시키는 increment라는 메서드를 정의합니다.

increment() 메서드는 먼저 dataSource에서 increment(forCount:) 메서드의 구현을 찾아 증가량을 찾으려고 시도합니다. increment() 메서드는 옵셔널 체이닝을 사용하여 increment(forCount:)의 호출을 시도하고 count값을 메서드의 단일 인수로 전달합니다.

두 가지 수준의 옵셔널 체이닝이 여기서 작동합니다. 첫 번째로 dataSource가 nil일 수 있으므로 dataSource의 이름 뒤에 물음표가 있어 dataSource가 nil이 아닐 때만 increment(forCount:)를 호출해야 함을 나타냅니다. 두 번째로 dataSource가 존재하더라고 옵셔널 요구사항인 increment(forCount:)가 구현되어있다는 보장이 없습니다. 따라서 increment(forCount:)가 구현되어있지 않을 가능성도 옵셔널 체이닝에 의해 처리됩니다. increment(forCount:)에 대한 호출은 increment(forCount:)가 존재하는 경우에만 발생합니다. 이것이 increment(forCount:)도 이름 뒤에 물음표가 있는 이유입니다.

이런 두 가지 이유로 인해 increment(forCount:) 호출이 실패할 수 있으므로 increment(forCount:)은 옵셔널 Int값을 반환합니다. increment(forCount:)CounterDataSource의 정의에서 옵셔널이 아닌 Int값을 반환하도록 되어있더라도 마찬가지 입니다. 두 개의 옵셔널 체이닝이 차례로 있지만 결과는 하나의 옵셔널로 래핑 됩니다. 여러 개의 옵셔널 체이닝에 관한 자세한 내용은 Linking Multiple Levels of Chaining을 참고해주세요.

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

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

다음 코드는 dataSource가 쿼리 될 때마다 상수 값 3을 반환하는 간단한 CounterDataSource 구현입니다. 이는 옵셔널 요구사항인 fixedIncrement 프로퍼티를 구현합니다.

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

ThreeSource의 인스턴스를 새로운 Counter 인스턴스의 dataSource로 사용할 수 있습니다.

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

위 코드는 새로운 Counter 인스턴스를 생성하고 dataSource를 새로운 ThreeSource 인스턴스로 설정합니다. counterincrement() 메서드를 네 번 호출하면 예상대로 countercount 프로퍼티는 increment()가 호출될 때마다 3씩 증가합니다.

다음은 Counter 인스턴스가 현재 count 값에서 0을 향해 증가 혹은 감소하도록 하는 TowardsZeroSource라는 좀 더 복잡한 data source입니다.

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 class는 CounterDataSource protocol의 옵셔널 increment(forCount:) 메서드를 구현하고 count 매개변수를 사용하여 계산할 방향을 계산합니다. count가 이미 0 이면 메서드는 0을 반환하고 더 이상 계산이 발생하지 않아야 한다고 나타냅니다.

기존 Counter 인스턴스와 함께 TowardsZeroSource의 인스턴스를 사용해서 -4에서 0까지 계산할 수 있습니다. count가 0이 되면 더 이상 계산을 하지 않습니다.

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

Protocol Extensions

Protocol은 메서드, 생성자, 서브 스크립트, 계산 프로퍼티 구현을 자신을 채택한 타입에게 제공하려면 extension을 사용하면 됩니다. 이를 통해 각 타입의 적합성, 전역 함수가 아닌 protocol 자체에 대한 동작을 정의할 수 있습니다.

예를 들어, RandomNumberGenerator protocol에 extension을 사용하여 randomBool()메서드를 제공하도록 할 수 있습니다. 이 메서드는 필수적인 random()메서드의 결과를 사용하여 임의의 Bool 값을 반환합니다.

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

Protocol에 extension을 사용함으로써 protocol을 채택한 모든 타입은 별다른 수정 없이 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"

Protocol의 extension은 자신을 채택한 타입에 구현을 추가할 수 있지만 다른 protocol를 상속할 수는 없습니다. Protocol의 상속은 항상 protocol을 선언할 때만 가능합니다.

Providing Default Implementations

Protocol extension을 사용해서 해당 protocol가 요구하는 모든 메서드 또는 계산 프로퍼티에 대한 기본 구현을 제공할 수 있습니다. 자신을 채택한 타입이 요구사항을 이미 구현한 경우에는 protocol extension에서 구현한 것 말고 해당 타입 내에서 구현한 것이 사용됩니다.

Note: extension에서 protocol의 요구사항을 구현하는 것은 옵셔널 protocol 요구사항과는 다릅니다. 채택한 타입이 자체적으로 요구사항을 구현할 필요는 없지만 이미 protocol extension에 구현이 되어있으므로 옵셔널 체이닝 없이 바로 사용할 수 있습니다.

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

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

Adding Constraints to Protocol Extensions

Protocol extension을 정의할 때 해당 타입이 extension의 메서드와 프로퍼티를 사용할 수 있기 전에 충족해야 하는 제약조건을 지정할 수 있습니다. 제네릭 where 절을 사용하여 extension을 사용할 protocol 이름 뒤에 제약조건을 작성할 수 있는데요, generic where절에 대한 자세한 내용은 Generic Where Clauses를 참고해주세요.

예를 들어 Element가 Equatable protocol을 준수하는 모든 컬렉션에만 적용되는 protocol extension을 정의할 수 있습니다. 컬렉션의 element를 표준 라이브러리의 일부인 Equatable protocol로 제한하면 ==, != 연산자를 사용하여 두 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, differentNumbersallEqual() 메서드를 사용할 수 있습니다.

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

Note: 어떤 타입이 동일한 메서드, 프로퍼티에 대한 구현을 제공하는 여러 개의 제약된 extension의 조건을 만족하는 경우 Swift는 가장 specialezed 한 제약조건에 해당하는 구현을 사용합니다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함