[Swift] 21. Protocol

도윤·2021년 8월 22일
0

Swift

목록 보기
20/21

제가 듣는 강의 내용 중 Protocol에 관한 내용을 학습했기 때문에 순서를 무시하고 Protocol에 관한 내용 복습 겸 내용을 정리해보겠습니다!


Protocol

프로토콜은 특정 일이나 함수에 적합한 요구사항이나 메서드나 프로퍼티의 청사진을 정의하는 것이다. 프로토콜은 class,enum,structure에서 채택할 수 있다. 프로토콜의 요구 사항을 만족하는 타입은 프로토콜에 conform한다고 말한다.


Protocol Syntax

프로토콜의 사용 방법은 class,structrue,enumerations과 비슷하다.

protocol SomeProtocol {
    // protocol definition goes here
}

정의한 프로토콜을 채택하는 방법은 다음과 같다.

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

만약 super클래스를 가지고 있다면, 슈퍼클래스를 제일 처음 적으면 된다.

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

Property Requirements

프로토콜은 자신이 채택한 프로퍼티의 타입에 대해 어떤 프로퍼티를 구현해야 하는지 요할 수도 있다. 프로포콜은 프로토콜이 저장 프로퍼티인지 계산 프로퍼티인지 구체화하지 않는다. 단지 프로퍼티의 타입과 이름만 구체화한다. 또한 프로토콜은 각 프로퍼티가 gettable 또는 getter&setter인지 반드시 구체화 해야한다.

만약 프로토콜이 gettable&settable되는 프로퍼티를 요구한다면, 프로퍼티는 constant이면 안된다. 만약 gettable이라면 let이여도 상관이 없다.

프로퍼티를 요구할때는 항상 변수 타입으로 선언해야 한다. gettable과 settable은 타입 표시 후 {getter,setter}가 쓰여야한다.

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

프로토콜에서 타입 프로퍼티를 선언하려면 static키워드를 사용해야 한다.
클래스나 구조체에서도 구현할때도 이 규칙이 적용된다.

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}
protocol FullyNamed {
    var fullName: String { get }
}

위의 코드는 하나의 프로터피를 요구하는 프로토콜이다. 이는 FullyNames 프로토콜을 채택한 타입은 fullName이라는 gettable 프로퍼티를 가지고 있어야 한다는 것을 의미한다.

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

FullyNamed를 채택한 Person 구조체이다.
위의 구조체는 Protocol이 선언한 규칙을 따르고 있다. 따르지 않는다면 Compile-error가 발생한다.

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이 protocol이 지정한 규칙들을 따르므로 그 외의 다른 코드는 어떻게 작성해도 상관이 없다.


Method Requirements

프로토콜은 자신을 채택한 타입에 메서드를 정의하도록 요구할 수 있다. 이때 요구하는 메서드는 인스턴스 메서드, 타입 메서드 모두 가능하다. 프로토콜에서 이러한 메서드를 정의할 땐 중괄호나 메서드의 본문은 필요 없다. 하지만 메서드에 필요한 매개변수는 정의해 줘야 하는데, 이때 default값은 지정할 수 없다.

마찬가지로 타입 메서드를 작성할 때 static을 사용해야 한다.

protocol SomeProtocol {
    static func someTypeMethod()
}
protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator는 random이라는 메서드가 double을 반환하는 타입을 갖도록 지정.

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 키워드를 사용하여 메서드를 요구해야 한다. 하지만 참조 타입인 class에서는 mutating 키워드를 사용하지 않아도 된다. 이 경우 프로토콜에 mutating 키워드가 있더라도 클래스에서 채택하여 사용할 땐 mutating을 써주지 않아도 된다.

protocol Togglable {
    mutating func toggle()
}
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

Initailizer Requirements

프로토콜은 타입에 맞게 특정한 생성자를 요구할 수 있다.
일반 생성자와 동일하게 프로토콜의 생성자를 사용하면 되지만 중괄호나 내용은 필요하지 않는다.

protocol SomeProtocol {
    init(someParameter: Int)
}

Class Implementations of Protocol Initializer Requirements

프로토콜에서 생성자는 designated 생성자, convenience 생성자 모두 요구할 수 있다. 생성자를 요구하는 프로토콜을 클래스에서 채택하면 required 식별자를 사용해서 생성자를 정의해야 한다.

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

만약 클래스 자체가 상속받을 수 없는 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
    }
}

Failable Initializer Requirements

실패가능한 생성자를 요구할 수도 있다.


Protocol as Types

