Swift Language Guide: Protocol

J.Noma·2021년 10월 7일
0

Swift : 문법

목록 보기
2/11
post-thumbnail

Reference
SwiftLanguageGuide : Protocol


Remind

  • 저장 프로퍼티를 요구하거나 확장할 수 없습니다
  • Protocol은 Nested type을 요구하거나 extension으로 기본구현을 제공할 수 없습니다
  • Protocol Extension으로 제공한 기본구현과 동일한 프로퍼티/메서드를 채택자가 구현하면 기본구현은 사용되지 않습니다. (단, override와는 다르게 타입 캐스팅으로 기본구현에 접근할 수 있습니다)
  • 프로토콜에서 제네릭이 필요할 때 사용하는게 associated type이다
  • 프로토콜의 요구사항/공통구현은 private이 불가능하다

🐶 서문

프로토콜은
어떤 타입이 특정 기능을 하기 위하여 위한 어떤 메소드, 프로퍼티, 일련의 요구사항들을 구현해야 하는지 정의하는 것이다.
일종의 청사진이라 볼 수 있다

어떤 타입이 특정 프로토콜을 따르도록 등록하는 것을, 우리는 프로토콜을 채택한다라고 표현한다

또한, 그 타입이 프로토콜의 요구조건을 만족한다면 프로토콜에 순응(conform)했다라고 표현한다

그리고, 프로토콜도 extend될 수 있다 (feat. extension)


🐱 Syntax

여러 개의 프로토콜을 채택할 수도 있습니다

Superclass가 있다면 먼저 명시해주고 다음으로 프로토콜을 명시하면 됩니다

// 프로토콜 선언
protocol SomeProtocol {
    // protocol definition goes here
}

// 프로토콜 채택
struct SomeStructure: SomeProtocol {
    // structure definition goes here
}
// Superclass가 있다면 먼저 명시
class SomeClass: SomeSuperclass, SomeProtocol {
    // class definition goes here
}


🐭 프로퍼티

프로토콜은 순응(conform)한 타입에게 프로퍼티 구현을 요구할 수 있습니다

참고로, stored 프로퍼티로 구현하냐 / computed 프로퍼티로 구현하냐는 프로토콜이 관여하지 않습니다

✔️ 명시해야 하는 것

  1. 이름
  2. 타입
  3. get / set - (set은 선택)
  4. static - (선택)

✔️ 유의사항

  • get/set 모두를 요구하면 상수로 정의할 수 없습니다

  • get만 요구한 경우 get/set 모두 구현해도 무관합니다

  • 변수(var)로만 정의 가능

  • 타입 프로퍼티는 static으로 통일
    (실제 구현이 classstatic이든)



🐹 메소드

프로퍼티와 마찬가지로, 타입에게 메소드 구현을 요구할 수 있습니다

✔️ 명시해야 하는 것

  1. 이름
  2. return 타입
  3. Argument
  4. static - (선택)

✔️ 유의사항

  • Argument에 Default value는 줄 수 없습니다
protocol RandomNumberGenerator {
    func random() -> Double
}
class LinearCongruentialGenerator: RandomNumberGenerator {
    func random() -> Double {
        lastRandom = ((lastRandom * a + c)
            .truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}


🐰 Mutating 메소드

짐작가듯이, mutating 메소드 앞에는 mutating을 붙혀주면 됩니다

✔️ 유의사항

  • class에는 없는 개념이므로 실제 구현에서 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
        }
    }
}


🦊 Initializer (생성자)

생성자를 요구할 수 있다

✔️ 명시해야 하는 것

  1. Argument
  2. required - (선택)
  3. init? - (선택)

required
Required Initializer

init?
Failable Initializer

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


🐔 Nested Type (안됨)

Nested type은 요구할 수 없습니다



🐻 타입처럼 사용하기

