Swift/Swift 가이드

[Swift] 공식 문서 - 프로토콜(Protocols) - 2

유정주 2022. 7. 31. 21:56
반응형

* Protocols 가이드 1편은 여기에서 볼 수 있습니다.

 

새로 배운 점

  • 제네릭(generic) 타입은 프로토콜 이름 뒤에 where 절이 있을 때만 프로토콜의 요구사항을 만족합니다.
  • Swift는 다음과 같은 커스텀 타입에 대해 Equatable 구현을 제공합니다.
    • Equatable을 따르는 저장 프로퍼티만 가진 구조체
    • Equatable을 따르는 associated 타입만 가진 열거형
    • associated 타입이 없는 열거형
  • Swift는 다음과 같은 커스텀 타입에 대해 Hashable 프로토콜을 따르도록 할 수 있습니다.
    • Hashable을 따르는 저장 프로퍼티만 가지고 있는 구조체
    • Hashable을 따르는 associated 타입만 가지고 있는 열거형
    • associated 타입이 없는 열거형
  • raw value가 없는 열거형에 Comparable 프로토콜을 따르도록 할 수 있습니다.
  • 프로토콜을 배열이나 딕셔너리 같은 콜렉션 타입의 아이템 타입으로 사용할 수 있습니다.
  • 프로토콜 합성(protocol composition)으로 여러 프로토콜들을 하나의 요구사항으로 합칠 수 있습니다.
  • is와 as 연산자를 사용하여 따르고 있는 프로토콜을 확인하고 지정된 프로토콜로 캐스팅할 수 있습니다.
  • 프로토콜에 선택적 요구사항을 정의할 수 있습니다. 이 요구사항들은 이 프로토콜을 따르는 타입에 반드시 구현될 필요가 없습니다.
  • 선택적 요구사항으로 메서드나 프로퍼티를 사용할 때, 이들의 타입은 자동으로 옵셔널이 됩니다.
  • someOptionalMethod?(someArgument)처럼 호출될 때 메서드 이름 뒤에 물음표를 사용하여 optional method의 구현을 체크합니다. 
  • 프로토콜은 이를 따르는 타입에 메서드, 이니셜라이저, 서브스크립트, 연산 프로퍼티 구현을 제공하도록 확장할 수 있습니다.
  • 프로토콜 익스텐션은 이를 따르는 타입에 구현을 추가할 수 있지만, 다른 프로토콜로부터 확장하거나 상속할 수는 없습니다.
  • 프로토콜 익스텐션을 사용해 프로토콜의 어떤 메서드나 연산 프로토콜에 대한 기본 구현을 제공할 수 있습니다.

 

Adding Protocol Conformance with an Extension

새로운 프로토콜을 따르도록 하여,

 이미 존재하는 타입을 확장할 수 있습니다.

Extensions는 새로운 프로퍼티, 메서드, 서브스크립트를 이미 존재하는 타입에 추가할 수 있고,

따라서 프로토콜의 요구사항도 추가할 수 있습니다.

 

예를 들어, TextRepresentable이라는 프로토콜은

텍스트로 표현하는 방법을 가지고 있는 어떤 타입으로 구현될 수 있습니다.

protocol TextRepresentable {
    var textualDescription: String { get }
}

 

아래 코드는 이전 포스팅에서 구현했던 Dice 클래스를 확장하여

TextRepresentable 프로토콜을 따르도록 합니다.

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

 

Dice 인스턴스들은 이제 TextRepresentable처럼 사용할 수 있습니다.

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"

마찬가지로 SnakesAndLadders 게임 클래스 또한 이 프로토콜을 따르도록 확장할 수 있습니다.

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)
// Prints "A game of Snakes and Ladders with 25 squares"

 

Conditionally Conforming to a Protocol

제네릭(generic) 타입은 프로토콜 이름 뒤에 where 절이 있을 때만 프로토콜의 요구사항을 만족합니다.

 

아래 익스텐션은 배열의 요소들의 타입이 TextRepresentable을 따를 때에만

Array 인스턴스가 TextRepresentable 프로토콜을 따르도록 합니다.

extension Array: TextRepresentable where Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joined(separator: ", ") + "]"
    }
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// Prints "[A 6-sided dice, A 12-sided dice]"

 

Declaring Protocol Adoption with an Extension

만약 타입이 이미 프로토콜의 모든 요구사항을 따르지만

아직 명시적으로 이 프로토콜을 따른다고 언급하지 않았을 때,

빈 익스텐션으로 프로토콜을 채택하도록 할 수 있습니다.

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