프로토콜 자체는 어떠한 기능을 하지 않는다. 하지만 코드에서 타입으로써 사용할 수 있다. 타입으로써 프로토콜을 사용하는 것은 existential type이라고 불리는데 프로토콜을 따르는 타입T가 존재한다.

  • 함수,메서드,생성자에서 매개변수 타입이나 반환 타입
  • 상수,변수,프로퍼티의 타입
  • 배열,dictionary와 같은 타입
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
    }
}

위의 코드에서 generator 프로퍼티는 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
    }
}
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

RandomNumberGenerator을 채택한 LinearCongruentialGenerator을 변수타입으로 사용하였다.


Delegation

클래스나 구조체가 책임의 일부를 다른 타입의 인스턴스에게 건내는 디자인 패턴이 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)
}

DiceGameDelegate 프로토콜을 이용하여 DiceGame의 진행 현황을 확인할 수 있다.
클래스 전용 프로토콜은 AnyObject로부터의 상속으로 표시됩니다.

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클래스의 정의이다.
DiceGameDelegate 프로토콜을 채택하여 DiceGame의 진행 상황을 추적할 수 있다.

여기에는 프로토콜을 따르는 paly() 메서드와 gettable dice 프로퍼티가 있다.
delegate가 게임을 하는것을 요구하지 않기 때문에 delegate 프로퍼티는 optional DiceGameDelegate로써 정의되었다. 또한 옵셔널 타입이라서 초기값은 Nil로 선언될 것이다. 이후 게임 인스턴스화자는 속성을 적합한 대리인으로 설정할 수 있습니다. DiceGameDelegate 프로토콜은 클래스 전용이기 때문에 참조 주기를 방지하기 위해 대리인을 weak으로 선언할 수 있습니다.

DiceGameDelegate은 게임 진행을 추적하는 세가지 메서드를 제공하고 있는데 이것은 DiceGame의 play()의 프로토콜을 잘 따르고 있다.

delegate프로퍼티는 optional DiceGameDelegate이기 때문에, play() 메서드는 delegate에서 함수 호출할 때 항상 옵셔널 체이닝을 통해 호출해야 한다. 만약 Nil이라면 에러없이 호출에 실패한다.
만약 delegate가 nil이 아니라면, delegate 메서드는 호출되고 매개변수로써 SnakesAndLadders 인스턴스에 전달된다.

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

gameDidStart는 플레이하려는 게임에 대한 일부 정보를 프린터하기 위해 game 매개변수를 사용한다. game 매개변수는 SnakeAndLadders가 아닌 DiceGame타입이고 GameDidStart(_:) DiceGame 프로토콜의 일부로 구현된 프로퍼티와 메서드만 사용하고 접근할 수 있다.


Adding Protocol Conformance with an Extension

기존 타입의 소스코드에 접근할 수 없는 경우에도 extension을 사용하여 새로운 프로토콜을 채택하고 준수할 수 있다. extension으로 프로토콜을 채택하면 기존의 인스턴스도 프로토콜을 자동으로 채택한다.

protocol TextRepresentable {
    var textualDescription: String { get }
}

어떠한 타입에 새로운 프로토콜을 채택할 수 있다.

위의 Dice클래스는 TextRepresentabel을 채택하고 따르면서 extended될 수 있다.

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

extension은 새로운 프로토콜을 채택한 것이다. 프로토콜의 이름은 type name뒤에 (:)으로 구분하여 붙이고 프로토콜의 모든 요구사항들의 실행은 extentsion의 중괄호안에서 제공된다.

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice
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

Generic타입은 타입의 Generic 매개변수가 프로토콜을 따를 때와 같은 상황에서만 요구사항을 충족할 수 있다. 타입을 확장할 때 제약조건을 나열하여 프로토콜을 따를 수 있다. 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]

Array 인스턴스가 TextRepresentable을 준수하는 타입의 요소를 작성할때마다 프로토콜을 준수하도록 하는 코드이다

Declaring Protocol Adoption with a Extension

프로토콜이 모든 요구사항을 따르지만 아직 프로토콜을 따른다고 작성하지 않았다면, 빈 extension을 이용하여 프로토콜을 채택하도록 만들 수 있다.

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"

이렇게 하면 Hamster 구조체는 TextRepresentable 프로토콜을 준수하게 된다. 프로토콜을 자동으로 채택할 수 없고 채택하기 위해서는 명시적으로 선언해줘야 하는 것을 알 수 있다.


Adopting a Protocol Using a Synthesized implementation

swift는 많은 경우에서 Equatable,hashable,Comparable 프로토콜을 따르는 것들을 제공한다.

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

Swift에서는 Equatable 합성 구현을 다음과 같은 사용자 정의 타입에 제공한다.

  • 저장 프로퍼티만 소유한 구조체들은 Equatable 프로토콜을 따른다.
  • associated types만 소유한 열거형은 Equatable 프로토콜을 따른다
  • assoicated type을 가지지 않는 열거형

