[Swift 문법] Swift 공식 문서 정리 - 21 - Protocols (프로토콜)
이번 글에서는 Swift 공식 문서의 21번째 단원인 Protocols을 읽고 정리한 글을 쓰려고 합니다.
Apple Swift 공식 문서 21단원 - Protocol
Protocol
Protocol은 특정 작업이나 기능에 적합한 메서드, 프로퍼티, 요구사항의 청사진을 정의합니다. 그런 뒤 이러한 요구사항의 실제 구현을 위해 Class, Struct, Enum에서 Protocol을 채택할 수 있습니다. 이때 Protocol이 요구하는 사항을 모두 충족하면 해당 타입은 Protocol을 준수한다고 합니다.
준수해야 하는 타입의 요구사항을 정의하는 것 외에도 요구사항의 일부를 구현하거나, 준수하는 타입에 추가 기능을 구현하기 위해 Protocol을 확장할 수도 있습니다.
Protocol Syntax
Protocol을 정의하는 방법은 Class, Struct, Enum과 매우 비슷합니다.
protocol SomeProtocol {
// protocol definition goes here
}
Class, Struct, Enum을 직접 구현할 때 Protocol을 채택하고 싶다면 타입의 이름 뒤에 채택하고 싶은 Protocol의 이름을 쓰면 됩니다. Protocol은 콤마를 사용해서 여러 개를 채택할 수도 있습니다.
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
Class의 경우 상속의 문법도 Protocol 채택과 동일한데요, 부모 Class를 갖는 경우에는 부모 Class의 이름을 가장 먼저 쓰고 그 뒤에 Protocol의 이름들을 나열하면 됩니다.
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
Property Requirements
Protocol은 특정 이름과 타입을 갖는 인스턴스 프로퍼티, 타입 프로퍼티를 사용해서 요구하는 타입을 정의합니다. Protocol은 프로퍼티가 저장 프로퍼티인지 계산 프로퍼티인지는 지정하지 않고 필요한 프로퍼티의 이름과 타입만 지정합니다. Protocol은 각각의 프로퍼티가 gettable, settable 한지도 지정해줘야 합니다.
Protocol에서 gettable
, settable
인 프로퍼티를 요구할 경우 해당 프로퍼티는 상수 저장 프로퍼티 혹은 읽기 전용 계산 프로퍼티로는 요구사항을 만족시킬 수 없습니다. Protocol이 gettable
프로퍼티만 요구할 경우에는 모든 종류의 프로퍼티로 요구사항을 만족시킬 수 있고, 이런 프로퍼티는 settable
에도 유효합니다.
프로퍼티의 요구사항은 항상 var
키워드와 함께 선언됩니다. gettable
, settable
프로퍼티는 타입 선언 뒤에 { get set }
으로 작성해서 나타내며 gattable
프로퍼티는 { get }
으로 작성하면 됩니다.
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
Protocol에서 타입 프로퍼티를 정의할 땐 항상 static
키워드를 사용해야 합니다. 이러한 규칙은 Class에 Protocol을 채택한 경우에 class
, static
키워드를 쓰는 경우에도 적용됩니다.
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
다음은 하나의 프로퍼티를 요구하는 protocol의 예제입니다.
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed
protocol은 풀네임을 제공하기 위해 준수해야 하는 타입을 요구합니다. Protocol은 다른 준수하는 타입의 특성에 대한 다른 것을 지정하지는 않습니다. Protocol은 타입에게 fullName
을 제공할 수 있어야 한다는 것만 정해줍니다. Protocol을 준수하는 모든 FullyNamed
타입에는 String 타입의 fullName
이라는 gettable
인스턴스 프로퍼티가 존재해야 합니다.
다음은 FullyNamed
protocol을 채택하고 준수하는 Struct에 대한 예입니다.
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"
위 예는 특정한 이름을 갖는 사람을 나타내는 Person
struct를 정의합니다. 첫 줄에 보면 FullNamed
protocol을 채택한 것도 볼 수 있습니다.
Person
의 각 인스턴스에는 String 타입의 fullName
이라는 단일 저장 프로퍼티가 있습니다. 이는 FullyNamed
protocol의 요구 사항을 만족시키기 때문에 Person
이 FullyNamed
protocol을 올바르게 준수했음을 의미합니다. (Swift는 protocol의 요구사항을 만족시키지 않으면 컴파일 타임에 오류를 발생시킵니다.)
다음은 FullyNamed
protocol을 채택하고 준수하는 좀 더 복잡한 class 예제를 보겠습니다.
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"
위 예제의 Starship
class는 FullyName
protocol의 요구사항인 fullName
프로퍼티를 계산 프로퍼티로 구현했습니다. Starship
class의 인스턴스는 name
, prefix
를 저장 프로퍼티로 갖고 있습니다. fullName
프로퍼티는 prefix
값이 있는 경우 이를 name
의 앞부분에 붙여서 Starship
의 fullName
을 만들게 됩니다.
Method Requirements
Protocol은 자신을 채택한 타입에게 특정 인스턴스 메서드나 타입 메서드를 요구할 수도 있습니다. 이러한 메서드는 일반적인 인스턴스 메서드, 타입 메서드와 같은 방식으로 protocol 내부에 작성되지만 중괄호나 메서드 본문은 없습니다. 일반적인 메서드와 동일한 규칙에 때라 가변 매개변수는 허용되지만, protocol 정의 내에서는 메서드 매개변수에 기본값을 지정할 수는 없습니다.
요구사항으로 타입 프로퍼티를 정의했을 때와 마찬가지로 타입 메서드를 요구할 때는 항상 static
키워드를 사용해야 합니다. 이는 protocol이 요구하는 메서드를 class가 정의할 때 class
, static
키워드를 사용하는 경우에도 동일합니다.
protocol SomeProtocol {
static func comeTypeMethod()
}
다음은 하나의 인스턴스 메서드를 요구하는 protocol을 정의하는 예입니다.
protocol RandomNumberGenerator {
func random() -> Double
}
RandomNumberGenerator
protocol은 자신을 채택한 타입에게 Double 타입을 반환하는 random
이라는 인스턴스 메서드를 정의하라고 요구합니다. Protocol의 일부로 지정하지는 않았지만 이 값은 0.0 ~ 1.0 사이의 숫자를 반환한다고 해보겠습니다.
RandomNumberGenerator
Protocol은 각각의 난수가 생성되는 방식에 대한 어떠한 것도 만들지 않았습니다. Generator가 난수를 생성하는 방법만 제공하면 되죠.
다음으로 RandomNumberGenerator
protocol을 채택하고 준수하는 class를 구현해보겠습니다. 이 class는 linear congruential generator로 알려진 pseudorandom number generator 알고리즘을 구현합니다.
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 Method Requirements
메서드가 인스턴스를 수정해야 하는 경우가 있습니다. 값 타입(struct, enum)에 대한 인스턴스 메서드의 경우 func
키워드 앞에 mutating
키워드를 배치해서 해당 메서드가 인스턴스의 값을 수정할 수 있음을 나타냅니다. 여기에 대한 자세한 설명은 Modifying Value Types from Within Instance Methods을 참고해주세요.
Protocol을 채택하는 모든 타입의 인스턴스를 변경하기 위한 인스턴스 메서드 요구사항을 정의하는 경우 mutating
키워드로 메서드에 표시해주면 됩니다. 이를 통해 struct, enum이 protocol이 요구하는 메서드를 인스턴스를 수정할 수 있는 메서드로 만들 수 있습니다.
Note: Protocol이 요구하는 인스턴스 메서드에
mutating
을 표시하더라도 class는 해당 메서드를 구현할 때mutating
키워드를 사용할 필요가 없습니다.mutating
은 struct, enum의 경우에만 사용합니다.
다음은 하나의 인스턴스 메서드를 요구하는 Togglable
protocol입니다. 이름에서 알 수 있듯 toggle()
메서드는 일반적으로 해당 타입의 프로퍼티를 수정해서 해당 타입의 상태를 전환하기 위한 메서드입니다.
toggle()
메서드는 Togglable
protocol 정의의 일부로 mutating
키워드로 표시되어 메서드가 호출될 때 해당 인스턴스의 상태를 변경할 수 있다는 것을 알려줍니다.
protocol Togglable {
mutating func toggle()
}
Struct, Enum이 Togglable
protocol을 채택하는 경우 mutating
키워드를 사용해서 toggle()
메서드를 구현하면 됩니다.
아래 예제는 OnOffSwitch
라는 Enum을 정의한 코드입니다. OnOffSwitch
는 on
, off
로 표시되는 두 상태를 toggle
합니다. Enum의 toggle()
구현에는 mutating
키워드를 사용해서 Toggleable
protocol의 요구사항과 일치하도록 해줍니다.
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
Initilaizer Requirements
Protocol은 자신을 채택한 타입에 특정 생성자를 요구할 수도 있습니다. 이러한 생성자는 일반 생성자와 정확히 같은 방식으로 protocol 정의의 일부로 만들지만 중괄호나 생성자 본문은 작성하지 않습니다.
protocol SomeProtocol {
init(someParameter: Int)
}
Class Implementations of Protocol Initializer Requirements
designated 생성자, convenience 생성자로 생성자를 요구하는 protocol을 채택한 class를 구현할 수 있습니다. 두 경우 모두 생성자에 required
를 표시해야 합니다.
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
required
생성자를 사용하면 class의 모든 서브 클래스도 해당 생성자를 명시적으로 혹은 상속된 구현을 통해 구현하므로, 부모 class가 채택 중인 protocol을 준수하도록 할 수 있습니다.
required
생성자에 대한 자세한 정보는 Required Initializers을 참고해주세요.
Note:
final
클래스는 서브 클래싱 할 수 없기 때문에final
클래스는 protocol이 요구하는 생성자에required
를 표시할 필요가 없습니다.final
에 대한 자세한 정보는 Preventing Overrides를 참고해주세요.
서브클래스가 슈퍼클래스의 designated 생성자를 override 하고 protocol에서 요구하는 생성자도 구현하는 경우 required
, override
수정자를 모두 사용해서 생성자를 구현합니다. 즉 슈퍼클래스는 protocol을 채택하지 않았는데, 서브클래스에서는 protocol을 채택한 경우는 아래와 같이 구현하면 됩니다.
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
}
}
Failable Initializer Requirements
Protocol은 Failable Initializers에 정의된 대로 실패 가능한 생성자를 요구사항으로 정의할 수 있습니다.
Protocol의 실패 가능한 생성자 요구사항은 자신을 채택한 타입이 실패 가능, 실패 불가능한 생성자가 있을 때 만족됩니다. 실패할 수 없는 생성자를 요구하는 protocol을 채택한 경우, 실패할 수 없는 생성자 또는 암시적으로 래핑 되지 않은 실패 가능한 생성자로 만족시킬 수 있습니다.
Protocol as Types
Protocol은 자체적으로 기능을 구현하지 않습니다. 하지만 코딩을 할 때 Protocol은 하나의 타입처럼 사용할 수 있습니다. Protocol을 타입처럼 사용하는 것을 existential type
이라고 하는 경우가 있는데 이는 "T가 protocol을 준수하도록 타입 T가 존재합니다."라는 문구에서 유래되었습니다.
다음을 포함해서 다른 타입이 허용되는 여러 위치에서 Protocol을 사용할 수 있습니다.
- 함수, 메서드, 생성자의 매개변수 타입 또는 반환 타입
- 상수, 변수, 프로퍼티의 타입
- 배열, 딕셔너리 등의 항목 타입
Note: Protocol이 타입이므로 (앞서 봤던
FullyNamed
,RandomNumberGenerator
와 같이..) Swift의 다른 타입들(예를 들어 Int, String, Double)처럼 이름이 대문자로 시작합니다.
다음은 protocol을 타입처럼 사용한 예입니다.
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
}
}
위 예는 보드게임에서 사용할 n면체 주사위를 나타내는 Dice
라는 새로운 class를 정의한 코드입니다. Dice
인스턴스에는 면의 수를 나타내는 Int 타입인 sides
프로퍼티와, 주사위를 굴린 값을 생성하는 RandomNumberGenerator
타입의 generator
프로퍼티가 있습니다.
generator
프로퍼티는 RandomNumberGenerator
타입입니다. 따라서 RandomNumberGenerator
protocol을 채택하는 모든 타입의 인스턴스를 사용할 수 있습니다. 인스턴스가 RandomNumberGenerator
protocol을 채택한다는 점을 제외하고 이 속성에 할당될 인스턴스에게 요구하는 것은 없습니다. generator
의 타입이 RandomNumberGenerator
이므로 Dice
class의 내부 코드는 이 protocol이 준수하는 모든 인스턴스에 적용되는 방식으로만 generator
를 사용할 수 있습니다. 즉 generator
의 기본 타입에 정의된 메서드나 프로퍼티를 사용할 수 없습니다. 하지만 DownCasting에서 설명한 슈퍼클래스를 서브클래스로 다운 캐스트 하는 것처럼, protocol 타입을 기본 타입으로 다운 캐스트 해서 사용할 수는 있습니다.
Dice
는 초기 상태를 설정하기 위한 생성자도 있습니다. 이 생성자에는 RandomNumberGenerator
타입의 generator
라는 매개변수가 있습니다. 새로운 Dice
인스턴스를 생성할 때 generator
매개변수에는 RandomNumberGenerator
를 채택한 모든 인스턴스 값을 전달할 수 있습니다.
Dice
는 1과 sided
사이의 정수 값을 반환하는 인스턴스 메서드인 roll
을 제공합니다. 이 메서드는 generator
의 random()
메서드를 호출해서 0.0 ~ 1.0 사이의 새로운 난수를 만들고 이 난수를 사용해서 범위 내의 값을 만듭니다. generator
는 RandomNumberGenerator
를 채택했기 때문에 random()
메서드가 반드시 존재합니다.
Dice
class를 사용해서 LinearCongruentialGenerator
인스턴스를 generator
로 사용해서 6면 주사위를 만드는 방법은 다음과 같습니다.
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
print("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4
Delegation
Delegation
은 class나 struct가 일부 책임을 다른 타입의 인스턴스에 넘길 수 있도록 하는 디자인 패턴입니다. Delegation 패턴은 위임된 책임을 캡슐화하는 protocol을 정의하여 구현됩니다. 따라서 protocol을 채택한 타입이 위임된 기능을 제공하도록 보장됩니다. Delegation을 사용하여 특정 작업에 응답하거나, 해당 소스의 기본 타입을 알 필요 없이 외부 소스에서 데이터를 검색할 수 있습니다.
아래 예는 주사위 기반 보드게임에 사용하기 위한 2개의 protocol을 정의한 코드입니다.
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
프로토콜은 주사위와 관련된 모든 게임에서 채택할 수 있는 Protocol입니다.
DiceGameDelegate
protocol을 채택해서 DiceGame
의 진행 상황을 추적할 수 있습니다. 강한 참조 사이클을 방지하기 위해 delegate는 weak
참조로 선언됩니다. weak
참조에 대한 자세한 내용은 Strong Reference Cycles BetweenClass Instances를 참고해주세요! 잠시 뒤에 나오는 Class-Only Protocols
섹션에서 나올 내용을 미리 말씀드리면, class 전용 protocol은 AnyObject
를 상속하는 것으로 표시하면 됩니다. Protocol을 class 전용으로 표시하면 이번 챕터의 뒷부분에 나오는 SnakesAndLadders
class의 delegate처럼 weak
참조로 선언할 수 있습니다.
다음은 원래 Control Flow에서 소개된 Snake and Ladders
게임 버전입니다. 이 버전은 DiceGame
protocol 채택하고 DiceGameDelegate
에게 진행상황을 알리기 위해 dice-roll에 Dice
인스턴스를 사용하도록 설정되었습니다.
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)
}
}
Snakes and Ladders
의 게임 설명을 보고 싶으시면 Control Flow - Break를 참고해주세요.
이번 게임 버전은 DiceGame
protocol을 채택한 SnakesAndLadders
라는 class로 래핑 됩니다. Protocol을 준수하기 위해 gattable dice
프로퍼티와 play()
메서드를 제공합니다. (dice
프로퍼티는 생성 후 변경할 필요가 없기 때문에 let
으로 선언되며 protocol은 gettable
만 요구하므로 문제가 없습니다.)
Snakes and Ladders
게임 보드 설정은 class의 init()
생성자에서 이뤄집니다. 모든 게임 논리는 Protocol의 필수 요소인 dice
프로퍼티를 사용해서 주사위 역할을 제공하는 protocol의 play()
메서드로 진행됩니다.
delegate
프로퍼티는 옵셔널 DiceGameDelegate
타입으로 정의되어있습니다. 게임을 플레이하기 위해 delegate
는 필요하지 않기 때문이죠. 옵셔널 타입이기 때문에 delegate
프로퍼티의 초기값은 nil입니다. 그런 뒤 게임을 생성한 곳에서 적절한 delegate
를 설정할 수 있습니다. DiceGameDelegate
protocol은 class 전용이므로 참조 주기를 방지하기 위해 weak
으로 선언할 수 있는 것도 볼 수 있습니다.
DiceGameDelegate
는 게임 진행상황을 추적하는 세 가지 메서드를 제공합니다. 이러한 세 가지 메서드는 위의 play()
메서드 내에서 게임 논리에 통합되어 새로운 게임이 시작되거나 새 차례가 오거나 게임이 종료될 때까지 호출됩니다.
delegate
프로퍼티는 옵셔널 DiceGameDelegate
이므로 play()
메서드는 delegate
에서 메서드를 호출할 때마다 옵셔널 체이닝을 사용합니다. delegate
프로퍼티가 nil이라면 이러한 호출은 정상적으로 실패하게 되고, nil이 아니라면 delegate
메서드가 호출되고 SnakesAndLadders
인스턴스가 매개변수로 전달됩니다.
다음 예는 DiceGameDelegate
protocol을 채택한 DiceGameTracker
라는 class를 정의한 코드입니다.
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")
}
}
DiceGameTracker
는 DiceGameDelegate
에 필요한 세 가지 메서드를 모두 구현해뒀습니다. 이런 메서드를 사용해서 게임에서 수행한 차례의 횟수를 알아냅니다. 게임이 시작되면 numberOfTurns
프로퍼티를 0으로 재설정하고 새로운 턴이 시작될 때마다 값을 증가시키며, 게임이 끝나면 총 턴 수를 출력합니다.
위 코드에 있는 gameDidStart(_:)
의 구현은 game
매개변수를 사용하여 플레이하려는 게임에 대한 이부 소개정보를 출력합니다. game
매개변수는 SnakesAndLadders
가 아닌 DiceGame
타입이므로 gameDidStart(_:)
는 DiceGame
프로토콜의 일부로 구현된 메서드와 프로퍼티에만 접근하고 사용할 수 있습니다. 그러나 메서드는 여전히 타입 캐스팅을 사용하여 기본 인스턴스 형식을 쿼리 할 수도 있습니다. 이 예제에서는 게임이 실제로 백 단에서 SnakesAndLadders
의 인스턴스인지 확인하고 그렇다면 적절한 메시지를 출력합니다.
gameDidStart(_:)
메서드는 전달된 game
매개변수의 dice
프로퍼티에도 접근합니다. game
은 DiceGame
protocol을 준수하므로 dice
프로퍼티가 반드시 존재하기 때문에 gameDidStart(_:)
메서드는 어떤 종류의 게임이 플레이되는지 상관없이 dice
의 side
프로퍼티에 접근하여 출력할 수 있습니다.
DiceGameTracker
가 작동하는 모습은 다음과 같습니다.
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
Adding Protocol Conformance with an Extension
기존 타입의 소스 코드에서는 접근할 수 없는 경우에, 기존 타입의 extension을 통해 새로운 protocol을 채택할 수도 있습니다. Extension
은 기존 타입에 새로운 프로퍼티, 메서드, 서브 스크립트를 추가할 수 있으므로 프로토콜이 요구하는 것도 만족시킬 수 있습니다. Extension
에 대한 자세한 내용은 Extension을 참고해주세요.
Note: 타입의 기존 인스턴스에도 해당 타입의 extension에 의해 protocol이 채택되면 자동으로 반영됩니다.
예를 들어 TextRepresentable
이라는 텍스트로 표현되는 방법이 있는 모든 타입을 구현할 수 있는 protocol입니다. 이는 자신에 대한 설명 혹은 현재 상태에 대한 텍스트 일 수 있겠죠.
protocol TextRepresentable {
var textualDescription: String { get }
}
아까 본 Dice
클래스도 extension을 사용하여 TextRepresentable
를 채택하고 준수하도록 할 수 있습니다.
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
위 extension은 Dice
가 원래 구현에서 제공하는 것과 동일한 방법으로 새로운 protocol을 채택합니다. Protocol 이름은 타입 이름 뒤에 콜론을 사용하여 작성하면 되고, Protocol의 모든 요구사항은 해당 extension의 중괄호 안에서 정의하면 됩니다.
이제 모든 Dice
인스턴스를 TextRepresentable
로도 처리할 수 있게 되었습니다!
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"
비슷하게 SnakesAndLadders
class도 TextRepresentable
protocol을 채택하고 준수하게 만들 수 있습니다.
extension SnakesAndLadders: TextRepresentable {
var textualDescription: String {
return "A game of Snakes and Ladders with \(finalSquare) squares"
}
}
print(game.textualDescription)
// Prints "A game of Snakes and Ladders with 25 squares"
Conditionally Conforming to a Protocol
제네릭 타입은 제네릭 매개변수가 protocol을 준수하는 경우와 같은 특정 조건에서만 protocol의 요구사항을 만족시킬 수 있습니다. 타입에 extension을 사용할 때 제약조건을 나열하여 제네릭 타입이 조건부로 protocol을 준수하도록 만들 수 있습니다. where
을 사용하여 채택하려는 protocol 이름 뒤에 제약조건을 작성하면 되는데요, where
절에 대한 자세한 내용은 Generic Where Clauses를 참고하세요!
다음 코드는 Array 인스턴스가 TextRepresentable
을 채택한 타입의 요소를 저장할 때만 TextRepresentable
protocol의 요구사항을 준수하도록 해줍니다.
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]"
Declaring Protocol Adoption with an Extension
타입이 protocol의 모든 요구 사항을 이미 준수하지만 해당 protocol을 채택한다고 명시하지는 않은 경우 빈 확장자를 사용하여 protocol을 채택하도록 할 수도 있습니다.
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
이제 Hamster
인스턴스는 TextRepresentable
이 필수 타입인 모든 곳에서 사용할 수 있습니다.
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"
Note: 타입이 요구 사항을 충족한다고 해서 protocol을 자동으로 채택하지는 않습니다. 언제나 protocol은 명시적으로 선언해야 채택할 수 있습니다.
Adopting a Protocol Using a Synthesized Implementation
Swift는 많은 간단한 경우에 Equtable
, Hashable
, Comparable
에 대한 protocol의 요구사항을 자동으로 준수할 수 있습니다. 이러한 synthesized implementation(합성 구현)을 사용하면 protocol의 요구 사항을 직접 구현하기 위해 반복적인 코드를 작성할 필요가 없습니다.
Swift는 다음 종류의 custom 타입에 대해 Equatable
synthesized implementation을 사용할 수 있습니다.
Equatable
protocol을 준수하는 저장 프로퍼티만 있는 structEquatable
protocol을 준수하는 associated type만 있는 enum- associated type이 없는 enum
==
의 synthesized implementation을 사용하려면 ==
연산자를 직접 구현하지 말고 코드에 Equatable
protocol을 채택합니다. Equatable
protocol은 !=
의 기본 구현을 제공합니다.
다음 코드는 Vector2D 구조와 유사한 3차원 위치 벡터 (x, y, z)에 대한 Vector3D struct를 정의한 코드입니다. x, y, z 프로퍼티 모두 Equatable
타입이므로 Vector3D는 ==
연산자의 synthesized implementation을 사용할 수 있습니다.
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는 다음 종류의 custom type에 대해 Hashable
의 synthesized implementation을 사용할 수 있습니다.
Hashable
protocol을 준수하는 저장 프로퍼티만 있는 structHashable
protocol을 준수하는 associated type만 있는 enum- associated type이 없는 enum
hash(into:)
의 synthesized implementation을 사용하려면 hash(into:)
메서드를 직접 구현하지 않고 코드에 Hashable
protocol을 채택합니다.
Swift는 raw Value가 없는 enum에 대해 Comparable
의 synthesized implementation를 제공합니다. Enum에 associated type이 있는 경우 모두 Comparable
protocol을 준수해야 합니다. <
의 synthesized implementation을 수신하려면 <
연산자를 직접 구현하지 않고 원래 enum이 선언된 코드에 Comparable
protocol을 채택합니다. Comparable
protocol의 기본 구현인 <=
, >
, >=
는 나머지 비교 연산자를 제공합니다.
다음 코드는 beginner, intermediate, expert 케이스를 갖는 SkillLevel
enum을 정의한 코드입니다. expert
는 보유한 별의 수에 따라 추가로 순위가 매겨집니다.
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)"
Collections of Protocol Types
Protocol은 아까 Protocols as Types
섹션에서 알아본 대로 Array
, Dictionary
와 같은 컬렉션에 저장할 수도 있습니다. 다음 코드는 TextRepresentable
을 저장할 Array
를 만드는 코드입니다.
let things: [TextRepresentable] = [game, d12, simonTheHamster]
이제 Array
의 항목을 for문을 통해 처리하면 각 항목에 대한 textualDesciption
을 출력할 수 있습니다.
for thing in things {
print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon
things
는 Dice
, DiceGame
, Hamster
타입이 아닌 TextRepresentable
타입입니다. 실제로는 그런 타입이라도 모두 TextRepresentable
타입이고 해당 타입에는 textualDesciprion
프로퍼티가 있으므로 위와 같이 사용이 가능합니다.
Protocol Inheritance
Protocol은 하나 이상의 다른 protocol을 상속할 수 있으며 상속된 요구사항에 요구사항을 추가할 수 있습니다. Protocol 상속 문법은 class의 상속과 유사하지만 상속할 여러 protocol을 쉼표로 구분하여 나열할 수 있습니다.
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// protocol definition goes here
}
다음은 위에서 나온 TextRepresentable
protocol을 상속하는 protocol의 예입니다.
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}
위 코드에서는 TextRepresntable
을 상속하는 새로운 protocol인 PrettyTextRepresentable
을 정의합니다. PrettyTextRepresentable
을 채택하는 모든 타입은 TextRepresntable
의 요구사항과 PrettyTextRepresntable
의 요구사항을 모두 만족시켜야 합니다. 위 코드에서는 PrettyTextRepresntable
는 String을 반환하는 prettyTextualDescription
이라는 gettable
프로퍼티를 제공하기 위한 하나의 요구사항을 추가했습니다.
SnakesAndLadders
class는 PrettyTextRepresntable
를 채택하고 준수하도록 extension 할 수도 있습니다.
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
}
}
위 코드는 PrettyTextRepresntable
protocol을 채택하고 SnakesAndLadders
타입에 대한 prettyTextualDescription
프로퍼티의 구현을 제공한다고 명시합니다. PrettyTextRepresntable
은 무엇이든 TextRepresntable
protocol을 채택해야 하며 따라서 prettyTextRepresntable
의 구현은 TextRepresntable
protocol에서 textualDescription
프로퍼티에 접근하며 시작됩니다. 콜론과 줄 바꿈을 추가하고 이를 예쁜 텍스트 표현의 시작으로 사용합니다. 그런 뒤 board
배열의 square 값을 통해 도형을 추가합니다.
- square의 값이 0보다 크면 사다리의 밑면이고 ▲로 표시합니다.
- square의 값이 0보다 작으면 뱀의 머리이고 ▼로 표시합니다.
- 둘 다 아니면 square의 값은 0이고 ○로 표시합니다.
이제 prettyTextualDescription
프로퍼티를 사용하여 SnakesAndLadders
인스턴스의 예쁜 텍스트를 출력할 수 있습니다.
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
Class-Only Protocol
Protocol의 상속 목록에 AnyObject
프로토콜을 추가해서 Protocol을 class만 채택할 수 있도록 제한할 수 있습니다.
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// class-only protocol definition goes here
}
위 코드에서 SomeClassOnlyProtocol
은 class 타입에서만 채택할 수 있습니다. SomeClassOnlyProtocol
을 채택하려고 하는 struct, enum은 컴파일 오류를 발생시킵니다.
Note: 해당 protocol의 요구사항에 의해 이미 정의된 동작이 참조 및 값 타입의 차이에 대한 설명은 Structures and Classes를 참고하세요.
Protocol Composition
동시에 여러 protocol을 준수하는 타입을 요구하는 것이 유용할 수 있습니다. protocol composition을 사용하여 여러 개의 protocol을 하나의 protocol로 결합할 수 있습니다. protocol composition은 composition에 있는 모든 protocol의 요구사항이 결합된 임시 로컬 protocol을 정의한 것처럼 작동합니다. protocol composition은 새로운 protocol 타입을 정의하지는 않습니다.
protocol composition은 SomeProtocol 및 AnotherProtocol 형식입니다. &
로 구분하여 필요한 만큼 protocol을 나열할 수 있습니다. Protocol 목록 외에도 protocol composition에는 필수 슈퍼 클래스를 지정하는 데 사용할 수 있는 하나의 클래스 타입이 포함될 수도 있습니다.
다음은 Named
, Aged
라는 2개의 protocol을 함수 매개변수에 대한 단일 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!"
위 예에서 Named
Protocol에는 name
이라는 gettable
String 프로퍼티를 요구합니다. Aged
protocol에는 age
라는 gettable
Int 프로퍼티를 요구합니다. 두 개의 protocol 모두 Person
이라는 struct에 채택되었네요.
위 예에서는 wishHappyBirthday(to:)
함수를 정의합니다. celebrator
매개변수 타입은 Named & Aged
이며, 이는 Named
, Aged
protocol을 모두 준수하는 타입을 의미합니다. 두 개의 필수 protocol을 모두 준수한다면 어떤 타입이건 함수의 매개변수로 사용될 수 있습니다.
그런 뒤 birthdayPerson
이라는 새로운 Person
인스턴스를 만들고 새로운 인스턴스를 wishHappyBirthday(to:)
함수에 전달합니다. Person
이 두 개의 protocol을 모두 준수하기 때문에 문제없이 wishHappyBirthdat(to:)
함수는 작동됩니다.
다음은 방금 예의 Named
protocol을 Location
class와 결합한 예제입니다.
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!"
beginConcert(in:)
함수는 Location & Named
타입의 매개변수를 사용합니다. 이는 Location
의 서브클래스이고 Named
protocol을 준수하는 모든 타입을 뜻합니다. 위 코드에서 City
는 두 조건을 모두 충족합니다.
Person
이 Location의 하위 클래스가 아니므로
beginConcert(in:)함수에
birthdayPerson을 전달하는 것은 옳지 않습니다. 마찬가지로
Namedprotocol을 준수하지 않는
Location의 서브 클래스를 만든 경우에도
beginConcert(in:)`에 전달하는 것이 옳지 않습니다.
Checking for Protocol Conformance
타입 캐스팅에 설명된 is
, as
연산자를 사용하여 protocol 적합성을 확인하고 특정 protocol로 캐스팅할 수 있습니다. protocol을 확인하고 캐스팅하는 것은 타입을 확인하고 캐스팅하는 것과 동일한 문법을 사용합니다.
is
연산자는 인스턴스가 protocol을 준수하면 true를 반환하고 그렇지 않으면 false를 반환합니다.as?
연산자는 protocol 타입의 옵셔널 값을 반환하고 인스턴스가 해당 protocol을 준수하지 않으면 nil을 반환합니다.as!
연산자는 다운 캐스트를 protocol 타입으로 강제하고 성공하지 못하면 런타임 에러를 발생시킵니다.
다음 예는 area
라는 gettable
Dobule 프로퍼티를 요구하는 HasArea
protocol을 정의한 코드입니다.
protocol HasArea {
var area: Double { get }
}
그리고 HasArea
protocol을 준수하는 Circle
, Country
클래스를 정의합니다.
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 }
}
Circle
class는 계산 프로퍼티로 선언된 area
프로퍼티를 기반으로 radius
라는 저장 프로퍼티를 구현합니다. 두 개의 class모두 HasArea
protocol을 준수하고 있네요.
다음은 HasArea
protocol을 준수하지 않는 Animal
이라는 class입니다.
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
Circle
, Country
, Animal
class는 함께 상속하는 class가 없습니다. 하지만 모두 class이므로 AnyObject
타입을 저장하는 Array에 저장할 수 있습니다.
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
Objects
array는 radius
가 2.0인 Circle
인스턴스, area
가 243610인 Country
인스턴스, legs
가 4인 Animal
인스턴스로 초기화됩니다.
이제 Objects
array를 for문을 사용하여 각각의 element를 검사해서 HasArea
protocol을 준수하는지 확인해봅시다.
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
배열의 객체가 HasArea
protocol을 준수할 때마다 as?
연산자가 반환하는 옵셔널 값은 objectWithArea
라는 상수로 래핑 해제됩니다. objectWithArea
상수는 HasArea
타입으로 알려져 있으므로 해당 area
프로퍼티에 접근하고 타입이 안전한 방식으로 print 될 수 있습니다.
각각의 객체는 캐스팅 프로세스에 의해 변경되지는 않고 계속해서 Circle
, Country
, Animal
하지만 objectWithArea
에 저장되는 시점에는 HasArea
타입이 되므로 area
프로퍼티에만 접근할 수 있습니다.
Optional Protocol Requirements
Protocol은 선택적 요구사항을 정의할 수도 있습니다. 이러한 요구사항은 protocol을 채택한 타입이 반드시 구현할 필요가 없습니다. 선택적 요구사항은 protocol 정의의 일부로 optional
이라는 접두사가 붙습니다. Objective-C와 함께 사용되는 코드를 작성할 수 있도록 선택적 요구사항을 사용할 수 있는데요, 이 경우 Protocol과 선택적 요구사항은 모든 @objc
프로퍼티로 표시되어야 합니다. @objc
프로토콜은 Objective-C class 또는 @objc
class에서 상속받은 class에서만 채택할 수 있습니다. 따라서 struct, enum에서는 채택될 수 없죠.
메서드나 프로퍼티를 선택적 요구 사항으로 선언하면 해당 타입은 자동으로 옵셔널 타입이 됩니다. 예를 들어 (Int) -> String
타입의 메서드는 ((Int) -> String)?
타입이 됩니다. 전체 함수 타입은 메서드의 반환 값이 아니라 옵셔널로 래핑 됩니다.
선택적 요구사항은 요구사항이 protocol을 준수하는 타입에 의해 구현되지 않았을 수 있기 때문에 옵셔널 체이닝으로 호출할 수 있습니다. someOptionalMethod?(someArgument)
와 같이 호출될 때 메서드 이름 뒤에 물음표를 넣어 옵셔널 메서드의 구현을 확인합니다. 옵셔널 체이닝에 대한 자세한 내용은 Optional Chaining를 참고해주세요.
다음 예에서는 외부 데이터 소스를 사용하여 increment
를 제공하는 Counter
라는 정수 계산 class를 정의할 겁니다. 이 data source는 두 가지 선택적 요구사항이 있는 CounterDataSource
protocol에 의해 정의됩니다.
@objc protocol CounterDataSource {
@objc optional func increment(forCount count: Int) -> Int
@objc optional var fixedIncrement: Int { get }
}
CounterDataSource
protocol은 increment(forCount:)
라는 옵셔널 메서드 요구사항과 fixedIncrement
라는 옵셔널 프로퍼티 요구사항을 정의합니다. 이러한 요구사항은 data Source가 Counter
인스턴스에 적절한 증분량을 제공하는 두 가지 다른 방법을 정의합니다.
Note: 엄밀히 말하면 protocol 요구사항을 구현하지 않아도
CounterDataSource
protocol을 준수하는 class를 만들 수 있습니다. 요구사항이 모두 옵셔널이기 때문이죠. 물론 굳이 그럴 필요는 없겠죠.
아래 구현된 Counter
class에는 CounterDataSource?
타입의 dataSource
프로퍼티가 있습니다.
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
}
}
}
Counter
class는 현재 값을 count
라는 변수 프로퍼티에 저장합니다. Counter
class는 또한 메서드가 호출될 때마다 count
프로퍼티를 증가시키는 increment
라는 메서드를 정의합니다.
increment()
메서드는 먼저 dataSource
에서 increment(forCount:)
메서드의 구현을 찾아 증가량을 찾으려고 시도합니다. increment()
메서드는 옵셔널 체이닝을 사용하여 increment(forCount:)
의 호출을 시도하고 count
값을 메서드의 단일 인수로 전달합니다.
두 가지 수준의 옵셔널 체이닝이 여기서 작동합니다. 첫 번째로 dataSource
가 nil일 수 있으므로 dataSource
의 이름 뒤에 물음표가 있어 dataSource
가 nil이 아닐 때만 increment(forCount:)
를 호출해야 함을 나타냅니다. 두 번째로 dataSource
가 존재하더라고 옵셔널 요구사항인 increment(forCount:)
가 구현되어있다는 보장이 없습니다. 따라서 increment(forCount:)
가 구현되어있지 않을 가능성도 옵셔널 체이닝에 의해 처리됩니다. increment(forCount:)
에 대한 호출은 increment(forCount:)
가 존재하는 경우에만 발생합니다. 이것이 increment(forCount:)
도 이름 뒤에 물음표가 있는 이유입니다.
이런 두 가지 이유로 인해 increment(forCount:)
호출이 실패할 수 있으므로 increment(forCount:)
은 옵셔널 Int값을 반환합니다. increment(forCount:)
가 CounterDataSource
의 정의에서 옵셔널이 아닌 Int값을 반환하도록 되어있더라도 마찬가지 입니다. 두 개의 옵셔널 체이닝이 차례로 있지만 결과는 하나의 옵셔널로 래핑 됩니다. 여러 개의 옵셔널 체이닝에 관한 자세한 내용은 Linking Multiple Levels of Chaining을 참고해주세요.
increment(forCount:)
을 호출한 뒤 반환하는 옵셔널 Int는 옵셔널 바인딩을 사용하여 amount
라는 상수로 래핑 해제됩니다. 옵셔널 Int에 값이 있으면 래핑 되지 않은 금액이 count
프로퍼티에 추가되고 증가가 완료됩니다.
dataSource
가 nil이거나 increment(forCount:)
를 구현하지 않아서 increment(forCount:)
의 값을 얻을 수 없는 경우 increment()
메서드는 dataSource
의 fixedIncrement
프로퍼티에서 값을 가져오려고 합니다. fixedIncrement
프로퍼티도 옵셔널 요구사항이므로 해당 값이 CounterDataSource
protocol에서 Int 타입으로 정의되어 있더라도 해당 값은 옵셔널 Int 타입입니다.
다음 코드는 dataSource가 쿼리 될 때마다 상수 값 3을 반환하는 간단한 CounterDataSource
구현입니다. 이는 옵셔널 요구사항인 fixedIncrement
프로퍼티를 구현합니다.
class ThreeSource: NSObject, CounterDataSource {
let fixedIncrement = 3
}
ThreeSource
의 인스턴스를 새로운 Counter
인스턴스의 dataSource
로 사용할 수 있습니다.
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
print(counter.count)
}
// 3
// 6
// 9
// 12
위 코드는 새로운 Counter
인스턴스를 생성하고 dataSource
를 새로운 ThreeSource
인스턴스로 설정합니다. counter
의 increment()
메서드를 네 번 호출하면 예상대로 counter
의 count
프로퍼티는 increment()
가 호출될 때마다 3씩 증가합니다.
다음은 Counter
인스턴스가 현재 count
값에서 0을 향해 증가 혹은 감소하도록 하는 TowardsZeroSource
라는 좀 더 복잡한 data source입니다.
class TowardsZeroSource: NSObject, CounterDataSource {
func increment(forCount count: Int) -> Int {
if count == 0 {
return 0
} else if count < 0 {
return 1
} else {
return -1
}
}
}
TowardsZeroSource
class는 CounterDataSource
protocol의 옵셔널 increment(forCount:)
메서드를 구현하고 count
매개변수를 사용하여 계산할 방향을 계산합니다. count
가 이미 0 이면 메서드는 0을 반환하고 더 이상 계산이 발생하지 않아야 한다고 나타냅니다.
기존 Counter
인스턴스와 함께 TowardsZeroSource
의 인스턴스를 사용해서 -4에서 0까지 계산할 수 있습니다. count
가 0이 되면 더 이상 계산을 하지 않습니다.
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
Protocol Extensions
Protocol은 메서드, 생성자, 서브 스크립트, 계산 프로퍼티 구현을 자신을 채택한 타입에게 제공하려면 extension을 사용하면 됩니다. 이를 통해 각 타입의 적합성, 전역 함수가 아닌 protocol 자체에 대한 동작을 정의할 수 있습니다.
예를 들어, RandomNumberGenerator
protocol에 extension을 사용하여 randomBool()
메서드를 제공하도록 할 수 있습니다. 이 메서드는 필수적인 random()
메서드의 결과를 사용하여 임의의 Bool 값을 반환합니다.
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
Protocol에 extension을 사용함으로써 protocol을 채택한 모든 타입은 별다른 수정 없이 randomBool()
메서드를 사용할 수 있게 됩니다.
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"
Protocol의 extension은 자신을 채택한 타입에 구현을 추가할 수 있지만 다른 protocol를 상속할 수는 없습니다. Protocol의 상속은 항상 protocol을 선언할 때만 가능합니다.
Providing Default Implementations
Protocol extension을 사용해서 해당 protocol가 요구하는 모든 메서드 또는 계산 프로퍼티에 대한 기본 구현을 제공할 수 있습니다. 자신을 채택한 타입이 요구사항을 이미 구현한 경우에는 protocol extension에서 구현한 것 말고 해당 타입 내에서 구현한 것이 사용됩니다.
Note: extension에서 protocol의 요구사항을 구현하는 것은 옵셔널 protocol 요구사항과는 다릅니다. 채택한 타입이 자체적으로 요구사항을 구현할 필요는 없지만 이미 protocol extension에 구현이 되어있으므로 옵셔널 체이닝 없이 바로 사용할 수 있습니다.
예를 들어 TextRepresentable
protocol을 상속하는 PrettyTextRepresentable
protocol은 prettyTextualDescription
프로퍼티의 default 구현을 제공하여 단순하게 textualDescription
프로퍼티에 접근한 결과를 반환할 수 있습니다.
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
Adding Constraints to Protocol Extensions
Protocol extension을 정의할 때 해당 타입이 extension의 메서드와 프로퍼티를 사용할 수 있기 전에 충족해야 하는 제약조건을 지정할 수 있습니다. 제네릭 where 절을 사용하여 extension을 사용할 protocol 이름 뒤에 제약조건을 작성할 수 있는데요, generic where절에 대한 자세한 내용은 Generic Where Clauses를 참고해주세요.
예를 들어 Element가 Equatable
protocol을 준수하는 모든 컬렉션에만 적용되는 protocol extension을 정의할 수 있습니다. 컬렉션의 element를 표준 라이브러리의 일부인 Equatable
protocol로 제한하면 ==
, !=
연산자를 사용하여 두 Element 간 비교를 할 수 있습니다.
extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}
allEqual()
메서드는 컬렉션의 모든 Element가 동일한 경우에만 true를 반환합니다.
두 개의 Int Array에 직접 사용해봅시다. 하나는 다른 모든 Element가 동일하지만 다른 하나는 그렇지 않습니다.
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
Int 타입은 Equatable
을 준수하기 때문에 equalNumbers
, differentNumbers
는 allEqual()
메서드를 사용할 수 있습니다.
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"
Note: 어떤 타입이 동일한 메서드, 프로퍼티에 대한 구현을 제공하는 여러 개의 제약된 extension의 조건을 만족하는 경우 Swift는 가장 specialezed 한 제약조건에 해당하는 구현을 사용합니다.