본문 바로가기
프로그래밍/Swift

[Swift] Protocols (2)

by 별준 2022. 2. 5.

References

Contents

  • Adding Protocol Conformance with an Extension
  • Adopting a Protocol Using a Synthesized Implementation
  • Collections of Protocol Types
  • Protocol Inheritance
  • Class-Only Protocols
  • Protocol Composition
  • Checking for Protocol Conformance
  • Optional Protocol Requirements
  • Protocol Extensions

이전 포스팅에 이어서 계속해서 프로토콜에 대해 알아보도록 하겠습니다.

[Swift] Protocols (1)

 


Adding Protocol Conformance with an Extension

새로운 프로토콜을 따르도록 만들어 이미 존재하는 타입(소스코드에 액세스할 수 없더라도)을 확장할 수 있습니다. Extensions는 새로운 속성, 메소드, 서브스크립트를 존재하는 타입에 추가할 수 있고, 따라서 프로토콜이 요구하는 어떠한 요구사항도 추가할 수 있습니다. Extension에 관한 내용은 아래 포스팅을 참조해주세요.

[Swift] Extensions

 

[Swift] Extensions

Refences https://docs.swift.org/swift-book/LanguageGuide/Extensions.html Contents Extension Syntax Computed Properties Initializers Methods Subscripts Nested Types Extensions 익스텐션(extension)을..

junstar92.tistory.com

 

예를 들어, 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 clause를 추가하여 적용할 수 있습니다.

제너릭과 관련한 내용은 다음 포스팅에서 다루도록 하겠습니다.

 

아래 익스텐션은 배열의 요소들의 타입이 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

스위프트는 자동으로 Equatable, Hashable, Comparable 프로토콜을 제공합니다. 이 synthesized implementation을 사용하면 프로토콜 요구사항을 직접 구현하기 위해 반복적인 코드 작성을 하지 않아도 됩니다.

 

스위프트는 다음와 같은 커스텀 타입의 Equatable 구현을 제공합니다.

  • Structures that have only stored properties that conform to the Equatable protocol
  • Enumerations that have only associated types that conform to the Equatable protocol
  • Enumerations that have no associated types

'==' 연산자 구현을 직접 하지 않고, Equatable을 따르도록 선언하여 '==' 연산자의 구현을 얻을 수 있습니다. Equatable 프로토콜은 '!=' 기본 구현도 제공합니다.

 

아래 예제 코드는 Equatable 프로토콜을 따르는 Vector3D 구조체를 정의하며, 이는 (x, y, z)의 3차원 위치의 벡터를 가집니다. 따라서 '=='을 직접 구현하지 않고 적절하게 사용할 수 있습니다.

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."

 

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

  • Structures that have only stored properties that conform to the Hashable protocol
  • Enumerations that have only associated types that conform to the Hashable protocol
  • Enumerations that have no asscoicated types

hash(into:)의 구현을 얻기 위해서 원래 선언을 포함하는 파일에서 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

프로토콜을 배열(array)나 딕셔너리(dictionary)와 같은 타입을 콜렉션(collection) 타입에 저장하기 위한 타입으로 사용할 수 있습니다. 아래 예제 코드는 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 속성을 가지고 있으며, 안전하게 thing.textualDescritpion에 액세스할 수 있습니다.

 


Protocol Inheritance

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

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

 

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

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

새로운 프로토콜인 PrettyTextRepresentable은 TextRepresentable로부터 상속받습니다. 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
}

위 코드에서 SomeClassOnlyProtocal는 오직 클래스 타입에서만 사용될 수 있습니다. 만약 이 프로토콜을 구조체나 열거형의 정의에 사용하면 런타임 에러가 발생합니다.

 


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이라는 구조체는 두 프로토콜을 따릅니다.

또한 wishHappyBirthday(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

[Swift] Type Casting

 

[Swift] Type Casting

References https://docs.swift.org/swift-book/LanguageGuide/TypeCasting.html Contents Defining a Class Hierarchy for Type Casting Checking Type Downcasting Type Casting for Any and Any Object Type ca..

junstar92.tistory.com

위 포스팅에서 살펴봤던 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 modifier를 붙여주어야 합니다. 선택적 요구사항을 사용하면 Objective-C에서도 호환되는 코드를 작성할 수 있습니다. 프로토콜과 선택적 요구사항은 모두 @objc attribute를 표시해주어야하며, @objc 프로토콜은 오직 Objective-C 클래스나 다른 @objc 클래스로부터 상속되는 클래스에서만 채택될 수 있습니다. 구조체나 열거형에서는 사용할 수 없습니다.

 

선택적 요구사항으로 메소드나 속성을 사용할 때, 이들의 타입은 자동으로 옵셔널(optional)이 됩니다. 예를 들어, (Int) -> String 타입의 메소드는 ((Int) -> String)? 타입이 됩니다. 함수 타입 전체가 옵셔널로 래핑되는 것이며, 메소드의 리턴값이 옵셔널이 되는 것은 아닙니다.

 

선택적 프로토콜 요구사항은 프로토콜을 따르는 타입에 의해서 구현되지 않았을 수도 있기 때문에 optional chaining과 함께 호출될 수 있습니다. someOptionalMehtod?(someArgument)와 같이 호출될 때 메소드 이름 뒤에 물음표를 사용하여 optional method의 구현을 확인합니다.

Optional Chaining에 대한 내용은 아래 포스팅을 참조해주세요.

[Swift] Optional Chaining

 

아래 예제 코드에서는 정수를 카운팅하는 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을 리턴하도록 fixedIncrement 속성 요구사항만을 구현합니다.

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

프로토콜은 이를 따르는 타입에 메소드, 이니셜라이저, 서브스크립트, computed 속성 구현을 제공하도록 확장할 수 있습니다. 이는 프로토콜 자체에 동작을 정의할 수 있도록 해줍니다.

 

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

프로토콜 익스텐션을 사용하여 프로토콜의 어떤 메소드나 computed 속성 요구사항에 대한 기본 구현을 제공할 수 있습니다. 만약 이 프로토콜을 따르는 타입이 요구되는 메소드나 속성에 대한 자신만의 구현을 제공한다면, 기본 구현 대신에 자신만의 구현이 사용됩니다.

 

예를 들어, 위에서 살펴본 PrettyTextRepresentable 프로토콜에서 요구하는 prettyTextualDescription 속성의 기본 구현을 다음와 같이 제공할 수 있습니다.

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

 

Adding Constraints to Protocol Extensions

프로토콜 익스텐션을 정의할 때, 특정 조건에서만 적용되도록 할 수 있습니다. 위에서 잠깐 살펴봤지만, 이는 제너릭 where clause를 사용하면 됩니다.

 

예를 들어, Collection 프로토콜의 익스텐션을 확장하여 Equatable 프로토콜을 따르는 요소들을 가지는 collection에 적용할 수 있습니다. 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"

 

'프로그래밍 > Swift' 카테고리의 다른 글

[Swift] Error Handling  (0) 2022.03.08
[Swift] Generics (제네릭)  (0) 2022.03.07
[Swift] Protocols (1)  (0) 2022.02.05
[Swift] Extensions  (0) 2022.02.03
[Swift] Type Casting  (0) 2022.01.29

댓글