어떤 타입에서 == 연산자를 사용하고 싶다면 직접 구현하지 않고 Equatable 프로토콜을 채택하면 된다. != 연산자의 기본 구현도 제공한다.

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에서는 Hashble 합성 구현을 다음과 같은 사용자 정의 타입에 제공한다.

  • Hashble 프로토콜을 따르는 저장 프로퍼티만 있는 구조체
  • Hashble 프로토콜을 따르는 associated type만 존재하는 열거형
  • assoicated type을 가지지 않는 열거형

hash(into:)를 사용하기 위해 스스로 구현할 필요 없이 Hashble 프로토콜을 따르도록 설정하면 된다.

Swift는 원시 값이 없는 열거형을 위해 Comparable 합성 구현을 제공한다. 만약 어떤 열거형이 Comparable 프로토콜을 채택했고 associated 타입들을 가지고 있다면 해당 타입들은 모두 Comparable 프로토콜을 준수해야 한다. 따라서 어떤 타입에 대해 < 연산자를 사용하고 싶다면 < 연산자를 직접 구현하지 말고 Comparable 프로토콜을 채택하면 된다. 이는 <=, >, >= 비교 연산자도 함께 제공한다.

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,Dictionary에 저장되는 타입으로도 사용될 수 있다.

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

위의 코드처럼 Array의 타입을 아까 정의한 TextRepresentable 프로토콜 타입으로 정의하면 해당 프로토콜을 준수하는 요소들로 Array를 채울 수 있다. 실제 타입은 달라도 모두 TextRepresentable 프로토콜을 준수하기 때문에 하나의 Array에 사용될 수 있다.


Protocol Inheritance

프로토콜은 다른 프로토콜을 상속할 수 있다. 이때 상속받은 프로토콜에서 요구하는 사항에 추가적인 사항을 요구할 수 있다. 프로토콜을 상속하는 방법은 클래스 상속과 비슷하다.

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

여러 프로토콜을 상속받으려면 (,)로 구분하여 여러개의 프로토콜을 작성하면 된다.

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

TextRepresentable 프로토콜을 상속받는 PrettyTextRepresentable 프로토콜이다. PrettyTextRepresentable을 채택한 어떤 것이라도 TextRepresentable 프로토콜의 요구사항도 모두 만족해야 한다. 게다가 PrettyTextRepresentable에서 정의한 요구사항 또한 만족해야 한다.

extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\n"
        for index in 1...finalSquare {
            switch board[index] {
            case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

위의 코드와 같이 기존에 존재하던 타입에 익스텐션을 사용하여 프로토콜을 추가로 채택할 수 있다.


Class-Only Protocol

프로토콜을 작성할 때 AnyObject를 상속받으면서 구조체나 열거형이 아닌 class에만 적용할 수 있는 프로토콜을 작성할 수 있다.

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

SomeClassOnlyProtocol는 class 타입에 의해서만 채택할 수 있다. 구조체나 열거형에서 채택할 경우 컴파일 에러가 발생.

value semantic보다는 reference semantic을 할 경우 주로 사용.


Protocol Composition

동시에 여러 프로토콜을 따르도록 사용할 수 있다. protocol composition으로 한개의 요구사항 속에 여러개의 프로토콜을 결합할 수 있다. 프로토콜 컴포지션은 결합하려는 모든 프로토콜에 대한 요구사항이 결합된 프로토콜을 정의한 것처럼 작동한다. 프로토콜 컴포지션은 새로운 프로토콜 타입을 정의하는 것은 아니다.

프로토콜 컴포지션은 SomeProtocol&AnotherProtocol 따위의 형태를 가진다. &로 구분하여 필요한 만큼 프로토콜을 나열할 수 있다. 프로토콜 컴포지션에는 프로토콜 목록 이외에도 슈퍼클래스를 지정하는 데 사용하는 클래스를 포함할 수 있다.

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

매개변수 celebrator는 Named&Aged 프로토콜을 따라야 한다. 이 매개변수는 Person 구조체로 생성한 인스턴스를 매개변수로 가질 수 있다.

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

위의 코드는 프로토콜과 클래스를 프로토콜 컴포지션으로 하나의 타입처럼 사용한 예이다. City클래스는 Location클래스를 상속받고 Named 프로토콜을 채택한 클래스이다. 해당 클래스를 beginConcert 함수에서 매개변수로 사용하기 위해 프로토콜 컴포지션을 사용했다. 즉 여기선 Location & Named 타입을 매개변수로 사용한 것이다.


Checking for Protocol Conformance

프로토콜을 따르는지 확인하거나 특정 프로토콜로 캐스팅하기 위해 as,is 연산자를 사용할 수 있다.

  • is 연산자 : 인스턴스가 프로토콜을 따르면 true,아니면 false를 반환한다.
  • as? : 프로토콜의 타입의 옵셔널 값을 반환하고 만약 따르지 않는다면 nil을 반환.
  • as! : 강제로 프로토콜 타입으로 다운 캐스팅하지만 만약 성공하지 않는다면 runtime error가 발생
protocol HasArea {
    var area: Double { get }
}
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 }
}
class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}
let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]
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

