티스토리 뷰
안녕하세요 :) Zedd입니다.
Swift 5.7에 추가된 Opaque Parameter Declarations을 보기 전에..
Opaque Type이 어떤건지!!!
Opaque Type은 Swift 5.1에 추가되었습니다.
![](https://blog.kakaocdn.net/dn/ThcAO/btqYxqnlSeP/4rYSY2DoLvk2m3GKLVxyCK/img.png)
Opaque Type을 직역하면 불분명한 타입 정도가 되겠네요.
불투명 타입이라고 부르는 사람도 있는데, 저는 불분명한 타입! 요게 더 와닿는것 같아서 ㅎ
# Generic
(갑자기) 우리에게 익숙한 Generic을 보겠습니다.
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
위 Stack 구조체는 Generic Type인 Element를 사용하고있습니다.
우리가 이 코드를 작성할 때 이렇게 생각할 수 있겠죠.
'아 Element로 어떤 타입이 올지 모르겠으니 특정 타입에 국한되게 만드는게 아니라 좀 추상적으로/일반적으로 만들어야겠다! Int든 String이든 내가 만든 Custom Type이든 모든 타입에서 동작할 수 있게!'
핵심은, 이 Stack 구조체를 작성할때 우리는 Element가 어떤 타입인지 알 수 없습니다.
언제 Element의 타입을 알수있냐?
var intStack = Stack<Int>()
사용하는 호출부에서 타입을 지정해줘야 비로소 Element의 타입이 결정되는것이죠.
Int로 지정했으므로, intStack.items의 타입은 [Int]로 나오는 것을 확인할 수 있습니다.
예제는 struct로 들었지만 Generic으로 작성된 함수(메소드)도 다 똑같습니다.
간단히 정리하면,
구현부 - 추상화하여 작성
호출부 - 구체적인 타입 지정 (타입을 알 수 있음)
# Opaque Type
Opaque Type은 some 키워드 + 프로토콜로 사용할 수 있습니다.
struct ContentView: View {
var body: ✅some View✅ {}
}
우리가 SwiftUI에서 주구장창 본 some View. 이것이 Opaque Type입니다.
Opaque Type은
- 함수(메소드)의 리턴타입
- stored property 타입
- computed property 타입
- subscripts
- (Swift 5.7 부터) 함수 파라미터 타입
으로 사용될 수 있습니다.
함수(메소드)의 리턴타입으로 사용된 Opaque Type의 간단한 예를 봅시다.
func makeArray() -> some Collection {
return [1, 2, 3]
}
이런 Opaque Type은 Generic과 반대로 작동하므로 위에서 Generic을 먼저 설명했던것인데요.
어떤 부분이 Generic과 반대인지 살펴봅시다.
위 코드에서는 [1, 2, 3]을 바로 리턴했는데요.
Generic때 처럼 내부가 추상적으로 작성되지 않고 KTX타고 가면서 봐도 구체타입인 [Int]을 리턴하는 것 같습니다.
makeArray함수 내부에서는 [Int]라는 구체타입을 알고있는 것이죠.
makeArray를 호출하는 호출부의 입장을 생각해보겠습니다.
Generic과 다르게 호출부에서 알 수 있는것은 아 Collection 프로토콜을 준수하는 어떤 구체 타입이구나..만 알 수 있습니다.
Generic과 다르게 호출부가 일반적/추상적으로 작성되어야 합니다.
var intStack = Stack<Int>()
이런식으로 호출부가 완전히 구체적인 타입을 알고 있던 Generic과는 완전히 반대되는 경우인데요.
[Generic]
구현부 - 추상화하여 작성
호출부 - 구체적인 타입 지정 (타입을 알 수 있음)
[Opaque Type]
구현부 - 구체적인 타입 지정 (타입을 알 수 있음)
호출부 - 추상화하여 작성
이것이 Opaque Type이 역 제네릭 타입(reverse generic type)으로 불리는 이유입니다.
그리고 이러한 특성때문에 "Opaque Type을 사용하여 리턴 타입의 정보를 숨길 수 있다"고 말하는 것입니다.
말 그대로 불분명한 타입인거죠.
# Protocol Type
Opaque Type은 some 키워드 + 프로토콜이었는데요.
이 some 키워드만 빠지면 우리가 자주 사용하는 Protocol Type입니다.
protocol TestProtocol {}
func somethings() -> some TestProtocol {} // Opaque Type
func somethings() -> TestProtocol {} // Protocol Type
호출부에서 somethings 함수를 호출했을 때,
호출부는 TestProtocol 타입을 준수하는 어떤 타입이구나!로 아는건 똑같아보이는데..
Protocol Type과 Opaque Type은 어떤점이 다를까요?
# Differences Between Protocol Types and Opaque Types
✔️ [Protocol Type을 리턴할 때] ✔️
Protocol Type을 리턴하는 함수 내에서는 조건에 따라 해당 Protocol을 준수하는 어떤 타입이든 리턴할 수 있습니다.
struct 오버워치: TestProtocol {}
struct 롤: TestProtocol {}
func somethings() -> TestProtocol {
let 재밌다 = true
if 재밌다 { return 롤() }
return 오버워치()
}
핵심 : 특정 조건에 따라 리턴하는 구체타입을 달리할 수 있다는 것
✔️ [Opaque Type을 리턴할 때] ✔️
위 코드에서 some 키워드만 추가해주겠습니다.
struct 오버워치: TestProtocol {}
struct 롤: TestProtocol {}
func somethings() -> ✅some✅ TestProtocol {
let 재밌다 = true
if 재밌다 { return 롤() }
return 오버워치()
}
그러면 아래와 같은 컴파일 에러가 발생하는데요.
Function declares an opaque return type, but the return statements in its body do not have matching underlying types
💡 Opaque Type으로 사용하려면 해당 프로토콜을 준수하는 단 하나의 구체타입을 리턴해야만 합니다 💡
func somethings() -> some TestProtocol {
return 롤()
}
이런식으로요.
아래와 같이 정리할 수 있습니다.
Protocol Type - 프로토콜을 준수하는 모든 타입을 참조할 수 있음 (a protocol type can refer to any type that conforms to the protocol)
Opauqe Type - 하나의 특정 구체 타입을 참조하지만 함수 호출자는 어떤 타입인지 볼 수 없음 (An opaque type refers to one specific type, although the caller of the function isn’t able to see which type)
이 둘을 딱 봤을 때 코드를 더 유연하게 작성할 수 있는 타입은 Protocol Type인데요.
struct 오버워치: TestProtocol {}
struct 롤: TestProtocol {}
func somethings() -> TestProtocol {
let 재밌다 = true
if 재밌다 { return 롤() }
return 오버워치()
}
Protocol을 준수하는 어떤 타입이든 특정 조건에 따라 리턴할 수 있게 때문에 유연성이 높은 코드를 작성할 수 있게 됩니다.
다만 이러한 유연성의 대가도 있는데요.
반환된 값에 대해 일부 작업을 수행할 수 없다는 것입니다. 예를 들어봅시다.
이렇게 두고,
struct 오버워치: TestProtocol {}
struct 롤: TestProtocol {}
func somethings() -> TestProtocol {
let 재밌다 = true
if 재밌다 { return 롤() }
return 오버워치()
}
-------
let a = somethings()
let b = somethings()
print(a == b)
a와 b가 같은지 비교해보려고 합니다.
⛔️ 문제 1. == 연산자가 TestProtocol에 포함되어있지 않기 때문에 ==를 추가하려고 시도
protocol TestProtocol: Hashable {}
extension TestProtocol {
static func == (...) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
(Equatable을 사용하지 않은건 hashValue를 사용하려고,..Hashable이 Equatable을 준수하고 있는건 아시죠!?)
⛔️ 문제 2. == 연산자는 좌항과 우항의 타입을 알아야함
보통 이 TestProtocol을 채택하는 구체적인 타입과 일치하는 Self를 파라미터로 사용하게 됩니다.
protocol TestProtocol: Hashable {}
extension TestProtocol {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
⛔️ 문제 3. Protocol 'TestProtocol' can only be used as a generic constraint because it has Self or associated type requirements
func somethings() -> TestProtocol {
let 재밌다 = true
if 재밌다 { return 롤() }
return 오버워치()
}
// Protocol 'TestProtocol' can only be used as a generic constraint because it has Self or associated type requirements
여기서 안본 사람이 없다는 '그 에러'..가 발생하게 됩니다. (물론 Xcode 14에서는 조금 다른 에러가 나오긴 하지만..)
지금은 Self제약 때문에 난 에러긴 하지만, 프로토콜이 Associated type을 가지고 있을때도 같은 에러가 발생합니다.
위에서 언급했지만 ==에서 사용된 Self는 TestProtocol을 채택하는 구체적인 타입을 의미합니다.
근데 컴파일시에는 이 Self가 어떤타입인지 모르기 때문에 에러가 나게 됩니다.
에러메시지에 나와있는 것 처럼,
프로토콜 내에서 Self나 Associated type을 사용할 때 이 프로토콜을 단독으로 리턴타입이나 파라미터타입으로 쓸 수 없고 generic constraint으로만 사용될 수 있습니다.
func somethings<T: TestProtocol>(source: T) -> Int {
return source.hashValue
}
이런식으로요. 리턴타입으로 사용할 수 없으니 구체타입인 Int를 리턴하게 해주었습니다.
let a = somethings(source: 롤())
let b = somethings(source: 오버워치())
print(a == b) // true or false
드디어 컴파일이 되지만.. 결과적으로 TestProtocol (Protocol Type)을 리턴하면서 ==를 사용할 수 있는 방법은 없습니다.
문제는 하나 더 있습니다. 있었습니다..? Swift 5.7에서 아래 문제(문제 4)는 발생하지 않습니다.
⛔️ 문제 4. Protocol 'TestProtocol' as a type cannot conform to the protocol itself
== 연산자 같은 구현은 전부 지우고, somethings 함수를 살짝 수정했습니다.
protocol TestProtocol {}
struct 오버워치: TestProtocol {}
struct 롤: TestProtocol {}
func somethings<T: TestProtocol>(source: T) -> TestProtocol {
return source
}
somethings함수는 TestProtocol을 준수하는 어떤 타입이든 파라미터로 받아서 그대로 리턴합니다.
let a = somethings(source: 롤())
let b = somethings(source: a) // Protocol 'TestProtocol' as a type cannot conform to the protocol itself
그럼 somethings(source: 롤())로 리턴한 a도 역시 TestProtocol을 준수하기 때문에 이 a를 그대로 파라미터로 넘길 수 있는데요.
이러면 컴파일에러가 발생하게 됩니다.
let a = somethings(source: 롤())
let b = somethings(source: 오버워치()) // ✅ OK
let b = somethings(source: a) // ⛔️ Error!
왜 오버워치로 넣은건 됐는데, a를 그대로 넣으면 에러가 나냐?
func somethings<T: TestProtocol>(source: T) -> TestProtocol {
return source
}
Generic은 컴파일시 프로토콜을 준수하는 구체적인 타입이 필요한데, a는 구체타입이 아니라 그냥 TestProtocol타입이기 때문에 컴파일이 되지 않습니다. (컴파일타임에 구체타입을 알 수 없어서)
그래서 generic이 아닌
func somethings(source: TestProtocol) -> TestProtocol {
return source
}
let a = somethings(source: 롤())
let b = somethings(source: a)
이런식으로 사용해야하죠.
⚠️ 문제 4는 Swift 5.7을 사용하는 Xcode 14에서는 에러가 안납니다~
관련 Proposal은 Implicitly Opened Existentials을 참고해주세요.
----
아무튼 이러한 문제가 발생하는 이유는..
Swift의 Protocol은 Type identity를 보존(preserve)하지 않기 때문인데요.
저는 도대체 이 Type Identity가 뭘까...헷갈려서 많이 찾아봤는데, 제가 이해한바를 간단하게 예제로 설명하자면
func somethings() -> TestProtocol {
return 롤()
}
이런식으로 사용했을 때,
somethings를 호출하는 호출부에서는 롤이라는 구체타입에 대한 정보를 전혀 모르고 TestProtocol로만 받게되는데...
즉, 호출되었을 때 롤이라는 구체타입에 대한 정보를 잃어버리기 때문에? Type Identity를 preserve(보존)하지 않는다..라고 말하는것 같아요?
그럼 Opaque Type은 Type Identity를 보존하냐? ➡️ 보존합니다.
그래서
protocol TestProtocol: Hashable {}
extension TestProtocol {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
struct 오버워치: TestProtocol {}
struct 롤: TestProtocol {}
func somethings() -> some TestProtocol {
return 롤()
}
let a = somethings()
let b = somethings()
print(a == b) // true
이것이 가능합니다.
왜냐면 Opaque Type으로 사용하여 타입은 숨겼지만, 컴파일러는 구체타입을 알기 때문에 Type Identity를 preserve(보존)할 수 있습니다.
이건 우리가 계속 봤던 예제라 넣은거고..
let a: some SignedInteger = 1
let b: some SignedInteger = 2
print(a == b) // false
간단하게 이런것이 그냥 가능하다!?
(예제를 함수로만 본 것 같아서.. 위에서 말했듯이 stored property도 Opaque Type으로 선언할 수 있습니다)
이 Opaque Type은 Swift 5.1나왔던 그 순간부터 쓰려고 했던 주제인데..이제야 ^^;
틀린 부분이 있다면 댓글로 알려주세요!
'Swift' 카테고리의 다른 글
[Swift] Escape sequence / Extended String Delimiters (1) | 2023.02.05 |
---|---|
[Swift 5.7] Actor 관련 warning들 원인 파악해보기 (0) | 2023.01.31 |
[Swift 5.7] Type inference from default expressions (0) | 2022.08.05 |
[Swift 5.7] Multi-statement closure type inference (1) | 2022.07.09 |
[Swift 5.7] if let shorthand (1) | 2022.07.09 |
- 피아노
- 스위프트 문법
- Accessibility
- Combine
- actor
- swift3
- iOS delegate
- fastlane
- np-hard
- np-complete
- ios 13
- 제이슨 파싱
- 회고
- github
- UIBezierPath
- Swift
- WidgetKit
- swift sort
- IOS
- swift delegate
- swift 공부
- swift array
- Xcode
- WKWebView
- SwiftUI
- 스위프트
- FLUTTER
- WWDC
- Git
- swift tutorial
- Total
- Today
- Yesterday