Search

[Swift 공식 문서] 22. Protocols - 2

Adopting a Protocol Using a Synthesized Implementation

Swift 는 Equatable, Hashable, Comparable 과 같은 프로토콜의 준수를 다양한 경우에 자동으로 준수하도록 하는 기능을 제공합니다.
Swift는 아래의 커스텀타입의 종류에 대해 Equatable 의 구현을 제공합니다.
Equatable 프로토콜을 준수하는 저장프로퍼티만을 가지는 Structure
Equatable 프로토콜을 준수하는 associtated type 만을 가지는 Enumeration
associated type 이 존재하지 않는 Enumeration
== 이 사용 가능하기 위해서, == 연산자를 스스로 구현안해도 된다., Equatable 에 대한 준수를 선언해주기만 하면 된다(자동). Equatable 프로토콜은 기본적으로 != 의 사용도 제공한다.
아래는 3차원 벡터를 모델링한 Vector3D 구조체에 대한 예시이다. x,y,z 프로퍼티들이 모두 Equatable 한 타입이므로 Vector3D 는 동등연산자가 사용 가능해진다.
struct Vector3D: Equatable { var x = 0.0, y = 0.0, z = 0.0 } let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0) let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0) if twoThreeFour == anotherTwoThreeFour { print("These two vectors are also equivalent.") } // Prints "These two vectors are also equivalent."
Swift
복사
Swift 는 아래의 커스텀 타입들에 대해 Hashable의 종합적인 구현을 제공한다.
Hashable 한 저장 프로퍼티만을 가지는 Structure
Hashable 한 associated type 을 가지는 enumeration
associated type 을 갖지 않는 Enumeration
종합적인 hash(into:) 의 사용을 위해서는 Hashable 의 준수를 선언해 주면 된다 (따로 메서드를 구현하지 않아도 됨 = 자동 제공).
Swift 는 또한 raw value 를 갖지 않는 enumeration 에 대해 종합적인 Comparable 의 구현을 자동으로 제공합니다. 열거형이 associated type 을 가지지 않는다면, Comparable 프로토콜을 모두가 준수할 것입니다. < 연산자를 직접 구현하지 않고 < 연산자를 구현하려면 Comparable 을 준수함을 선언만 해주면 됩니다. Comparable 프로토콜은 기본적으로 <=, >, >= 와 같은 비교 연산자들을 기본적으로 제공합니다.
아래의 SkillLebel 열거형은 그에 대한 예시입니다.
enum SkillLevel: Comparable { case beginner case intermediate case expert(stars: Int) } var levels = [SkillLevel.intermediate, SkillLevel.beginner, SkillLevel.expert(stars: 5), SkillLevel.expert(stars: 3)] for level in levels.sorted() { print(level) } // Prints "beginner" // Prints "intermediate" // Prints "expert(stars: 3)" // Prints "expert(stars: 5)"
Swift
복사
모든 case 가 associated type 을 가지지 않으므로 Comparable 을 준수함으로, SkillLevel 옆에 Comparble 을 준수함을 명시만 해주면다면 비교 연산자를 사용할 수 있습니다.

Collections of Protocol Types

프로토콜을 컬렉션에 저장되는 타입을 나타내는 데에도 사용이 가능합니다. 아래 예시는 TextRepresentable 을 준수하는 타입들을 담는 어레이를 나타냅니다,
let things: [TextRepresentable] = [game, d12, simonTheHamster]
Swift
복사
이렇게 선언된 어레이는 순회가 가능해지고 각각에 대해 text decription 을 출력할 수 있습니다.
for thing in things { print(thing.textualDescription) } // A game of Snakes and Ladders with 25 squares // A 12-sided dice // A hamster named Simon
Swift
복사
위 예시에서 thingTextRepresentable 타입임에 주목하자. thingDice 타입도 DiceGame 타입도 Hamster 타입도 아니다. TextRepresentable 타입이므로 TextRepresentable 의 구현사항인 textualDescription 프로퍼티에는 접근이 가능해진다.

Protocol Inheritance

프로토콜은 또 다른 프로토콜로 상속되어 더 많은 요구사항을 추가할 수 있다. 프로토콜 상속의 구문은 클래스의 상속과 비슷하지만, 클래스와 달리 콤마로 구분해 여러개의 프로토콜을 동시에 상속 받을 수 있다.
protocol InheritingProtocol: SomeProtocol, AnotherProtocol { // protocol definition goes here }
Swift
복사
아래는 TextRepresentable 을 상속받은 프로토콜의 예시이다,
protocol PrettyTextRepresentable: TextRepresentable { var prettyTextualDescription: String { get } }
Swift
복사
위 예시에서는 새 프로토콜 PrettyTextRepresentable 을 정의했다. 이 프로토콜은 TextRepresentable 프로토콜을 상속 받는다. 따라서 PrettyTextRepresentable 을 채택한다면 PrettyTextRepresentable 의 요구사항 뿐만 아니라 TextRepresentable 의 요구사항을 만족해야한다.
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 } }
Swift
복사

