티스토리 뷰
프로토콜은 메소드. 프로퍼티 그리고 기능을 게공하기 위한 적합한 것들의 청사진을 정의합니다. 프로토콜은 프로토콜의 요구사항을 준수하는 클래스, 구조체 또는 열거형에서 채택할 수 있습니다. 프로토콜의 요구사항을 만족시키는 타입을 프로토콜을 '준수(conform)'한다고 부릅니다.
Protocol 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
프로토콜은 프로토콜을 준수하는 타입에게 특정 이름과 타입을 가지고 있는 인스턴스/타입 프로퍼티를 요구할 수 있습니다. 프로토콜은 ㅡ프로퍼티가 연산인지 저장인지 신경쓰지 않으며 단지 이름과 타입에만 신경씁니다. 프로토콜은 또한 각 프로퍼티가 읽기전용인지 읽기/쓰기 전용인지도 식별합니다.
만약 프로토콜이 읽기/쓰기 프로퍼티를 요구할 경우, 상수 저장 프로퍼티나 읽기 전용 연산 프로퍼티는 준수사항이 될 수 없습니다. 만약 프로토콜이 읽기전용 프로퍼티를 요구할 경우 어떤 형태의 프로퍼티건 준수사항을 만족합니다.
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
프로퍼티 요구는 항상 변수 프로퍼티로 정의됩니다. (var 접미사가 붙는 형태로) 읽기/쓰기 프로퍼티는 { get set }이, 읽기 전용 프로퍼티는 { get } 이 선언 뒤에 붙습니다.
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
class 또는 static 키워드든간에 프로코톨에서는 타입 프로퍼티 요구 선언은 접미사에 static을 붙이는 형태로 합니다.
Method Requirements
프로토콜은 프로토콜을 준수하는 타입에게 특정 이름과 타입을 가지고 있는 인스턴스/타입 프로퍼티를 요구할 수 있습니다 일반적인 인스턴스/타입 메소드와 동일한 방법으로 작성됩니다. 그러나 메소드 내부는 작성하지 않습니다. Variadic parameter(까먹었으면 검색), 파라미터의 기본값등이 허용되나 프로토콜에선 사용할 수 없습니다.
protocol SomeProtocol {
static func someTypeMethod()
}
타입 프로퍼티가 그러했던 것처럼 타입 메소드 앞에는 static이 붙어야 합니다.(class건 static이건 간에)
protocol RandomNumberGenerator {
func random() -> Double
}
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"
인스턴스 메소드 요구 선언의 모습. 문법적으론 볼 건 없지만 아래 예제가 프로토콜의 사용도 및 사용방법을 보여주는 것 같아 가져왔다.
RandomBumberGenerator 프로토콜의 random 메소드는 기능이 하나도 들어있지 않고, 즉 구현방법은 알려주지 않고 반환값만 알고 있다. 프로토콜을 준수하는 타입에게 각자 구현방법을 맡기는 것이다. 예제의 경우 linear congruential generator 라고 알려진 알고리즘을 이용해 구현했다.
Mutating Method Requirements
가끔은 값 타입에서 메소드를 통해 자신의 인스턴스를 mutate할 필요가 있습니다. 이 과정은 mutating 키워드를 func 키워드 앞에 붙이는 것으로 사용할 수 있다고 전에 설명된 바 있습니다. 만약 프로토콜을 채택한 타입의 인스턴스를 mutate할 의도로 인스턴스 메소드를 정의한다면, 그냥 똑같이 쓰면 됩니다.
protocol Togglable {
mutating func toggle()
}
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 SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
위 예제에서 볼 수 있듯이 프로토콜의 이니셜라이저를 클래스의 designated 이니셜라이저나 convenience 이니셜라이저로 준수시켜 구현할 수 있습니다. 두 가지의 경우 다 선언할 때 required 수식어를 붙입니다.
만약 서브클래스가 슈퍼클래스의 designated 이니셜라이저를 오버라이드 하고 그 이니셜라이저를 프로토콜의 이니셜라이저를 준수시키는데 사용하려 한다면 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
}
}
Protocols as Types
프로토콜은 어떠한 기능도 스스로는 구현할 수 없음에도 불구하고 코드 내에서 타입으로 사용할 수 있습니다. 프로토콜을 타입으로 사용하는 것은 가끔 existential type으로 불립니다. 프로토콜을 준수하는 T라는 타입이 있다~ 라는 뜻입니다.
메소드.함수,초기자에서 파라미터 타입, 리턴 타입으로, 상수 변수 프로퍼티의 타입으로, 또는 배열, 딕셔너리 다른 컨테이너의 아이템의 타입으로서 사용될 수 있습니다.
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
}
}
generator 상수에 초기화 과정에서 RandomBumberGenerator를 준수한 어떤 타입의 인스턴스든 들어갈 수 있습니다.
Delegation
델리게이션은 클래스나 구조체가 그들의 책임을 다른 타입의 인스턴스에게 전가(위임)하는 디자인 패턴입니다. 이 디자인 패턴은 위임되어질 책임들을 프로토콜에 캡슐화하므로서 구현됩니다. (앞서 설명했듯이 프로토콜을 타입으로서 주는 건 프로토콜을 준수하는 어떤 타입을 의미하는데 이것이 delegate, 준수하는 타입을 의미한다.) 델리게이션은 타입에 대한 정보가 필요없이도 특정 액션 또는 데이터를 가져올 수 있습니다.
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)
}
AnyObject(전 챕터에 배움)을 통해 클래스만 사용할 수 있는 프로토콜을 만들었습니다.
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)
}
}
(weak용도는 나중에 더 알아보자) delegate 변수에 DiceGameDelegate 프로토콜 타입을 담아 DiceGameDelegate을 준수할 어떤 타입의 인스턴스가 있다고 가정하고 사용합니다.
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")
}
}
실제 구현부 입니다. 전 코드에서는 이 인스턴스에 관한 구현부는 알 필요없이 게임이 시작, 턴이 바뀔 때, 게임이 종료될 때 프로토콜을 준수하는 메소드들을 사용할 수 있습니다.
Adding Protocol Conformance with an Extension
프로토콜을 존재하는 타입을 익스텐션시켜 채택할 수 있습니다. 내가 소스 코드에 접근할 수 없는 내장된 타입도 마찬가지 입니다.
protocol TextRepresentable {
var textualDescription: String { get }
}
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
Conditionally Conforming to a Protocol
제네릭 타입은 특정 상황에서만 프로토콜을 준수할 수 있게 만들 수 있습니다. where 절을 이름 다음에 사용하므로서 구현합니다.
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]"
요소들이 TextRepresentable 프로토콜을 준수했을 경우에만 가능하도록 만들었습니다.
Declaring Protocol Adoption with an Extension
만약에 타입이 프로토콜의 요구사항을 모두 준수하고 있지만 아직 채택하지 않은 상황이라면, 빈 익스텐션을 사용할 수 있습니다.
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
Adopting a Protocol Using a Synthesized Implementation
Swift는 많은 곳에서 사용되는 Equatable, Hashable, Comparable 자동적으로 채택합니다. 이 프로토콜을 구현하기 위해 반복적인 보일러플레이트를 작성할 필요가 없습니다.
Equatable는 다음 타입에서 이미 채택되었습니다.
저장 프로퍼티만 가지고 있는 구조체
associated type만 가지고 있는 열거형(associated type이 Equatable을 채택한 상태여야 한다)
associated type이 없는 열거형
Hashable는 다음 타입에서 이미 채택되었습니다.
저장 프로퍼티만 가지고 있는 구조체
associated type만 가지고 있는 열거형(associated type이 Hashable을 채택한 상태여야 한다)
associated type이 없는 열거형
Comparable의 경우 열거형은 raw value를 가지고 있지 않아도 채택된 상태지만 associated type이 있으면 명시해줘야 합니다.( 실제 코드에서 테스트 해보니 associated type이 기본 데이터 타입이면 따로 < 등의 구현이 필요없는데 커스텀 타입을 넣어주면 구현이 필요하다.)
Collections of Protocol Types
위에서 언급했듯이 프로토콜을 타입으로 저장할 수 있고 이는 배열이나 딕셔너리같은 Collection에도 타입으로서 사용할 수 있습니다.
let things: [TextRepresentable] = [game, d12, simonTheHamster]
for thing in things {
print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon
Protocol Inheritance
프로토콜도 다른 프로토콜을 상속가능합니다. 클래스의 것과 비슷하나 콤마를 사용해서 여러개의 프로토콜을 상속할 수 있습니다.
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// protocol definition goes here
}
Protocol Composition
프로토콜 합성은 SomeProtocol & AnotherProtocol로 표시하며 두가지 프로토콜을 채택한 타입을 의미합니다.
func wishHappyBirthday(to celebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
Checking for Protocol Conformance
is 는 인스턴스가 프로토콜을 채택했다면 true를 아니면 false를 반환합니다.
as? 는 옵셔널 값으로 다운캐스팅합니다. 만약 프로토콜을 채택하지 않았다면 nil을 내보냅니다.
as! 는 강제로 프로토콜 타입으로 다운캐스팅하고 실패하면 런타임에러를 발생시킵니다.
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
//Circle과 Country를 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
Optional Protocol Requirements
프로토콜 준수사항 중 optional 키워드를 앞에 붙여서 프로토콜을 채택할 때 무조건 채택할 필요가 없는 준수사항을 만들 수 있습니다.
옵셔널 준수사항은 Objective-C와 상호운용가능합니다. 프로토콜과 옵셔널 준수사항은 @objc 을 분여줘야 합니다. @objc 프로토콜은 클래스 타입만 채택가능합니다.
프로토콜의 옵셔널 준수사항의 메소드와 프로퍼티를 사용하려고 할 때 타입은 자동적으로 옵셔널이 됩니다.(프로토콜을 준수한 클래스에서의 이야기가 아니라 프로토콜을 타입으로 사용할 때 이야기)(함수는 리턴타입만 옵셔널이 아니라 함수 형태 자체가 옵셔널로 묶인다)
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
}
}
}
Protocol Extensions
프로토콜은 메소드, 초기자, 서브스크립트 그리고 연산 프로퍼티를 익스텐션해서 작성할 수 있습니다.
Providing Default Implementations
프로토콜 익스텐션을 이용해 메소드 또는 연산 프로퍼티 준수사항을 기본 구현해놓을 수 있습니다. 프로토콜 준수사항을 구현했다면 그걸 사용하고 구현한 것이 없다면 기본 구현을 사용하게 됩니다. 옵셔널 준수사항과는 다르게 기본 구현 값이 보장되어 있으므로 옵셔널 체이닝을 사용할 필요가 없습니다.
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
Adding Constraints to Protocol Extensions
익스텐션 뒤에 where 절을 붙여 특정 상황에만 익스텐션을 사용할 수 있게 만들 수 있습니다,
extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}
'Swift Language Guide' 카테고리의 다른 글
Generics(제네릭) (0) | 2020.11.30 |
---|---|
Extensions (0) | 2020.11.24 |
타입 캐스팅 + 중첩 타입 (0) | 2020.11.24 |
Error Handling(에러 핸들링) (0) | 2020.11.19 |
Optional Chaining(옵셔널 체이닝) (0) | 2020.11.18 |