본문 바로가기
Swift 공식 가이드/Swift 3

Protocols (3)

by 토끼찌짐 2017. 3. 25.

Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Protocols 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.



프로토콜 3편입니다. Objective-C에 비해 스위프트에서는 프로토콜이 강력해졌는데요. 예를 들면 프로토콜 확장을 통해서 그 프로토콜을 따르는 녀석들에게 메서드를 제공해줄 수도 있습니다. 이번 시간에는 프로토콜 합성, 프로토콜을 따르는지 체크, 프로토콜 확장하는 법을 알아보고, 이런 것들을 통해 우리가 무엇을 할 수 있는지 살펴보겠습니다.

1, 2편을 아직 안 읽으셨다면 먼저 읽어보세요.

Protocols 1편 링크 > http://wlaxhrl.tistory.com/57

Protocols 2편 링크 > http://wlaxhrl.tistory.com/58



프로토콜 합성Protocol Composition

프로토콜 합성은 한번에 여러 프로토콜을 따르는 타입이 필요할 때 유용하게 활용할 수 있다. SomeProtocol & AnotherProtocol 형태로 합성하면 되고, 필요한 만큼 & 로 연결해서 합성할 프로토콜을 추가할 수 있다.

(여담이지만 스위프트2에서는 protocol<Some, Another, ...> 식이었습니다. 스위프트3에서는 protocol이라는 키워드조차 없어졌네요. 이제 Objective-C만 했던 사람이 Some & Another & ... 을 보면 이게 뭔가 할 것 같습니다ㅎㅎ)

일단 예제를 보자.

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("생일축하해 \(celebrator.name), 너는 이제 \(celebrator.age)살이 되었구나")
}

let birthdayPerson = Person(name: "예림", age: 23)
wishHappyBirthday(to: birthdayPerson)
// "생일축하해 예림, 너는 이제 23살이 되었구나"

<note> 프로토콜 합성은 새로운 영구적인 프로토콜 타입을 만드는 것이 아니라, 프로토콜들의 요구사항을 합쳐놓은 임시 로컬 프로토콜temporary local protocol을 정의하는 것이다. scope를 벗어나면 의미가 없어지는 것.



프로토콜을 따르는지 체크하기(= 프로토콜 일치 확인 = Checking for Protocol Conformance)

어떤 타입이 특정 프로토콜을 따르고 있는지 체크해보기 위해 isas 연산자를 사용할 수 있다. 타입 캐스팅 단원에서 다룬 타입 체크와 유사한 방식이다.

  • is : 인스턴스가 특정 프로토콜을 따르면 true, 아니면 false 리턴

  • as? : 인스턴스가 특정 프로토콜을 따르면 프로토콜 타입의 옵셔널 값을 리턴, 아니면 nil 리턴

  • as! : 강제로 프로토콜 타입으로 다운캐스팅하고, 실패 시에는 런타임 에러


예제를 보자.

protocol HasArea {
    var area: Double { get }
}

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

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

// conform X
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 {
        // 실제로 object는 Circle, Country 타입이지만
        // objectWithArea는 HasArea 타입으로 다운캐스팅 되어서
        // objectWithArea를 통해서는 area 프로퍼티에만 접근 가능하다
        // (Circle, Country 자체의 프로퍼티에 접근불가)
        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



프로토콜의 선택적 요구사항Optional Protocol Requirements

프로토콜에 선택적 요구사항optional requirements을 정의할 수 있다. 선택적 요구사항은 프로토콜을 따르는 타입이 반드시 구현하지 않아도 되는 요구사항이며 optional 수식어로 표기한다.

프로토콜이 @objc 속성일 때만 선택적 요구사항을 정의할 수 있다. 해당 프로토콜이 Objective-C와 상호운용될 일이 없더라도 @objc 속성으로 만들어야 한다. 또한 그렇게 정의된 프로토콜은 Objective-C 클래스를 상속받은 클래스, @objc 클래스만이 따를 수 있다. (구조체, ENUM은 불가)

옵셔널 요구사항이 되면 그 파라미터/메서드의 타입 자체가 자동으로 옵셔널이 된다. 예를 들어 (Int) -> String((Int) -> String)?이 된다.

프로토콜을 따르는 타입이 선택적 요구사항을 구현하지 않았을 가능성이 있기 때문에, 선택적 요구사항은 옵셔널 체이닝을 이용해 호출하도록 한다. 예를 들어 someOptionalMethod?(someArgument).

// 수를 더하는 방식을 위한 프로토콜
// 옵셔널 요구사항을 위해 프로토콜과 요구사항에 둘 다 @objc를 붙여야 한다
@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    // 옵셔널이기 때문에 ((Int)->(Int))? 타입이 된다
    
    @objc optional var fixedIncrement: Int { get }
    // 옵셔널이기 때문에 Int? 타입이 된다
}
// <note> 요구사항이 모두 optional이기 때문에
// 요구사항을 하나도 맞추지 않아도 이 프로토콜을 따를 수 있다


// 수를 더하는 데 쓰일 클래스
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
        }
    }
}


이제 CounterDataSource를 따르는 클래스들의 예제를 보자.

// CounterDataSource 프로토콜을 구현한 클래스 (프로퍼티 요구사항만 맞춤)
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 프로토콜을 구현한 클래스2 (메서드 요구사항만 맞춤)
@objc class TowardZeroSource: 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 // 0으로 초기화
counter.dataSource = TowardZeroSource()

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