Class-Only Protocols

AnyObject 프로토콜을 상속받아, 상속받은 프로토콜을 클래스 타입에만 채택 가능하도록 제한할 수 있습니다.
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol { // class-only protocol definition goes here }
Swift
복사
SomeClassOnlyProtocol 은 이제 class 에만 채택이 가능해 집니다. 구조체나 열거형에 해당 정의를 실행하려 한다면 컴파일 에러가 발생합니다.
정의한 프로토콜이 referece 타입임이 요구되거나 요구사항이 referece 타입임을 필요로 하는 경우에 사용하면 됩니다.

Protocol Composition

하나의 타입에 여러개의 프로토콜을 준수시키는 것은 유용합니다.
protocol composition 을 사용하면, 여러개의 프로토콜을 하나로 조합할 수 있습니다. protocol composition 을 사용하면, 임시로 새 프로토콜을 정의한 것 처럼 보이는데요. 하지만 프로토콜 컴포지션은 어떠한 프로토콜도 새로이 정의하지 않습니다.
& 을 사용해서 프로토콜들을 조합할 수 있습니다. 아래를 보실게요.
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!"
Swift
복사
위 예시를 보시면 NamedAged 프로토콜을 조합해서 파라미터의 요구사항으로 정의해놓은 것을 볼 수 있습니다. Person 구조체는 이 두 프로토콜을 모두 준수하기에, celebrator 파라미터에 전달할 수 있게 됩니다.
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!"
Swift
복사
위와 같이 클래스의 상속 여부와 프로토콜의 준수여부를 조합하여 사용할 수도 있습니다.
Location & Named 타입이 의미하는 바는, 아래와 같습니다.
Location 클래스를 상속받으며,
Named 프로토콜을 준수하는 타입
위 두조건을 모두 만족해야 Location & Named 타입으로 사용이 가능합니다.
하지만 이 또한 새로운 타입이나 프로토콜을 만드는 것이 아닙니다.

Checking for Protocol Conformance

isas 연산자를 사용해서 프로토콜의 준수여부를 점검할 수 있다. 프로토콜 점검 및 캐스팅은 아래와 같은 구문으로 작동한다.
is 연산자는 해당 인스턴스가 프로토콜을 준수하는 경우에만, true 를 리턴한다.
as? 연산자는 프로토콜 타입의 옵셔널한 값을 리턴한다. 다운캐스팅이 실패한 경우 nil 값을 리턴하게 된다.
as! 연산자는 강제로 프로토콜의 타입을 다운캐스팅하며, 실패하는 경우 런타임에러를 발생시킨다.
protocol HasArea { var area: Double { get } }
Swift
복사
class Circle: HasArea { let pi = 3.1415927 var radius: Double var area: Double { return pi * radius * radius } init(radius: Double) { self.radius = radius } } class Country: HasArea { var area: Double init(area: Double) { self.area = area } }
Swift
복사
위 두 클래스는 모두 HasArea 프로토콜을 준수한다.
class Animal { var legs: Int init(legs: Int) { self.legs = legs } }
Swift
복사
위 Animal 클래스는 프로토콜을 준수하지 않는다.
let objects: [AnyObject] = [ Circle(radius: 2.0), Country(area: 243_610), Animal(legs: 4) ]
Swift
복사
위 3가지 클래스들의 인스턴스를 하나의 어레이에 담았다.
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
Swift
복사
하나의 어레이에 담겨져 있어 for 문으로 순회할 수 있다. 순회함과 동시에 각각이 HasArea 를 준수하는 프로토콜 타입으로 다운캐스팅을 시도한다.
HasArea 프로토콜을 준수하는 경우, 다운캐스팅에 성공하고, HasArea 의 필수 요구사항에 해당하는 프로퍼티 또는 메서드에 접근이 가능해진다. 위 예제에서는 area 프로퍼티에 접근이 가능해진다.
이 순회과정에서 다운캐스팅이 일어났으나, 각각의 인스턴스들은 본연의 타입임에 주의하자, 각각 Circle, Country, Animal 타입이다. 하지만 objectWithArea 상수에서는 HasArea 의 프로토콜을 준수함만이 보장되므로, area 프로퍼티에만 접근 가능하다.

Optional Protocol Requirements

프로토콜에 선택적 요구사항을 정의할 수 있다. 선택적 요구사항은 꼭 구현하지않아도 프로토콜을 준수하는 데 문제가되지 않는다. 선택적 요구사항을 활용하면, Objective C 코드와 상호작용하는 코드를 작성하기 쉽다. 프로토콜과 선택적요구사항 모두 @objc 특성으로 표시되어야한다. @objc 프로토콜들은 오직 @objc 클래스 로 부터 상속받은 클래스에만 적용이 가능함에 주의하자.
선택적 요구사항내의 프로퍼티 또는 메서드를 사용할 때, 객체의 타입은 자동으로 optional 한 타입이된다. 예를 들어, (Int) → String 타입은 ((Int) → String)? 타입이 된다. 함수의 리턴타입이 아니라, 전체 함수 타입이 optional 로 감싸져 있음에 주의하자
선택적 요구사항은 optional chaining 에 의해 호출이 가능하다. ? 물음표를 활용해서, optional 메서드가 구현되어있는지 점검해야한다. 이런식으로 말이다. someOptionalMethod?(someArgument)
@objc protocol CounterDataSource { @objc optional func increment(forCount count: Int) -> Int @objc optional var fixedIncrement: Int { get } }
Swift
복사
위 예시에서 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 } } }
Swift
복사
CounterDataSourceincrement 메서드는 optional 한 구현사항이므로, Counterincrement() 내부에서 dataSourceincrement 에 접근할 때, ? 와 함께 구현여부를 체크하면서 호출하는 모습을 볼 수 있다.
두번의 optional chaining 이 일어나고 있는 데,
1.
먼저 dataSource 가 nil 인지 확인하고
2.
dataSource 에 increment 가 구현되어있는 지 확인한다.
optional 메서드는 리턴 타입이 optional 하지 않아도 optional 한 값을 리턴하게 된다.
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
Swift
복사
아래는 더 복잡한 예시의 TowardsZeroSource 이다, 여기에는 CounterDataSource 의 optional 구현사항인 increment 가 아래와 같이 정의되어 있다.
class TowardsZeroSource: 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 counter.dataSource = TowardsZeroSource() for _ in 1...5 { counter.increment() print(counter.count) } // -3 // -2 // -1 // 0 // 0
Swift
복사

