원문: https://www.avanderlee.com/swift/existential-any/
Existential any in Swift explained with code examples
Existentials in Swift are defined using the any keyword and provide dynamic types, but also come with performance impact you should know.
www.avanderlee.com
Existential any를 사용하면 type 앞에 any 키워드를 붙여 existential types를 정의할 수 있다. 간단히 말해 existential는 “any type이지만 protocol ’something’을 준수한다.”를 의미한다.
이 글에서는 existential types이 무엇인지, 언제 사용하는지, 성능에 어떻게 영향을 미치는지 설명한다.
What does Existential any mean?
protocol 앞에 any 키워드를 사용해서 특정 protocol의 existential type을 정의한다. 예를 들어 Content Protocol을 준수하는 모든 any type의 변수를 정의해보면,
protocol Content {
var id: UUID { get }
var url: URL { get }
}
struct ImageContent: Content {
let id = UUID()
let url: URL
}
let content: any Content = ImageContent(...)
SE-335는 existential any를 도입했고, Swift 5.6은 처음으로 이를 이용할 수 있게 했다. Swift 5.7은 existential을 강화해서, associated types과 같은 결합된(Combined) existentials를 사용할 수 있게 해준다. 예를 들어, Content protocol을 재정의해서 UUID type의 제약 조건이 있는 Identifiable protocol을 상속하고, 그에 따라 existential로 사용할 수 있다.
protocol Content: Identifiable where ID == UUID {
var url: URL { get }
}
struct ImageContent: Content {
let id = UUID()
let url: URL
}
let content: any Content = ImageContent(...)
Constrained existentials
SE-346의 Primary Associated Types의 도입으로, 제한된 existentials를 정의할 수 있다. 이걸 더 설명하기 위해, Primary associated type “Iamge”와 “ImageFetching” protocol을 설명하려 한다.
protocol ImageFetching<Image> {
associatedtype Image
func fetchImage() -> Image
}
protocol 정의의 generic parameter는 primary가 되어야 하는 associated type의 이름과 일치하는 primary associated type을 정의한다. 이 경우 “Image”를 primary associated type으로 정의했다.
코드의 제약 조건으로 primary associated type을 사용할 수 있다. 예를 들어, “UIImageView”에서 extension을 정의해서, image fetch를 사용해 image를 구성하는 것:
extension UIImageView {
func configureImage(with imageFetcher: any ImageFetching<UIImage>) {
image = imageFetcher.fetchImage()
}
}
“UIImageView”에는 “UIImage” type이 필요하므로, parameter type을 ImageFetching으로 제한했지만, associated type인 “UIImage”를 가지도록 했다. 즉, “IamgeFetching” protocol로 제한했지만, associated type으로 “UIImage”를 정의해야 한다.
참고: 위의 예에서는 some을 사용하는 것이 좋음.
When to use Existentials
이전의 예에서 이미 existentials를 사용해야하는 때에 대해 이해했고, existential any를 사용해야 한다면 다음을 따르면 좋을 것.
- concrete types부터 시작하는 것을 먼저 고려하고, 처음부터 복잡하게 만들지 마라
- type flexibility가 더 필요한 경우 some 키워드를 사용해서 opaque types로 이동해라
- 임의의(random) 값의 저장이 필요한 경우, some을 any로 변경해라.
위 단계를 더 설명하기 위해 image fetching에 대한 이야기를 계속하겠다. 예를 들어, remote image fetcher를 만들 수 있다:
public struct RemoteImageFetcher: ImageFetching {
let url: URL
public func fetchImage() -> UIImage {
// ..
}
}
다음은 image fetching factory에서 지정된 URL에 대한 image fetcher를 반환하는 작업이다:
public struct ImageFetcherFactory {
public static func imageFetcher(for url: URL) -> RemoteImageFetcher {
RemoteImageFetcher(url: url)
}
}
아직 dynamic으로 만들 필요가 없으므로, concrete “RemoteImageFetcher” type으로 시작하는 것이 가장 쉬운 방법이다.
The value of existentials for framework development
그러나 외부에서 사용하는 3rd party library나 SDK를 개발하는 경우, implementors는 변경 사항을 방지하기 위해 필요한 최소 API만 노출하려 한다. 예를 들어, “RemoteImageFetcher” type을 노출하거나, “ExternalImageFetcher”의 이름을 변경할때, implementors가 그에 따라 구현을 업데이트 해야 한다.
대신, 2단계에 따라 코드를 다시 작성하면 some 키워드를 사용할 수 있다.
struct RemoteImageFetcher: ImageFetching {
let url: URL
func fetchImage() -> UIImage {
return UIImage()
}
}
public struct ImageFetcherFactory {
public static func imageFetcher(for url: URL) -> some ImageFetching {
RemoteImageFetcher(url: url)
}
}
보다시피 더 이상 “RemoteImageFetcher” type은 publically visible이므로 정의할 필요가 없다. 대신, API 사용자가 정환한 concrete type을 알 필요 없이 “some ImageFetching”을 반환한다.
마지막으로, local image를 다룰 수 있는 다른 image fetcher를 소개할 수 있다:
struct LocalImageFetcher: ImageFetching {
let url: URL
func fetchImage() -> UIImage {
return UIImage()
}
}
factory method를 업데이트하면 다음과 같다.
public struct ImageFetcherFactory {
public static func imageFetcher(for url: URL) -> some ImageFetching {
if url.isFileURL {
return LocalImageFetcher(url: url)
} else {
return RemoteImageFetcher(url: url)
}
}
}
그럼 다음과 같은 오류가 발생한다.
Function declares an opaque return type ‘some ImageFetching’, but the return statements in its body do not have matching underlying types
opaque type은 사용되는 범위 내에서 concrete type이여야 한다. “LocalImageFetcher” 나 “RemoteImageFetcher”를 반환하기 때문에 컴파일러는 더 이상 outcome type을 정적으로 결정할 수 없다. 이 경우 existential로 return type을 정의해야 한다.
public struct ImageFetcherFactory {
public static func imageFetcher(for url: URL) -> any ImageFetching {
if url.isFileURL {
return LocalImageFetcher(url: url)
} else {
return RemoteImageFetcher(url: url)
}
}
}
즉, 다음과 같다.
ImageFetching protocol에 따라 any type을 반환해야 한다.
컴파일러가 반환된 유형을 정적으로 예측할 수 없기 때문에, 성능에 영향을 미칠 수 있다.
The impact of existentials on performance
SE-335 proposal에서 인용한 바와 같이
이 제한은 any로 그러한 유형에 any 주석을 달음으로써 언어에서 existential type의 영향을 명시적으로 만든다. his proposal makes the impact of existential types explicit in the language by annotating such types with any.
즉, type을 existential로 정의할 때 더 잘 알 수 있다. 그 이유는 existentials가 성능에 미치는 영향과 관련이 있다. protocol에 맞는 값을 저장할 수 있고, 동적으로 변경이 가능하기 때문에 concrete type 보다 더 비싸다. 이를 설명하기 위해 다음 코드 예제를 보자.
var anyContent: any Content = ImageContent(…)
anyContent = VideoContent(…)
위의 코드 예제는 성공적으로 컴파일되고 “anyContent”가 동적으로 변경될 수 있음을 보여준다. 이 때문에 existential types는 dynamic memory가 필요하다. 또한, pointer indirection을 초래하고, dynamic method dispatch 때문에 최적화할 수 없다. 이것이 무엇을 의미하는지 자세히 설명하지 않고, 우리는 any를 사용하지 않는 것이 더 낫다는 결론을 내릴 수 있다.
var someContent: some Content = ImageContent(…)
someContent = VideoContent(…)
컴파일러는 some 키워드를 사용해서 dynamic type이 변경 불가능함을 나타낸다.
Cannot assign value of type ‘VideoContent’ to type ‘some Content’
Enforced starting from Swift 6
기존의 existentials의 성능이 어떠한 영향을 미치는지 개발자로 하여금 너무 숨겨져 있었기 때문에, Swift 팀은 any 키워드를 도입하기로 했다. 이는 Swift 6 부터 강제로 변경해야 한다.
Conclustion
Swift의 Existentials 사용하면 특정 protocol에 맞는 dynamic value를 정의할 수 있다. primary associated types을 사용하여 existentials을 특정 경계로 제한할 수 있다. Swift 팀은 개발자들이 보이지 않을 수도 있는 성능 영향을 명시적으로 수용할 수 있도록 any 키워드를 도입했다.
'Swift > 학습' 카테고리의 다른 글
KeyPath (0) | 2023.06.13 |
---|---|
2022 WWDC Design protocol interfaces in Swift (0) | 2023.05.03 |
Swift Opaque Types(번역) (0) | 2023.03.28 |
enum으로 특정 단위를 명확하게 표현하기 (0) | 2022.09.25 |
2022 WWDC Embrace Swift Generics (1) | 2022.07.04 |