프로토콜 확장

프로토콜은 자신을 따르는 타입에게 메서드와 프로퍼티를 제공하기 위해 확장될 수 있다.

간단한 예제를 보자.

protocol RandomNumberGenerator {
    func random() -> Double
}

// 확장하여 randomBool 이라는 메서드를 추가했다
extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

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) % m)
        return lastRandom / m
    }
}

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.37464991998171"
print("And here's a random Boolean: \(generator.randomBool())")
// 프로토콜을 따르는 것만으로도 randomBool 메서드를 사용가능
// Prints "And here's a random Boolean: true"



<기본 구현 제공하기>

프로토콜 확장을 통해 프로토콜의 요구사항(메서드, 프로퍼티)에 대한 기본 구현을 제공할 수 있다. 물론 프로토콜을 따르는 타입이 알아서 요구사항을 구현했다면 확장을 통한 기본 구현 대신 자신이 구현한 코드가 동작한다.

<note> 프로토콜 확장을 통한 프로토콜 요구사항의 기본 구현과, 프로토콜 선택적 요구사항은 구분된다. 프로토콜을 따르는 타입이 요구사항을 구현하지 않아도 된다는 것은 두 경우 모두 해당하지만, 전자의 경우에는 옵셔널 체이닝 없이도 호출될 수 있다. (후자는 호출할 때 옵셔널 체이닝이 꼭 필요)

protocol TextRepresentable {
    var textualDescription: String { get }
}

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

extension PrettyTextRepresentable {
    var prettyTextualDescription: String {
        return textualDescription
    }
}
// 이제 PrettyTextRepresentable를 따르는 타입은
// prettyTextualDescription 프로퍼티를 스스로 구현하지 않아도
// 기본 구현이 자동으로 사용된다


class MyTextRepresentable: PrettyTextRepresentable {
    var textualDescription: String = "My Text Representable";
}

let myTextRepresentable = MyTextRepresentable()
print(myTextRepresentable.textualDescription)
print(myTextRepresentable.prettyTextualDescription)
// My Text Representable
// My Text Representable



<프로토콜 확장에 제약을 추가Adding Constraints to Protocol Extensions>

프로토콜 확장을 정의할 때, 확장이 제공하는 메서드/프로퍼티를 사용하기 위해, 그 프로토콜을 따르는 타입이 반드시 지켜야 하는 제약을 지정할 수 있다. 제약은 where 절을 사용하여 표기한다.

예제로 알아보자.

// Collection의 아이템이 
// TextRepresentable 프로토콜을 따르고 있어야만 허용
extension Collection where Iterator.Element: TextRepresentable {
    var textualDescription: String {
        let itemAsText = self.map {
            $0.textualDescription
        }
        return "[" + itemAsText.joined(separator: ",") + "]"
    }
}

struct Hamster: TextRepresentable {
    var name: String
    var textualDescription: String {
        return "\(name)라는 이름의 햄스터"
    }
}

let hamster1 = Hamster(name: "햄토리")
let hamster2 = Hamster(name: "햄순이")
let hamster3 = Hamster(name: "햄돌이")

let hamsters = [hamster1, hamster2, hamster3]
print(hamsters.textualDescription)
// "[햄토리라는 이름의 햄스터, 햄순이라는 이름의 햄스터, 햄돌이라는 이름의 햄스터]"

let notHamster = "나는 그냥 스트링"
let testList: [Any] = [hamster1, hamster2, notHamster]
print(testList.textualDescription)
// 컴파일 에러.
// Type 'Any' does not conform to protocol 'TextRepresentable'



<note> 만약 타입이 제한된 확장constrained extensions을 여러 개 중복으로 만족시키고, 그 확장들에서 제공하는 메서드/프로퍼티의 이름이 같을 경우에는 어떻게 될까? 가장 세분화된 제한이 되어있는the most specialized constraints 확장을 사용하게 된다. 이게 무슨 말인지 예제를 통해 알아보자.

extension Collection where Iterator.Element: TextRepresentable {
    var textualDescription: String {
        let itemAsText = self.map {
            $0.textualDescription
        }
        return "[" + itemAsText.joinWithSeparator(",") + "]"
    }
}

extension CollectionType where Iterator.Element: Any {
    var textualDescription: String {
        return "[ Any 타입의 아이템들 ]"
    }
}

let hamster1 = Hamster(name: "햄토리")
let hamster2 = Hamster(name: "햄순이")
let hamster3 = Hamster(name: "햄돌이")

// hamsters에 들어있는 아이템은 앞에서 정의한 두 개의 확장에 모두 들어맞는 케이스
let hamsters = [hamster1, hamster2, hamster3]
print(hamsters.textualDescription)
// "[햄토리라는 이름의 햄스터, 햄순이라는 이름의 햄스터, 햄돌이라는 이름의 햄스터]" 가 출력된다
// 첫 번째 확장을 주석처리하면?
// 그때는 "[ Any 타입의 아이템들 ]"가 출력된다


'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글

Access Control  (2) 2017.04.01
Generics  (0) 2017.03.29
Protocols (2)  (0) 2017.03.25
Protocols (1)  (0) 2017.03.23
Extensions  (1) 2017.03.23