iOS 스터디 week5. 프로토콜
프로토콜
프로토콜은 타입이다.
- 함수를 호출할 때, “프로토콜”을 파라미터로 전달할 수 있음.
- 함수에서 “프로토콜”을 반환할 수 있음.
- “프로토콜”을 변수에 할당할 수 있음.
구조체, 열거형, 클래스랑 비슷한 타입이라는 뜻이다. → 일급객체로 취급한다.(타입으로 사용하는 조건)
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()
tv.turnOn()
tv.turnOff()
//Remote프로토콜을 지정하면 달라짐.
let sbox: Remote = SetTopBox()
sboxtv.turnOn()
sbox.turnOff()
sbox.doNetflix() //이건 안됨.
인스턴스를 찍어낼 때 프로토콜로 지정해서 찍어내면, 프로토콜 선언 메서드들만 사용할 수 있다. 그 외에 해당 함수의 프로토콜에서 지정된 함수 이외에 다른 메서드를 지정하고 싶다면, 다운캐스팅을 하면 된다.
(sbox as? SetTopBox)?.doNetflix()
프로토콜 타입 취급의 장점
- 배열로 담을 수 있음.
let electronic: [Remote] = [tv, sbox]
for item in electronic {
item.turnOn()
}
- 파라미터로 사용할 수 있음.
func turnOnSomeElectronics(item: Remote) {
item.turnOn()
}
turnOnSomeElectronics(item: tv)
turnOnSomeElectronics(item: sbox)
- 프로토콜의 준수성 검사
특정타입이 프로토콜을 채택하고 있는지 확인
tv is Remote
sbox is Remote
프로토콜 타입으로 저장된 인스턴스가 더 구체적인 타입인지 확인 가능
electronic[0] is TV
electronic[1] is SetTopBox
업캐스팅(as) 무조건 성공
let newBox = sbox as Remote
newBox.turnOn()
newBox.turnOff()
다운캐스팅(as?/as!)
let sbox2: SetTopBox? = electronic[1] as? SetTopBox
sbox2?.doNetflix()
프로토콜의 상속
실제로 프로토콜을 이용해서 상속을 하는 경우는 많이 없지만, 애플이 swift내에서 많은 것들을 프로토콜의 상속을 바탕으로 구조화해놓음.
다중상속도 가능하다.
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() { }
}
프로토콜의 채택 및 전용 프로토콜 타입
protocol SomeProtocol: AnyObject { // AnyObject는 클래스 전용 프로토콜
func doSomething()
}
// 구조체에서는 채택할 수 없게 됨 ⭐️
struct AStruct: SomeProtocol {
}
// 클래스에서만 채택 가능
class AClass: SomeProtocol {
func doSomething() {
print("Do something")
}
}
AnyObject는 프로토콜임. 그렇기 때문에 AnyObject를 범용적 타입으로 사용할 수 있었던 것이고 다운캐스팅(as? / as!)해서 구체적인 실제타입으로 사용할 수 있던 것.
프로토콜의 합성 문법
// 프로토콜을 합성하여 임시타입으로 활용 가능
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
// 하나의 타입에서 여러개의 프로토콜을 채택하는 것 당연히 가능 (다중 상속과 비슷한 역할)
struct Person: Named, Aged {
var name: String
var age: Int
}
프로토콜을 &로 연결해서, 프로토콜 두개를 병합해서 타입으로 사용하는 것 가능
@어트리뷰트 키워드
(1) 선언에 대한 추가정보 제공
@available(iOS 10.0, macOS 10.12, *)
class SomeType { // "SomeType" 선언은 iOS 10.0 버전이상에서만 읽을 수 있음
}
(2) 타입에 추가정보 제공
func doSomething(completion :@escaping ()->()){
}
//나중에 배움
선택적인(구현해도 안해도 되는) 멤버 선언하기
- 프로토콜에서 요구사항 구현시, 선택적인 멤버로 구현가능 하도록 변형가능 (다만, 본 기능은 오브젝티브C에서 지원하는 기능임)
- @objc키워드를 프로토콜의 선언앞에 붙여서, 추가적인 정보 제공 (오브젝티브C에서 읽을 수 있는 코드라는 의미)
- @objc optional 을 멤버 앞에 선언 ➡ 해당 멤버는 선택적 요구사항으로 바뀜 (클래스 전용 프로토콜이기 때문에, 구조체 / 열거형에서는 채택하지 못함)
@objc protocol Remote {
@objc optional var isOn: Bool { get set }
func turnOn()
func turnOff()
@objc optional func doNeflix()
}
//프로토콜 앞에는 "@objc"추가
//멤버 앞에는 "@objc optional"을 모두 추가
프로토콜의 확장 ➡ 디폴트 선언
프로토콜을 채택한 타입에서 실제 메서드 구현을 반복(코드 중복 구현)해야하는 불편함 제거하고 단순히 기본구현(default)을 제공하는 개념
protocol Remote {
func turnOn()
func turnOff()
}
class ~~
struct ~~
extension Remote {
func turnOn() { print("리모콘 켜기") }
func turnOff() { print("리모콘 끄기") }
func doAnotherAction() {
print("리모콘 또 다른 동작")
}
}
var tv7 = TV1()
tv7.turnOn()
tv7.turnOff()
tv7.doAnotherAction()
// 따로 선언하지 않아도 기본값으로 작동됨.
여기에도 우선순위가 있다.
요구사항의 메서드 우선순위가 적용되는데, 이 때 프로토콜 메서드 테이블을 만들게 됨.
- (채택) 구현시 해당 메서드를 반환하고,
- 구현되지 않는다면 extenstion에서 기본으로 지정된 메서드가 나오게 됨.
class Ipad: Remote { // Remote프로토콜을 채택한 클래스
func turnOn() { print("아이패드 켜기") }
func doAnotherAction() { print("아이패드 다른 동작") }
}
//====================================
let ipad: Ipad = Ipad()
ipad.turnOn() // 클래스 - V테이블
ipad.turnOff() // 클래스 - V테이블
ipad.doAnotherAction() // 클래스 - V테이블
//====================================
let ipad2: Remote = Ipad()
ipad2.turnOn() // 프로토콜 - W테이블
ipad2.turnOff() // 프로토콜 - W테이블
ipad2.doAnotherAction() // 프로토콜 - Direct (직접 메서드 주소 삽입)
기본 요구사항은 Remote 프로토콜에 있는 turnOn()과 tuenOff()를 무조건 구현해야함.
[Class Virtual 테이블]
- func turnOn() { print(“아이패드 켜기”) }
- func turnOff() { print(“리모콘 끄기”) }
- func doAnotherAction() { print(“아이패드 다른 동작”) }
[Protocol Witness 테이블] - 요구사항
- func turnOn() { print(“아이패드 켜기”) }
- func turnOff() { print(“리모콘 끄기”) }
프로토콜-프로토콜 확장-클래스-인스턴스 이 과정을 이해해보자.
때떄로 프로토콜의 용량이 클때는 Heap 영역에 저장하기도 한다. 기본적으로는 Stack영역에서 처리. 예를 들면, str타입은 애플이 구조체로 지정되어있고, 우리가 문자열 인스턴스를 찍어내면 stack에서 보통 처리가 되지만, 아주 긴 문자열의 경우는 Heap에서 처리된다고 함.
프로토콜 지향 프로그래밍
상속의 단점
- 하나의 클라스만 상속가능하지, 다중 상속은 불가능.
- 기본적인 상위클래스의 메모리 구조를 따라갈 수 밖에 없음. 필요하지 않은 속성이나 메서드도 다 받아야만 함.
- 구조체, 열거형등은 클래스의 상속을 받지 못함.
- 여러 개의 프로토콜의 다중 상속이 가능함.
- 메모리 구조에 대한 특정 요구사항이 없음. 필요한 속성/메서드만 채택이 가능함. -@obj Optional
- 모든 타입에서 채택이 가능 (벨류 타입도 가능)
- 확장을 통해 재정의가 가능하고, 채택을 통해 타입 제약도 가능.
- 조합의 장점을 잘 살려서 재사용성과/구성을 높일 수 있다.