→ 메서드, 프로퍼티 그리고 특정 작업이나 기능의 부분이 적합한 다른 요구사항의 청사진을 정의한다. 프로토콜은 요구사항의 구현을 제공하기 위해 클래스, 구조체 또는 열거형에 의해 채택될 수 있다. 프로토콜의 요구사항에 충족하는 모든 타입은 프로토콜을 준수한다고 한다. 요구사항을 지정하는 것 외에도 그 일부를 구현하거나 준수하는 타입에 추가 기능을 구현하기 위해 프로토콜을 확장할 수 있다.
protocol SomeProtocol {
// protocol definition goes here
}
// 이런식으로 어떤 타입이 프로토콜을 채택할 때, 타입의 이름 뒤에 명시한다.
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
매개변수를 쓴다던지, 할당할 때 타입을 명시해주는데(ex. String) 어떤 스트링도 다 들어올 수 있다. 스트링이지만 해당 타입을 따르는 스트링만 들어올 수 있게하려고?
protocol SomeProtocol {
var mustBeSettable: Int { get set } // 읽기쓰기 가능
var doesNotNeedToBeSettable: Int { get } // 읽기 전용
}
struct SomeStructure: SomeProtocol {
// 해당 프로토콜을 채택하는 과정. 콤마로 구분해서 여러개의 프로토콜을 채택할 수 있음.
}
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// 부모 클래스를 상속받는 경우에는, 클래스를 프로토콜보다 선행하여 적어야한다.
}
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
// 타입 프로퍼티는 항상 static 키워드를 붙여서 정의해야한다.
}
프로토콜은 해당 프로토콜을 준수하는 어떤 타입에게 특정 이름과 타입을 가진 instance property 또는 type property를 요구할 수 있다. 프로토콜은 이 프로퍼티가 저장 프로퍼티인지, 연산 프로퍼티인지 명시하지 않고, 오직 프로퍼티의 이름과 타입만 요구한다. 또한, 프로토콜은 각 프로퍼티에 읽기만 가능한지, 읽기와 쓰기가 모두 가능한지 명시해야한다.
프로퍼티 요구사항은 항상 변수 프로퍼티로 선언되어야 한다. gettable과 settable프로퍼티는 선언 다음에 { get set }
을 쓰고, gettable프로퍼티는 { get }
을 써준다.
프로토콜 요구 프로퍼티의 get/set이란?
저장 프로퍼티 관점 : get=외부에서 프로퍼티 값을 꺼내갈 수 있게 해라, set=외부에서 프로퍼티 값을 수정할 수 있게 해라
(상수는 초기화 이후 수정 불가하니까 get set은 상수로 구현 못함)
연산 프로퍼티 관점 : get=연산 프로퍼티의 getter (자신의 값을 남의 값을 통해 구함)를 구현해라, set=연산 프로퍼티의 setter (남의 값을 자신의 값을 통해 구함)를 구현해라
get set (읽기쓰기) : 상수(=불변 저장 프로퍼티)로 구현 불가, 읽기쓰기 변수로 구현함 (=가변 저장 프로퍼티 또는 getter/setter가 둘다 있는 가변 연산 프로퍼티)
get (읽기 전용) : 상수 가능, 읽기쓰기 변수 또는 읽기전용 변수로 구현함. 프로토콜에서 get
은 '읽기전용'보다는 "(외부기준) 읽기는 반드시 뚫어 놓고 쓰기는 그러던지 말던지 알아서 해라~"
// MARK: gettable 읽기전용
protocol FullyNamed {
var fullName: String { get }
}
// 저장 프로퍼티
struct Person: FullyNamed {
var name: String
}
var allie = Person(fullName: "Allie Park")
allie.fullName = "Sally Kim"
// 연산 프로퍼티
struct Person: FullyNamed {
var name: String
var fullName: String {
return name
}
}
var allie = Person(name: "Allie Park")
allie.fullName = "Sally Kim" // 읽기전용이라 오류!!
struct Person: FullyNamed {
var name: String
var fullName: String {
get {
return name
}
set {
name = newValue
}
}
}
var allie = Person(name: "Allie Park")
allie.fullName = "Sally Kim" // settable이라 ok!!
// MARK: 읽기 및 쓰기 전용
protocol fullyNamed {
var fullName: String { get set }
}
struct Person: fullyNamed {
var name: String
var fullName: String {
return name
}
}
// protocol 이 읽기 및 쓰기전용을 요구했는데 set이 충족이 안되어 오류발생.
protocol SomeProtocol {
func someMethod()
func anotherMethod(name: String, age: Int) -> Int
func protocolMethod() -> String
}
static
키워드를 붙여야 한다. 클래스에서 구현할 때, 타입 메서드 요구사항 앞에 항상 class 또는 static
키워드가 붙는 경우에도 마찬가지!protocol SomeProtocol {
static func someTypeMethod()
static func anotherTypeMethod()
}
class Allie: SomeProtocol {
static func someTypeMethod() {
}
static func anotherTypeMethod() {
}
}
class Child: Allie {
override static func anotherTypeMethod() {
} // 서브클래스에서 재정의 가능!@
}
// MARK: 단일(single) 인스턴스 메소드를 요구하는 프로토콜 예제
protocol RandomNumberGenerator {
func random() -> Double
}
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0 // 기본값을 할당해서 init 필요없음
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
}
}
mutating
키워드를 붙여 해당 메서드가 속한 인스턴스와 해당 인스턴스의 모든 프로퍼티를 수정할 수 있음을 나타내준다. 만약 프로토콜을 채택하는 곳이 열거형이나 구조체인데, 프로퍼티 변경이 필요하다면!! 애초에 프로토콜에서 mutating
으로 메서드를 만들라고 요구하면 된다! 클래스라면 키워드 빼고 작성 가능!!required
라는 modifier를 표시해주어야함!! 구조체에서는 필요없음.required
를 사용하면 해당 프로토콜을 준수하는 클래스의 모든 하위 클래스들 역시 이니셜라이저 요구사항을 구현하는 것을 보장 받을 수 있기 때문이다. 만약 final 클래스이면 서브클래스화 할 수 없기 때문에 안써줘두 된다. (final은 override 재정의 막아주는 애)protocol SomeProtocol {
init(someParameter: Int)
} // 요렇게~
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
// MARK: 서브클래스가 슈퍼클래스의 init을 재정의하고, 프로토콜에서 요구하는 init과 일치하는 경우
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
}
}
RandomNumberGenerator
라는 프로토콜을 채택하지는 않았지만, generator라는 저장프로퍼티의 타입으로 지정해줬다. generators는 RandomNumberGenerator타입이기 때문에, random()
이라는 메서드는 구현해줘야 할 의무는 없고, 그냥 random()에 접근할 수 있게 되는것이다.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
}
}
→ Delegation은 클래스나 구조체가 책임을 일부 다른 타입의 인스턴스로 넘길 수 있게 하는 디자인 패턴이다.
protocol DiceGame {
var dice: Dice { get }
func play()
}
/// DiceGame의 책임을 DiceGameDelegate를 따르는 인스턴스에 위임
protocol DiceGameDelegate: AnyObject { // AnyObject로 선언하면 클래스만 이 프로토콜을 채택할 수 있는 속성이 된다.
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}
class SnakesAndLadders: DiceGame { // 프로토콜의 조건에 맞추기위해 dice라는 프로퍼티는 gettable하게 구현되어 있고, play()메소드가 구현되어 있다.
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() { // 바로 위에 정의한 delegate가 DiceGameDelegation 옵셔널타입이므로 play() 메소드는 delegate의 메소드를 호출할때마다 옵셔널 체이닝을 한다.
square = 0
/// DiceGameDelegation의 게임 진행상황을 tracking하는 메소드(게임 시작)
delegate?.gameDidStart(self)
gameLoop: while square != finalSquare {
let diceRoll = dice.roll()
/// DiceGameDelegation의 게임 진행상황을 tracking하는 메소드(게임 진행)
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]
}
}
/// DiceGameDelegation의 게임 진행상황을 tracking하는 메소드(게임 종료)
delegate?.gameDidEnd(self)
}
}
// DiceGameDelegate 프로토콜을 채택한 DiceGameTracker클래스
class DiceGameTracker: DiceGameDelegate { // 프로토콜의 조건에 맞추기위해 3개의 메소드가 구현되어 있다.
var numberOfTurns = 0
func gameDidStart(_ game: DiceGame) {
numberOfTurns = 0
if game is SnakesAndLadders { // 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")
}
}