Hamster 인스턴스는 이제 TextRepresentable이 요구되는 타입에서 사용될 수 있습니다.

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"

 

Adopting a Protocol Using a Synthesized Implementation

Swift는 자동으로 Equatable, Hashable, Comparable 프로토콜을 제공합니다.

이 synthesized implementation을 사용하면 프로토콜 요구사항을 직접 구현하지 않아도 됩니다.

synthesized implementation는 반복적인 코드 작성을 줄여줍니다.

 

Swift는 다음과 같은 커스텀 타입에 대해 Equatable 구현을 제공합니다.

  • Equatable을 따르는 저장 프로퍼티만 가진 구조체
  • Equatable을 따르는 associated 타입만 가진 열거형
  • associated 타입이 없는 열거형

'=='와 '!=' 연산자 구현을 직접 하지 않아도 Equatable을 따르도록 선언하여

'==' 연산자와 '!=' 구현을 얻을 수 있습니다.

 

아래 예제는 Equatable 프로토콜을 따르는 Vector#D 구조체입니다.

'=='을 구현하지 않아도 '=='를 사용할 수 있습니다.

struct Vector3D: Equatable {
    var x = 0.0, y = 0.0, z = 0.0
}
 
let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
if twoThreeFour == anotherTwoThreeFour {
    print("These two vectors are also equivalent.")
}
// Prints "These two vectors are also equivalent."

 

Swift는 다음과 같은 커스텀 타입에 대해 Hashable 프로토콜을 따르도록 할 수 있습니다.

  • Hashable을 따르는 저장 프로퍼티만 가지고 있는 구조체
  • Hashable을 따르는 associated 타입만 가지고 있는 열거형
  • associated 타입이 없는 열거형

위 타입에서 Hashable을 채택하면 hash(into:) 메서드를 직접 구현할 필요가 없습니다.

 

또한 raw value가 없는 열거형에 Comparable 프로토콜을 따르도록 할 수 있습니다.

enum SkillLevel: Comparable {
    case beginner
    case intermediate
    case expert(stars: Int)
}
var levels = [SkillLevel.intermediate, SkillLevel.beginner,
              SkillLevel.expert(stars: 5), SkillLevel.expert(stars: 3)]
for level in levels.sorted() {
    print(level)
}
// Prints "beginner"
// Prints "intermediate"
// Prints "expert(stars: 3)"
// Prints "expert(stars: 5)"

 

Collections of Protocol Types

프로토콜을 배열이나 딕셔너리 같은 콜렉션 타입의 아이템 타입으로 사용할 수 있습니다.

아래 예제는 TextRepresentable의 배열을 생성합니다.

let things: [TextRepresentable] = [game, d12, simonTheHamster]

 

배열에서 항목들을 순회하고, 각 아이템의 텍스트 description을 출력할 수 있습니다.

for thing in things {
    print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

thing은 TextRepresentable 타입입니다.

따라서 textualDescription 프로퍼티를 가지고 있으며 접근할 수 있습니다.

 

Protocol Inheritance

클래스의 상속처럼 프로토콜도 하나 이상의 다른 포로토콜을 상속받을 수 있습니다.

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}

아래 예제는 TextRepresentable 프로토콜을 상속 받는 프로토콜을 정의합니다.

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

새로운 프로토콜인 PrettyTextRepresentable로부터 상속 받습니다.

PrettyTextRepresentable을 따르면 반드시 TextRepresentable에 의해

강제되는 모든 요구사항을 만족해야 하며 PrettyTextRepresentable에 의해

강제되는 추가적인 요구사항도 만족해야 합니다.

예제 코드에서 PrettyTextRepresentable는 prettyTextualDescription이라는

gettable 프로퍼티를 요구사항에 추가합니다.

 

아래 코드는 SnakesAndLadders 클래스를 

PrettyTextRepresentable 프로토콜을 따르도록 확장한 예제입니다.

extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\n"
        for index in 1...finalSquare {
            switch board[index] {
            case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}

prettyTextualDescription은 SnakesAndLadders 인스턴스의 text description을 출력할 수 있습니다.

print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

 

Class-Only Protocols

프로토콜 상속 리스트에 AnyObject 프로토콜을 추가하여

클래스 타입만 따를 수 있도록 제한할 수 있습니다.

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // class-only protocol definition goes here
}

위 코드에서 SomeClassOnlyProtocol은 오직 클래스 타입에서만 채택할 수 있습니다.

만약 이 프로토콜을 구조체나 열거형의 정의에 사용하면 컴파일 에러가 발생합니다.

Class-Only 프로토콜은 정의된 동작이 값 타입이 아닌 참조 타입이라고 가정하거나 요구할 경우 사용하세요.

 

