일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 앱버전구하기
- Xcode
- Python
- spring
- 파이썬서버
- FileOwner
- JavaScript
- Swift
- 웹
- iOS계산기
- ios
- 개발기록
- subscript
- 계산기앱
- 딩동말씀
- FLASK
- 스프링
- Xib
- 자바스크립트
- jQuery
- iOS앱배포
- 계산기앱만들기
- 앱배포
- 스위프트
- AJAX
- customclass
- DispatchGroup
- 맥
- iOS배포
- MainScheduler
- Today
- Total
개발하는 뚝딱이
[swift] 프로토콜 본문
책 <스위프트 프로그래밍 3판 ;야곰 지음> 을 정리한 글입니다.
1. 프로토콜 (Protocol)
- 프로토콜은 특정 역할을 하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진
- 구조체, 클래스, 열거형은 프로토콜을 채택해서 특정 기능을 실행하기 위한 프로토콜의 요구사항을 실제로 구현할 수 있음
- 프로토콜의 요구사항을 모두 따르는 타입은 '해당 프로토콜을 준수한다 (conform)'이라고 표현
- 프로토콜은 정의를 하고 제시를 한 뿐, 스스로 기능을 구현하지 않음
2. 채택
protocol AProtocol {
// 프로토콜 정의
}
protocol AnotherProtocol {
// 프로토콜 정의
}
struct SomeStruct: AProtocol, AnotherProtocol {
// 구조체 정의
}
class SomeClass: AProtocol, AnotherProtocol {
// 클래스 정의
}
enum SomeEnum: AProtocol, AnotherProtocol {
// 열거형 정의
}
3. 프로토콜 요구사항
3.1 프로퍼티 요구
- 프로토콜 요구사항은 항상 var 키워드를 사용한 변수 프로퍼티로 정의
- 읽고 쓰기가 모두 가능한 프로퍼티는 { get set }
- 읽기 전용 프로퍼티는 프로퍼티의 정의 뒤에 { get } 이라고 명시해줌
- 타입 프로퍼티를 요구하려면 static 키워드 사용
protocol SomeProtocol {
var settableProperty: String { get set }
var notNeedToBeSettable: String { get }
}
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
static var anotherTypeProperty: Int { get }
}
- Sendable 프로토콜에선 읽기 전용 프로토콜로 from을 정의했으나
채택하는 클래스에선 읽고 쓰기가 가능한 프로퍼티로 구현해도 문제 없음
// Sendable 프로토콜과 Sendable 프로토콜을 준수하는 Message와 Mail 클래스
protocol Sendable {
var from: String { get }
var to: String { get }
}
class Message: Sendable {
var sender: String
var from: String { // 읽기 전용
return self.sender
}
var to: String
init(sender: String, receiver: String) {
self.sender = sender
self.to = receiver
}
}
class Mail: Sendable {
var from: String // 읽고 쓰기 가능
var to: String
init(sender: String, receiver: String) {
self.from = sender
self.to = receiver
}
}
3.2 메서드 요구
- 프로토콜 정의에서 특정 인스턴스 메서드나 타입 메서드를 요구할 수 있음
- 다만 메서드의 실제 구현부인 중괄호 {} 부분은 제외하고
메서드의 이름, 매개변수, 반환 타입 등만 작성해 가변 매개변수도 허용
- 프로토콜의 메서드 요구에서는 매개변수 기본값을 지정할 수 없음
- 타입 메서드를 요구할 때는 타입 프로퍼티의 요구와 마찬가지로 static 키워드로 명시
- static 키워드를 사용하여 요구한 타입 메서드를 클래스에서
실제 구현할 때는 static 키워드나 class 키워드 어느 쪽을 사용해도 무방
// 무언가를 발신할 수 있는 기능
protocol Sendable {
var from: Sendable { get }
var to: Receiveable? { get }
func send(data: Any)
static func isSendableInstance(_ instance: Any) -> Bool
}
// 무언가를 수신받을 수 있는 기능
protocol Receiveable {
func received(data: Any, from: Sendable)
}
class Message: Sendable, Receiveable {
// 발신은 Sendable 프로토콜을 준수하는 타입의 인스턴스여야 함
var from: Sendable {
return self
}
// 상대방은 수신 가능한 객체, 즉 Receivable 프로토콜을 준수하는 타입의 인스턴스여야 함
var to: Receiveable?
// 메시지를 발신
func send(data: Any) {
guard let receiver: Receiveable = self.to else {
print("Message has no receiver")
return
}
// 수신 가능한 인스턴스의 received 메서드 호출
receiver.received(data: data, from: self.from)
}
// 메시지를 수신
func received(data: Any, from: Sendable) {
print("Message received \(data) from \(from)")
}
// class 메서드이므로 상속이 가능
class func isSendableInstance(_ instance: Any) -> Bool {
if let sendableInstance: Sendable = instance as? Sendable {
return sendableInstance.to != nil
}
return false
}
}
// 수신, 발신이 가능한 Mail 클래스
class Mail: Sendable, Receiveable {
var from: Sendable {
return self
}
var to: Receiveable?
func send(data: Any) {
guard let receiver: Receiveable = self.to else {
print("Mail has no received")
return
}
receiver.received(data: data, from: self.from)
}
func received(data: Any, from: Sendable) {
print("Mail received \(data) from \(from)")
}
// static 메서드이므로 상속이 불가능
static func isSendableInstance(_ instance: Any) -> Bool {
if let sendableInstance: Sendable = instance as? Sendable {
return sendableInstance.to != nil
}
return false
}
}
// 두 Message 인스턴스 생성
let myPhoneMessage: Message = Message()
let yourPhoneMessage: Message = Message()
myPhoneMessage.send(data: "Hello") // Message has no receiver
myPhoneMessage.to = yourPhoneMessage
myPhoneMessage.send(data: "Hello") // Message received Hello from Message
// 두 Mail 인스턴스 생성
let myMail: Mail = Mail()
let yourMail: Mail = Mail()
myMail.send(data: "Hi") // Mail has no received
myMail.to = yourMail
myMail.send(data: "Hi") // Mail received Hi from Mail
// Mail과 Message 모두 Sendable과 Receiveable 프로토콜을 준수하므로 서로 주고받을 수 있다
myMail.to = myPhoneMessage
myMail.send(data: "Bye") // Message received Bye from Mail
// String은 Sendable 프로토콜을 준수하지 않음
print(Message.isSendableInstance("Hello")) // false
print(Message.isSendableInstance(myPhoneMessage)) // true
// yourPhoneMessage는 to 프로퍼티가 설정되지 않아서 보낼 수 없음
print(Message.isSendableInstance(yourPhoneMessage)) // false
print(Mail.isSendableInstance(myPhoneMessage)) // true
print(Mail.isSendableInstance(myMail)) // true
3.3 가변 메서드 요구
- 값 타입 (구조체와 열거형)의 인스턴스 메서드에서 자신 내부의 값을 변경하고자 할 때는 func 앞에 mutating을 적어 메서드에서 인스턴스 내부의 값을 변경한다는 것을 표현
- class 구현에서는 mutating 키워드를 안써도 됨
- Resettable 프로토콜에서 가변 메서드를 요구하지 않으면, 값 타입의 인스턴스 내부 값을 변경하는 mutating 메서드는 구현이 불가능
protocol Resettable {
mutating func reset() // 가변메서드 요구
}
class Person: Resettable {
var name: String?
var age: Int?
func reset() {
self.name = nil
self.age = nil
}
}
struct Point: Resettable {
var x: Int = 0
var y: Int = 0
mutating func reset() {
self.x = 0
self.y = 0
}
}
enum Direction: Resettable {
case east, west, south, north, unknown
mutating func reset() {
self = Direction.unknown
}
}
3.4 이니셜라이저 요구
- 프로퍼티, 메서드 등과 같이 이니셜라이저를 정의할 뿐 구현하지 않음
- 클래스 타입에서 프로토콜의 이니셜라이저 요구에 부합하는 이니셜라이저를 구현할 때는 지정 이니셜라이저인지
편의 이니셜라이저인지 중요하지 않음
- 대신 이니셜라이저 요구에 부합하는 이니셜라이저를 구현할 때는 required 식별자를 붙인 요구 이니셜라이저로 구현해야 함
protocol Named {
var name: String { get }
init(name: String)
}
struct Pet: Named {
var name: String
init(name: String) {
self.name = name
}
}
class Person: Named {
var name: String
required init(name: String) {
self.name = name
}
}
- 만약 클래스 자체가 상속받을 수 없는 final 클래스라면 required 식별자를 붙여줄 필요가 없음
- 상속할 수 없는 클래스의 요청 이니셜라이저 구현은 무의미하기 때문
final class Person: Named {
var name: String
init(name: String) {
self.name = name
}
}
- 만약 특정 클래스에 프로토콜이 요구하는 이니셜라이저가 이미 구현되어 있는 상황에서 그 클래스를 상속받은 클래스가
있다면, requirred와 override 식별자를 모두 명시하여 프로토콜에서 요구하는 이니셜라이저를 구현해줘야 함
- requirred와 override 위치는 상관없음
protocol Named {
var name: String { get }
init(name: String)
}
class School {
var name: String
init(name: String) {
self.name = name
}
}
class MiddleSchool: School, Named {
required override init(name: String) {
super.init(name: name)
}
}
- 실패 가능한 이니셜라이저 요구를 포함하는 Named 프로토콜과 Named 프로토콜을 준수하는 다양한 타입들
- 실패 가능한 이니셜라이저를 요구하는 프로토콜을 준수하는 타입은, 해당 이니셜라이저를 구현할 때
실패 가능한 이니셜라이저와 일반적인 이니셜라이저 상관없이 구현 가능
protocol Named {
var name: String { get }
init?(name: String)
}
struct Animal: Named {
var name: String
init!(name: String) {
self.name = name
}
}
struct Pet: Named {
var name: String
init(name: String) {
self.name = name
}
}
class Person: Named {
var name: String
required init(name: String) {
self.name = name
}
}
class School: Named {
var name: String
required init(name: String) {
self.name = name
}
}
4. 프로토콜의 상속과 클래스 전용 프로토콜
- 프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항보다 더 많은 요구사항 추가 가능
- 프로토콜 상속 문법은 클래스의 상속 문법과 유사
protocol Readable {
func read()
}
protocol Writeable {
func write()
}
protocol ReadSpeakable: Readable {
func speak()
}
protocol ReadWriteSpeakable: Readable, Writeable {
func speak()
}
class SomeClass: ReadWriteSpeakable {
func read() {
print("Read")
}
func write() {
print("Write")
}
func speak() {
print("Speak")
}
}
- 클래스 전용 프로토콜 정의
- 프로토콜의 상속 리스트에 class 키워드를 추가해 프로토콜이 클래스 타입에만 채택될 수 있도록 제한할 수 있음
- 클래스 전용 프로토콜로 제한하기 위해 프로토콜의 상속 리스트의 맨 처음에 class 키워드가 위치해야 함
protocol ClassOnlyProtocol: class, Readable, Writeable {
// 추가 요구사항
}
class SomeClass: ClassOnlyProtocol {
func read() { }
func write() { }
}
// 오류! ClassOnlyProtocol 프로토콜은 클래스 타입에만 채택 가능
struct SomeStruct: ClassOnlyProtocol {
func read() { }
func write() { }
}
5. 프로토콜 조합과 프로토콜 준수 확인
- 하나의 매개변수가 여러 프로토콜을 모두 준수하는 타입이어야 한다면 하나의 매개변수에 여러 프로토콜을
한 번에 조합하여 요구 할 수 있음
- 프로토콜을 조합하여 요구할 때는 SomeProtocol & AnotherProtocol과 같이 표현
- 또, 하나의 매개변수가 프로토콜을 둘 이상 요구할 수도 있음. 이 때도 &를 여러 프로토콜 이름 사이에 써줌
- 특정 클래스의 인스턴스 역할을 할 수 있는지 확인 가능
- 구조체나 열거형 타입은 조합할 수 없음
- 조합 중 클래스 타입은 한 타입만 조합 가능
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
class Car: Named {
var name: String
init(name: String) {
self.name = name
}
}
class Truck: Car, Aged {
var age: Int
init(name: String, age: Int) {
self.age = age
super.init(name: name)
}
}
func celebrateBirthday(to celebrator: Named & Aged) {
print("Happy birthday \(celebrator.name)!! Now you are \(celebrator.age)")
}
let yagom: Person = Person(name: "yagom", age: 99)
celebrateBirthday(to: yagom) // Happy birthday yagom!! Now you are 99
let myCar: Car = Car(name: "Boong Boong")
//celebrateBirthday(to: myCar) // Aged를 충족하지 않음
//var someVariable: Car & Truck & Aged // 클래스 & 프로토콜 좋바에서 클래스는 한 타입만 가능
var someVariable: Car & Aged
someVariable = Truck(name: "Truck", age: 5)
someVariable = Truck(name: "Tuesday", age: 123)
//someVariable = myCar // Aged를 충족하지 않음
- 프로토콜도 하나의 타입이므로, 데이터의 타입 캐스팅을 똑같이 사용 가능
- is 연산자를 통해 해당 인스턴스가 특정 프로토콜을 준수하는지 확인할 수 있음
- as? 다운캐스팅 연산자를 통해 다른 프로토콜로 다운캐스팅 시도 가능
- as! 다운캐스팅 연산자를 통해 다른 프로토콜로 강제 다운캐스팅 가능
print(yagom is Named) // true
print(yagom is Aged) // true
print(myCar is Named) // true
print(myCar is Aged) // false
if let castedInstance: Named = yagom as? Named {
print("\(castedInstance) is Named")
// Person(name: "yagom", age: 99) is Named
}
if let castedInstance: Aged = yagom as? Aged {
print("\(castedInstance) is Aged")
// Person(name: "yagom", age: 99) is Aged
}
if let castedInstance: Named = myCar as? Named {
print("\(castedInstance) is Named")
// Car is Named
}
if let castedInstance: Aged = myCar as? Aged {
print("\(castedInstance) is Aged")
// 출력 없음, 캐스팅 실패
}
6. 프로토콜의 선택적 요구
- 프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정 가능
- 선택적 요구사항을 정의하고 싶은 프로토콜은 objc 속성이 부여된 프로토콜이여야 함
- objc 속성이 부여되는 프로토콜은 Objective-C클래스를 상속받은 클래스에서만 채택 가능
- 열거형이나 구조체 등에서는 objc 속성이 부여된 프로토콜은 아예 채택 불가능
- 선택적 요구사항은 optional 식별자를 요구사항의 정의 앞에 붙여주면 됨
- 만약 메서드나 프로퍼티를 선택적 요구사항으로 요구하게 되면, 요구사항의 타입은 자동적으로 옵셔널이 됨
- 메서드의 매개변수나 반환타입이 옵셔널이 된 것이 아니라 메서드 자체의 타입이 옵셔널이 된 것
- 선택적 요구사항은 그 프로토콜을 준수하는 타입에 구현되어 있지 않을 수 있기 때문에 옵셔널 체이닝을 통해 호출 가능
import Foundation // @objc 사용을 위한 Foundation import
@objc protocol Moveable {
func walk()
@objc optional func fly()
}
// 걷기만 할 수 있는 호랑이
class Tiger: NSObject, Moveable {
func walk() {
print("Tiger walks")
}
}
// 걷고 날 수 있는 새
class Bird: NSObject, Moveable {
func walk() {
print("Bird walks")
}
func fly() {
print("Bird flys")
}
}
let tiger: Tiger = Tiger()
let bird: Bird = Bird()
tiger.walk() // Tiger walks
bird.walk() // Bird walks
bird.fly() // Bird flys
var moveableInstance: Moveable = tiger
moveableInstance.fly?() // nil
moveableInstance = bird
moveableInstance.fly?() // Bird flys
7. 프로토콜 변수와 상수
- 프로토콜 이름을 타입으로 갖는 변수 또는 상수에는 그 프로토콜을 준수하는 타입의 어떤 인스턴스라도 할당 가능
protocol Named {
var name: String { get }
}
struct Animal: Named {
var name: String
init!(name: String) {
self.name = name
}
}
struct Pet: Named {
var name: String
init(name: String) {
self.name = name
}
}
class School: Named {
var name: String
required init(name: String) {
self.name = name
}
}
class Person: Named {
var name: String
required init(name: String) {
self.name = name
}
}
var someNamed: Named = Animal(name: "Animal")
someNamed = Pet(name: "Pet")
someNamed = Person(name: "Person")
someNamed = School(name: "School")
8. 위임을 위한 프로토콜
- 위임 Delegate 은 클래스나 구조체가 자신의 책임이나 임무를 다른 타입의 인스턴스에게 위임하는 디자인패턴
- 책무를 위임하기 위해 정의한 프로토콜을 준수하는 타입은 자신에게 위임될 일정 책무를 할 수 있다는 것을 보장
- 사용자의 특정 행동에 반응하기 위해 사용되기도 하며, 비동기 처리에도 많이 사용
- 위임 패턴 Delegate Pattern은 애플의 프레임워크에서 사용하는 주요한 패턴 중 하나
- 위임 패턴을 위해 다양한 프로토콜이 'OOOODelegate'라는 식의 이름으로 정의되어 있음
'swift' 카테고리의 다른 글
[swift] 스위프트를 더 스위프트스럽게 사용하기 (0) | 2021.03.11 |
---|---|
[swift] 프로토콜 초기구현 (0) | 2020.09.28 |
[swift] 상속 (0) | 2020.09.05 |
[swift] 서브스크립트 (0) | 2020.06.25 |
[swift] 모나드 (0) | 2020.06.23 |