지난 시간엔 프로토콜의 형태와 사용방법에 채택/구현방법에 대해 알아 보았다 이번시간에는 프로토콜로 할 수 있는 추가적인 기능에 대해서 알아볼것이다.
프로토콜으로 배열이나 set같은 컬렉션으로 만들 수 있다.
// TextRepresentable protocol 정의
protocol TextRepresentable {
var textualDescription: String { get }
}
// Game 구조체
struct Game: TextRepresentable {
var name: String
var textualDescription: String {
return "Game: \(name)"
}
}
// Dice 구조체
struct Dice: TextRepresentable {
var sides: Int
var textualDescription: String {
return "A dice with \(sides) sides"
}
}
// Hamster 구조체
struct Hamster: TextRepresentable {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
// 프로토콜을 준수하는 인스턴스 생성
let game = Game(name: "Chess")
let d12 = Dice(sides: 12)
let simonTheHamster = Hamster(name: "Simon")
// TextRepresentable 프로토콜을 준수하는 타입의 컬렉션 생성
let things: [TextRepresentable] = [game, d12, simonTheHamster]
// 컬렉션 순회하며 textualDescription 호출
for thing in things {
print(thing.textualDescription)
}
여기서 아래의 코드에 집중해보자
let things: [TextRepresentable] = [game, d12, simonTheHamster]
이처럼 프로토콜을 타입으로 이용해서 인스턴스 컬렉션을 만들 수 있다.
이렇게 하면 타입(클래스/구조체/열거형)이 달라도 같은 프로토콜을 준수하는 인스턴스 끼리 묶을 수 있다.
프로토콜도 다른 타입들처럼 상속을 할 수 있다.
기존의 상속한 요구사항 위에 요구사항을 더 추가할 수 있다.
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 프로토콜 제약사항은 여기에...
}
만약 이처럼 SomeProtocol, AnotherProtocol을 상속받는다면 InheritingProtocol을 채택하고 구현하는 타입은 InheritingProtocol과 SomeProtocol, AnotherProtocol의 제약사항을 모두 지켜야 한다.
클래스에서만 사용할 수 있게 프로토콜을 설계할 수 있다.
바로 AnyObject프로토콜을 상속받는 프로토콜을 만들면 된다.
protocol SomeClassOnlyProtocol: AnyObject{
// 클래스 전용 프로토콜 제약사항은 여기에...
}
// 두 개의 프로토콜 정의
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("Happy Birthday, \(celebrator.name)! You are \(celebrator.age) years old!")
}
let person = Person(name: "Alice", age: 30)
wishHappyBirthday(to: person)
// 출력: Happy Birthday, Alice! You are 30 years old!
- Named와 Aged라는 두 프로토콜을 정의
- Person 구조체는 Named와 Aged 프로토콜을 모두 준수함
• name과 age 속성을 제공하여 두 프로토콜을 충족- wishHappyBirthday(to:) 함수는 인자로 Named & Aged 타입을 요구하는데 즉, 이 함수는 Named와 Aged 프로토콜을 모두 준수하는 객체만 받을 수 있다.
- Person은 두 프로토콜을 모두 준수하기 때문에, wishHappyBirthday 함수의 인자로 전달할 수 있음
프로토콜 혼합은 함수나 메서드에서 다양한 타입을 처리할 때 유용하게 쓰일 수 있다.
예를 들어, 어떤 객체가 특정한 여러 기능을 제공해야 할 때, 클래스를 상속하지 않고 여러 프로토콜을 혼합하여 타입을 제약하게 할 수 있다.
타입캐스팅에서 사용하는 is, as 연산자를 이용해서 인스턴스의 타입이 특정 프로토콜을 따르고 있는지 확인 할 수 있다.
(자세한 내용은 타입캐스팅(링크x)에서)
is 는
인스턴스가 프로토콜을 준수한다면 true 를 반환하고 그렇지 않으면 false 를 반환합니다.as? 는
프로토콜의 타입의 옵셔널 값을 반환하고 인스턴스가 프로토콜을 준수하지 않으면 nil을 반환한다.as! 는
프로토콜 타입으로 강제로 다운 캐스팅 하고 다운 캐스트가 성공하지 못하면 런타임 에러를 발생시킨다.
// 프로토콜 정의
protocol HasName {
var name: String { get }
}
protocol CanRun {
func run()
}
// 클래스가 프로토콜을 구현
class Person: HasName, CanRun {
var name: String
init(name: String) {
self.name = name
}
func run() {
print("\(name) is running!")
}
}
class Dog: HasName {
var name: String
init(name: String) {
self.name = name
}
}
// 객체 생성
let person = Person(name: "Alice")
let dog = Dog(name: "Buddy")
// 배열에 넣기
let things: [Any] = [person, dog]
// 타입 검사: is 연산자 사용
for thing in things {
if thing is HasName {
print("This thing has a name.")
}
if thing is CanRun {
print("This thing can run.")
}
}
// 타입 캐스팅: as? 연산자 사용
for thing in things {
if let nameable = thing as? HasName {
print("This thing's name is \(nameable.name).")
}
if let runnable = thing as? CanRun {
runnable.run()
}
}
출력결과
This thing has a name.
This thing can run.
This thing has a name.
This thing's name is Alice.
Alice is running!
This thing's name is Buddy.
extension을 이용해서 프로토콜의 확장또한 가능하다. 이때 프로토콜을 확장하면 요구사항의 구현을 할 수 있다.
새로운 요구사항을 추가하는것은 불가능하다.
// Protocol 정의
protocol Describable {
var description: String { get }
}
// Protocol 확장: 새로운 메서드 추가
extension Describable {
func printDescription() {
print(description)
}
}
// Struct가 프로토콜을 채택하고 기본 속성 구현
struct Car: Describable {
var description: String
}
let car = Car(description: "A red sports car")
car.printDescription() // 출력: A red sports car
이처럼 기존의 요구사항으로 정의했던 description 메소드를 extension부분에서 구현할 수 있다.
(계산 프로퍼티도 가능)
이렇게 하면 타입에서는 해당 타입을 채택 하는것 만으로도 새로운 메소드를 사용할 수 있게 된다.
이를 기본구현 이라고 한다.
프로토콜확장에 where키워드를 이용해서 특정 제약을 추가 할 수 있다 이해가 어렵다면 제네릭을 공부하고 다시 보도록 하자.
// Protocol 정의
protocol Summable {
static func +(lhs: Self, rhs: Self) -> Self
}
// Protocol 확장: 제약 있는 확장
extension Summable where Self: Numeric {
func doubled() -> Self {
return self + self
}
}
// Int와 Double은 Numeric 프로토콜을 준수하므로 Summable도 자동으로 채택함
extension Int: Summable {}
extension Double: Summable {}
let intValue: Int = 10
let doubleValue: Double = 5.5
print(intValue.doubled()) // 출력: 20
print(doubleValue.doubled()) // 출력: 11.0
프로토콜은 Swift에서 강력한 기능을 제공한다. 잘사용한다면 코드가 한단계 더 발전 할 기회를 제공한다.
- 프로토콜끼리 상속이 가능하다
- 클래스 전용 프로토콜을 만들 수 있다.
- 프로토콜을 타입처럼 이용해 컬렉션에도 사용할 수 있다.
- 프로토콜을 채택했는지 확인가능하다.
- extension을 이용해서 프로토콜자체를 확장 할 수 있다.