-
[Swift] 프로토콜🍏Swift 2022. 8. 9. 19:09
오늘 배운 프로토콜에 대해서 상세하게 정리해봤습니다 :)
프로토콜의 개념
- 프로토콜의 필요성
클래스와 상속의 단점에는 다중 상속이 불가능, 기본적인 상위 클래스의 필요하지 않은 속성 메서드도 다 상속됨 그리고 클래스에서만 가능하다는 점들이 있는데 프로토콜은 이런 단점들을 보완할 수 있는 것임
protocol CanFly { func fly() // 구체적인 구현은 하지 않고 자격증을 채택한 곳에서 } class Bird1 { var isFemale = true func layEgg() { if isFemale { print("새가 알을 낳는다.") } } } class Eagle1: Bird1, CanFly { //CanFly 자격증을 채택 // isFemale // layEgg() func fly() { print("독수리가 하늘로 날라올라 간다.") } func soar() { print("공중으로 활공한다.") } } //구조체에서도 채택 가능함 struct Airplane1: CanFly { func fly() { print("비행기가 날아간다") } }문법
- 기본 문법
// 1)정의 protocol MyProtocol { // 최소한의 요구사항 나열 func doing() -> Int } // 2)채택(클래스, 구조체, 열거형에서 채택 가능) class Class: MyProtocol{ } // 3)구현(프로토콜에서 요구하는 사항을 직접구현) class Class: MyProtocol{ func doing() -> Int{ return 7 } } //상속이 있는 경우에 //상속하려는 클래스를 먼저 선언하고 그 다음 채택하려는 프로토콜 선언 class Person: Student, MyProtocol{ }속성 요구사항 정의
- 인스턴스 속성 요구사항
최소한의 요구사항을 지정하고 저장 속성 / 계산 속성으로 모두 구현 가능
//인스턴스 속성 요구사항 //{ get } -> 저장 속성: let / var // -> 계산 속성: 읽기(get) / 읽기쓰기(get/set) //{ get set } -> 저장 속성: var // -> 계산 속성: 읽기쓰기(get/set) protocol RemoteMouse{ var id: String { get } //let 저장속성 / var 저장속성 / 읽기계산속성 / 읽기,쓰기 계산속성 var name: String { get set } //var 저장속성 / 읽기,쓰기 계산속성 static var type: String { get set } //타입 저장 속성 (static) //타입 계산 속성 (class) } // 채택하면 최소한의 요구사항을 정확하게 따라서 구현해야함 // 인스턴스 저장/계산 속성 struct TV: RemoteMouse { let id: String = "456" var name: String = "삼성티비" static var type: String = "리모콘" } let myTV = TV() myTV.id myTV.name TV.type- 타입 속성 요구사항
최소한의 요구사항 지정하고 채택 시 저장 타입 속성에서 static 키워드로만 구현 가능하고 클래스에서 채택 시 계산 타입 속성에서 static / class 키워드로 모두 구현 가능
// 타입 속성 // 1) 저장 타입 속성으로 구현 class SmartPhone: RemoteMouse { var id: String { return "777" } var name: String { get { "아이폰" } set { } } static var type: String = "리모콘" // 타입 저장 속성은 재정의 원칙적 불가능 } // 2) 계산 타입 속성으로 구현 class Ipad: RemoteMouse { var id: String = "777" var name: String = "아이패드" class var type: String { // 클래스에서만 static / class 키워드로 모두 구현 가능(class키워드는 재정의 가능) get { "리모콘" } set { } } }메서드 요구사항 정의
- 메서드 요구사항
메서드의 헤드부분(인풋/아웃풋)의 형태만 요구사항으로 정의 mutating 키워드: (구조체로 제한하는 것은 아님) 구조체에서 저장 속성 변경하는 경우 구조체도 채택 가능하도록 허락하는 키워드 타입 메서드: 클래스에서 채택 시 static / class 키워드로 모두 구현 가능(class 키워드는 재정의 가능)
// 1)정의 protocol RandomNumber { static func reset() //최소한 타입 메서드가 되야함 (class로 구현해서 재정의를 허용하는 것도 가능) func random() -> Int //mutating func doSomething() } // 2)채택 / 3)구현 class Number: RandomNumber { static func reset() { print("다시 셋팅") } func random() -> Int { return Int.random(in: 1...100) } } // 1) 정의 protocol Togglable { mutating func toggle() // mutating의 키워드는 메서드 내에서 속성 변경의 의미일뿐(클래스에서 사용 가능) } // 2) 채택 / 3) 구현 enum OnOffSwitch: Togglable { case on case off mutating func toggle() { switch self { // .on .off case .off: self = .on case .on: self = .off } } } class BigSwitch: Togglable { var isOn = false func toggle() { // mutating 키워드 필요없음 (클래스 이기 때문) isOn = isOn ? false : true } }- 생성자 요구사항
클래스에서 생성자 채택시 required를 붙여야 함(하위에서 구현을 강제) 클래스가 final로 선언되면 required 생략 가능 클래스에서는 반드시 지정 생성자로 구현할 필요 없음(편의 생성자로 구현도 가능)
//예1 protocol SomeProtocol { // 생성자를 요구사항으로 지정 가능 init(num: Int) } class SomeClass: SomeProtocol { required init(num: Int) { // 실제 구현 } } class SomeSubClass: SomeClass { // 하위 클래스에서 생성자 구현 안하면 필수 생성자는 자동 상속 // required init(num: Int) } //예2 protocol AProtocol { init() } class ASuperClass { init() { // 생성자의 내용 구현 } } class ASubClass: ASuperClass, AProtocol { // AProtocol을 채택함으로 "required" 키워드 필요하고, 상속으로 인한 "override(재정의)" 재정의 키워드도 필요 required override init() { // 생성자의 내용 구현 } } // init?() 요구사항 -> init() / init?() / init!()로 구현가능 // init() 요구사항 -> init?() 로 구현불가능(범위가 더 넓어지는 것 안됨) //예3 // 실패가능 생성자 protocol AProto { init?(num: Int) //(객체의 타입이 맞을까?) AClass? <- AClass은 범위가 속해있음 } // 구조체에서 채택 (required 키워드는 필요없음) struct AStruct: AProto { // init?(num: Int) {} //이것도 가능 init(num: Int) {} //init!(num: Int) {} // 이것도 괜찮음 } // 클래스에서 채택 class AClass: AProto { required init(num: Int) {} //편의생성자로도 구현 가능 }- 서브 스크립트 요구사항
최소한의 요구사항을 지정 { get } → 최소한 읽기(get)는 구현 / 읽기 쓰기(get/set) 모두 구현 가능 { get set } → 반드시 읽기 쓰기(get/set) 모두 구현
protocol DataList { subscript(idx: Int) -> Int { get } // (서브스크립트 문법에서) get 필수 (set 선택) } struct DataStructure: DataList { // subscript(idx: Int) -> Int { // 읽기 전용 서브스크립트로 구현한다면 // get { // return 0 // } // } subscript(idx: Int) -> Int { // (최소한만 따르면 됨) get { return 0 } set { // 구현은 선택 // 상세구현 생략 } } }protocol Certificate { func doSomething() } class Person { } // 관습적으로 본체보다는 확장에서 채택 구현(코드의 깔끔한 정리 가능하기 때문) extension Person: Certificate { func doSomething() { print("Do something") } }타입으로서의 프로토콜과 기타 문법
- 프로토콜은 타입
프로토콜을 변수에 할당할 수 있음 함수를 호출할때 프로토콜을 파라미터로 전달 가능 함수에서 프로토콜을 반환할 수 있음
// 프로토콜은 First Class Citizen(일급객체)이기 때문에 타입(형식)으로 사용할 수 있음 protocol Remote { func turnOn() func turnOff() } class TV: Remote { func turnOn() { print("TV켜기") } func turnOff() { print("TV끄기") } } struct SetTopBox: Remote { func turnOn() { print("셋톱박스켜기") } func turnOff() { print("셋톱박스끄기") } func doNetflix() { print("넷플릭스 하기") } } let tv = TV() let sbox = SetTopBox() //프로토콜 타입 취급의 장점 let electronic: [Remote] = [tv, sbox] //프로토콜의 형식으로 담겨있음 for item in electronic { // 켜기, 끄기 기능만 사용하니 타입캐스팅을 쓸 필요도 없음 (다만, 프로토콜에 있는 멤버만 사용가능) item.turnOn() } func turnOnSomeElectronics(item: Remote) { item.turnOn() } turnOnSomeElectronics(item: tv) turnOnSomeElectronics(item: sbox)- 프로토콜 준수성 검사
is / as 연산자 사용 가능 is 연산자: 특정 타입이 프로토콜을 채택하고 있는지 확인 (참 또는 거짓) / 그 반대도 확인 가능
as 연산자: 타입 캐스팅 (특정 인스턴스를 프로토콜로 변환하거나, 프로토콜을 인스턴스 실제 형식으로 캐스팅)// 1) is연산자 // 특정타입이 프로토콜을 채택하고 있는지 확인 tv is Remote sbox is Remote // 프로토콜 타입으로 저장된 인스턴스가 더 구체적인 타입인지 확인 가능 electronic[0] is TV electronic[1] is SetTopBox // 2) as연산자 // 업캐스팅(as) let newBox = sbox as Remote newBox.turnOn() newBox.turnOff() // 다운캐스팅(as?/as!) let sbox2: SetTopBox? = electronic[1] as? SetTopBox sbox2?.doNetflix() //앞에 타입이 옵셔널이라면 ?로 명시해줘야함 //(electronic[1] as? SetTopBox)?.doNetflix()- 프로토콜의 상속
프로토콜은 프로토콜 간의 상속도 가능하고 다중 상속도 가능
protocol Remote { func turnOn() func turnOff() } protocol AirConRemote { func Up() func Down() } protocol SuperRemoteProtocol: Remote, AirConRemote { // 프로토콜끼리, 상속 구조를 만드는 것이 가능 ⭐️ // func turnOn() // func turnOff() // func Up() // func Down() func doSomething() } // 프로토콜의 채택 및 구현 class HomePot: SuperRemoteProtocol { func turnOn() { } func turnOff() { } func Up() { } func Down() { } func doSomething() { } }- 클래스 전용 프로토콜
AnyObject는 클래스 전용 프로토콜로 구조체에서는 사용 불가
// 클래스 전용 프로토콜로 선언 // (AnyObject프로토콜을 상속) protocol SomeProtocol: AnyObject { //AnyObject는 클래스 전용 프로토콜 func doSomething() } // 구조체에서는 채택할 수 없게 됨 //struct AStruct: SomeProtocol { // //} // 클래스에서만 채택 가능 class AClass: SomeProtocol { func doSomething() { print("Do something") } }- 프로토콜 합성 문법
프로토콜을 &로 연결해서 프로토콜 두 개를 병합해서 타입으로 사용하는 것 가능
// 프로토콜을 합성하여 임시타입으로 활용 가능 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("생일축하해, \\(celebrator.name), 넌 이제 \\(celebrator.age)살이 되었구나!") } let birthdayPerson = Person(name: "홍길동", age: 20) wishHappyBirthday(to: birthdayPerson) let whoIsThis: Named & Aged = birthdayPerson // 임시적인 타입으로 저장 (두개의 프로토콜을 모두 채택한 타입만 저장 가능)프로토콜 지향 프로그래밍
💡 여러 개의 프로토콜 채택 가능(다중 상속과 유사)
💡 메모리 구조에 대한 특정 요구사항 없음(필요한 속성 / 메서드만 채택도 가능)
💡 모든 타입에서 채택 가능(벨류 타입도 가능)
💡 확장에서 구체적 정의(재정의 가능)참고 자료
Allen’s Swift Master School
꿀팁
💡 프로토콜의 채택은 관습적으로 확장에서 구현하는 것을 권장함
💡 프로토콜은 보통 한 폴더에 모아서 관리하고 프로토콜의 파일명은 해당 기능에 맞게 써주는 게 좋음
💡 프로토콜을 원하는 파일에서 채택할 때 위치는 제일 앞인 상속받는 클래스 이외에는 어디에다 채택을 해줘도 상관없음
💡 프로토콜을 채택한 것에 따라서 코드가 일관성 있게 짜일 수 있기 때문에 많이 사용함
💡 inputView는 키보드 대체할 아이를 넣어주는 것'🍏Swift' 카테고리의 다른 글
[Swift] Optional (0) 2023.02.19 [Swift] Enumeration (0) 2023.02.09 [Swift] Access Control (0) 2023.02.06 [Swift] map, filter, reduce 등 고차함수 (0) 2023.02.03 [Swift] 메서드의 종류 (0) 2022.10.13