Protocol Composition

동시에 여러 프로토콜을 따르도록 할 수도 있습니다.

프로토콜 합성(protocol composition)으로 여러 프로토콜들을 하나의 요구사항으로 합칠 수 있습니다.

 

프로토콜 합성은 합성되는 모든 프로토콜들의 요구사항을 갖는 임시 로컬 프로토콜을 정의하는 것과 같습니다.

프로토콜 합성에서 새로운 프로토콜 타입은 정의하지 않습니다.

 

프로토콜 합성은 'SomeProtocol & AnotherProtocol'의 형태를 갖습니다.

필요한 만큼 프로토콜을 나열할 수 있으며, '&'로 구분합니다.

 

아래 예제 코드는 Named와 Aged라는 두 프로토콜을 조합하여

하나의 프로토콜 합성 요구사항을 함수 파라미터에 사용하고 있습니다.

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're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"

위 예제에서 Named 프로토콜은 하나의 gettable String 타입인 name 프로퍼티를 가지고 있습니다.

Aged 프로토콜은 gettable Int 타입인 age를 가집니다.

Person 구조체는 두 프로토콜을 따릅니다.

 

또한 withHappyBirthday(to:) 함수를 정의하고 있는데,

이 함수의 파라미터 celebrator의 타입은 Named & Aged 입니다.

새로운 Person 인스턴스를 생성하고 이 인스턴스를 wishHappyBirthday(to:) 함수의 파라미터로 전달합니다.

Person은 두 가지 프로토콜을 따르기 때문에 호출이 유효합니다.

 

아래 예제는 Named 프로토콜과 Location 클래스를 조합합니다.

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}
 
let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"

beginConcert(in:) 함수는 Location & Named 타입의 파라미터를 전달 받습니다.

여기에서 City는 두 요구사항을 모두 만족합니다.

 

Checking for Protocol Conformance

Type Casting에서 봤듯이 is와 as 연산자를 사용하여

따르고 있는 프로토콜을 확인하고 지정된 프로토콜로 캐스팅할 수 있습니다.

프로토콜을 확인하고 캐스팅하는 것은 타입을 확인하고 캐스팅하는 것과 완전히 동일한 문법을 따릅니다.

  • 만약 인스턴스가 프로토콜을 따른다면 true, 아니라면 false를 반환
  • downcast 연산자 as?는 프로토콜의 타입의 옵셔널 값을 리턴하고, 만약 해당 프로토콜을 따르지 않는다면 nil이 됨
  • downcast 연산자 as!는 강제로 프로토콜 타입으로 다운캐스트하며, 다운캐스트를 실패하면 런타임 에러가 발생함

아래 예제 코드는 HasArea라는 프로토콜을 정의하며,

이 프로토콜에는 하나의 gettable Double 요구사항을 가집니다.

protocol HasArea {
    var area: Double { get }
}

아래는 HasArea 프로토콜을 따르는 Circle과 Country 클래스를 정의합니다.

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

그리고 비교를 위해 HasArea를 따르지 않는 Animal 클래스를 정의합니다.

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

 

Circle, Country와 Animal은 기본 클래스를 공유하지 않지만,

이들은 모두 클래스이기 때문에 AnyObject 타입의 값으로 초기화되는 배열에 저장할 수 있습니다.

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

 

배열을 순회하면서 배열의 각 object가 HasArea 프로토콜을 따르고 있는지

체크해보도록 합시다.

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

처음 두 인스턴스(Circle, Country 타입)는 HasArea를 따르기 때문에 area 값이 리턴되고

마지막 인스턴스(Animal 타입)는 HasArea 프로토콜을 따르지 않기 때문에 else절이 실행됩니다.

 

Optional Protocol Requirements

프로토콜에 선택적 요구사항을 정의할 수 있습니다.

이 요구사항들은 이 프로토콜을 따르는 타입에 반드시 구현될 필요가 없습니다.

선택적 요구사항은 프로토콜 정의 앞에 optional 수식어를 붙입니다.

 

선택적 요구사항을 사용하면 Objective-C에서도 호환되는 코드를 작성할 수 있습니다.

프로토콜과 선택적 요구사항은 모두 @objc를 표시해야 하며

@objc 프로토콜은 오직 Objective-C 클래스나 다른 @objc 클래스로부터 

상속되는 클래스에서만 채택될 수 있습니다.

구조체나 열거형에서는 사용할 수 없습니다.

 

선택적 요구사항으로 메서드나 프로퍼티를 사용할 때, 이들의 타입은 자동으로 옵셔널이 됩니다.

