GGURUPiOS

Swift 공식 문서 정리 - (22) Protocols 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (22) Protocols

꾸럽 2023. 5. 14. 15:41

Protocols (프로토콜)

프로토콜은 특정 기능 수행에 필수적인 요소를 나타내는 청사진 임

프로토콜을 만족시키는 타입을 프로토콜을 따른다(conform) 고 말함.

프로토콜에 필수 구현을 추가하거나 추가적인 기능을 더하기 위해 프로토콜을 확장 하는 것이 가능함

프로토콜 문법

protocol SomeProtocol {
    // protocol definition goes here
}

프로토콜을 따르는 타입을 정의하기 위해서는 타입 이름 뒤에 콜론(:) 을 붙이고 따를 프로토콜 이름을 적는다.

여러개라면 콤마로 구분해주자

만약 슈퍼클래스가 있는 경우 프로토콜 앞에 슈퍼클래스 이름을 먼저 나열하자

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

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 FullyNamed {
    var fullName: String { get }
}

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

다음과 같이 계산된 프로퍼티로 사용 가능

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"

메소드 요구사항

프로토콜에서는 필수 인스턴스 메소드와 타입 메소드를 명시할 수 있음

하지만 메소드 파라미터의 기본 값은 프로토콜 안에서 사용 불가

protocol SomeProtocol {
    static func someTypeMethod()
}

필수 메소드 지정시 함수명과 반환 값을 지정할 수 있고, 구현에 사용하는 괄호 ( {} )는 적지 않아도 됨

protocol RandomNumberGenerator {
    func random() -> Double
}

다음 코드는 따르는 프로토콜의 필수 메소드 random() 을 구현한 클래스

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 키워드를 사용해 인스턴스에서 변경 가능하다는 것을 표시할 수 있음

이 mutating 키워드는 값타입 형에서만 사용한다.

프로토콜에 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

이니셜라이저 요구사항

프로토콜에서 필수로 구현해야 하는 이니셜라이저를 지정할 수 있음

protocol SomeProtocol {
    init(someParameter: Int)
}

클래스에서 프로토콜 필수 이니셜라이저의 구현

프로토콜에서 특정 이니셜라이저가 필요하다고 명시했기 때문에 구현에서 해당 이니셜라이저에 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
    }
}

실패가능한 초기자 요구사항

프로토콜에서 실패가능한 이니셜라이저를 선언할 수 있음

타입으로써의 프로토콜

프로토콜도 하나의 타입으로 사용됨

때문에 다음과 같이 타입 사용이 허용되는 모든곳에 프로토콜을 사용할 수 있음

  • 함수, 메소드, 이니셜라이저의 파라미터 타입 혹은 리턴 타입
  • 상수, 변수, 프로퍼티의 타입
  • 컨테이너인 배열, 사전 등의 아이템 타입

프로토콜은 타입이기 때문에 첫 글자를 대문자로 적어주자.

다음 예시를 보자

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 프로토콜을 따르는 인스턴스를 넣는다.

위임 (Delegation)

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

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

DiceGame 프로토콜을 선언하고 DiceGameDelegate에 선언해서 실제 DiceGame의 행위와

관련된 구현을 DiceGameDelegate를 따르는 인스턴스에 위임함

DiceGameDelegate를 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는 DiceGame를 따르고 DiceGameDelegate를 따르는 델리게이트 delegate를 갖습니다. 
게임을 실행(play()) 했을 때 
delegate?.gameDidStart(self), 
delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll),
delegate?.gameDidEnd(self)를 실행합니다.
delegate는 게임을 진행시키는데 반드시 필요한 건 아니라서 옵셔널로 정의돼 있습니다.
*/

실제 DiceGameDelegate를 상속하는 delegate 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")
    }
}

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

게임의 tracking 관련된 작업은 DiceGameTracker가 위임받아 그곳에서 실행 됨

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

이미 존재하는 타입에 익스텐션으로 새 프로토콜을 따르게 할 수 있음

protocol TextRepresentable {
    var textualDescription: String { get }
}

익스텐션을 이용해 Dice를 TextPeresentable 프로토콜을 따르도록 구현 해보자

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

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

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

특정 조건을 만족시킬때만 프로토콜을 따르도록 제한할 수 있음

where 절을 사용해 정의

아래 예제는 TextRepresentable을 따르는 Array중에 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]"

익스텐션을 이용해 프로토콜 채용 선언하기

만약 어떤 프로토콜을 충족에 필요한 모든 조건을 만족하지만

아직 그 프로토콜을 따른다는 선언을 하지 않았다면 그 선언을 빈 익스텐션으로 선언할 수 있음

아래 코드는 프로토콜을 따른다는 선언은 익스텐션에 하고, 실제 프로토콜을 따르기 위한 구현은 구조체 원본에 구현

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"

