준수하는 타입이 구현해야 하는 요구사항을 정의합니다.
프로토콜은 메서드, 프로퍼티, 그리고 특정 작업이나 기능의 부분이 적합한 다른 요구사항의 청사진을 정의합니다. 프로토콜은 요구사항의 구현을 제공하기 위해 클래스, 구조체, 또는 열거형에 의해 채택될 수 있습니다. 프로토콜의 요구사항에 충족하는 모든 타입은 프로토콜에 준수(conform)한다고 합니다.
준수하는 타입의 요구사항을 지정하는 것 외에도 요구사항의 일부를 구현 하거나 준수하는 타입에 추가 기능을 구현하기 위해 프로토콜을 확장할 수 있습니다.
클래스, 구조체, 그리고 열거형과 유사한 방법으로 프로토콜을 정의합니다.
protocol SomeProtocol {
}
사용자 정의 타입은 콜론으로 구분된 타입의 이름 뒤에 특정 프로토콜의 이름을 위치시켜 정의의 부분으로 특정 프로토콜을 채택합니다. 여러 프로토콜은 콤마로 구분되고 리스트화 할 수 있습니다.
struct SomeStructure: FirstProtocol, AnotherProtocol {
}
클래스가 상위 클래스를 가진 경우에 콤마로 구분하여 채택한 모든 프로토콜 전에 상위 클래스 이름을 위치 시킵니다.
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
}
프로토콜은 특정 이름과 타입을 가진 인스턴스 프로퍼티 또는 타입 프로퍼티를 제공하기 위해 모든 준수하는 타입을 요구할 수 있습니다. 프로토콜은 요구된 프로퍼티 이름과 타입만 지정하고 프로퍼티가 저장된 프로퍼티 또는 계산된 프로퍼티 인지에 대한 것은 지정하지 않습니다. 프로토콜은 각 프로퍼티가 gettable인지 gettable과 settable 인지도 지정해 줘야 합니다.
프로토콜이 gettable과 settable인 프로퍼티를 요구할 경우 프로퍼티 요구사항은 저장된 프로퍼티 상수 또는 읽기전용 계산된 프로퍼티는 충족할 수 없습니다. 프로토콜이 gettabl인 프로퍼티 만 요구할 경우 이 요구사항은 모든 종류의 프로퍼티에 충족될 수 있고 이것이 유용한 경우 settable 또한 프로퍼티에 대해 유효합니다.
프로퍼티 요구사항은 항상 var 키워드와 함께 변수 프로퍼티로 선언됩니다. gettable과 settable 프로퍼티는 타입 선언 뒤에 { get set }으로 작성하여 나타내고 gettabl 프로퍼티는 { get } 으로 작성하여 나타냅니다.
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
타입 프로퍼티 요구하려면 프로토콜에 정의할 때 static 키워드를 접두사로 둡니다. 이 규칙은 타입 프로퍼티 요구사항이 클래스에 의해 구현될 때 class 또는 static 키워드를 붙일 수 있는 경우에도 적용됩니다.
protocol AnotherProtocol{
static var someTypeProperty: Int { get set }
}
다음은 단일 인스턴스 프로퍼티 요구사항을 가지는 프로토콜의 예입니다.
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed 프로토콜은 완변학 이름을 제공하기 위해 준수하는 타입을 요구합니다. 이 프로토콜은 다른 준수하는 타입을 지정하지 않으며 타입이 자체에 대한 전체 이름을 제공해야 된다고만 지정합니다. 이 프로토콜은 모든 FullyNamed 타입이 String 타입의 fullNamed 이라는 gettable 인스턴스 프로퍼티를 가져야 합니다.
다음은 FullyNamed 프로토콜은 채택하고 준수하는 구조체에 대한 예입니다.
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullNamed is "John Appleseed"
이 예제는 특정 이름을 가진 사람을 나타내는 Person 이라는 구조체를 정의합니다. 정의의 첫번째 줄의 부분으로 FullyNamed 프로토콜을 채택합니다.
Person의 각 인스턴스는 String 타입의 fullNamed 이라는 단일 저장된 프로퍼티를 가집니다. 이것은 FullyNamed 프로토콜의 단일 요구사항과 일치하고 Person은 프로토콜을 올바르게 준수하고 있다고 얘기합니다.
다음은 FullyNamed 프로토콜을 채택하고 준수하는 더 복잡한 클래스입니다.
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 프로퍼티 요구사항을 구현합니다. 각 Starship 클래스 인스턴스는 필수로 name과 옵셔널 prefix를 저장합니다. fullName 프로퍼티는 prefix 값이 존재하면 사용하고 스타십에 대한 전체 이름을 생성하기 위해 name의 앞에 추가합니다.
프로토콜은 준수하는 타입에 의해 구현되기 위해 지정한 인스턴스 메서드와 타입 메서드를 요구할 수 있습니다. 이 메서드는 일반적인 인스턴스와 타입 메서드와 같은 방식으로 명시적으로 프로토콜의 정의의 부분으로 작성되지만 중괄호가 없거나 메서드 본문이 없습니다. 일반적인 메서드와 같은 규칙에 따라 가변 파라미터는 허용됩니다. 그러나 기본 값은 프로토콜의 정의 내에서 메서드 파라미터에 대해 지정될 수 없습니다.
타입 프로퍼티 요구사항과 마찬가지로 프로토콜에 정의될 때 static 키워드를 항상 타입 메서드 요구사항 앞에 표기합니다. 클래스에 의해 구현될 때 타입 메서드 요구사항에 class 또는 static 키워드가 접두사로 붙는 경우에도 마찬가지입니다.
protocol SomeProtocol {
static func someTypeMethod()
}
아래의 예제는 단일 인스턴스 메서드 요구사항을 가지는 프로토콜을 정의합니다.
protocol RandomNumberGenerator {
func random() -> Double
}
RandomNumerGenerator 프로토콜은 호출될 때마다 Double값을 반환하는 random이라는 인스턴스 메서드를 가지는 모든 준수하는 타입을 요구합니다. 프로토콜 부분으로 지정되지 않았지만 이 값은 0.0부터 1.0미만의 숫자라고 가정합니다.
RandomNumberGenerator 프로토콜은 각 난수가 생성되는 방법에 대해 어떠한 것도 가정하지 않습니다. 단순히 생성기가 새로운 난수를 생성하는 표준 방법을 제공하면 됩니다.
다음은 RandomNumberGenerator 프로토콜을 채택하고 준수하는 클래스의 구현입니다. 이 클래스는 선형 합동 생성기로 알려진 의사 난수생성기 알고리즘을 구현합니다.
class LinearCongruentialGenerator: RandomNumerGenerator {
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.
메서드가 속한 인스턴스를 수정 또는 변경 해야하는 경우가 있습니다.
값 타입(구조체와 열거형)에 대한 인스턴스 메서드의 경우 메서드의 func 키워드 앞에 mutating 키워드를 위치시켜 메서드가 속한 인스턴스와 인스턴스의 모든 프로퍼티를 수정할 수 있음을 나타냅니다.
프로토콜을 채택하는 모든 타입의 인스턴스를 변경하기 위한 프로토콜 인스턴스 메서드 요구사항을 정의하는 경우 프로토콜의 정의의 부분으로 mutating 키워드로 메서드를 표시합니다. 이를 통해 구조체와 열거형이 프로토콜을 채택하고 메서드 요구사항을 충족할 수 있습니다.
Note
mutating으로 프로토콜 인스턴스 메서드 요구사항을 표시하면 클래스에 대한 해당 메서드의 구현을 작성할 때mutating키워드를 작성할 필요가 없습니다.mutating키워드는 구조체와 열거형에 의해서만 사용됩니다.
아래의 예제는 toggle 이라는 단일 인스턴스 메서드 요구사항을 정의하는 Togglable 이라는 프로토콜을 정의합니다. 이름에서 알 수 있듯이 toggle() 메서드는 해당 타입의 프로퍼티를 수정하여 모든 준수하는 타입의 상태를 전환하거나 반전하기 위한 것입니다.
toggle() 메서드는 호출될 때 준수하는 인스턴스의 상태를 변경하기 위한 메서드를 나타내기 위해 Togglable 프로토콜 정의의 부분으로 mutating 키워드로 표시됩니다.
protocol Togglable {
mutating func toggle()
}
구조체 또는 열거형에 대해 Togglable 프로토콜을 구현하면 해당 구조체 또는 열거형은 mutating 으로 표시된 toggle() 메서드의 구현을 제공하는 프로토콜을 준수할 수 있습니다.
아래의 예제는 OnOffSwitch 라는 열거형을 정의합니다. 이 열거형은 열거형 케이스인 on 과 off를 나타내기 위해 2개의 상태를 변경합니다. 이 열거형의 toggle 구현은 Togglable 프로토콜의 요구사항을 일치시키기 위해 mutating 으로 표시됩니다.
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
프로토콜은 준수하는 타입에 초기화 구문을 구현하도록 요구할 수 있습니다. 일반적인 초기화 구문과 동일한 방식으로 명시적으로 프로토콜의 정의의 부분으로 초기화 구문을 작성하지만 중괄호 또는 초기화 구문 본문 없이 작성합니다.
protocol SomeProtocol {
init(someParameter: Int)
}
지정된 초기화 구문 또는 편의 초기화 구문으로 준수하는 클래스에 프로토콜 초기화 구문 요구사항을 구현할 수 있습니다. 이 모든 케이스에 대해 requried 수식어와 함께 초기화 구문 구현에 표시해야 합니다.
class Someclass: SomeProtocol {
required init(someParameter: Int) {
//initializer implementation goes here
}
}
required 수식어를 사용하면 준수하는 클래스의 모든 하위 클래스에 초기화 구문 요구사항의 명시적 또는 상속된 구현을 제공하여 프로토콜을 준수할 수 있습니다.
하위 클래스가 상위 클래스의 지정된 초기화 구문을 재정의 하고 프로토콜로 부터 일치하는 초기화 구문 요구사항이 구현되면 required 와 override 수식어 둘 다 초기화 구문 구현에 표시합니다.
protocol SomeProtocol {
init()
}
class SOmeSuperClass {
init() {
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
required override init() {
}
}
프로토콜 자체는 어떤 기능도 구현하지 않습니다. 이런점과 상관없이 코드에서 타입으로 프로토콜을 사용할 수 있습니다.
타입으로 프로토콜을 사용하는 가장 일반적인 방법은 일반 제약조건으로 프로토콜을 사용하는 것입니다. 일반 제약 조건이 있는 코드는 프로토콜을 준수하는 어떠한 타입에서 동작할 수 있고 특정 타입은 API를 사용하는 코드에 의해 선택됩니다. 예를 들어, 하나의 인자를 가지고 그 인자의 타입이 제네릭인 함수를 호출할 때 호출자는 타입을 선택합니다.
불투명한 타입을 가지는 코드는 프로토콜을 준수하는 일부 타입에서 동작합니다. 기본 타입은 컴파일 시간에 알 수 있으며 API 구현은 해당 타입을 선택하지만 해당 타입의 식별자는 API의 클라이언트로 부터 숨깁니다. 불투명한 타입을 사용하면 추상 레이어를 통해 API의 자세한 구현이 노출되는 것을 방지할 수 있습니다. 예를 들어, 함수로부터 특정 반환 타입을 숨기고 값이 지정된 프로토콜을 준수하는 것만 보장합니다.
박스형 프로토콜 타입을 가지는 코드는 런타임 때 선택된 프로토콜을 준수하는 모든 타입에서 동작합니다. 런타임 유연성을 지원하기 위해 Swift는 필요할 때 성능 비용응 가지는 박스 라고 알려진 간접 참조 수준을 추가합니다. 유연성 때문에 Swift는 컴파일 시에 기본 타입을 알 수 없습니다. 이것은 프로토콜에 의해 요구되는 멤버만 접근할 수 있다는 의미입니다. 기본 타입의 다른 API에 접근하려면 런타임 시 캐스팅이 필요합니다.
위임(Delegation)은 클래스 또는 구조체가 책임의 일부를 다른 타입의 인스턴스에 넘겨주거나 위임할 수 있도록 하는 디자인 패턴입니다. 이 디자인 패턴은 위임된 기능을 제공하기 위해 준수하는 타입(대리자라고 함)이 보장되도록 위임된 책임을 캡슐화하는 프로토콜을 정이ㅡ하여 구현합니다. 위임은 특정 작업에 응답하거나 해당 소스의 기본 타입을 알 필요 없이 외부 솟에서 데이터를 검색하는데 사용할 수 있습니다.
아래 예제는 주사위 기반 보드게임에 사용할 2가지 프로토콜을 정의합니다.
protocol DiceGame {
var dice: Dice { get }
func play()
}
protocol DiceGameDelegate: AnyObject {
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartWithNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}
DiceGame 프로토콜은 주사위를 포함하는 모든 게임에 의해 채택될 수 있는 프로토콜 입니다.
DiceGameDelegate 프로토콜은 DiceGame의 진행사항을 추적하기 위해 채택될 수 있습니다. 강한 참조 사이클을 방지하기 위해 위임자는 약한 참조로 선언욉니다. 클래스 전용 프로토콜은 AnyObject의 상속으로 표시됩니다. 다음은 제어 흐름(Control Flow) 에서 기존에 도입된 Snakes and Ladders 게임의 버전입니다. 이 버전은 주사위 굴림에 대해 Dice인스턴스를 사용하기 위해 채택하고 DiceGame 프로토콜을 채택하고 진행사항에 대해 알리기 위해 DiceGameDelegate를 채택합니다.
class SnakesAndLadders: DiceGame {
let finalSquare = 25
let dice = Dice(slides: 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:
breadk gameLoop
case let newSquare where newSquare > finalSquare:
continue gameLoop
default:
square += diceRoll
square += board[square]
}
}
delegate?.gameDidEnd(self)
}
}