프로토콜이란?
특정 기능 수행에 필수적인 요소를 정의한 청사진
protocol SomeProtocol {
// protocol definition goes here
}
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
var
로 선언해야 함.protocol SomeProtocol {
var mustBeSettable: Int { get set } // gettable과 settable 프로퍼티
var doesNotNeedToBeSettable: Int { get } // gettable 프로퍼티
}
// 타입 프로퍼티는 static 키워드로 선언
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
// 하나의 프로퍼티를 갖는 프로토콜 선언
protocol FullyNamed {
var fullName: String { get }
}
// 이 프로토콜을 따르는 구조체를 선언한다.
// stored property로 사용
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"
// computed property로 사용
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"
protocol SomeProtocol {
static func someTypeMethod()
}
protocol RandomNumberGenerator {
func random() -> Double
}
// 필수 메소드 random()를 구현한 클래스
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
키워드(func
키워드 앞)를 사용해 메소드가 속한 인스턴스와 인스턴스의 모든 프로퍼티를 수정 가능함을 표시할 수 있음 - 값 타입(구조체와 열거형) 형에만 사용프로토콜에 mutating을 명시한 경우 이 프로토콜을 따르는 클래스 형을 구현할 때는 메소드에 mutating을 명시하지 않아도 된다.
예제
// 프로토콜 정의
protocol Togglable {
mutating func toggle()
}
// 이 프로토콜을 따르는 값타입 형에서 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
protocol SomeProtocol {
init(someParameter: Int)
}
required
키워드를 붙여줘야 함required
키워드를 사용하면 준수하는 클래스의 모든 하위 클래스에 이니셜라이저 요구사항의 명시적 또는 상속된 구현을 제공하여 프로토콜을 준수할 수 있음class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
final 클래스는 하위 클래스가 될 수 없으므로 required 키워드를 표시하지 않아도 됨
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
}
}
주사위 예제
sides
generator
class Dice {
let sides: Int
let generator: RandomNumberGenerator // generator 상수의 타입으로 사용
init(sides: Int, generator: RandomNumberGenerator) { // 이니셜라이저의 파라미터형으로 사용
self.sides = sides
self.generator = generator
}
func roll() -> Int {
return Int(generator.random() Double(sides)) + 1
}
}
// Dice를 초기화할 때 generator 파라미터 부분에 RandomNumberGenerator 프로토콜을 따르는 인스턴스를 넣는다.
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
위임이란?
주사위 기반 보드게임 예제
// 2가지 프로토콜 정의하기
protocol DiceGame { // 주사위를 포함하는 모든 게임에 채택될 수 있는 프로토콜
var dice: Dice { get }
func play()
}
// 실제 DiceGame의 행위와 관련된 구현을 DiceGameDelegate를 따르는 인스턴스에 위임
protocol DiceGameDelegate: AnyObject { // DiceGame의 진행사항을 추적하기 위해 채택
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}
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)
}
}
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")
}
}
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
이미 존재하는 타입에 새 프로토콜을 따르게 하기 위해 익스텐션을 사용할 수 있음
protocol TextRepresentable {
var textualDescription: String { get }
}
// 익스텐션을 이용해 Dice를 TextRepresentable 프로토콜을 따르게 하기
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
where
절을 사용해 정의예제
TextRepresentable
을 따르는 Array 중에 Array의 각 원소가 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]"
타입이 이미 프로토콜의 모든 요구사항을 준수하지만 해당 프로토콜을 채택한다고 아직 명시하지 않은 경우 빈 확장을 사용하여 프로토콜을 채택하도록 할 수 있다.
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
Hamster
의 인스턴스는 TextRepresentable
이 요구된 타입 어디서든 사용될 수 있다.
let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"
타입은 요구사항이 충족된다고 해서 프로토콜을 자동으로 채택하지 않는다. 항상 프로토콜 채택을 명시적으로 선언해야 한다.
Swift는 다음과 같은 사용자 정의 타입에 대해 Equatable
의 합성된 구현을 제공
Equatable
프로토콜을 준수하는 저장된 프로토콜만 있는 구조체Equatable
프로토콜을 준수하는 연관된 타입만 있는 열거형Swift는 다음과 같은 사용자 정의 타입에 대해 Hashable
에 합성된 구현을 제공
Hashable
프로토콜을 준수하는 저장된 프로퍼티만 가지는 구조체Hashable
프로토콜을 준수하는 연관된 타입만 가지는 열거형프로토콜은 배열, 딕셔너리와 같은 콜렉션에 저장되기 위해 타입으로 사용될 수 있음.
프로토콜은 하나 이상의 다른 프로토콜을 상속할 수 있고 상속한 요구사항 위에 요구사항을 더 추가할 수 있음.
콤마(,)로 구분하여 여러 개의 상속된 프로토콜을 목록화할 수 있음
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// protocol definition goes here
}
프로토콜 채택을 프로토콜의 상속 목록에 AnyObject
프로토콜을 추가하여 구조체 또는 열거형이 아닌 클래스 타입으로 제한할 수 있음.
protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
// class-only protocol definition goes here
}
참조 구문만 필요한 프로토콜을 정의하기 위해 클래스 전용 프로토콜을 사용함
동시에 여러 프로토콜을 따르는 타입 선언
예제) Person은 Named와 Aged 프로토콜을 동시에 따르는 구조체
wishHappyBirthday
메소드의 celebrator
파라미터는 Named
프로토콜과 Aged
프로토콜을 동시에 따르는 타입으로 선언하기 위해 Named & Aged로 표시함
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!"
예제) Location 프로토콜과 위의 Named 프로토콜을 따르는 City 클래스를 구현한 예
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!"
is
연산자를 이용하면 어떤 타입이 특정 프로토콜을 따르는지 확인할 수 있음. boolas?
는 특정 프로토콜 타입을 따르는 경우 그 옵셔널 타입의 프로토콜 타입으로 다운캐스트를 하게 되고 따르지 않는 경우는 nil을 반환as!
는 강제로 특정 프로토콜을 따르도록 정의함. 만약 다운캐스트에 실패하면 런타임 에러 발생protocol HasArea {
var area: Double { get }
}
// HasArea 프로토콜을 따르는 Circle 클래스와 Country 클래스를 선언한다.
// Circle 클래스에서는 area 프로퍼티를 계산된 프로퍼티로 구현했고,
class Circle: HasArea {
let pi = 3.1415927
var radius: Double
var area: Double { return pi radius radius }
init(radius: Double) { self.radius = radius }
}
// Country 클래스에서는 저장 프로퍼티로 구현한다.
class Country: HasArea {
var area: Double
init(area: Double) { self.area = area }
}
// HasArea를 따르지 않는 Animal이라는 클래스 선언
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
// Circle, Country, Animal의 인스턴스를 objects라는 배열에 넣는다.
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
// objects 배열을 순회하며 as? HasArea 구문을 사용해 HasArea 프로토콜을 따르는지 확인하고 따르는 경우 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를 따르기 때문에 area값이 반환되고
// 마지막 인스턴스는 HasArea를 따르지 않기 때문에 else 절이 실행된다.
프로토콜을 선언하면서 필수 구현이 아닌 선택적 구현 조건을 정의할 수 있다.
이 프로토콜의 정의를 위해서 @objc 키워드를 프로토콜 앞에 붙이고, 개별 함수 혹은 프로퍼티에는 @objc와 optional 키워드를 붙인다.
@objc 프로토콜을 클래스 타입에서만 채용될 수 있고 구조체나 열거형에서는 사용할 수 없음.
예제) 두 가지 선택적 구현을 할 수 있는 CounterDataSource 프로토콜
@objc protocol CounterDataSource {
@objc optional func increment(forCount count: Int) -> Int
@objc optional var fixedIncrement: Int { get }
}
CounterDataSource를 따르면서 구현은 하나도 하지 않는 클래스를 선언할 수는 있지만, 좋은 방법은 아니다. 만약 하나도 구현하지 않는다면 필요하지 않다는 것이므로 굳이 그렇게 선언할 필요는 없다.
// 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
}
}
}
// random()을 따르는 RandomNumberGenerator에 randomBool()을 따르도록 추가한 예
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
기본 구현을 제공하는데 익스텐션을 사용할 수 있음
(특정 프로토콜을 따르는 타입 중에서 그 프로토콜의 요구사항에 대해 자체적으로 구현한게 있으면 그것을 사용하고 아니면 기본 구현 사용)
프로토콜에서는 선언만 할 수 있는데 익스텐션을 이용해 기본 구현을 제공할 수 있음
프로토콜 익스텐션에 의해 구현된 기본 구현은 옵셔널 요구조건과 다르다.
공통점 - 둘 다 꼭 구현하지 않아도 됨. 차이점 - 사용 시 기본 구현은 옵셔널 체이닝을 사용하지 않아도 됨.
예제) PrettyTextRepresentable
의 prettyTextualDescription
프로퍼티의 구현을 그냥 textualDescription
을 반환하도록 구현한 예
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
프로토콜 익스텐션이 특정 조건에서만 적용되도록 선언할 수 있음 - where 절 이용
예제) Collection
엘리먼트가 Equatable
인 경우에만 적용되는 allEqual()
메소드를 구현한 예
extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
// 각 배열의 엘리먼트는 모두 Equatable 프로토콜을 따르기 때문에 allEqual()을 호출할 수 있음
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"