프로토콜 타입 컬렉션

프로토콜을 Array, Dictionary 등 컬렉션 타입에 넣기위한 타입으로 사용 가능함.

아래는 TextRepresentable 프로토콜을 따르는 객체 Array에 대한 선언

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

// Array의 모든 객체는 TextRepresentable 을 따르므로 textualDescription 프로퍼티 가짐

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

프로토콜 상속

클래스 상속같이 프로토콜도 상속할 수 있음

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

TextRepresentable 프로토콜을 상속 받은 프로토콜 (PrettyTextRepresntable)

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

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 에 PrettyTextRepresenetable 프로토콜을 채택하고 구현 

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

클래스 전용 프로토콜

클래스 타입에만 사용가능한 프로토콜을 선언하기 위해서는 프로토콜에 AnyObject를 추가하면 됨

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // class-only protocol definition goes here
}
// 참조 구문만 필요한 프로토콜을 정의하기 위해 클래스 전용 프로토콜을 사용함

프로토콜 합성

동시에 여러 프로토콜을 따르는 타입을 선언할 수도 있음

예제를 보자

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

wishHappyBrithdat 메소드의 celebrator 파라미터는 Named프로토콜과 Aged 프로토콜을 동시에 따르는 타입으로 선언

아래 예제는 Location 프로토콜과 위의 Named 프로토콜을 따르는 City 클래스를 구현한 예시임

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

프로토콜 순응 확인

어떤 타입이 특정 프로토콜을 따르는지 확인하는 방법

  • is연산자를 이용하면 어떤 타입이 특정 프로토콜을 따르는지 확인할 수 있습니다. 특정 프로토콜을 따르면 true를 아니면 false를 반환합니다.
  • as?는 특정 프로토콜 타입을 따르는 경우 그 옵셔널 타입의 프로토콜 타입으로 다운캐스트를 하게 되고 따르지 않는 경우는 nil을 반환합니다.
  • as!는 강제로 특정 프로토콜을 따르도록 정의합니다. 만약 다운캐스트에 실패하면 런타임 에러가 발생합니다.

예제를 살펴보자

area라는 double값을 필요로 하는 HasArea 프로토콜 선언

protocol HasArea {
    var area: Double { get }
}

HasArea 프로토콜을 따르는 Cricle, Country 클래스와 프로토콜을 따르지 않는 Animal 클래스 선언

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}

// Cricle은 area를 계산된 프로퍼티로 구현

class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

// Country 는 area를 저장 프로퍼티로 구현

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

프로토콜을 확인하기 위해 배열안에 넣고, for 문으로 다운캐스트 하며 확인

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

선택적 프로토콜 요구 조건

프로토콜을 선언하면서 필수 구현이 아닌 선택적 구현 조건을 정의 할 수 있음

@objc키워드를 프로토콜 앞에 붙이고, 개별 함수 혹은 프로퍼티에는 @objc와 optional 키워드를 붙임

@objc 프로토콜은 클래스 타입에서만 채용될 수 있고 구조체나 열거형에서는 불가

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

위의 CounterDataSource를 따르면서 구현은 하나도 하지 않는 클래스를 선언 할 수 있음

→ 기술적으로는 가능하나, 좋은 구현방법이 아님

→ 하나도 구현하지 않는다면 필요하지 않으므로, 그냥 선언을 하지말 것

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

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

프로토콜 익스텐션

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

아래 코드는 random() 을 따르는 RandomNumberGenerator 에 randomBool()을 따르도록 추가한 예시임

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"

익스텐션을 이용해 구현을 추가할 수는 있어도 다른 프로토콜로 확장/상속 할수는 없음

→ 익스텐션이 아닌 프로토콜 자체에 구현해야 함

기본 구현 제공

익스텐션을 기본 구현을 제공하는데 사용할 수 있음

특정 프로토콜을 따르는 타입 중에서 그 프로토콜의 요구사항에 대해 자체적으로 구현한게 있으면 그것을 사용하고 아니면 기본 구현을 사용하게 됨.

즉 프로토콜에서는 선언만 할 수 있는데 익스텐션을 이용해 기본 구현을 제공할 수 있음

프로토콜 익스텐션에 의해 구현된 기본 구현은 옵셔널 요구조건과 다름

둘다 꼭 구현하지 않아도 되는것은 같지만 사용시 기본 구현은 옵셔널 체이닝을 사용하지 않아도 됨 ( 기본구현된게 있으므로)

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

프로토콜 익스텐션에 제약 추가

프로토콜 익스텐션이 특정 조건에서만 적용되도록 선언할 수 있음

(위의 조건적으로 프로토콜 따르기랑 같은 듯?)

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"