예를 들어, (Int) -> String 타입의 메서드는 ((Int) -> String)? 타입이 됩니다.

함수 타입 전체가 옵셔널로 래핑되며 메서드의 리턴값이 옵셔널이 되는 것은 아닙니다.

 

선택적 프로토콜 요구사항은 프로토콜을 따르는 타입에 의해서

구현되지 않았을 수도 있기 때문에 옵셔널 체이닝과 함께 호출될 수 있습니다.

someOptionalMethod?(someArgument)처럼

호출될 때 메서드 이름 뒤에 물음표를 사용하여 optional method의 구현을 체크합니다. 

 

아래 예제 코드는 정수를 카운팅하는 Counter 클래스를 정의합니다.

이 클래스는 CounterDataSource 프로토콜을 채택하며 2개의 선택적 요구사항을 가집니다.

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}

 

다음 코드는 Counter 클래스를 정의하며,

타입이 CounterDataSource?인 옵셔널 dataSource 프로퍼티를 가집니다.

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

 

아래 코드는 위에서 정의한 CounterDataSource 프로토콜을 따르는 간단한 구현 예제입니다.

매 쿼리마다 상수 3을 리턴하도록 구현했습니다.

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

ThreeSource의 인스턴스를 생성해 Counter 인스턴스의 dataSource에 설정할 수 있습니다.

생성된 Counter 인스턴스는 increment()를 호출할 때마다 3씩 증가시킵니다.

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

 

다음 코드는 TowardsZeroSource라는 조금 더 복잡한 data source를 정의합니다.

class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

TowardsZeroSource 클래스는 선택적으로 

CounterDataSource 프로토콜의 increment(forCount:) 메소드만을 구현합니다.
이 클래스의 인스턴스를 Counter 인스턴스의 dataSource로 설정하여 0으로 향하는 카운터를 사용할 수 있습니다.

counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0

 

Protocol Extensions

프로토콜은 이를 따르는 타입에 메서드, 이니셜라이저, 서브스크립트, 연산 프로퍼티 구현을

제공하도록 확장할 수 있습니다.

이는 프로토콜 자체에 동작을 정의할 수 있도록 해줍니다.

protocol RandomNumberGenerator {
    func random() -> Double
}
 
class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom * a + c)
            .truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}

예를 들어, RandomNumberGenerator 프로토콜을 확장하여 randomBool()을 제공할 수 있도록 합니다.

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

프로토콜을 확장하면 이 프로토콜을 따르는 타입은 

추가적인 수정없이 추가된 메서드를 바로 사용할 수 있습니다.

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// Prints "And here's a random Boolean: true"

프로토콜 익스텐션은 이를 따르는 타입에 구현을 추가할 수 있지만,

다른 프로토콜로부터 확장하거나 상속할 수는 없습니다.

프로토콜 상속은 항상 프로토콜 선언에서 지정됩니다.

 

Providing Default Implementations

프로토콜 익스텐션을 사용해 프로토콜의 어떤 메서드나 연산 프로토콜에 대한 기본 구현을 제공할 수 있습니다.

만약 이 프로토콜을 따르는 타입이

요구되는 메서드나 프로퍼티에 대한 자신만의 구현을 제공한다면 기본 구현 대신 자신만의 구현을 사용할 수 있습니다.

 

예를 들어, 위에서 살펴본 PrettyTextRepresentable 프로토콜에서 요구하는

prettyTextualDescription 프로퍼티의 기본 구현을 다음과 같이 제공할 수 있습니다.

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

 

Adding Constraints to Protocol Extensions

프로토콜 익스텐션을 정의할 때 특정 조건에서만 적용되도록 할 수 있습니다.

이는 제네릭 where절을 사용하면 됩니다.

 

예를 들어, Collection 프로토콜의 익스텐션을 확장하여 Equatable 프로토콜을 따르는

요소들을 가지는 콜렉션에 적용할 수 있습니다.

Equatable 프로토콜의 따르는 콜렉션의 원소로 제약함으로서

'==' 연산자와 '!=' 연산자를 사용하여 두 원소 간의 일치/불일치를 비교할 수 있습니다.

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}

allEqual()은 콜렉션의 모든 원소들이 일치할 때만 true를 리턴합니다.

 

하나는 모든 원소들이 같은 정수 배열이고,

다른 하나는 같지 않은 정수 배열에 대해 allEqual 메서드를 호출하는 예제 코드입니다.

let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
 
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

 

 

참고

https://docs.swift.org/swift-book/LanguageGuide/Protocols.html

 

 

반응형