https://www.youtube.com/watch?v=SPhATsEQR74
Opaque Type vs Protocol & Associated Type & Generics
Generics
- 어떤 타입이더라도 적용 가능한 유연성을 보장하는 방법
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let tempA = a
a = b
b = tempA
}
- 들어오는 데이터의 타입을 모르더라도 받아들일 수 있음
- 특정 프로토콜을 따르는 데이터 타입만을 받아들일 수도 있음(
Decodable
등을 따르는 데이터 타입만을 파라미터로 받아들여 디코딩한 결과를 리턴하는 게 대표적인 예시)
class someClass { ... }
protocol someProtocol { ... }
func someFunction<T: someClass, U: someProtocol>(someT: T, someU: U) {
}
- 스위프트가 제공하는 거의 모든 유용한 공통 오퍼레이터가 제네릭을 통해 구현
public init<H>(_ base: H) where H: Hashable
- 위와 같이
Hashable
프로토콜을 따르는 타입을 통해 이니셜라이즈한다는 뜻
Associated Types
- 프로토콜을 보다 유연하게 사용하고자 할 때 사용 가능
protocol someProtocol {
associatedtype SomePlaceholder
func someFunctionUsing(type: SomePlaceholder)
func someOtherFunctionReturning() -> SomePlaceholder
}
- 특정 프로토콜 내부에서
associatedtype
을 통해 연관 타입을 선언한다면, 해당 프로토콜을 따르는 구조체/클래스에서 해당 타입을 명확하게 지정 가능
- 현 시점에서는 불분명한 타입에 지나지 않음
struct someStruct: someProtocol {
typealias SomePlaceholder = Int
func someOtherFunctionReturning() -> Int {
}
func someFunctionUsing(type: Int) { }
}
typealias
를 통해 해당 프로토콜이 지정한 associatedtype
을 구체적으로 지정
- 해당 타입을 리턴/파라미터로 사용하는 함수에 현 시점의 구체적인 타입을 그대로 적용 가능
protocol stackable {
associatedtype stackType
var stack: [stackType] { get set }
func pop() -> stackType
func push(_ item: stackType)
}
struct stringStack: stackable {
var stack: [String]
func pop() -> String {
}
func push(_ item: String) {
}
typealias stackType = String
}
- 연관 타입을 해당 프로토콜을 따르는 구체적인 구조체/클래스에서
typealisas
를 통해 직접 설정 가능하다는 게 핵심
protocol Constainer {
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}
- 컨테이너 프로토콜은
Equatable
을 따르는 타입을 연관 타입으로 받아, mutating
, subscript
등 특정 함수를 내재하는 프로토콜
Opaque + associatedtype
- 프로토콜이 사용한 연관타입은 오파크와 함께 사용할 시 큰 시너지
enum CardType {
case gold
case platinum
}
protocol CardNumberProtocol { }
extension String: CardNumberProtocol { }
protocol Card {
associatedtype CardNumber: CardNumberProtocol
var type: CardType { get set }
var limit: Int { get set }
var number: CardNumber { get set }
func validateCardNumber(number: CardNumber)
}
- 카드 프로토콜의 연관 타입으로 선언된
CardNumber
는 위의 CardNumberProtocol
을 준수하는 타입임
- 타입, 리미트, 넘버 등 프로토콜 내 변수 및
validateCardNumber
와 같은 연관 타입을 인자로 쓰는 함수 역시 존재
struct VisaCard: Card {
typealias CardNumber = String
var type: CardType = .gold
var limit: Int = 100_000
var number: String = "1111 2222 3333 4444"
func validateCardNumber(number: String) {
}
}
struct MasterCard: Card {
typealias CardNumber = String
var type: CardType = .platinum
var limit: Int = 500_000
var number: String = "1111 2222 3333 4444"
func validateCardNumber(number: String) {
}
}
- 위의 프로토콜을 따르는 구체적인 구조체
- 연관타입이 각 구조체 내부에서
typealias
로 선언되어 구체화되어 있음
func getLoadEligibility() -> Bool {
getUserCard().limit >= getLoanEligibilityCard().limit
}
func getUserCard() -> some Card {
MasterCard()
}
func getLoanEligibilityCard() -> some Card {
VisaCard()
}
- 함수의 리턴 타입으로 카드 프로토콜 자체를 리턴하려면 컴파일러가 연관타입으로 감춰놓은 실제 타입을 알지 못하기 때문에 컴파일러 에러가 발생
some
이라는 오파크 타입 키워드를 통해 해당 타입을 '감싸'줄 때 비로소 리턴할 수 있는데, 실제로 제네릭의 완전히 반대 방향으로 작동
- 실제 리턴하는 구체적인 실제 타입을
Card
로는 알아차리지 못하기 때문에(연관 타입의 실제 표현은 각 구조체 내부에서 하도록 설정되었음), some
키워드를 통해 어떤 타입인지는 모르겠으나 해당 프로토콜을 따르는 구조체라서 인지 가능
- SwiftUI 프레임워크의 모든 UI를 그리는 구조체가
some View
를 따르는 연산 프로퍼티인데, 이 또한 같은 맥락