Protocols 01

오랜만에 돌아온 iOS Doc 훑어보기! 이번엔 Protocol을 정리해보자. Swift 문서는 여기!

  • 프로토콜은 method, property, other requirements의 정의를 담아놓은 그릇.
  • class, struct, enum에 채택(adopted)되어 채택한 곳에서 구현하게 된다.
  • 프로토콜이 요구하는 것을 모두 정의하면 프로토콜을 conform 했다고 한다.
  • 프로토콜의 일부만을 구현할 수도 있고(선택 옵션), protocol을 확장해서도 사용할 수 있다.

Protocol Syntax

프로토콜 정의 및 사용

protocol SomeProtocol {
   // protocol definition goes here
}

struct SomeStructure: FirstProtocol, SecondProtocol {
   // Structure 구현
}

class SomeClass: SomeSuperClass, FirstProtocol, SecondProtocol {
   // Class 구현
}
  • 프로토콜을 채택할 때는 : 프로토콜 과 같이 나열해서 채택한다.
    • 프로토콜은 여러개 채택할 수 있다.
  • 만약 클래스가 상속 관계인 경우 Super Class 뒤에 프로토콜을 추가한다.

Property Requirements

프로퍼티 속성 표기

protocol SomeProtocol {
   var mustBeSettable: Int { get set }
   var doesNotNeedToBeSettable: Int { get } // read-only
   static var someTypeProperty: Int { get set }
}
  • 프로토콜에서는 프로토콜을 채택하는 곳에서 구현해야 할 프로퍼티를 추가할 수 있다.
    • stored property인지 computed property인지를 지정하지는 않는다. 오로지 속성 이름과 유형만 지정!
    • gettable과 settable을 지정하기 위해 get, set을 붙여줘야 한다.
  • prefix type property requirements: static 키워드를 앞에 붙인다.

예시


FullyNamed {
   var fullName: String { get }
}

   
struct Person: FullyNamed {
   var fullName: String
}

let john = Person(fullName: "John Appleseed")
// john.fullName : "John Appleseed"
  • FullyNamed라는 프로토콜은 채택하면 fullName을 구현해야 한다.
    • Person이라는 struct는 FullNamed 프로토콜을 채택했고 이를 준수하기 위해 var fullName: String 프로퍼티를 선언헀다. Protocol에서 요구했던 String 타입의 gettable 프로퍼티로 프로토콜을 올바르게 준수했다.
      • 만약 프로토콜 요구사항을 준수하지 못한 경우 컴파일 에러!
class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"
  • 위 예시와 다르게 class에서는 fullName을 computed property로 선언했다.
    • Protocol은 type과 gettable, settable만 원하는 모양이라면 computed property든 stored property든 상관 없다! 여기서는 gettable 하기만 하면 되기 때문에 computed propety로 선언.

Method Requirements

표기

protocol SomeProtocol {
    static func someTypeMethod()    // property와 마찬가지로 static하게 쓰고 싶다면 static 키워드를 붙여주면 된다.
}
  • Protocol은 instance method나 type method 또한 구현을 요구할 수 있다.
  • 메서드 명 + 반환형으로 일반 메서드를 작성하는 것과 동일하지만 구현부가 없다. (구현은 protocol을 채택한 곳에서 하기 때문에!)
  • 메서드 parameter에 기본값을 지정하는 것은 안된다. (func someTypeMethod(someValue: Int = 0)은 안됨)

예시

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
    }
}

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"
  • 프로토콜을 준수하기 위해 LinearCongruentialGenerator는 random 메서드를 구현해두었다.
    • 하지만 프로토콜에서 랜덤 값을 리턴하는 방식은 정해주지 않았기 때문에 구현부는 작성자의 마음대로(?) 만들 수 있다.

Mutating Method Requirements

표기

protocol Togglable {
    mutating func toggle()
}
  • 프로토콜에서는 mutating func도 정의할 수 있다.
    • 단 mutating 키워드는 struct 또는 enum에서만 사용되기 때문에 class가 채택해야 하는 프로토콜에서는 사용할 수 없다.

예시

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}

var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on
  • enum OnOffSwitch 에서 toggle을 구현해 Togglable을 준수했다. 내부에서 자기 자신에 대한 값을 변경하기 때문에 mutating 사용 적절.

Initializer Requirements

프로토콜은 특정 init을 구현하는 것을 요구할 수 있다.

Class Implementations

protocol SomeProtocol {
    init(someParameter: Int)
}

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // initializer implementation goes here
    }
}

class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // initializer implementation goes here
    }
}
  • protocol에서 구현하도록 요구한 init 메서드를 작성할 때는 designated init 또는 convenience init으로 구현할 수 있다.
    • 두 경우 모두 required modifier를 붙여주어야 한다.
    • The use of the required modifier ensures that you provide an explicit or inherited implementation of the initializer requirement on all subclasses of the conforming class, such that they also conform to the protocol.
  • 만약 subclass가 superclass의 disignated initializer를 오버라이드 해야 하면서 그 init이 프로토콜을 준수하기 위해 구현해야 하는 init과 일치하는 경우에는 required와 override를 모두 붙여준다.
    • SomeSuperClass의 init을 오버라이드 하면서 SomeProtocol의 init 메서드를 구현해야 하므로 required override init() 으로 작성했다.

You don’t need to mark protocol initializer implementations with the required modifier on classes that are marked with the final modifier, because final classes can’t subclassed.

Failable Initializer Requirements

  • Failable init도 프로토콜에 정의될 수 있다.
    • nonfailable init requirement는 nonfailable init 또는 implicitly unwrapped failable init으로 구현될 수 있다.

Protocol as Types

  • 프로토콜을 Type으로 사용하는 것을 existential type 이라고 하는데, 이는 “there exists a type T such that T conforms to the protocol”이라는 문구에서 유래한다.
  • 프로토콜을 사용할 수 있는 다른 타입들
    • function, method, init의 parameter type 또는 return 타입
    • constant, variable, property의 타입
    • array dictionary, other container의 타입

프로토콜이 type이기 때문에 프로토콜의 이름의 첫 글자를 대문자로 한다. Int나 String 처럼!

예시

class Dice {
    let sides: Int
    let generator: RandomNumberGenerator
    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4
  • 위에서 봤던 RandomNumberGenerator 프로토콜을 generator의 타입으로 선언했다.
    • 추후 protocol 타입에서 protocol을 채택한 클래스 타입으로 다운캐스팅 할 수 있다.
  • 또 init의 generator parameter도 RandomNumberGenerator로 선언했다.
    • generator가 RandomNumberGenerator를 준수하는 무언가임을 알기 때문에 우리는 init 메서드 내에서 random()을 호출할 수 있다.

Delegation

  • iOS 개발할 때 많이 쓰던 delegate pattern 걔 맞다..!!
    • 내가 할 일 중 일부를 다른 유형의 인스턴스가 처리할 수 있도록 해주는 걔!!
    • 다른 유형의 인스턴스에서 내가 할 일을 적어둔 목록(protocol)을 채택해 필요한 메서드를 구현하므로 내 코드를 알 필요 없다.

Categories:

Updated:

Leave a comment