Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Protocols 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.
들어가며
프로토콜은 특정 task나 functionality를 위한 메서드, 프로퍼티, 그 외 요구사항들의 청사진을 정의한다. 클래스, 구조체, ENUM은 프로토콜을 적용하여 그 요구사항들의 실제 구현을 제공한다. 어떤 타입이 특정 프로토콜의 요구사항을 만족시키면, 그 타입이 프로토콜을 따른다conform고 말한다.
필요 시 프로토콜을 확장하여 그것을 따르는 타입에게 도움이 되는 기능을 제공하거나 할 수도 있다.
문법
프로토콜의 정의 문법은 클래스, 구조체, ENUM과 매우 유사하다.
protocol SomeProtocol {
// 프로토콜 정의가 여기에 온다
}
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 구조체 정의가 여기에 온다
// 이 구조체는 FirstProtocol과 AnotherProtocol을 따르고 있다
}
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 클래스 정의가 여기에 온다
// 이 클래스는 SomeSuperClass를 상속받는다
// 이 클래스는 FirstProtocol, AnotherProtocol을 따르고 있다
// 슈퍼클래스가 존재할 경우 프로토콜보다 먼저 나오도록 위치시킨다
}
프로퍼티 요구사항
프로토콜은 자신을 따르는 타입(conforming type)에게 "특정 이름과 타입을 가진 프로퍼티를 제공해야 한다"고 요구할 수 있다.
종류 : 인스턴트 프로퍼티, 타입 프로퍼티
프로퍼티가 stored인지 computed인지는 명시하지 않는다.
프로퍼티의 이름과 타입을 명시해야 한다.
프로퍼티가 읽기/쓰기 속성인지 읽기 전용인지 명시해야 한다.
프로토콜이 읽기/쓰기용 프로퍼티를 요구한다면 -> 읽기/쓰기가 가능한 프로퍼티로만 요구사항을 만족시킬 수 있다. (읽기전용 프로퍼티로는 안 된다는 말)
프로토콜이 읽기전용 프로퍼티를 요구한다면 -> 어떤 종류의 프로퍼티라도 요구사항을 만족시킬 수 있다. (읽기/쓰기용 프로퍼티로 구현해도 된다는 말)
프로퍼티 요구사항은 항상 변수로 선언되어야 한다. (var 키워드)
타입 프로퍼티 요구사항 앞에는 static 키워드를 붙인다. 클래스가 이 프로토콜을 따를 때에는 그 타입 프로퍼티 앞에 class 또는 static 키워드를 붙이도록 한다. (오버라이드 되게 하려면 class 키워드 쓰기)
protocol SomeProtocol {
var mustBeSettable: Int { get set } // 읽기/쓰기용
var doseNotNeedToBeSettable: Int { get } // 읽기전용
}
protocol AnotherProtocol {
static var someTypeProperty: Int { get set } // 타입 프로퍼티
}
예제를 살펴보자.
// 인스턴스 프로퍼티 요구사항 1개만 가진 프로토콜
protocol FullyNamed {
var fullName: String { get }
}
// 프로토콜을 따르는 간단한 구조체
struct Person: FullyNamed {
// stored 프로퍼티로 fullName 프로퍼티를 제공한다
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// 프로토콜을 따르는 조금 복잡한 클래스
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
// computed 프로퍼티로 fullName 프로퍼티를 제공한다
var fullName: String {
return (prefix != nil ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
메서드 요구사항
프로토콜은 자신을 따르는 타입(conforming type)에게 "특정한 메서드를 제공해야 한다"고 요구할 수 있다.
종류 : 인스턴스 메서드, 타입 메서드
메서드 요구사항은 프로토콜 정의부에서 일반적인 메서드와 같은 방식으로 작성하면 된다. 단, 대괄호와 메서드 본문은 뺀다. 예제를 보면 무슨 말인지 쉽게 이해된다.
가변 파라미터Variadic parameters도 허용된다.
파라미터의 디폴트 값을 명시할 수 없다.
타입 메서드 요구사항 앞에는 static 키워드를 붙인다. 클래스가 이 프로토콜을 따를 때에는 그 타입 메서드 앞에 class 또는 static 키워드를 붙이도록 한다. (오버라이드 되게 하려면 class 쓰기)
protocol SomeProtocol {
static func someTypeMethod()
}
예제를 살펴보자.
// 인스턴스 메서드 요구사항 1개만 가진 프로토콜 (난수생성 프로토콜)
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("랜덤넘버: \(generator.random())")
// Prints "랜덤넘버: 0.37464991998171"
print("한 번 더 랜덤넘버: \(generator.random())")
// Prints "한 번 더 랜덤넘버: 0.729023776863283"
Mutating 메서드 요구사항
값 타입(구조체, ENUM 등)의 인스턴스 메서드 안에서 자신(인스턴스)을 수정modify, mutate할 필요가 있다면 메서드 앞에 mutating 키워드를 붙였었다. 이 룰은 프로토콜 정의부에서도 똑같이 적용된다.
예제를 보자.
// mutating 메서드 요구사항 1개만 가진 프로토콜
protocol Togglable {
mutating func toggle()
}
// 프로토콜을 따르는 ENUM
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 는 이제 on 상태
<note> 프로토콜의 요구사항으로 mutating 인스턴스 메서드를 선언했더라도, 클래스가 이 프로토콜을 따를 경우에는 메서드 앞에 mutating 키워드를 쓰지말라. mutating 키워드는 오직 값 타입(구조체, ENUM)을 위한 것이다.
이니셜라이저 요구사항
프로토콜은 자신을 따르는 타입(conforming type)에게 "특정 이니셜라이저를 제공해야 한다"고 요구할 수 있다.
이니셜라이저 요구사항은 프로토콜 정의부에서 일반적인 이니셜라이저와 같은 방식으로 작성하면 된다. 단, 대괄호와 이니셜라이저 본문은 뺀다.
protocol SomeProtocol {
init(someParameter: Int)
}
<이니셜라이저 요구사항을 클래스에서 구현하기>
프로토콜에 정의된 이니셜라이저를 클래스에서 구현할 수 있다. designated 이니셜라이저 또는 convenience 이니셜라이저로 구현할 수 있는데, 두 경우 모두 반드시 required 수식어를 붙여야 한다.
protocol SomeProtocol {
init(someParameter: Int)
}
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// 이니셜라이저 구현이 여기에
}
}
required 수식어를 붙임으로써 이 클래스를 상속받은 서브클래스들 역시 프로토콜에 정의된 이니셜라이저를 구현함을 보장받을 수 있다. (* 클래스의 이니셜라이저 앞에 required 수식어를 붙이면 모든 서브클래스는 해당 이니셜라이저를 구현해야 한다. 자세한 것은 Initialization 포스팅 3편에서 다뤘다. 보러가기)
<note> final 수식어가 붙은 클래스의 경우에는 required 수식어를 붙일 필요가 없다. final 클래스는 어차피 서브클래싱이 불가능하기 때문이다.
만약 서브클래스가 슈퍼클래스로부터 designated 이니셜라이저를 오버라이드하고, 그 이니셜라이저가 자신이 따르고 있는 프로토콜의 이니셜라이저와 형태가 일치한다면, required와 override 수식어를 둘 다 이니셜라이저 구현에 붙인다. 예제를 보자.
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// "required" from SomeProtocol conformance;
// "override" from SomeSuperClass
required override init() {
}
}
<Failable 이니셜라이저 요구사항>
프로토콜은 자신을 따르는 타입(conforming type)에게 "failable 이니셜라이저(init?)를 제공해야 한다"고 요구할 수 있다.
failable 이니셜라이저 요구사항은 -> failable 이니셜라이저 또는 nonfailable 이니셜라이저(= 일반 이니셜라이저)로 만족시킬 수 있다. (* failable 이니셜라이저를 서브클래스에서는 nonfailable 이니셜라이저로 오버라이드할 수 있는 것과 같다. 이에 대해서는 Initialization 포스팅 3편에서 다뤘다. 보러가기)
nonfailable 이니셜라이저(=일반 이니셜라이저) 요구사항은 -> nonfailable 이니셜라이저(일반 이니셜라이저)나 암시적 언랩핑 failable 이니셜라이저(init!)로 만족시킬 수 있다.
타입으로서의 프로토콜
프로토콜 그 자체는 어떤 기능도 구현하고 있지 않다. 그러나 모든 프로토콜은 코드 상에서 "타입"과 같은 역할을 할 수 있다.
즉, 프로토콜을 타입으로 사용할 수 있다. (fully-fledged type)
함수, 메서드, 이니셜라이저에서 파라미터 타입이나 리턴 타입으로 쓰일 수 있다.
상수, 변수, 프로퍼티의 타입으로 쓰일 수 있다.
배열, 딕셔너리, 그 외 컨테이너 타입에 들어있는 아이템 타입으로 쓰일 수 있다.
<note> 프로토콜도 타입으로 쓰일 수 있으니, 프로토콜의 이름은 대문자로 시작하라. (Swift에서 타입의 이름은 대문자로 시작함, Int, String, Double 등)
그럼 프로토콜을 타입으로 쓰고 있는 예제를 보자.
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
}
}
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
}
}
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
print("주사위 : \(d6.roll())")
}
// 주사위 : 3
// 주사위 : 5
// 주사위 : 4
// 주사위 : 5
// 주사위 : 4
objective-c에서는 id<프로토콜이름>으로 프로토콜 타입을 표현해야 했는데, 이제 프로토콜이름으로 바로 타입처럼 쓸 수 있게 되었다. 상속이 아니라 프로토콜로 문제를 해결하기가 더 편해진 것이다.
이번 편에서는 프로토콜의 간단한 사항들을 알아보았다. 다음 2편에서는 Delegation부터 시작해서 나머지 사항들에 대해 알아보자.
2편을 작성하였습니다. http://wlaxhrl.tistory.com/58
'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글
Protocols (3) (2) | 2017.03.25 |
---|---|
Protocols (2) (0) | 2017.03.25 |
Extensions (1) | 2017.03.23 |
Nested Types (0) | 2017.03.23 |
Type Casting (0) | 2017.03.23 |