프로토콜은 실제로 어떠한 기능도 직접 구현하지는 않습니다.
그럼에도 불구하고 당신은 프로토콜을 (어떤 경우에) 완전한 타입으로 사용할 수 있습니다

참고로, 타입으로써 프로토콜을 사용하는 것은 때때로 existential 타입이라고 불립니다.
existential이란 표현은 "
T가 어떤 프로토콜을 준수하기 위해 '타입'T가 존재(exist)한다"라는 구문에서 파생되었습니다 (무슨 말일까.. 프로토콜을 준수하기 위해 타입이 존재한다?)

📌 타입들에게 허용되는 많은 곳에서 프로토콜 또한 사용할 수 있습니다

  • 함수/메서드/생성자의 parameter나 return
  • 상.변수/프로퍼티
  • collection의 요소

참고로, 프로토콜도 타입이므로 첫 글자는 대문자로 사용합니다


✔️ 타입으로써의 프로토콜은 upcasting 개념이 가미된 것이다

예제를 먼저 살펴봅시다

protocol RandomNumberGenerator {
    func random() -> Double
}

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 프로토콜을 준수하는 어떠한 타입이든 들어갈 수 있습니다

대신, 타입이 프로토콜로 지정되어 있으므로 해당 프로토콜에 명세되어 있는 프로퍼티/메서드만 사용할 수 있는 상태입니다.

이는 마치 부모 타입으로 upcastring된 것과 유사한 원리로, downcasting을 통해 원래 타입으로 되돌리면 원래 타입에만 정의된 프로퍼티/메서드를 사용할 수 있습니다 (as키워드)



🐼 Delegation

Delegation은 어떤 class나 struct가 자신의 역할/책임을 다른 타입의 객체에게 넘기는 디자인 패턴입니다

이 디자인 패턴은 위임할 책임들을 프로토콜에 정의함으로써 구현됩니다
이로써 delegation을 위해 만든 프로토콜을 준수하는 타입(= delegate)은 위임받은 기능들을 제공할 수 있음을 보장하게 됩니다

✔️ Protocol+Delgation의 용도

Delegation은 특정 action에 반응할 때도 사용되고,
외부소스로부터 data를 찾아오려는데 해당 소스의 근본(underlying)타입을 모를 때 사용될 수 있습니다
(ex. Table view는 각 cell에 대한 data를 불러와서 띄워줘야 하는데, cell마다 타입이 죄다 다를지언정 Table view는 이를 몰라도 불러올 수 있음)

✔️ 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)
}
// 위임자(from)
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)
    }
}
// 위임대상(to)
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")
    }
}

Delegation 패턴 만들기

1. 위임자(from)가 위임대상(to)을 프로퍼티로 갖는다 (직접소유든 참조든)

2. 위임자(from)가 해야할 기능을 위임자의 메서드에 직접 구현하지 않고 위임대상(to)의 메서드에 구현한다
(여기서, 위임대상(to) 메서드에서 위임자(from) 객체를 알아야 하는 경우(아마 많은 경우), 파라미터로 넘기거나 프로퍼티로 참조를 저장하는 등의 방법이 있다. 덕분에 순환참조 risk가 있어 weak 사용 고려 필요)

3. 기능을 수행하기 위해 위임자(from)의 메서드 내에서 위임대상(to)의 메서드를 호출함으로써 대신 수행하도록 만든다

✔️ 그래서, Delegation을 왜 Protocol로 구현하는가?

Cocoa Framework에서 적극적으로 사용되고 있는데 가장 큰 이유는 확장성/재사용성 때문이다

(우선, 이 장점은 직전 챕터에서 프로토콜을 타입처럼 사용할 수 있다는 점 덕분이다. 이해가 안되면 직전 챕터 복습고고)

Framework에선 기본 뼈대만 만들어놓고
본 내용이라 할 수 있는 프로토콜의 요구사항 구현은 개발자가 하도록 만들었다

