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)
어떤 타입이 특정 프로토콜을 따르고 있는지 체크해보기 위해 is와 as 연산자를 사용할 수 있다. 타입 캐스팅 단원에서 다룬 타입 체크와 유사한 방식이다.
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 |