Country와 Circle은 모두 HasArea 프로토콜을 따르고 있으므로 as? 를 사용했을 때 성공하지만 Animal은 성공하지 않으므로 nil값이 반환된다.


Optional Protocol Requirements

프로토콜에서 요구사항을 optional으로 정의할 수 있다. 이렇게 정의한 요구사항은 프로토콜을 채택하고 준수할 때 반드시 정의하지 않아도 된다. 선택적 요구 사항을 설정할 땐 optional를 붙여줄 수 있다. Objective-C와 상호 운용되는 코드를 작성하기 위해 선택적 요구사항을 사용할 수 있는데 이땐 @objc 타입으로 표시되어야 한다. @objc 프로토콜은 Objective-C 클래스 또는 다른 @objc 클래스에서 상속된 클래스만 채택할 수 있고 구조체나 열거형에서는 채택할 수 없다.

선택적 요구 사항에서 메서드 또는 프로퍼티를 사용하면 해당 타입이 자동으로 옵셔널 값이 된다. 예를 들어 (Int) -> String 타입의 메서드가 ((Int) -> String)? 타입이 되는 것이다.

옵셔널 프로토콜은 옵셔널 체이닝으로 호출될 수 있다.

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}
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라는 클래스를 정의한 코드이다. 이 클래스에서 정의한 dataSource 프로퍼티는 CounterDataSource 프로토콜을 채택한 인스턴스이며 이 프로토콜에는 두 가지 optional 요구 사항이 있다.

Counter 클래스의 increment() 메서드에서 dataSource에 increment 메서드가 구현되어있는지 확인하고 구현 여부에 따라 실행을 다르게 하는 조건문이 작성되어있다. 이러한 부분은 옵셔널 체이닝으로 처리한 것을 볼 수 있다

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

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

위의 코드는 CounterDataSource를 채택한 ThreeSource 클래스를 정의하고 이를 Counter 클래스 인스턴스의 프로퍼티에 할당하여 사용한 예이다. fixedIncrement 프로퍼티를 3으로 할당하여 호출될 때마다 count 프로퍼티의 값이 3씩 증가되게 만든 예이다.

class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0

increment가 정의되어 있어서 dataSource를 TowardsZeroSource()로 선언했을 때 count의 implement가 optional 체이닝에서 성공하므로 increment가 정상적으로 호출된다.


Protocol Extensions

프로토콜은 메서드,생성자,서브스크립트, 계산 프로퍼티를 extension할 수 있다. 이를 통해 각 타입의 적합성, 전역 기능이 아닌 프로토콜 자체에서 동작을 정의할 수 있다.

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// Prints "And here's a random Boolean: true

위의 코드는 RandomNumberGenerator 프로토콜에 익스텐션을 사용하여 randomBool메서드의 구현을 추가한 것이다. 프로토콜 익스텐션은 프로토콜을 준수하는 타입에 구현을 추가할 수 있지만 프로토콜을 확장하거나 다른 프로토콜에서 상속할 수는 없다. 프로토콜 상속은 항상 프로토콜 선언 시 지정된다.

Providing Default Implementations

계산 프로퍼티나 메서드 어디에도 default 구현을 제공하는 프로토콜을 작성할 수 있다 이런 프로토콜을 채택한 타입이 필수 메서드나 프로퍼티의 자체 구현을 정의하였을 경우엔 익스텐션에서 제공한 구현 말고 타입이 정의한 구현이 사용된다.

익스텐션에서 제공하는 기본 구현이 있는 프로토콜 요구 사항은 선택적 요구사항과는 다르다. 익스텐션에서 제공하는 구현은 프로토콜을 채택한 타입에서 자체 구현을 제공할 필요는 없지만 만약 구현했다면 옵셔널 체인 없이 호출할 수 있다는 차이점이 있다.

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

Adding Contraints to Protocol Extensions

where절을 이용하여 프로토콜에 제약을 작성할 수 있다

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

element가 Equatable을 따르는 컬렉션에만 적용되는 프로토콜이다.

0개의 댓글