Swift 中的 some 关键字是什么

43 阅读12分钟

原文:What is the “some” keyword in Swift? – Donny Wals

💡 如果你想返回一个包含关联类型的协议,你需要使用 some 关键字将其声明为不透明返回类型

如果你花了一些时间使用 SwiftUI,或者如果你看了今年关于 SwiftUI 的 WWDC 视频,你可能已经注意到 SwiftUI 中的视图有一个类型为 some View ,名为 body 的属性。some 关键字在 Swift 5.1 中是新的,它是一个叫做**不透明结果类型(SE-0244)**功能的一部分。那么这个 some 关键字是什么呢?如何在你的代码中使用它?

我打算在这篇博文中回答这些问题。我们将首先探讨什么是不透明结果类型,更具体地说,它们解决了什么问题。接下来,我们会看看不透明结果类型是如何在 SwiftUI 中使用的,我们会发现这是否是一个你可能未来会在代码中采用的 Swift 功能。

探索不透明结果类型

为了充分理解不透明结果类型所解决的问题,最好是对泛型有一个扎实的了解。如果你对泛型完全不熟悉,我推荐你阅读我写的这些帖子,让自己尽快掌握:

如果你对泛型的学习不感兴趣,只想了解不透明的结果类型和 some 关键字是什么,那也可以。只是要注意,如果不了解泛型,这篇文章中的一些内容可能会让人困惑。

在 Swift 中,我们可以使用协议来为我们的对象定义接口或契约。当一个东西符合一个协议时,我们就知道它可以做某些事情,或者有某些属性。这意味着你可以写这样的代码:

protocol ListItemDisplayable {
  var name: String { get }
}

struct Shoe: ListItemDisplayable {
  let name: String
}

var listItem: ListItemDisplayable = Shoe(name: "a shoe")

当使用这个 listItem 属性时,只有 ListItemDisplayable 暴露的属性会暴露给我们。当你想拥有一个 ListItemDisplayable 的 Item 数组时,这一点特别有用,因为其中的具体类型可能不仅仅是 Shoe

struct Shoe: ListItemDisplayable {
  let name: String
}

struct Shorts: ListItemDisplayable {
  let name: String
}

var mixedList: [ListItemDisplayable] = [Shoe(name: "a shoe"),
                                        Shorts(name: "a pair of shorts")]

编译器将我们的 ShoeShorts 对象视为 ListItemDisplayable,所以这个列表的用户不会知道他们处理的是鞋子、短裤、牛仔裤还是其他东西。他们只知道数组中的任何东西都可以在列表中显示,因为它符合 ListDisplayable 协议。

具有关联类型的协议的不透明结果类型

上一节展示的灵活性确实很酷,但我们可以进一步推动我们的代码:

protocol ListDataSource {
  associatedtype ListItem: ListItemDisplayable

  var items: [ListItem] { get }
  var numberOfItems: Int { get }
  func itemAt(_ index: Int) -> ListItem
}

上面定义了一个 ListDataSource,它持有一些遵守 ListItemDisplayable 协议的 Item 的列表。我们可以使用遵守这个协议的对象作为列表视图的数据源对象,或者集合视图的数据源对象,这是非常整洁的。

我们可以定义一个视图模型生成器对象,它将根据我们传递给它的项目的种类,生成一个 ListDataSource

struct ShoesDataSource: ListDataSource {
  let items: [Shoe]
  var numberOfItems: Int { items.count }

  func itemAt(_ index: Int) -> Shoe {
    return items[index]
  }
}

struct ViewModelGenerator {
  func listProvider(for items: [Shoe]) -> ListDataSource {
    return ShoesDataSource(items: items)
  }
}

然而,这段代码无法编译,因为 ListDataSource 是一个包含关联类型的协议。我们可以通过指定 ShoesDataSource 作为返回类型而不是 ListDataSource (作为返回类型)来解决这个问题,但这将暴露出一个我们想对 ViewModelGenerator 的使用者隐藏的实现细节。listProvider(for:) 的调用者真正需要知道的是,我们将从这个方法中返回一个 ListDataSource。我们可以将生成器重写如下,以使我们的代码得到编译:

struct ViewModelGenerator {
  func listProvider(for items: [Shoe]) -> some ListDataSource {
    return ShoesDataSource(items: items)
  }
}

💡 如果你想返回一个包含关联类型的协议,你需要使用 some 关键字将其声明为不透明返回类型

通过使用 some 关键字,编译器可以强制执行一些事情,同时向 listProvider(for:) 的调用者隐藏它们:

  • 我们返回遵守 ListDataSource 协议的东西。
  • 返回的对象的关联类型符合 ListDataSource 设置的任何要求。
  • 我们总是从 listProvider(for:) 返回相同的类型。

尤其是最后这一点很有意思。在 Swift 中,我们依靠编译器做大量的编译时类型检查来帮助我们编写(类型)安全和一致的代码。而反过来,编译器也会使用所有这些关于类型的信息来优化我们的代码,以确保它尽可能快地运行。协议对于编译器来说往往是一个问题,因为它们意味着某种动态性,这使得编译器很难在编译时进行某些优化,这意味着我们在运行时将会受到(非常小的)性能消耗,因为运行时需要进行一些类型检查以确保正在发生的事情是有效的

因为 Swift 编译器可以强制执行上面列出的东西,它可以进行与我们使用具体类型时相同的优化,但我们有能力向返回不透明类型的函数或属性的调用者隐藏具体类型。

不透明的结果类型和 Self 要求

由于编译器可以在编译时强制执行类型约束,我们可以做其他有趣的事情。例如,我们可以比较作为不透明类型返回的 Items,而我们不能对协议做同样的事情。让我们看一个简单的例子:

protocol ListItemDisplayable: Equatable {
  var name: String { get }
}

func createAnItem() -> ListItemDisplayable {
  return Shoe(name: "a comparable shoe: \(UUID().uuidString)")
}

上面的内容不能编译,因为 Equatable 有一个 Self 的要求。它想比较 Self 的两个实例,而这两个实例的类型是一样的。这意味着我们不能使用 ListItemDisplayable 作为常规的返回类型,因为一个协议本身没有类型信息。我们在这里需要 some 关键字,这样编译器就会在我们调用 createAnItem() 时找出并执行 ListItemDisplayable 的类型:

func createAnItem() -> some ListItemDisplayable {
  return Shoe(name: "a comparable shoe: \(UUID().uuidString)")
}

编译器现在可以确定我们总是从这个函数中返回 Shoe,这意味着它知道由 createAnItem() 返回的项目的 Self 是什么,这意味着这个项目可以被认为是 Equatable。这意味着现在可以用下面的代码来创建两个项目并进行比较:

let left = createAnItem()
let right = createAnItem()

print(left == right)

这里真正酷的是,左边和右边都隐藏了它们所有的类型信息。如果你调用 createAnItem(),你所知道的就是你得到了一个列表项。并且你可以将这个列表项与同一函数返回的其他列表项进行比较。

作为反向泛型的不透明返回类型

Swift 关于不透明结果类型的文档有时将其称为反向泛型,这是个很好的描述。**在不透明结果类型(面世)以前,使用包含关联类型的协议作为返回类型的唯一方法是将协议放在该方法的泛型约束上。**这里的缺点是,方法的调用者可以决定由函数返回的类型,而不是让函数本身决定:

protocol ListDataSource {
  associatedtype ListItem: ListItemDisplayable