기본 뼈대는 프로토콜 '정의부'과 이 프로토콜을 타입처럼 쓰는 인스턴스 '사용부'를 말하며,
이렇게 구현하면 해당 프로토콜을 채택하는 모든 타입이 하나의 사용부를 같이 쓸 수 있으므로 재사용성을 취할 수 있다

본 내용 구현을 프로토콜을 통해 개발자가 하므로 추가가 용이하다. 즉, 확장성이 좋다
우리가 새로운 무언가를 만들기 위해 할 일은 단지 Framework가 '정의'한 프로토콜을 준수하는 타입을 만들고
Framework가 구현해놓은 '사용부'를 재사용하면 Framework가 만들어놓은 판 위에서 바로 뛰어놀 수 있게 된다
(증말 똑똑해..)

Framework가 프로토콜을 정의하는 경우가 아니더라도,
일반적으로 확장성/재사용성을 취하기 위해 Protocol+Delegation 조합을 사용할 수 있다



🐻‍❄️ 프로토콜 추가 채택시키기 (feat. extension)

extension을 통해, 어떤 타입의 소스코드에 접근할 수 없더라도 새로운 프로토콜을 채택시키고 요구사항 구현을 추가할 수 있습니다

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

❗️주의❗️
프로토콜 간 상속을 추가하는 건 extension으로 할 수 없습니다
지금 다루는 것은 '일반타입'이 '채택할 프로토콜'을 추가하는 것입니다

✔️ 조건부 채택 (feat. 제네릭 Where절)

제네릭 타입은 특정 조건 하에서만 프로토콜 요구사항을 만족할지도 모릅니다
(예로, 타입의 제네릭 파라미터가 프로토콜을 준수할 때)

아무튼, 이건 제네릭 타입만 가능합니다

당신은 extension에 where절로 조건을 나열해서, 제네릭 타입이 어떤 프로토콜을 선택적으로 채택하도록 할 수 있습니다.

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

✔️ maybe.. 요구사항도 함께 확장해야 합니다

프로토콜을 추가해주려는데, 이미 요구사항들을 만족하고 있는 상태였기는..
희박하므로 extension을 해줄 때 해당 프로토콜에 대한 요구사항 구현도 같이 추가해주는 경우가 흔합니다

물론 이미 만족하면 프로토콜 추가만 하고 구현은 안 해줘도 됨



🐨 채택만 하면 자동 구현되는 프로토콜

원래 프로토콜을 채택하면 그 세부내용을 타입에 구현해야 합니다

그런데, Swift 표준 라이브러리의 일부 프로토콜은 채택만 해도 자동으로 구현됩니다.

예로, Equatable, Hashable, Comparable 등이 있는데 어떤 타입이 이 프로토콜들은 채택만 하면 ==연산자 같은 것들을 바로 쓸 수 있습니다. 신기하죠?

하지만, 이런 자동구현은 특정 조건 하에서만 자동으로 들어갑니다

각 프로토콜의 자동 구현 조건을 살펴보겠습니다
(3개 프로토콜 전부 class에선 자동 구현되지 않습니다)

Equatable 자동 구현 조건
1. 구조체의 stored 프로퍼티 타입들이 전부 "Equatable"할 때
2. 열거형의 associated type이 전부 "Equatable"할 때
3. 열거형의 "associated type"이 없을 때

Hashable 자동 구현 조건
1. 구조체의 stored 프로퍼티 타입들이 전부 "Hashable"할 때
2. 열거형의 associated type이 전부 "Hashable"할 때
3. 열거형의 "associated type"이 없을 때


🐯 프로토콜 collection

프로토콜을 타입으로 사용할 수 있다는 점 덕분에, 하나의 collection에 해당 프로토콜을 채택하는 서로 다른 타입의 인스턴스들을 저장할 수 있습니다

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

for thing in things {
    print(thing.textualDescription)
}

