→ 메서드, 프로퍼티 그리고 특정 작업이나 기능의 부분이 적합한 다른 요구사항의 청사진을 정의한다. 프로토콜은 요구사항의 구현을 제공하기 위해 클래스, 구조체 또는 열거형에 의해 채택될 수 있다. 프로토콜의 요구사항에 충족하는 모든 타입은 프로토콜을 준수한다고 한다. 요구사항을 지정하는 것 외에도 그 일부를 구현하거나 준수하는 타입에 추가 기능을 구현하기 위해 프로토콜을 확장할 수 있다.
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")
}
}