티스토리 뷰
Protocols — The Swift Programming Language (Swift 5.5)
Protocols A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of tho
docs.swift.org
1차 수정 (2022 / 03 / 14) : 의역, 오타 및 스타일 수정
Protocols
프로토콜은 특정 작업 및 기능에 적합한 메소드, 프로퍼티, 기타 요구사항에 대한 청사진을 정의합니다. (청사진 = 미리 계획을 세워 놓는 것) 프로토콜은 요구사항을 실제 구현을 제공하기 위해 클래스, 구조체, 열거형에 적용됩니다. 프로토콜의 요구사항을 만족시키는 모든 타입을 "프토로콜을 준수(conform)한다" 라고 말합니다.
프로토콜을 준수하는 타입이 반드시 구현해야하는 특정 요구사항에 더해서, 요구사항 중 일부를 구현하기 위해 프로토콜을 확장(extend)하거나 추가 기능을 구현할 수 있습니다.
Protocl Syntax(프로토콜 구문)
프로토콜은 클래스, 구조체, 열거형과 매우 유사한 방식으로 정의합니다.
protocol SomeProtocol {
// protocol definition goes here
}
사용자 지정 타입은 정의의 일부로 타입의 이름 뒤에 특정 프로토콜을 적용시킵니다. 하나 이상의 프로토콜을 적용할 수 있습니다. 여러 개의 프로토콜은 , 로 구별합니다.
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
클래스가 슈퍼클래스를 가진다면, 슈퍼클래스를 적고 프로토콜을 적어줍니다.
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
Property Requirements(프로퍼티 요구사항)
프로토콜은 특정 이름과 타입을 가지는 타입 프로퍼티 혹은 인스턴스 프로퍼티를 제공하기 위해서 적합한 타입을 요구할 수 있습니다. 프로토콜은 프로퍼티가 저장 프로퍼티(stored property) 인지 연산 프로퍼티(computed property) 인지 지정하지 않습니다. 오직 필요한 프로퍼티의 이름과 타입만 명시합니다. 프로토콜은 각 프로퍼티가 gettable 인지 gettable & settable 인지 명시합니다.
프로토콜이 프로퍼티가 gettable & settable 되기를 요구한다면, 해당 프로퍼티 요구사항은 상수 저장 프로퍼티 (constant stored property) 혹은 읽기-전용 연상 프로퍼티 (read-only computed property) 로 충족될 수 없습니다.
프로토콜이 프로퍼티가 gettable이 되기만을 원한다면, 요구사항은 어떤 종류의 프로퍼티가 와도 다 만족됩니다. 사용자의 코드에 유용하다면 프로퍼티가 settable이 되는 것도 유요합니다.
프로퍼티 요구사항은 var 키워드와 함께 항상 변수 프로퍼티(variable property)로 정의됩니다. {get set}을 사용해 gettable & settable 프로퍼티를 작성합니다. {get} 만 사용하면 gettable 프로퍼티를 작성합니다.
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
프로토콜에서 "타입 프로퍼티 요구사항"을 정의할 때, 접두사 statice 키워드를 항상 사용합니다. 이 규칙은 클래스로 구현할 때 타입 프로퍼티 요구사항에 class, static 키워드를 접두로 붙일 수 있는 경우에도 적용됩니다.
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
아래는 단일 인스턴스 프로퍼티 요구사항을 가지는 프로토콜입니다.
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed 프로토콜은 풀네임을 제공하기 위해 적절한 타입을 요구합니다. 프로토콜은 준수해야하는 타입의 특성에 대해 아무것도 명시하지 않습니다. 단지 타입이 반드시 풀네임을 제공해야 한다는 것만 명시합니다. 이 프로토콜은 "모든 FullNamed 타입은 반드시 String 타입의 fullName 이라는 gettable 인스턴스 프로퍼티를 가져야 한다" 고 명시합니다.
아래 예제는 FullyNamed 프로토콜을 적용하고 준수하는 간단한 구조체입니다.
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"
Person 이라는 구조체를 정의하고 특정 인물의 이름을 명시합니다.
각 Person 인스턴스는 String 타입의 단일 저장 프로퍼티 fullName을 가집니다. 이를 통해 FullyNamed 프로토콜 요구사항을 맞춥니다. Person이 프로토콜을 정확히 준수한다고 할 수 있습니다. 프로토콜 요구사항을 만족시키지 못하면, Swift는 컴파일할 때 에러를 발생합니다.
아래 예제는 FullNamed 프로토콜을 준수하는 클래스입니다.
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"
클래스는 fullName 프로퍼티 요구사항을 연산 읽기-전용 프로퍼티(computed read-only property)로 구현했습니다. 각 Starship 클래스 인스턴스는 필수적인 name 과 옵셔널 prefix가 저장됩니다. fullName 프로퍼티는 prefix가 존재하면 사용합니다.
Method Requirements (메소드 요구사항)
프로토콜을 준수하는 타입을 구현하기 위해, 프로토콜은 특정 인스턴스 메소드와 타입 메소드를 요구할 수 있습니다. 이 메소드들은 프로토콜 정의의 일부로 작성됩니다. 일반 인스턴스 메소드 및 타입 메소드를 작성하는 방식과 정확히 같은 방식으로 작성합니다. 단 { } 이나 메소드 본문은 작성하지 않습니다.
일반 메소드와 똑같은 규칙에 따라 변수 파라미터가 허용됩니다. 단 프로토콜 정의 내에 있는 메소드의 파라미터에 기본값을 명시할 수는 없습니다.
타입 프로퍼티 요구사항처럼, 프로토콜 내부에서 "타입 메소드 요구사항"이 정의될 때 접두사 static 키워드가 항상 옵니다. 클래스로 구현될 때 메소드 요구사항에 class 혹은 static 키워드가 접두사로 와도 마찬가지입니다.
protocol SomeProtocol {
static func someTypeMethod()
}
아래 예제는 프로토콜에 단일 인스턴스 메소드 요구사항를 구현했습니다.
protocol RandomNumberGenerator {
func random() -> Double
}
이 RandomNumberGenerator 프로토콜은 random 이라는 인스턴스 메소드를 가지기 위해 모든 준수 타입을 요구합니다. random 메소드는 호출시 Double 값을 리턴합니다. 비록 프토토콜의 일부로 명시되지 않았어도, 이 값은 0.0 ~ 1.0 사이 값을 가질것으로 추측됩니다.
RandomNumberGenerator 프로토콜은 각 랜덤 넘버 생성에 대해 어떤 가정도 하지 않습니다. 단지 새로운 랜덤 넘버 생성 표준 방식을 제공하면 됩니다.
아래 예제는 RandomNumberGenerator 프로토콜을 준수하는 클래스를 구현합니다. 이 클래스는 "linear congruential 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 (변형 메소드 요구사항)
종종 메소드가 속한 인스턴스를 수정할 경우가 있습니다. 값 타입에 대한 인스턴스(구조체와 열거형) 메소드에 대해 func 키워드 앞에 mutating 키워드를 작성합니다. 이를 통해 이 메소드는 "인스턴스 그 자체와 해당 인스턴스의 모든 프로퍼티를 수정하는 것"이 허용됩니다.
"프토토콜을 적용하는 모든 타입의 인스턴스를 변형시킬 의도"가 있는 프로토콜 인스턴스 메소드 요구사항을 정의한다면, 프로토콜 정의에서 메소드 앞에 mutating 키워드를 사용해줍니다. 이를 통해 구조체와 열거형이 프로토콜을 적용하고 메소드 요구사항을 만족시킬 수 있게 해줍니다.
클래스를 위해 해당 메소드를 구현할 때 mutating 키워드를 사용하지 않아도 됩니다. mutating 키워드는 구조체와 열거형에서만 사용됩니다.
아래 예제는 Togglable 프로토콜을 정의합니다. toggle 이라는 단일 인스턴스 메소드 요구사항을 정의합니다. toggle 메소드는 일반적으로 해당 타입의 프로퍼티를 수정하여 일치하는 타입의 상태로 전환합니다.
toggle 메소드는 Togglable 프로토콜 정의 내에서 mutating 키워드와 함께 작성됩니다. 이를 통해 메소드가 호출될 때 준수 인스턴스의 상태를 변형시킬 수 있다는 것을 나타냅니다.
protocol Togglable {
mutating func toggle()
}
구조체와 열거형을 위해 Togglable 구현한다면, 해당 구조체와 열거형은 mutating으로 표현된 toggle() 을 구현함으로써 프로토콜을 만족시켜야 합니다.
아래 예제는 OnOffSwitch 라는 열거형을 정의합니다. 이 열거형 토글은 두 가지 상태 on, off 를 가집니다. 열거형의 toggle 메소드는 mutating으로 표시되어 Togglable 프로토콜을 준수합니다.
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
Initializer Requirements (이니셜라이져 요구사항)
프로토콜은 특정 이니셜라이저를 형식을 준수하여 구현해야 할 수 있습니다. 일반 이니셜라이져와 똑같은 방식으로 프로토콜 정의에 작성합니다. 단 { } 이나 본문 없이 작성합니다.
protocol SomeProtocol {
init(someParameter: Int)
}
Class Implementations of Protocol Initializer Requirements (프로토콜 이니셜라이저 요구사항의 클래스 구현)
적합한 클래스에 지정 이니셜라이져(designated initializer) 또는 편의 이니셜라이져 (convenience initializer) 로써 프로토콜 이니셜라이져 요구사항을 구현할 수 잇습니다. 두 가지 경우 모두 required 수정자(modifier)를 표시해야 합니다.
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
required 수정자의 사용은 해당 클래스의 모든 서브클래스에서 "이니셜라이져 요구사항에 대한 명시적이거나 상속된 구현"을 제공한다는 것을 보장합니다. 모든 서브클래스도 프로토콜을 준수하도록 말입니다.
final 수정자를 사용하는 final 클래스에 대해서는 required 수정자를 구현할 필요가 없습니다. final 클래스는 서브클래스가 없기 때문입니다.
만약 서브클래스가 슈퍼클래스의 지정 이니셜라이져를 재정의하는데 프로토콜로부터 이니셜라이져 요구사항을 만족시키면 정의한다면, required와 override 수정자를 같이 작성합니다.
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(실패가능 이니셜라이져 요구사항)
프로토콜은 준수하는 타입을 위해 실패가능 이니셜라이져 요구사항을 정의할 수 있습니다.
실패 가능 이니셜라이져는 적합한 타입의 실패가능 이니셜라이져 혹은 nonfailabe 이니셜라이져에 의해 만족될 수 있습니다. nonfailabe 이니셜라이져 요구사항은 nonfailabe 이니셜라이져 혹은 암시적 언래핑된 실패가능 이니셜라이져에 의해 구현될 수 있습니다.
Protocol as Types (타입 프로토콜)
프로토콜은 그 자체로 어떤 기능도 구현할 수 없습니다. 그럼에도 코드에서 프로토콜을 완전히 초기화된 타입으로 사용할 수 있습니다. 프로토콜을 타입으로 사용하는 것은 existential type 이라고도 불립니다. 이것은 프로토콜을 준수하는 타입 T가 존재한다는 것을 의미합니다.
아래와 같이 다른 타입이 허용되는 여러 위치에서 프로토콜을 사용할 수 있습니다.
- 함수, 메소드, 이니셜라이져에서 파라미터 타입 혹은 리턴 타입
- 상수, 변수, 프로퍼티의 타입
- 배열(array), 사전(dictionary), 다른 container 의 항목 타입
프로토콜이 타입이기 때문에, 대문자로 그 이름을 시작합니다
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
}
}
예제는 Dice라는 새로운 클래스를 정의합니다. Dice 인스턴스는 sides 라는 정수 프로퍼티를 가집니다. 주사위의 랜덤 넘버를 생성하는 generator 프로퍼티도 정의합니다.
getnerator 프로퍼티는 RandomNumverGenerator 타입을 가집니다. 따라서 RandomNumverGenerator 을 채택하는 모든 타입의 인스턴스로 설정할 수 있습니다. RandomNumverGenerator 프로토콜을 준수하는 인스턴스를 제외하고 다른 인스턴스들은 요구되지 않습니다. RandomNumverGenerator 타입이기 때문에, Dice 클래스 내부의 코드는 이 프로토콜을 준수하는 genrators 와 상호작용할 수 있습니다. 즉, generator의 기본 타입에 의해 정의된 메소드나 프로퍼티를 사용할 수 없다는 말입니다. 그러나 슈퍼클래스에서 서브클래스로 다운캐스트(downcast) 하는 것과 같은 방식으로 프로토콜 타입에서 기본 타입으로 다운캐스트할 수 있습니다.
Dice는 이니셜라이져도 가집니다. 이 이니셜라이져는 RandomNumverGenerator 타입의 generator라는 파라미터를 가집니다. 새 Dice 인스턴스를 초기화할 때 프로토콜을 준수하는 모든 타입의 값을 통과시킬 수 잇습니다.
Dice는 하나의 인스턴스 메소드 roll 을 가집니다. 이 메소드는 랜덤 넘버 생성을 위해 generator 의 random() 메소드를 호출합니다. 생성된 랜덤넘버로 주사위 숫자(1~6) 에 맞는 숫자를 생성합니다. generator가 RandomNumverGenerator 프로토콜을 채택했기 때문에, random() 메소드를 가져야 합니다.
아래는 어떻게 Dice 클래스를 사용하는지 보여줍니다. LinearCongrunentialGenerator 인스턴스를 임의의 숫자 생성기로 하여 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)은 클래스나 구조체의 역할을 다른 타입의 인스턴스로 위임할 수 있는 디자인 패턴입니다. 이 디자인 패턴은 위임 역할(delegated responsibilities) 을 캡슐화(encapsulate) 한 프로토콜을 정의함으로써 구현합니다. 위임된 기능을 제공할 수 있는 적합한 타입이 보장되어야 합니다. 위임은 특정 동작에 반응하거나 외부 소스에서 해당 소스의 기본 타입 없이 데이터를 검색하기위해 사용합니다.
아래 예제는 두 개의 프로토콜을 정의합니다.
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 프로토콜은 dice를 가지는 모든 게임에 채택될 수 있습니다.
DiceGameDelegate 프로토콜을 채택하여 DiceGame의 진행상황을 추적할 수 있습니다. 강한 참조(strong reference) 사이클을 막기 위해 위임은 약한 참조(weak reference) 로 정의됩니다.
프토콜을 클래스 전용으로 표시해 다음 예제에서 SpaksAndLadders 클래스는 위임자가 약한 참조를 사용해야한다고 선언할 수 있습니다. 클래스 전용 프로토콜은 AnyObject의 상속으로 표시됩니다.
아래 예제는 Snakes and Ladders 게임입니다. 이 버전은 주사위 역할에 Dice 인스턴스를 사용하고 DiceGame 프로토콜을 채택하며 진행상황을 DiceGameDelegate에 알리도록 변경되었습니다.
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)
}
}
이 버전의 게임은 DiceGame 프로토콜을 채택한 SnakesAndLadders로 구성되어 있습니다. 프로토콜을 만족시키기 위해 gettable dice 프로퍼티와 play() 메소드를 제공합니다. (초기화 후에 변경할 필요가 없으므로 dice 프로퍼티는 상수 프로퍼티로 선언됩니다. 프로토콜은 반드시 gettable 이여야 합니다)
Snakes and Ladders 게임 보드 설정은 init() 이니셜라이져로 설정됩니다. 모든 게임 로직은 프로토콜의 play() 메소드로 작동됩니다. 프로토콜이 요구하는 dice 프로퍼티를 사용하여 주사위 값을 제공합니다.
delegate 프로퍼티는 옵셔널 DiceGameDelegate로 선언된 것을 주목하세요. 위임은 게임을 진행하기 위해 필요하지 않기 때문입니다. 옵셔널 타입이기 때문에, delegate 프로퍼티의 초기값은 자동으로 nil이 됩니다. 그 후, 게임 instantiator 는 적절한 위임에 프로퍼티를 설정할 수 있는 옵션을 가지게 됩니다. DiceGameDelegate 프로토콜은 클래스-전용이므로, 참조 사이클을 막기위해 위임을 weak로 선언할 수 있습니다.
DiceGameDelegate는 게임 과정을 추적하기 위해 3가지 메소드를 제공합니다. 이 3가지 메소드는 위에서 설명한 play() 메소드에서 통합되었습니다. 새로운 게임이 시작하거나 새로운 턴이 시작하거나 게임이 끝나면 호출됩니다.
delegate 프로퍼티는 옵셔널 DiceGameDelegate 이기 때문에 play() 메소드는 delegate를 호출할 때마다 옵셔널 체이닝을 사용합니다. 만약 delegate 프로퍼티가 nil 이면, 이 delegate는 에러 없이 깔끔하게 실패합니다. delegate 프로퍼티가 nil 이 아니면, delegate는 메소드를 호출하고 SnakesAndLadders 인스턴스를 파라미터로 넣어줍니다.
아래 예제는 DiceGameDelegate 프로토콜을 적용한 DiceGameTracker 클래스입니다.
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 는 3 가지 메소드를 모두 구현해 DiceGameDelegate 프로토콜을 충족시킵니다.
gameDidStart(_:) 구현에서 game 파라미터를 사용하여 곧 실행될 게임 정보를 출력합니다. game 파라미터는 DiceGame 타입을 가집니다. 따라서 gameDidStart(_:) 은 DiceGame 프로토콜의 일부로 구현된 메소드와 프로퍼티에만 접근하고 사용할 수 있습니다.
그러나 메소드는 여전히 기본 인스턴스 타입을 요청하기 위한 타입 캐스팅을 사용할 수 있습니다. 예제에서 , 백그라운드에서 game이 실제로 SnakesAndLadders의 인스턴스인지 확인합니다.
gameDidStart(_:) 메소드는 전달된 game 파라미터의 dice 프로퍼티에도 접근합니다. game이 DiceGame프로토콜을 준수한다고 알려졌기 때문에, dice 프로퍼티를 가지는 것이 보장됩니다. 따라서 어떤 종류의 게임이 실행되는지 상관없이, gameDidStart(_:) 는 dice의 sides 프로퍼티에 접근하고 출력할 수 잇습니다.
아래는 DiceGaemTracker가 작동하는 것을 보여줍니다.
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 (확장에 프로토콜 준수 추가)
기존의 타입에 프로토콜을 채택하고 준수하기 위해 확장할 수 있습니다. 기존 타입의 소스코드에 접근할 필요가 없습니다. 확장(Extenstion) 은 기존의 타입에 새로운 프로퍼티, 메소드, 첨자구문을 추가할 수 있습니다. 프로토콜이 요구하는 요구사항을 추가할 수도 있습니다.
확장에서 인스턴스 타입에 해당 준수가 추가되면, 기존 타입의 인스턴스는 자동으로 프로토콜을 채택하고 준수합니다.
아래 예제의 TextRepresentable 프로토콜을은 텍스트로 표현할 수 있는 모든 타입을 구현할 수 있습니다. 이것은 자신에 대한 설명일 수도 있고, 현재 상태의 텍스트 버전일 수 있습니다.
protocol TextRepresentable {
var textualDescription: String { get }
}
앞서 선언한 Dice 예제를 확장해 TextRepresentable 프로토콜을 채택하고 준수할 수 있습니다.
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
이 Extension은 새로운 프로토콜을 채택합니다. 기존 방식과 동일하게 이름 뒤에 : 를 사용합니다. { } 안에서 프로토콜의 요구사항을 준수합니다.
모든 Dice 인스턴스는 TextRepresentable 로 취급될 수 있습니다.
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"
비슷하게 SnakesAndLadders 게임 클래스도 확장해서 TextRepresentable 프로토콜을 채택하고 준수할 수 있습니다.
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 (조건부 프로토콜 준수)
제네릭(generices) 타입은 특정 조건에서만 프로토콜 요구사항을 준수할 수 있습니다. 타입의 제네릭 파라미터가 프로토콜을 준수할 때 처럼 말입니다. 타입을 확장할 때 제약조건(contraints) 을 나열함으로써, 조건부로 프로토콜을 준수하는 제네릭 타입을 만들 수 있습니다. 제네릭 where 절을 작성하여 이러한 제약 조건을 채택할 프로토콜 이름 뒤에 작성합니다.
아래 확장은 Array 인스턴스가 TextRepresentable을 준수하는 타입의 항목을 저장할 때마다 TextRepresentable 준수하도록 합니다.
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 (확장을 통한 프로토콜 채택 선언)
타입이 이미 프로토콜의 요구사항을 모두 만족시켰지만 아직 프로토콜을 채택하지 않았다면, 빈 확장으로 프로토콜을 채택할 수 있습니다.
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
이제 TextRepresentable 이 필수 타입인 모든 Hamster 인스턴스를 사용할 수 있습니다.
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"
타입은 단순히 요구사항을 만족했다고 프로토콜을 채택하지 않습니다. 프로토콜을 채택을 명시적으로 선언해주어야 합니다.
Adopting a Protocol Using a Synthesized Implementation (합성 구현을 사용한 프로토콜 채택)
Swift는 많은 간단한 경우에 Equable, Hashable 및 Comparable에 대한 프로토콜 준수를 자동으로 제공할 수 있습니다. 이렇게 합성된 구현을 사용하면 프로토콜 요구 사항을 직접 구현하기 위해 반복 상용구 코드를 작성할 필요가 없습니다.
Swift는 아래의 사용자 정의 타입에 대해 Equatable의 합성구현을 제공합니다.
- Equatable 프로토콜을 준수하는 저장 프로퍼티(stored properties) 만 있는 구조체
- Equatable 프로토콜을 준수하는 associated types 만 있는 열거형
- associated types 이 없는 열거형
== 연산자를 직접 구현하지 않고 ==의 합성 구현을 사용하기 위해, 원본 선언이 들어있는 파일에서 Equatable 준수를 선언합니다. Equatable 프로토콜은 != 의 기본 구현을 제공합니다.
아래 예제는 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는 아래의 사용자 정의 타입에 대해 Hashable 의 합성구현을 제공합니다.
- Hashable 프로토콜을 준수하는 저장 프로퍼티(stored properties) 만 있는 구조체
- Hashable 프로토콜을 준수하는 associated types 만 있는 열거형
- associated types 이 없는 열거형
hash(into:) 를 직접 구현하지 않고 hash(into:) 의 합성 구현을 사용하기 위해, 원본 선언이 들어있는 파일에서 Hashable준수를 선언합니다.
Swift는 원시값을 가지지 않는 열거형에 대해 Comparable의 통합 구현을 제공합니다. 만약 열거형이 associated types을 가지면, 모두 Comparable 프로토콜을 준수해야 합니다. <를 직접구현 하지 않고 <의 통합구현을 사용하기 위해서, 원본 열거형 선언을 포함하는 파일에서 Comparable 준수를 선언합니다. Comparable 프토콜을 다른 비교연산자 <=, >, >= 도 기본으로 제공합니다.
아래 예제는 Comparable을 적용한 열거형 SkillLevel 입니다.
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 (프로토콜 타입 컬렉션)
프로토콜은 배열이나 사전(Dictionary)과 같은 컬렉션(collection)에 저장되는 타입으로 사용할 수 있습니다.
아래 예제는 TextRepresentable 배열을 생성합니다.
let things: [TextRepresentable] = [game, d12, simonTheHamster]
이제 배열에 있는 항목을 반복(iterative) 할 수 있습니다.
for thing in things {
print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon
TextRepresentable 타입의 things 상수를 주목하십시오. Dice, DiceGame, Hamster 타입이 아닙니다. 백그라운드에서 실제 인스턴스가 해당 유형 중 하나에 해당되도 말입니다. 그럼에도 TextRepresentable 타입이고 TextRepresentable 인 모든 항목은 textualDescription 프로퍼티를 가지는 것으로 알려져 있기 때문에, loop 에서 thing.textualDescription 에 접근하는 것이 안전합니다.
Protocol Inheritance (프로토콜 상속)
프로토콜은 하나 이상의 프로토콜을 상속할 수 있으며 상속되는 요구사항에 요구사항을 더 추가할 수 있습니다. 프로토콜 상속 구문은 클래스 상속 구문과 비슷합니다. 하지만 여러 개의 상속 프로토콜을 받을 수 있습니다.
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// protocol definition goes here
}
아래 예제는 TextRepresentable 프로토콜을 상속받습니다
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}
예제에서 TextRepresentable을 상속하는 새로운 프로토콜 PrettyTextRepresentable 을 선언합니다. PrettyTextRepresentable 을 채택하는 모든 것들은 TextRepresentable 요구사항을 만족해야합니다. 또 PrettyTextRepresentable 의 추가 요구사항도 만족해야 합니다. 예제에서 PrettyTextRepresentable 은 단일 요구사항 prettyTextualDescription 을 추가합니다.
SnakesAndLadders 클래스는 PrettyTextRepresentable 을 채택하고 준수하기 위해 확장할 수 있습니다.
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
}
}
예제의 확장에서는 PrettyTextRepresentable 을 채택하고 SnakesAndLadders 타입에 prettyTextualDescription 프로퍼티를 구현한다고 명시합니다. PrettyTextRepresentable 은 TextRepresentable 을 만족해야 합니다.
또 prettyTextualDescription 구현은 TextRepresentable 프로토콜의 textualDescription 에 접근함으로써 시작합니다.
- square 의 값이 0 보다 크면, 사다리의 바닥이고 ▲ 로 표현됩니다
- square 의 값이 0 보다 작으면, 뱀의의 머리이고 ▼ 로 표현됩니다
- 그렇지 않으면 square의 값은 0 이고 ○로 표현되는 "자유" square 입니다
prettyTextualDescription 프로퍼티는 모든 SnakesAndLadders 인스턴스의 pretty 텍스트 설명을 출력할 수 있습니다.
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
Class-Only Protocols (클래스 전용 프로토콜)
프로토콜 채택을 클래스 타입으로 한정할 수 있습니다. 이를 위해 프로토콜의 상속 리스트에 AnyObject를 작성합니다.
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// class-only protocol definition goes here
}
위 예제에서 SomeClassOnlyProtocol 는 클래스 타입에 의해서만 채택됩니다. 구조체나 열거형 정의에 적용하려고 하면 컴파일-타임 에러가 발생합니다.
해당 프로토콜의 요구사항에 의해 정의된 동작이 "값 타입이 value semantics 보다 reference semantics"를 가지고 있다고 가정되거나 요구될 때, 클래스 전용 프로토콜을 사용합니다.
Protocol Composition (프로토콜 합성)
한 타입이 동시에 여러 프로토콜을 준수하도록 요구하는 것이 유용할 수도 있습니다. 프로토콜 합성 (protocol composition) 을 사용해 여러 프로토콜을 하나로 합칠 수 잇습니다. 프로토콜 합성은 "합성 안에 있는 모든 프로토콜의 결합된 요구사항을 가진 임시 로컬 프로토콜을 정의한 것"처럼 작동합니다. 프로토콜 합성은 새로운 프로토콜 타입을 정의하지 않습니다.
프로토콜 합성은 SomeProtocl & AnotherProtocl 형식을 가집니다. & 을 사용해 필요한 프로토콜을 얼마든지 나열할 수 있습니다. 프로토콜 합성에는 프로토콜 목록 외에도 하나의 클래스 타입도 포함될 수 있습니다. 이걸 이용해 슈퍼클래스를 지정하는데 사용할 수 있습니다.
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 와 Aged가 있습니다. Name 프로토콜을 하나의 요구사항 name 을 가집니다. Aged 프로토콜도 하나의 요구사항 age를 가집니다. 두 개의 프로토콜은 구조체 Person에 채택됩니다.
예제에서 wishHappyBirthday(to:) 함수도 정의합니다. celebrator 파라미터의 타입은 Named & Aged 입니다. 이는 Namd 와 Aged 프로토콜을 만족하는 모든 타입" 을 의미합니다. 두 가지 프로토콜을 모두 준수하는 한 어떤 특정 타입이 함수를 통과하는 것은 문제가 아닙니다.
예제는 birthdayPerson 이라는 새로운 Person 인스턴스를 생성합니다. wishHappyBirthday(to:) 에 이 새로운 인스턴스를 넣습니다. Person이 프로토콜을 준수하기 때문에 호출이 유효합니다.
아래 예제는 Name 프로토콜과 Location 클래스를 합성합니다.
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 프로토콜을 준수해야 한다" 는 의미입니다. 예제에서 City는 두 개를 모두 만족합니다.
beginConcert(in:)에 birthdayPerson를 넣는 것은 유효하지 않습니다. Person은 Location의 서브클래스가 아니기 때문입니다. 마찬가지로 Location의 서브클래스를 만들어도 Name 프로토콜을 준수하지 않으면 beginConcert(in:)에 넣을 수 없습니다.
Checking for Protocol Conformance (프로토콜 준수 확인)
프로토콜을 준수하는지 확인하기 위고 특정 프로토콜을 캐스팅하기 위해 is 와 as 연산자를 사용할 수 있습니다. 프로토콜에 대한 확인 및 캐스팅은 "타입에 대한 확인 및 캐스팅" 과 정확히 같은 구문을 가집니다.
- is 연산자는 인스턴스가 프로토콜을 준수하면 true를 리턴하고, 그렇지 않으면 false를 리턴합니다
- 다운캐스트 연산자의 as? 버전은 프로토콜 타입의 옵셔널 값을 리턴합니다. 인스턴스가 프로토콜을 준수하지 않으면 리턴된 옵셔널 값은 nil 입니다.
- 다운캐스트 연산자의 as! 버전은 다운캐스트를 프로토콜 타입으로 강제시킵니다. 만약 다운캐스트가 성공하지 않으면 런타임 에러를 발생시킵니다.
아래 예제는 HasArea 프로토콜을 정의합니다. area라는 단일 프로퍼티 요구사항을 가집니다.
protocol HasArea {
var area: Double { get }
}
아래 두 개의 클래스 Circle 과 Country는 HasArea 프로토콜을 채택합니다.
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 클래스는 radius 프로퍼티에 기반해 area 프로퍼티 요구사항을 연산 프로퍼티(computed property)로 구현합니다. Country 클랫는 area 요구사항을 직접 저장 프로퍼티(stored property) 로 구현합니다. 두 클래스 모두 정확이 HasArea 프로토콜을 준수합니다.
아래 Animal 클래스는 HasArea 프로토콜을 준수하지 않습니다.
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
Circle, Country, Animal 클래스는 기본 클래스를 공유하지 않습니다. 그럼에도 불구하고 세 클래스와 세 타입의 인스턴스는 "AnyObject 타입의 값을 저장하는 배열"을 초기화하는데 사용할 수 있습니다.
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
objects 배열은 Circle(반지름 2), Country(영토 243,610 = 영국 크기), Animal (다리 4개)로 초기화됩니다.
objects 배열은 이제 반복(iterative) 될 수 있습니다. 배열의 각 항목은 HasArea 프로토콜을 준수하는지 확인할 수 있습니다.
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 프로토콜을 준수할 때마다, as? 연산자에 의해 리턴된 옵셔널 값은 objectWithArea라는 상수로 옵셔널 바인딩되어 들어갑니다. objectWithArea 상수는 HasArea 타입으로 알려져 있습니다. 따라서 area 프로퍼티에 접근할 수 있고 안전한 방법으로 출력할 수 있습니다.
기본 objects 는 캐스팅 과정에 의해 변경되지 않습니다. 여전히 Circle, Country, Animals 입니다. 그러나 objectWithArea 상수에 저장되는 시점에, HasArea 타입으로만 알려지고 area 프로퍼티만 접근할 수 있습니다.
Optional Protocol Requirements (옵셔널 프로토콜 요구사항)
프로토콜에 옵셔널 요구사항을 정의할 수 있습니다. 이 요구사항은 프로토콜을 준수하는 타입으로 구현할 필요가 없습니다. 옵셔널 요구사항은 프로토콜 정의를 할 때 optional 수정자를 접두사로 사용합니다.
옵셔널 요구사항은 Objective-C와 상호 운용되는 코드를 작성할 수 있습니다. 프로토콜과 옵셔널 요구사항 모두 @objc attribute 로 표시해야합니다. @objc 프로토콜은 Objective-C 클래스 또는 기타 @objc 클래스에서 상속된 클래스만 채택할 수 있습니다. 구조체나 열거형에 의해서는 채택될 수 없습니다.
옵셔널 요구사항에 메소드나 프로퍼티를 사용할 때, 그 타입은 자동적으로 옵셔널이 됩니다. 예를 들어, (Int) -> String 타입 메소드는 ((Int) -> String)? 이 됩니다. 전체 함수 타입은 메소드의 반환 값이 아닌 옵셔널로 래핑됩니다.
옵셔널 프로토콜 요구사항은 옵셔널 체이닝으로 호출될 수 있습니다. 해당 요구사항이 프로토콜을 준수하는 타입에 의해서 구현되지 않는 가능성을 설명합니다. 메소드가 호출될 때 메소드 이름 뒤에 ? 를 작성하여 옵셔널 메소드의 구현을 확인합니다.
아래 예제는 integer-counting 클래스 Counter를 정의합니다. 외부 데이터 소스를 자용하여 증가하는 양을 제공합니다. 이 데이터 소스는 CounterDataSource 프로토콜에 의해 정의됩니다. 프로토콜은 두 개의 옵셔널 요구사항을 가집니다.
@objc protocol CounterDataSource {
@objc optional func increment(forCount count: Int) -> Int
@objc optional var fixedIncrement: Int { get }
}
CounterDataSource 프로토콜은 옵셔널 메소드 요구사항 increment(forCount:) 를 정의합니다. 옵셔널 프로퍼티 요구사항 fixedIncrement 도 정의합니다. 이 요구사항은 데이터 소스가 Counter 인스턴스에 적절한 증가 금액을 제공하는 두 가지 방법을 제공합니다
엄격히 말하면, 프로토콜 요구 사항을 구현하지 않고도 CounterDataSource 를 준수하는 사용자 지정 클래스를 작성할 수 있습니다. 두 방법 모두 옵셔널입니다. 기술적으로 허용되지만, 이는 좋은 데이터 소스를 만드는데 적합하지 않습니다.
아래 정의된 Counter 클래스는 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 클래스는 현재 값을 변수 프로퍼티 count에 저장합니다. Counter 클래스는 increment 메소드도 정의합니다.
increment() 메소드는 먼저 데이터 소스에서 increment(forCount:) 메소드의 구현을 검색해서 증가 금액 검색을 시도합니다. increment() 메소드는 옵셔널 체이닝을 사용해 increment(forCount:) 호출을 시도합니다. 그리고 현재 count 값을 메소드의 단일 인수로 통과시킵니다.
여기에 두 단계의 옵셔널 체이닝이 실행됩니다.
첫 번째로 dataSource 가 nil 일 가능성이 있습니다. 따라서 dataSource? 로 작성해 nil 이 아닐때 만 increment(forCount:)를 호출하는 것을 나타냅니다.
두 번째로 dataSource가 존재하더라도 increment(forCount:)이 구현한다는 보장이 없습니다. 이는 옵셔널 요구사항이기 때문입니다. increment(forCount:)이 구현되지 않을 가능성도 옵셔널 체이닝으로 처리됩니다. increment(forCount:)이 존재할 때만 increment(forCount:)이 호출됩니다. 따라서 increment(forCount:)? 로 작성됩니다.
이 두가지 이유로 increment(forCount:) 호출이 실패할 수 있기 때문에, 호출은 옵셔널 Int 값을 리턴합니다. 이는 increment(forCount:) 이 "CountDataSource 정의에서 옵셔널이 아닌 Int 값을 반환하는 것"으로 정의되어도 마찬가지입니다. 두 개의 옵셔널 체인 작업이 차례로 수행되지만, 결과는 여전히 단일 옵셔널로 래핑됩니다.
increment(forCount:) 호출 이후에 리턴된 옵셔널 Int는 언래핑되어 amount 상수에 들어갑니다. 옵셔널 Int 값이 포함되어 있는 경우 (위임과 메소드가 동시에 존재하며 메소드가 값을 리턴하는 경우), 언래핑된 amount 는 저장 count 프로퍼티에 추가되고 증가가 완료됩니다.
increment(forCount:) 메소드로부터 값을 검색할 수 없는 경우, increment() 메소드는 데이터 소스의 fixedIncrement 프로퍼티에서 값 검색을 시도합니다. fixedIncrement 프로퍼티 는 옵셔널 요구사항이기도 합니다. fixedIncrement 가 CountDataSource 프로토콜 정의에서 옵셔널이 아닌 Int 프로퍼티로 정의되어도, 그 값은 옵셔널 Int 값입니다.
여기에 간단한 CounterDataSource 예제가 있습니다. 데이터 소스가 상수 값 3을 매번 요청할 때마다 반환해 줍니다. 옵셔널 fixedIncrement 프로퍼티 요구사항을 통해 이를 구현합니다.
class ThreeSource: NSObject, CounterDataSource {
let fixedIncrement = 3
}
새 인스턴스 Counter 를 위해 ThreeSource 인스턴스를 데이터 소스로 사용할 수 있습니다.
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
print(counter.count)
}
// 3
// 6
// 9
// 12
코드는 새 인스턴스 Counter 를 생성합니다. 데이터 소스를 새 인스턴스 ThreeSource로 설정합니다. counter의 increment() 메소드를 4번 호출합니다. 기대한대로 counter의 count 프로퍼티는 increment() 호출마다 3 씩 증가합니다.
아래 예제는 조금 복잡한 데이터 소스 TowardsZeroSource를 정의합니다. Counter 인스턴스를 현재 count 값에서 0을 향해 위 혹을 아래로 카운트 합니다.
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 클래스는 CounterDataSource 프로토콜로부터 옵셔널 메소드 increment(forCount:) 를 구현합니다. 어느 방향으로 카운트 할지 결정하기 위해 count 인수 값을 사용합니다. count 가 이미 0 이면 메소드는 0을 리턴해 더 이상 카운트 할 필요가 없다는 것을 나타냅니다.
TowardsZeroSource 의 인스턴스를 기존의 Counter 인스턴스와 함께 사용할 수 있습니다. 이를 통해 -4 부터 0 까지 카운트합니다. counter 가 0에 도달하면, 카운트는 종료됩니다.
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
Protocol Extensions (프로토콜 확장)
프로토콜은 메소드, 이니셜라이져, 첨자 구문(subscript), 계산 프로퍼티(computed property) 구현을 준수 타입에 제공하도록 확장할 수 잇습니다. 이는 각 타입의 개별 준서 혹은 전역 함수가 아닌, 프로토콜 자신이 동작을 정의하도록 허용합니다.
예를 들어, RandomNumberGenerator 프로토콜은 randomBool() 메소드를 제공함으로써 확장될 수 있습니다.
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
프로토콜에 확장을 생성함으로써, 모든 준수 타입은 추가적인 수정없이 자동적으로 이 메소드 구현을 가집니다.
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"
프로토콜 확장은 준수 타입에 구현을 추가할 수 있습니다. 하지만 프로토콜 확장을 만들거나 다른 프로토콜로부터 상속을 받지는 못합니다. 프로토콜 상속은 항상 프로토콜 정의 자체에 명시됩니다.
Providing Default Implementations (기본 구현 제공)
해당 프로토콜의 메소드나 연산 프로퍼티 요구사항(computed property requirement) 에 기본 구현을 제공하기 위해 프로토콜 확장을 사용할 수 있습니다. 만약 준수 타입이 자신의 "요구되는 메소드나 프로퍼티 구현"을 제공한다면, 해당 구현은 확장에서 제공하는 것 대신 사용됩니다.
확장에서 제공하는 기본 구현이 포함된 프로토콜 요구 사항은 옵셔널 프로토콜 요구사항과 다릅니다. 준수 타입은 둘 다 자체 구현을 제공할 필요가 없지만, 기본 구현이 있는 요구사항은 옵셔널 체이닝 없이 호출할 수 있습니다.
예를 들어, TextRepresentable 를 상속하는 PrettyTextRepresentable 프로토콜은 요구 프로퍼티 prettyTextualDescription 에 기본 구현을 제공할 수 잇습니다. 이를 통해 textualDescription 프로퍼티 접근의 결과를 간단히 리턴합니다.
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
Adding Constraints to Protocol Extensions (프로토콜 확장에 제약 조건 추가)
프로토콜 확장을 정의할 때, 제약조건(constraints)를 명시할 수 있습니다. 확장의 메소드와 프로퍼티를 사용할 수 있기전에 준수타입이 반드시 충족되어야 합니다. 이 제약조건을 제네릭 where 절을 작성하여 확장하는 프로토콜 이름 뒤에 작성할 수 있습니다.
예를 들어 Collection 프로토콜의 확장을 정의할 수 있습니다. 이 확장은 Equable 프로토콜을 준수하는 요소의 모든 컬렉션에 적용됩니다. 컬렉연 요소를 (표준 라이브러리의 일부인) Equatable 프로토콜에 제한함으로써, == 와 != 연산자를 작성할 수 있습니다. 이를 통해 두 요소가 같은지 다른지 확인할 수 있습니다.
extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}
allEqaul() 메소드는 컬렉션의 모든 요소가 같을 때만 true 를 리턴합니다.
두 개의 배열이 있습니다. 하나는 모든 요소가 똑같고, 다른 하나는 그렇지 않습니다.
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
배열이 Collection을 준수하고 정수가 Equatable, equalNumbers, differentNumbers를 준수하기 때문에 allEqaul() 메소드를 사용할 수 있습니다.
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"
준수 타입이 동일한 "메소드나 프로퍼티에 대한 구현을 제공하는 여러 개의 제약된 확장(multiple constrained extensions)에 대한 요구사항"을 충족시킨다면, Swfit는 가장 전문화된 제약 조건에 해당하는 구현을 사용합니다.
프로토콜 = 규칙
메소드, 프로퍼티, 클래스 등이 지켜야할 규칙을 미리 만들어 놓을 수 있다.
요구사항으로 프로퍼티, 메소드, 이니셜라이져를 작성할 수 있다.
여러 개를 같이 사용할 수 있다.
상속할 수 있다.
옵셔널 요구사항을 사용할 수 있다.
중요한 내용은 이 정도 인 것 같습니다.
이렇게 해서 길고 긴 프로토콜이 끝이 났습니다....
길어도 너무 기네요 😂
수식어가 너무 많아 해석하는 것도 힘들고 한 번에 이해하는 것도 힘드네요
암튼 끝! 😁
'Swift > Swift 공식문서' 카테고리의 다른 글
[Swift] 공식문서 24 - Opaque Types (불투명 타입) (0) | 2021.11.06 |
---|---|
[Swift] 공식문서 23 - Gernerics (제네릭) (0) | 2021.11.05 |
[Swift] 공식문서 21 - Extensions (익스텐션) (0) | 2021.11.02 |
[Swift] 공식문서 20 - Nested Types (중첩 타입) (0) | 2021.11.01 |
[Swift] 공식문서 19 - Type Castng (타입 캐스팅) (0) | 2021.10.31 |
- Total
- Today
- Yesterday
- Swift문법
- 부스트캠프7기
- 책리뷰
- 개발
- Swift DocC
- 필독서
- 코딩테스트
- 프로그래머스
- 디자인 패턴
- todo앱
- UX
- 부스트캠프
- Swift 서버
- vapor
- 책후기
- Combine
- Swift공식문서
- 애플
- SwiftUI
- swiftUI 기초
- ios
- 코딩 테스트
- Swift
- Swift 디자인 패턴
- 부스트캠프iOS
- TODO
- 책
- 날씨어플
- 코딩
- 앱개발
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |