原文:Some keyword in Swift: Opaque types explained with code examples
Swift 中的 some 关键字声明了不透明类型,Swift 5.1 引入了它以支持不透明结果类型。许多工程师在编写 SwiftUI 视图的主体时第一次体验使用不透明类型。但是,通常不清楚某些关键字的作用以及何时在其他地方使用它们。
随着 SE-0341 中不透明参数声明的引入,你可以在更多地方开始采用 some 关键字。在本文中,我将解释什么是不透明类型以及何时应该使用它们。
什么是不透明类型?
**不透明类型允许你在不定义具体类型的情况下描述预期的返回类型。**我们今天使用不透明类型的一个常见地方是在 SwiftUI 视图的主体内:
var body: some View { ... }
起初,看起来我们正在返回一个协议类型。不过, some 关键字在这里至关重要,因为它允许编译器访问实际类型信息并执行优化。例如,编译器可以看到以下返回类型:
var body: VStack { ... }
// or:
var body: Text { ... }
整个视图层次结构是不透明的,允许编译器知道返回的视图主体的确切大小。编译器可以优化代码,并且需要更少的堆分配。在不深入细节的情况下,你可以说我们为编译器提供了更多信息,而不仅仅是声明我们希望通过附加 some 关键字返回 View 协议。
不匹配基础类型的不透明返回类型
在编写 SwiftUI 视图时,您可能会遇到以下错误:
Function declares an opaque return type ‘
some View’, but the return statements in its body do not have matching underlying types 函数声明了一个不透明的返回类型 “some View”,但其主体中的 return 语句没有匹配的底层类型
触发错误的代码示例如下所示:
func makeFooterView(isPro: Bool) -> some View {
if isPro {
return Text("Hi there, PRO!") // 返回类型是 Text
} else {
return VStack { // 返回类型是 VStack<TupleView<(Text, Button<Text>)>>
Text("How about becoming PRO?")
Button("Become PRO", action: {
// ..
})
}
}
}
如你所见,我们返回两种类型的视图:当 isPro 返回 true 时返回 VStack,否则返回 Text 视图。
如前所述,编译器希望通过 some 关键字了解底层的具体类型。不透明类型需要在值的范围内固定,所以我们不能在同一个方法范围内返回不同的类型。
我们可以通过使用包装器来解决上述代码,例如 VStack:
func makeFooterView(isPro: Bool) -> some View {
return VStack {
if isPro {
Text("Hi there, PRO!")
} else {
Text("How about becoming PRO?")
Button("Become PRO", action: {
// ..
})
}
}
}
但是,我们现在要添加一个额外的容器,只有在 isPro 返回 true 时才需要。因此,最好使用 @ViewBuilder 属性重写上面的代码,让您可以使用结果构建器:
@ViewBuilder
func makeFooterView(isPro: Bool) -> some View {
if isPro {
Text("Hi there, PRO!")
} else {
VStack {
Text("How about becoming PRO?")
Button("Become PRO", action: {
// ..
})
}
}
}
使用不透明类型隐藏类型信息
some 关键字允许通过提供它支持的协议来描述返回值,从而允许相应地隐藏具体类型。在开发模块时,您可以使用不透明类型来隐藏不想暴露给实现者的具体类型。
例如,如果您提供一个用于获取图像的 package,您可以定义一个图像获取器:
struct RemoteImageFetcher {
// ...
}
您可以通过图像提取器工厂 ImageFetcherFactory 提供图像提取器:
public struct ImageFetcherFactory {
public func imageFetcher(for url: URL) -> RemoteImageFetcher
}
API 在 Swift package 模块中定义,需要 public 关键字才能将 API 公开给实现者。由于我们定义了具体的 RemoteImageFetcher 返回类型,编译器现在要求我们将 RemoteImageFetcher 转换为可公开访问的代码:
如果不使用 some 关键字来使用不透明类型,我们可能需要公开比预期更多的代码。
我们可以通过定义一个公共 ImageFetching 协议并将其用作返回类型来解决这个问题:
public protocol ImageFetching {
func fetchImage() -> UIImage
}
struct RemoteImageFetcher: ImageFetching {
func fetchImage() -> UIImage {
// ...
}
}
public struct ImageFetcherFactory {
public func imageFetcher(for url: URL) -> ImageFetching { // 返回协议类型
// ...
}
}
返回一个没有关联类型(associatedtype)的协议在不使用不透明类型的情况下也能正常工作,但是一旦我们(在协议中)定义了关联类型 Image:
public protocol ImageFetching {
associatedtype Image
func fetchImage() -> Image
}
public struct ImageFetcherFactory {
public func imageFetcher(for url: URL) -> ImageFetching { // 返回包含关联类型的协议
// ...
}
}
我们会遇到以下错误:
Protocol ‘ImageFetching’ can only be used as a generic constraint because it has Self or associated type requirements Protocol ‘ImageFetching’ 只能用作泛型约束,因为它具有 Self 或关联类型要求
解决此类错误需要定义不透明类型。让我们开始吧!
解决协议只能作为泛型约束的错误
在 Swift 中使用协议和关联类型时,经常会遇到以下错误:
Protocol ‘X’ can only be used as a generic constraint because it has Self or associated type requirements 协议 “X” 只能用作泛型约束,因为它具有 Self 或关联类型要求
该协议要求您提供有关泛型约束的信息;如果没有额外的细节,编译器无法解决这些问题。我们允许编译器使用 some 关键字读出附加信息:
public func imageFetcher(for url: URL) -> some ImageFetching { ... }
虽然我们将使编译器能够读出所有必要的信息,但我们仍然可以隐藏实现细节,例如下面使用的具体 RemoteImageFetcher 类型。
使用不透明的返回类型解决了上面示例中的泛型约束,但是在使用协议作为函数参数时我们也可能遇到同样的错误:
public extension UIImageView {
// Protocol 'ImageFetching' can only be used as a generic constraint because it has Self or associated type requirements
func configureImage(with imageFetcher: ImageFetching) {
// Member 'fetchImage' cannot be used on value of protocol type 'ImageFetching'; use a generic constraint instead
image = imageFetcher.fetchImage()
}
}
上述错误仅在 Xcode 13 中抛出。 Xcode 14 带有 Swift 5.7 和一些关于不透明和存在类型的改进。 SE-0341 Opaque Parameter Declarations 是已实施的提案之一,允许在参数声明中使用不透明类型。虽然,编译器会告诉你:
Use of protocol ‘ImageFetching’ as a type must be written ‘any ImageFetching’ 使用协议 ‘ImageFetching’ 作为类型必须写成 ‘any ImageFetching’
我将在稍后的另一篇文章中解释 existential any,但现在,您唯一需要知道的是您可以使用 any 或 some。换句话说,我们可以改变我们的方法如下:
func configureImage(with imageFetcher: some ImageFetching)
使用主要关联类型和使用 some 约束
虽然这解决了与我们的方法定义相关的编译器错误,但我们仍然会遇到以下有关图像获取的错误:
public extension UIImageView {
func configureImage(with imageFetcher: some ImageFetching) {
// Cannot assign value of type '<anonymous>.Image' to type 'UIImage'
image = imageFetcher.fetchImage()
}
}
编译器会建议您强制将值解包到 UIImage,但我们不想冒运行时异常的风险。因此,我们将看看另一个 Swift 5.7 特性,它使用标准库中的 SE-358 主要关联类型实现,允许我们为图像获取器配置主要关联类型:
public protocol ImageFetching<Image> { // ???: 将关联类型设置为泛型
associatedtype Image
func fetchImage() -> Image
}
通过匹配协议名称声明中的关联类型 Image,我们为图像获取协议配置了主要关联类型。 Swift 5.7 还附带了提案 SE-0346 对主要关联类型的轻量级相同类型要求,允许我们现在更新我们的扩展方法以仅约束 UIImage 类型:
public extension UIImageView {
func configureImage(with imageFetcher: some ImageFetching<UIImage>) {
image = imageFetcher.fetchImage()
}
}
所有编译器错误都已解决,我们已经配置了我们的方法,以便编译器知道我们正在处理一些返回 UIImage 的图像获取器。 UIImageView 期望其图像属性具有相同的类型,我们可以相应地配置获取的图像。
通过在上述示例中使用不透明类型,我们消除了公开 public 代码的需要,从而允许我们在内部重构代码而无需发布破坏性更改。在使用内部 API 进行正常开发和提供框架时,获得这种灵活性至关重要。
用 some 替换泛型
**some 关键字也可以作为语法糖来替代泛型,提高可读性。如果一个泛型参数只在一个地方使用,我们可以用不透明的类型来代替它。**
例如,我们可以定义一个自定义打印方法:
func printElement<T: CustomStringConvertible>(_ element: T) {
print(element)
}
泛型参数只在一个地方使用,所以我们可以相应地使用 some 关键字替换它:
func printElement(_ element: some CustomStringConvertible) {
print(element.description)
}
也就是说,你可以使用不透明类型 some Protocol 作为 T where T: Protocol 的简写,提高代码的可读性。
总结
Swift 中的不透明类型可帮助你简化代码并提高可读性。 Swift 5.7 引入了许多改进,使得可以在更多地方受益于 some 关键字。使用主要关联类型和不透明类型约束,我们可以创建功能强大的 API。编译器可以优化代码,同时我们保留隐藏具体类型的能力。
如果您想提高您的 Swift 知识,请查看 Swift 类别页面。如果您有任何其他提示或反馈,请随时与我联系或在 Twitter 上给我发推文。
谢谢!