Protocol Extensions

프로토콜은 메서드, 생성자, 첨자접근, 계산프로퍼티를 제공하기위해 확장될 수 있다. 이를 통해, 프로토콜 그자체에 행동을 정의할 수 있다.
extension RandomNumberGenerator { func randomBool() -> Bool { return random() > 0.5 } }
Swift
복사
프로토콜에 extension 을 생성하면, 이 프로토콜을 준수하는 모든 타입은 자동으로 해당 구현사항을 갖게된다. 어떠한 변경사항도 없이!!
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"
Swift
복사
RandomNumberGenerator 를 준수하는 모든 타입이 randomBool() -> Bool 을 얻게된다.
프로토콜에 extension 을 적용하면 이를 준수하는 타입에 구현사항을 추가할 수 있지만, 프로토콜 자체를 확장하거나, 다른 프로토콜을 상속받게 할 수 없다. 프로토콜의 상소긍ㄴ 항상 프로토콜의 정의부에서만 가능하다.

Providing Default Implementation

프로토콜의 extension 을 사용해서 기초 구현사항을 모든 메서드, 컴퓨티드 프로퍼티의 요구사항에 제공할 수 있다. 만약 준수하는 타입이 요구되는 메서드와 프로퍼티를 구현했다면, extension 에 정의된 것 기본 구현사항 대신에 타입에 정의된 구현사항이 사용된다.
예를들어, 아래의 경우 PrettyTextRepresentableprettyTextualDescription 기본 구현사항을 extension 에서 제공하고 있으나, 타입에서 prettyTextualDescription 를 새로이 구현하게 되면 타입에 정의된 내용이 실행되며, extension 에 구현된 사항은 무시된다.
extension PrettyTextRepresentable { var prettyTextualDescription: String { return textualDescription } }
Swift
복사

Adding Constraints to Protocol Extensions

프로토콜의 extension을 작성할 때, 준수하는 타입이 만족해야하는 제약조건을 명시할 수 있습니다. 확장하고자 하는 프로토콜의 이름 뒤에 이러한 제약조건을 작성하세요.
예를 들어, 아래의 Collection 프로토콜의 extension 은 Equatable 프로토콜을 만족하는 요소를 담는 모든 collection 에 적용됩니다. 컬렉션의 요소가 Equatable 프로토콜을 준수하는 것으로 제한함으로서, 기본 구현사항의 구현부에서 == 또는 != 를 사용할 수 있게 됩니다.
extension Collection where Element: Equatable { func allEqual() -> Bool { for element in self { if element != self.first { return false } } return true } }
Swift
복사
위 예제의 allEqual() 메서드는 컬렉션 내부의 모든 원소가 동일한 경우에 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"
Swift
복사
만약 어레이의 타입이 해당 제약조건 즉, 담겨있는 요소가 Equatable 하지 않을 경우에는 이러한 기본 구형사항을 적용 받지 않습니다.