Swift : 기초문법 [프로토콜#1 - 프로퍼티 요구사항]
Swift : 기초문법 [프로토콜#2 - 메서드 요구사항]
프로토콜 조합
하나의 매개변수가 여러 프로토콜을 모두 준수하는 타입이어야만 한다면, 하나의 매개변수에 여러 프로토콜을 한 번에 조합하여 요구 가능하다.
- 엠 퍼센트(
&
)를 여러 프로토콜 이름 사이에 사용한다.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)
}
}
Named
와 Aged
프로토콜이 읽기 전용 프로퍼티로 정의되어있고, Car
클래스는 Named
만 채택 하고 Truck
는 Car
과 Aged
를 채택한 후에 이니셜 라이저로 초기화해줬다.
Truck
클래스를 보면 Car
의 name
과 Aged
의 age
가 get만 요구 했기 때문에 어떠한 종류의 프로퍼티로도 요구사항을 만족할 수 있다.
class Truck: Car, Aged { }
//클래스 밖에 있는 함수.
//Named와 Aged 프로토콜이 파라미터 안에 & 으로 조합 되어 있다.
//즉, Named 프로토콜과 Aged 프로토콜 둘 다 준수하는 타입 이여야 한다.
func celebrateBirthday(to celebrator: Named & Aged) {
print("올해로 \(celebrator.age)세가 되신 \(celebrator.name)님 생신 축하드립니다!")
}
Named
와 Aged
프로토콜이 파라미터 안에 &
으로 조합되어 있다. 즉, Named
프로토콜과 Aged
프로토콜 둘 다 준수하는 타입이어야 한다는 의미!
호출은 아래와 같이 가능한데, 오류가 나는 부분을 주의 깊게 보는 것을 추천한다!
let Seogun: Person = Person(name: "서근", age: 99)
celebrateBirthday(to: Seogun) //올해로 99세가 되신 서근님 생신 축하드립니다!
let myCar: Car = Car(name: "붕붕이")
//error! Aged 프로토콜(age)을 충족하지 않음
celebrateBirthday(to: myCar)
//error! 클래스 & 프로토콜 조합에서 클래스 타입은 한 타입만 조합 가능하다
var someVariable: Car & Aged & Truck
//Car클래스의 인스턴스 역할도 수행 가능하며,
//Aged 프로토콜을 준수하는 인스턴스만 할당 가능하다
var someVariable: Car & Aged
someVariable = Truck(name: "트럭", age: 5)
//error! myCar은 Aged프로토콜을 준수하지 않았으므로 오류
someVariable = myCar
전체 코드
<hide/>
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)
}
}
//클래스 밖에 있는 함수.
//Named와 Aged 프로토콜이 파라미터 안에 & 으로 조합 되어 있다.
//즉, Named 프로토콜과 Aged 프로토콜 둘 다 준수하는 타입 이여야 한다.
func celebrateBirthday(to celebrator: Named & Aged) {
print("올해로 \(celebrator.age)세가 되신 \(celebrator.name)님 생신 축하드립니다!")
}
let Seogun: Person = Person(name: "서근", age: 99)
celebrateBirthday(to: Seogun) //올해로 99세가 되신 서근님 생신 축하드립니다!
let myCar: Car = Car(name: "붕붕이")
//celebrateBirthday(to: myCar) //error! Aged 프로토콜(age)을 충족하지 않음
//error! 클래스 & 프로토콜 조합에서 클래스 타입은 한 타입만 조합 가능하다
//var someVariable: Car & Aged & Truck
//Car클래스의 인스턴스 역할도 수행 가능하며,
//Aged 프로토콜을 준수하는 인스턴스만 할당 가능하다
var someVariable: Car & Aged
someVariable = Truck(name: "트럭", age: 5)
//error! myCar은 Aged프로토콜을 준수하지 않았으므로 오류
//someVariable = myCar
위 예제와 또 다른 예제를 한번 더 보자!!
protocol Named {
var name: String { get }
}
class Address {
var city: String
var zipCode: Int
init(city: String, zipCode: Int) {
self.city = city
self.zipCode = zipCode
}
}
Named
프로콜과 Address
클래스를 생성했다. 이제 이 프로토콜과 클래스를 채택/상속한 클래스를 하나 더 만들어보면
class Delivery: Address, Named {
var name: String
init(name: String, city: String, zipCode: Int) {
self.name = name
super.init(city: city, zipCode: zipCode)
}
}
Delivery
클래스는 Address
클래스를 상속받고, Named
프로토콜을 채택한 것을 확인할 수 있는데, Named
프로토콜이 요구한 name
을 저장 프로퍼티로 구현해줬다.
그리고 class
에 기본값이 없기 때문에 init
으로 초기화를 해줬다!
//파리미터로 클래스인 Address와 Named 가 있다.
//이 의미는, Address의 하위 클래스 이머, Named 프로토콜을 준수하는 모든 타입!
func whereTo(to customer: Address & Named) {
print("\(customer.name)님께 \(customer.city) 지역으로 배달을 시작합니다.")
}
이 코드를 보면, 위에서는 파라미터에 프로토콜 & 프로토콜
을 넣어줬지만, 지금은 클래스 & 프로토콜
조합인 것을 확인할 수 있는데, 이 의미는 Address
의 하위 클래스이며, Named
프로토콜을 준수하는 모든 타입을 의미한다.
이것이 위에서 말한 '프로토콜의 목록 외에도, 프로토콜 조합에는 필요한 슈퍼 클래스를 지정하는 데 사용할 수 있는 클래스 타입이 포함될 수 있다'를 의미한 코드이다.
주의해야 할 점은 Address
를 상속받고 있지 않거나, Named
프로토콜을 준수하고 있지 않다면, 이 파라미터로 들어오지 못한다.
let seogun: Delivery = Delivery(name: "서근", city: "대전", zipCode: 33223)
whereTo(to: seogun) //서근님께 대전 지역으로 배달을 시작합니다.
let mijin: Delivery = Delivery(name: "미진", city: "서울")
whereTo(to: mijin) //error! Address 클래사의 zipCode 요구 조건을 충족하지 않아 오류
프로토콜 준수 확인
- 타입캐스팅에 사용했던
is
와as
연산자를 통해 대상이 프로토콜을 준수하는지 확인 가능 - 확인 후 특정 프로토콜로 캐스팅 가능
is
연산자가 프로토콜을 준수하면true
반환. 그렇지 않으면false
- 다운캐스팅 연산자
as?
는 프로토콜 타입의 옵셔널 값을 반환. 인스턴스가 해당 프로토콜을 준수하지 않으면 값은nil
as!
는 강제로 프로토콜 타입을 설정, 다운캐스팅이 실패하면 런타임 오류!
위에서 사용한 코드를 가져와서 프로토콜 준수 여부를 아래와 같이 확인 가능하다.
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
}
}
let Seogun: Person = Person(name: "서근", age: 99)
let myCar: Car = Car(name: "붕붕이")
print(Seogun is Named) //true
print(Seogun is Aged) //true
print(myCar is Named) //true
print(myCar is Aged) //false
if let castedInstance: Named = Seogun as? Named {
print("\(castedInstance)는 Named 이다.")
} //Person(name: "서근", age: 99)는 Named 이다.
if let castedInstance: Aged = Seogun as? Aged {
print("\(castedInstance)는 Aged 이다.")
} //Person(name: "서근", age: 99)는 Aged 이다.
if let castedInstance: Person = myCar as? Person {
print("\(castedInstance)는 Person 이다.")
} else {
print("Error!")
} //Error!
if let castedInstance: Aged = myCar as? Aged {
print("\(castedInstance)는 Aged 이다.")
} //출력 없음. 캐스팅 실패
이렇게 보면 데이터 타입의 타입캐스팅과 똑같다는 것을 확인할 수 있다. 프로토콜도 하나의 타입이기 때문에 당연한 것!
또 다른 예제를 보자면
protocol Named {
var name: String { get }
}
class Aclass: Named {
var name: String
init(name: String) {
self.name = name
}
}
class Bclass {
var name: String
init(name: String) {
self.name = name
}
}
Named
라는 프로토콜이 있고, Aclass
는 Named
프로토콜을 채택. Bclass
는 아무것도 채택하지 않았다.
이제 인스턴스를 생성하고 AnyObject
타입의 빈 배열을 생성해 그 안에 AClass
의 인스턴스와 BClass
인스턴스를 append
시켜준다.
let firstInstance: Aclass = Aclass(name: "서근")
let secondInstance: Bclass = Bclass(name: "미진")
var array = [AnyObject]()
array.append(firstInstance)
array.append(secondInstance)
그런 다음 이 배열을 for
구문을 하고 as?
다운캐스팅해줄 수 있다. Aclass
는 Naemd
프로토콜을 준수했고, Bclass
는 채택하지 않았기 때문에 결과는 아래와 같다.
var array = [AnyObject]()
array.append(firstInstance)
array.append(secondInstance)
for index in array {
if let index = index as? Named {
print(index.name) // "서근"
} else {
print("Naemd를 채택하지 않음")
//"Naemd를 채택하지 않음" Bclass의 "미진"은 Named를 채택하지 않았기 때문!
}
}
옵셔널(선택적) 프로토콜 요구 사항
- 프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정 가능하다.
- 옵셔널 요구사항 앞에
optional
수정자가 접두어로 붙는다. - 옵셔널 요구사항을 정의하고 싶은 프로토콜은 반드시
@objc
속성으로 부여된 프로토콜이어야 한다. @objc
프로토콜은Objective-C
클래스 나 다른@objc
클래스를 상속받은 클래스에서만 사용할 수 있다.- 구조체나 열거형에 의해 채택될 수 없다.
- 옵셔널 요구사항에서 메서드나 프로퍼티를 사용하면 해당 타입이 자동으로 옵셔널이 된다.
- 예를 들어,
(Int) -> String
타입의 메서드가((Int) -> String)?
타입이 된다. - 요구사항이 프로토콜을 준수하는 타입으로 구현되지 않았을 가능성을 나타내기 위해 옵셔널체이닝을 사용하고, 호출 가능
- 옵셔널 요구를 하면 프로토콜을 준수하는 타입에 해당 요구사항을 구현할 필요 없다.
Foundation 프레임워크
@objc
속성을 사용하려면 Foundation
프레임워크 모듈을 import
해야 한다.
import Foundation
@objc protocol Moveable {
func walk()
@objc optional func fly()
}
class Tiger: NSObject, Moveable {
func walk() {
print("호랑이가 걷는다.")
}
}
class Bird: NSObject, Moveable {
func walk() {
print("새가 걷는다")
}
func fly() {
print("새가 난다.")
}
}
let tiger: Tiger = Tiger()
let brid: Bird = Bird()
tiger.walk()
brid.fly()
var movableInstance: Moveable = tiger
//옵셔널체이닐 fly?()를 통해 실제로 메서드가 구현되었는지 호출 시도
movableInstance.fly?() // 출력 없음. tiger에는 fly 메서드가 없기 때문
movableInstance = brid
movableInstance.fly?() // "새가 난다"
Moveable
프로토콜은 옵셔널 요구사항인 fly()
메서드가 있기 때문에 @objc
속성을 부여했고, @objc
속성을 사용하기 위해 Tiger
과 Bird
클래스에 각각 Object-C
의 클래스인 NSObject
를 상속받았다.
Tiger
는 날 수 없기 때문에 fly()
메서드를 구현하지 않았다. (옵셔널 요구사항이기 때문에 정의하지 않아도 됨)
Bird
는 날 수 있기 때문에 fly()
메서드를 구현했다.
그리고 각 클래스의 인스턴스를 구현하여 호출 가능하다.
또, var movableInstance
에 Moveable
프로토콜이 할당되어있는데, 인스턴스 타입에 실제로 fly()
메서드가 구현되어 있는지 확인하려면 옵셔널 체이닝을 통해 확인 가능하다.
옵셔널 체이닝을 사용하기 위해서는 메서드 이름 뒤에 물음표(?
)를 붙여 표현한다. fly?()
읽어주셔서 감사합니다🤟
'SWIFT > Grammar' 카테고리의 다른 글
Swift : 기초문법 [제네릭 - Generic] (0) | 2022.02.03 |
---|---|
Swift : 기초문법 [익스텐션 - Extensions] (0) | 2022.02.03 |
Swift : 기초문법 [프로토콜#4 - 익스텐션, 컬렉션 타입, 상속] (0) | 2022.01.30 |
Swift : 기초문법 [프로토콜#3 - 타입으로서의 프로토콜, Delegate] (0) | 2022.01.28 |
Swift : 기초문법 [프로토콜#2 - 메서드 요구사항] (0) | 2022.01.27 |