  var items: [ListItem] { getvar numberOfItems: Int { get }
  func itemAt(_ index: Int) -> ListItem

  init(items: [ListItem])
}

// 示例一:将 ListDataSource 协议作为泛型约束条件
func createViewModel<T: ListDataSource>(for list: [T.ListItem]) -> T {
  return T.init(items: list)
}

// 示例二:将 ListDataSource 作为不透明返回类型
func createOpaqueViewModel<T: ListItemDisplayable>(for list: [T]) -> some ListDataSource {
  return GenericViewModel<T>(items: list)
}

let shoes: GenericViewModel<Shoe> = createViewModel(for: shoeList)
let opaqueShoes = createOpaqueViewModel(for: shoeList)

上面代码中的两个方法都返回完全相同的 GenericViewModel。这里的主要区别是:

在第一种情况下,调用者决定它想为它的鞋子列表拥有一个 GenericViewModel<Shoe>,它将得到一个 GenericViewModel<Shoe> 的具体类型。

在使用 some 的例子中,调用者只知道它将得到一些 ListDataSource,用来保存它的 ListItemDisplayable 项目的列表。这意味着 createOpaqueViewModel 的实现现在可以决定它要做什么。

在这种情况下,我们可以选择返回一个泛型的视图模型。我们也可以选择返回不同类型的视图模型,重要的是我们总是从函数体中返回相同的类型,并且返回的对象符合 ListDataSource

在你的项目中使用不透明返回类型

当我在研究不透明返回类型并试图为这篇文章想出一些例子时,我注意到要想出在普通项目中使用不透明返回类型的理由其实并不容易。在 SwiftUI 中,它们起到了关键作用,这可能会让你相信,不透明返回类型在某个时候会在很多项目中变得很普遍。

我个人认为情况不会是这样的。不透明返回类型是对一个非常特殊问题的解决方案,而我们大多数人并不从事这个领域的工作。**如果你正在构建框架或高度可重用的代码,并在许多项目和代码库中工作,不透明返回类型会让你感兴趣。**你很可能想在协议的基础上写出灵活的代码,其中包含关联类型,你作为框架的构建者,可以完全控制返回的具体类型,而不需要向调用者暴露任何泛型类型。

不透明返回类型的另一个考虑因素可能是它们的运行时性能。正如前面所讨论的,协议有时会迫使编译器将某些检查和查找工作推迟到运行时进行,这将带来性能上的损失。不透明返回类型可以帮助编译器进行编译时的优化,这真的很酷,但我相信这对大多数应用来说并不重要。除非你写的代码真的必须被优化到核心部分,否则我不认为运行时的性能损失足以让你在代码库中抛出不透明结果类型。当然,除非它对你有很大的意义。或者,如果你确定在你的案例中,性能的好处是值得的。

我在这里真正想说的是,作为返回类型的协议对性能来说并不突然可怕。事实上,它们有时是达到你所需要的灵活性水平的唯一途径。例如,如果你需要从你的函数中返回一个以上的具体类型,这取决于某些参数。你无法用不透明返回类型来做到这一点。

这给我带来了一个可能是最不有趣但又最简单的方法,即在你的代码中开始使用不透明返回类型。如果你在代码中有些地方指定了一个协议作为返回类型,但你知道你从该函数中只返回一种具体类型,那么使用不透明返回类型是有意义的。事实上,Swift 团队正在考虑在 Swift 6.0 中,只要你使用协议作为一个类型,就会推断出 some。这可能永远不会进入 Swift 6.0,但它确实表明 Swift 团队很重视 some 是一个好的默认值,只要你能尝试。

使用 some 的一个更有趣的考虑是在你已经定义了一个单一用途的泛型的地方。例如,在下面的情况下,你可以使用 some 来代替泛型:

class MusicPlayer {
  func play<Playlist: Collection<Track>>(_ playlist: Playlist) { /* ... */ }
}

在这个例子中,我们的 play 函数有一个泛型参数 Playlist,它被约束为一个持有 Track 对象的 Collection。我们能写出这种约束,要感谢 Swift 5.7 的主要关联类型。在这篇文章中了解更多关于主要关联类型的信息。如果我们只在一个地方使用 Playlist 泛型,比如一个函数参数,那么从 Swift 5.7 开始,我们可以使用 some 来代替泛型。Swift 5.7 允许我们在函数参数中使用 some,这是一个巨大的进步:

class MusicPlayer {
  func play(_ playlist: some Collection<Track>) { /* ... */ }
}

好多了,不是吗?

综上所术

在这篇文章中,你看到了不透明返回类型解决了什么问题,以及通过向你展示几个例子,它们可以被如何使用。你了解到,如果你想返回一个遵守(包含关联类型的)协议的对象,不透明返回类型可以作为一种返回类型。这是因为编译器在编译时进行了几次检查,以弄清一个协议的关联类型的真实类型。你还看到,出于类似的原因,不透明返回类型有助于解决所谓的 Self 要求。接下来,你看到了不透明结果类型在某些情况下是如何充当反向泛型的,它允许方法的实现者确定符合协议的返回类型,而不是让方法的调用者决定。

接下来,我对不透明结果类型在你的应用程序中可能会出现的情况给出了一些见解。随着 Swift 5.7 能够在更多的地方使用 some,而不仅仅是返回类型,我认为 some 将成为一个非常有用的工具,它将帮助我们在很多地方使用具体性类型而不是存在性(协议),这应该使我们的代码更加高性能和健壮。

如果你有任何问题、反馈,或者你有我在这篇文章中没有涉及到的不透明返回类型的出色应用,我很想在 Twitter 上听到你的意见。