위 예제와 같이 iterate over도 가능하지만,
기억해야 할 점은 thing은 프로토콜 타입(TextRepresentable)이므로 프로토콜에 정의된 프로퍼티/메서드만 사용할 수 있는 상태입니다
(본래 타입의 것을 사용하고 싶다면 타입 캐스팅 필요)



🦁 프로토콜 상속

프로토콜은 다른 프로토콜을 상속하여 요구사항들을 전달받고 거기에 자신의 요구사항들도 정의하여 추가할 수 있습니다
(syntax는 class와 비슷하지만 다중 상속이 가능하다는 차이가 있습니다)

상속관계에 있는 프로토콜을 채택한 타입은 최초 부모부터 자식 프로토콜까지에 정의된 그 모든 요구사항들을 만족해야 합니다



🐮 Class-only 프로토콜

어떤 프로토콜이 conform하려는 타입이 참조타입일 것을 가정/요구한다면, class만 상속가능하도록 프로토콜에 제한을 걸 수 있습니다

정의하려는 프로토콜이 AnyObject를 상속받게 하면 class-only 프로토콜이 됩니다

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

SomeClassOnlyProtocol을 class가 아닌 타입이 채택하면 컴파일 에러를 유발합니다



🐮 프로토콜 composition

타입은 여러 프로토콜을 채택할 수 있는데 '프로토콜 composition'을 통해 하나의 요구사항으로 결합시킬 수 있습니다

프로토콜 composition은 마치 모든 프로토콜의 요구사항들이 결합된 '임시 local 프로토콜'을 정의한 것처럼 동작합니다
(어떤 새로운 프로토콜을 정의하는게 아님에 주의)

프로토콜 composition은 & 키워드를 사용하여 프로토콜들을 결합시킵니다 (개수는 얼마든지 가능)

프로토콜들을 결합하는 것 뿐만 아니라 class도 하나 포함시킬 수 있습니다

아래와 같이 채택뿐 아니라 타입(함수파라미터, collection 등)으로 사용가능합니다

아래 예제에서 Named & Aged을 타입으로 쓸 때는, 두 프로토콜을 '모두' 채택한 타입만 올 수 있습니다

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

class를 포함시킨 composition 예제도 살펴봅시다
마찬가지로 Location & Named은 class를 상속함과 '동시에' 프로토콜을 따르는 타입만 올 수 있습니다

protocol Named {
    var name: String { get }
}
class Location {
    var latitude = 30.0
    var longitude = 40.0
}
class City: Location, Named {
    var name: String = "Daegu"
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}


🐷 프로토콜 타입 캐스팅

프로토콜도 타입이므로 타입캐스팅 연산자 isas를 적극 활용할 수 있습니다

식상하지만 한번 더 정리해보자면..

  • is : 해당 프로토콜을 채택하는 인스턴스인지 확인. Boolean 반환
  • as? : 해당 프로토콜로 캐스팅. optional을 반환
  • as! : 해당 프로토콜로 강제 캐스팅.

AnyObject도 프로토콜입니다
모든 class는 AnyObject 프로토콜을 채택하고 있다는 점을 이용한 타입캐스팅 예제를 살펴봅시다

protocol HasArea {
	var area: Dobule { get }
}
class Country: HasArea {
    var area = 10.0
}
class Animal {
	var legs = 4
}

let objects: [AnyObject] = [
	Country(),
    Animal()
]

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}


🐸 Optional 요구사항

반드시 따르지 않아도 되는 요구사항을 optional 키워드를 앞에 붙혀서 정의할 수 있다

대체 이걸 어디 쓰느냐? Objective-C와 상호작용하는 코드에서.

그래서, 프로토콜과 요구사항 모두에 @objc attribute를 붙혀야 합니다

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

이 프로토콜은 'class'만 가능하며, NSObject같은 'Objective-C class' 혹은 '@objc class'를 상속하는 class만 채택이 가능합니다
(라고는 하는데 일반 class도 되더이다.. 수정됐나?;)

