개발, 공부, 일상 블로그

[Swift] 프로토콜 (Protocols)

|
[Swift] 프로토콜 (Protocols)

프로토콜

특정 기능 수행에 필수적인 요소를 정의한 청사진.
프로토콜을 만족시키는 타입을 프로토콜을 따른다(conform)고 말한다.
프로토콜에 필수 구현을 추가하거나 추가적인 기능을 더하기 위해 프로토콜을 확장(extend)할 수 있다.

자바의 interface와 유사하다.

프로토콜 문법

protocol 키워드를 사용하여 정의

protocol SomeProtocol {
    ...
}

프로토콜을 따르는 타입을 정의하기 위해서 상속처럼 표현한다.

class SomeClass: SomeProtocol, AnotherProtocol {
    ...
}

서브클래싱인 경우 슈퍼클래스를 프로토콜 앞에 적어준다.

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

프로퍼티 요구사항

프로토콜에서는 프로퍼티가 저장된 프로퍼티인지 계산된 프로퍼티인지 명시하지 않는다.
하지만 프로퍼티의 이름타입 그리고 gettable, settable 한지는 명시한다.
필수 프로퍼티는 항상 var로 선언해야한다.

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

타입 프로퍼티는 static 키워드를 적어 선언한다.

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

메소드 요구사항

타입 메소드

protocol SomeProtocol {
    static func someTypeMethod()
}

인스턴스 메소드

protocol RandomNumberGenerator {
    func random() -> Double
}

변경 가능한 메소드 요구사항

protocol Togglable {
    mutating func toggle()
}
enum ToggleSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = ToggleSwitch.off
lightSwitch.toggle()

초기자 요구사항

protocol SomeProtocol {
    init(someParameter: Int)
}

프로토콜에서 특정 이니셜라이저가 필요했다고 명시했기 때문에 구현에서 required 키워드를 붙여줘야 한다.

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        ...
    }
}

타입으로써의 프로토콜

타입 사용이 허용되는 모든 곳에 프로토콜을 사용할 수 있다. (자바의 interface 처럼)

  • 함수, 메소드, 이니셜라이저의 파라미터 타입 혹은 리턴 타입
  • 상수, 변수, 프로퍼티의 타입
  • 컨테이너인 배열, 사전 등의 아이템 타입
protocol RandomNumberGenerator {
    func 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
    }
}

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

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("주사위의 눈: \(d6.roll())")
}

위임

클래스 혹은 구조체 인스턴스에 특정 행위에 대한 책임을 넘길 수 있게 해주는 디자인 패턴

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}
class OddEven: DiceGame {
    let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
    weak var delegate: DiceGameDelegate?
    func play() {
        delegate?.gameDidStart(self)
        delegate?.game(self, diceRoll: dice.roll())
        delegate?.gameDidEnd(self)
    }
}

익스텐션을 이용해 프로토콜 따르게 하기

이미 존재하는 타입에 새 프로토콜을 따르게 하기 위해 익스텐션을 사용할 수 있다.

protocol TextRepresentable {
    var textualDescription: String { get }
}

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "\(sides)면체 주사위"
    }
}

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription) // 12면체 주사위

조건적으로 프로토콜 따르기

where 절을 사용하여 Array의 원소들이 특정 프로토콜을 따르는 경우에만 프로토콜을 따르도록 할 수 있다.

extension Array: TextRepresentable where Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}

let d6 = Dice(sides: 12, generator: LinearCongruentialGenerator())
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())

let dices = [d6, d12]
print(dices.textualDescription) // [6면체 주사위, 12면체 주사위]

익스텐션을 이용해 프로토콜을 따른다고 선언하기

이미 프로토콜의 모든 조건을 만족하지만 프로토콜을 따른다는 선언을 하지 않은 경우
빈 익스텐션으로 선언할 수 있다.

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}
// textualDescription이 구현되어 있으므로 TextRepresentable 프로토콜을 따를 수 있다.

프로토콜 상속

프로토콜도 프로토콜을 상속할 수 있다.

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    ...
}

클래스 전용 프로토콜

클래스 전용 프로토콜인 경우 프로토콜에 AnyObject를 추가한다.

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    ...
}

선택적 프로토콜 요구조건

@objc 키워드를 사용하여 필수 구현이 아닌 선택적 구현 조건을 정의할 수 있다.
프로토콜 앞에 @objc 키워드를 붙이고, 함수나 프로퍼티에 @objcoptional 키워드를 붙인다.
@objc 프로토콜은 클래스 타입에서만 사용할 수 있다.

@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() {
        // dataSource의 increment 메소드가 구현되어있지 않을 수 있기 때문에 옵셔널 체이닝을 이용한다.
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        // dataSource의 fixedIncrement 프로퍼티가 구현되어있지 않을 수 있기 때문에 옵셔널 체이닝을 이용한다.
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

프로토콜 익스텐션

익스텐션을 이용해 프로토콜을 확장할 수 있다.

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        // 이미 정의된 random 메소드를 사용하여 randomBool() 메소드를 추가
        return random() > 0.5
    }
}