옵셔널 프로토콜을 타입으로 사용하면
optional 프로토콜에 정의한 메서드/프로퍼티는 자동으로 옵셔널 타입이 됩니다
(예로, (Int) -> String타입의 메서드는 ((Int) -> String)?가 됩니다)
그러므로, optional 프로토콜의 요구사항은 옵셔널 타입임을 명시하기 위해 '옵셔널 체이닝'으로 호출될 수 있습니다

❗️주의❗️
물론, 옵셔널 프로토콜을 타입으로 쓸 때 그런 것이고, 이를 채택하여 구현한 custom class 타입일 때는 구현이 되어 있음이 명백하니 옵셔널이 아닙니다

옵셔널 메서드 호출예시

someOptionalMethod?(someArgument)

Swift Language Guide 본문에 예제가 많으므로 필요 시 참고



🐵 프로토콜의 extension (상속과 유사)

"프로토콜의 extension이란, 요구사항을 추가하는 것?"이 아닙니다
요구사항이 아니라 일반적인 타입의 extension처럼 '구현체'를 추가하게 됩니다
(이거 왠지.. 채택만 하면 구현도 자동으로 되는 프로토콜들이 머릿 속을 스친다..)

// 원래 프로토콜 정의부 : 요구사항만 정의
protocol RandomNumberGenerator {
	func random() -> Double  
}

// 프로토콜 extension : 구현체를 정의
extension RandomNumberGenerator {
    func randomBool() -> Bool { 
        return random() > 0.5
    }
}

이렇게 프로토콜의 extension을 주면, 프로토콜을 채택하는 타입들에게 채택과 동시에 구현체까지 전달하여 별도로 구현은 안해도 된다

즉, extension을 갖춘 프로토콜을 채택하는 것은 마치 '상속'과 유사한 효과를 준다

❗️주의❗️
저장 프로퍼티와 Nested type은 프로토콜 extension으로 구현해줄 수 없다
계산 프로퍼티와 메서드만 가능

❗️주의❗️
extension에서 'protocol'을 추가하는 것(=프로토콜 상속)은 할 수 없다
프로토콜 상속은 정의부에서만 설정할 수 있다

✔️ Default 구현 제공하기

프로토콜 extension은 메서드나 계산 프로퍼티의 default 구현을 제공하기 위해 사용될 수 있습니다

하지만 conforming 타입이 자신만의 구현을 만들면, 프로토콜 extension으로 추가한건 사용되지 않습니다

📝 NOTE
'conforming타입이 구현해도 되고 안해도 된다'는 관점에서 옵셔널 프로토콜과 유사하다고 생각할 수 있는데, extension은 옵셔널 체이닝이 불필요하다는 차이가 있습니다

✔️ 조건부 프로토콜 extension (feat. 제네릭 where절)

위에서 다루었던 제네릭 '일반타입'에 조건걸기과 유사하게 제네릭 '프로토콜'에 대해서도 조건부로 extension을 걸 수 있습니다

마찬가지로 제네릭 where절을 활용합니다

아래 예제에서 Collection 프로토콜 중(제네릭이므로 여러 종류가 있을 수 있음) Element가 Equatable을 준수하는 경우에 한해 allEqual() 메서드를 추가해줍니다

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

📝 NOTE
하나의 프로토콜에 대하여 이런 조건부 extension을 여러 개를 걸 수 있습니다.
그러면 어떤 경우엔, 여러 개의 조건을 모두 만족시켜서
하나의 프로토콜에 조건부 extension이 여러 개 적용되는 경우도 있습니다

그런데, 이 extension들이 같은 메서드/프로퍼티 구현체를 추가해준다면 어느 것을 따르게 될까요?
Swift는 가장 specialized한 조건이 걸린 구현체를 따르게 한다고 합니다
(specialized가 무슨 의미지..)

profile
노션으로 이사갑니다 https://tungsten-run-778.notion.site/Study-Archive-98e51c3793684d428070695d5722d1fe

0개의 댓글