Swift 内置类型的泛型介绍

146 阅读9分钟

原文:An introduction to generics in Swift using its built-in types – Donny Wals

每当我们写代码时,我们都希望我们的代码是精心设计的。我们希望它是灵活、优雅和安全的。我们希望确保 Swift 的类型系统和编译器尽可能多地抓住我们的错误。特别有趣的是,Swift 的类型系统可以帮助我们避免明显的错误。例如,Swift 不允许你把一个 Int 类型值赋给一个 String 属性。

Swift 的类型安全机制可以防止你写出类型不匹配的代码!

var anInt = 10
anInt = "Hello, World!" // Error!

Swift 编译器会向你显示一个错误,解释说你不能把一个 String 类型值赋给一个 Int,你就会明白这一点。如果某样东西被声明或推断为某种类型,它就不能持有除它开始时的类型以外的任何类型。

不过,这并不总是那么简单。有时你需要写代码,你真的不知道你在处理什么类型。你所知道的是,你要确保无论发生什么,该类型都不能改变。如果这对你来说听起来并不熟悉,或者你想知道谁在他们正确的头脑中会想要这样,请继续阅读,这篇文章是为你准备的。

逆向数组

一个需要我前面描述的灵活性的对象的一个很好的例子是数组。考虑到 Swift 中的数组是为了保存特定类型的对象而创建的,无论是具体类型还是协议,数组与我之前给你展示的错误并没有什么不同。让我们把这个例子改编成数组,这样你就能明白我的意思了:

var arrayOfInt = [1, 2, 3]
arrayOfInt = ["one", "two", "three"]

如果你试图运行这段代码,你会看到一个错误,说明你不能把 Array<String> 的对象赋值给 Array<Int>。而这正是你需要泛型的那种神奇之处。

数组是以这样的方式创建的,它们可以与你扔给它们的任何类型一起工作。唯一的条件是,数组是同质的,换句话说,它只能包含单一类型的对象。

那么这在 Swift 中是如何定义的呢?像数组这样的泛型对象是什么样子的?我不会向你展示 Swift 标准库中的准确实现,而是向你展示它的简化版本:

public struct Array<Element> {
  // complicated code that we don’t care about right now
}

这里有趣的部分是在尖括号之间的:<Element>在 Swift 标准库中不存在 Element 这个类型。它是一个捏造的类型,只存在于数组的上下文中。它指定了一个占位符,用于真正的、具体的类型通常会被使用的地方。

让我们在 Array 周围建立一个小的包装器,这将帮助你更多地了解这个问题:

struct WrappedArray<OurElement> {
  private var array = Array<OurElement>()

  mutating func append(_ item: OurElement) {
    array.append(item)
  }

  func get(atIndex index: Int) -> OurElement {
    return array[index]
  }
}

请注意,我们不使用 Element,而是使用 OurElement 这个名字。这只是为了证明 Element 在 Swift 中真的不存在。在这个结构体中,我们创建了一个数组。我们通过使用它的完整泛型语法 Array<OurElement>() 来做到这一点。使用下面的符号也可以做到这一点 [OurElement]()。其结果是一样的。

接下来,在 appendget 方法中,我们分别接受和返回 OurElement我们不知道 OurElement 会是什么。我们所知道的是,我们数组中的元素,我们追加到数组中的元素和我们从数组中获取的元素,都将具有相同的类型。

要使用你的简单数组包装器,你可能会写这样的东西:

var myWrappedArray = WrappedArray<String>
myWrappedArray.append("Hello")
myWrappedArray.append("World")
let hello = myWrappedArray.get(atIndex: 0) // "Hello"
let world = myWrappedArray.get(atIndex: 1) // "World"

这很好,对吗?试着给 myWrappedArray 添加一个 Int,或者其他东西。Swift 不会让你这么做,因为你通过在尖括号之间放置 String 来指定 OurElement 只能是 String 类型,而不是 myWrappedArray

你可以通过在角括号之间放置不同的类型来创建容纳其他类型的 WrappedArray。你甚至可以使用协议来代替具体类型。

var codableArray = WrappedArray<Codable>

以上将允许你在 codableArray 中添加各种 Codable 对象。注意,如果你试图用 get 从数组中检索它们,你会得到一个 Codable 对象,而不是你可能期望的符合要求的类型。

var codableArray = WrappedArray<Codable>

let somethingCodable = Person()
codableArray.append(somethingCodable)
let item = codableArray.get(0) // item is Codable, not Person

原因是 get 返回 OurElement,而你指定 OurElementCodable 的。

与数组类似,Swift 有 Set(Set<Element>)、Dictionary(Dictionary<Key, Value>)和许多其他的泛型对象。请记住,只要你看到尖括号之间的东西,它就是一个泛型类型,而不是一个真正的(具体的)类型。

在我们看一个你有朝一日可能会在自己的代码中使用的泛型的例子之前,我想告诉你,函数也可以指定自己的泛型参数。这方面的一个很好的例子是 JSONDecoder 上的解码方法:

func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
  // decoding logic
}

如果你没有使用过这个方法,它通常会像这样被调用:

let result = try? decoder.decode(SomeType.self, from: data) // result is SomeType

让我们把解码的方法签名拆开:

  • func decode<T>decode方法指定它使用一个叫做 T 的泛型对象。同样,T 只是一个占位符,代表真正的类型,就像前面例子中的 ElementOurElement 那样。
  • (_ type: T.Type, from data: Data) :这里有一个参数是 T.Type。这意味着我们调用这个方法时必须指定我们要将数据解码成的类型。在这个例子中,我使用了 SomeType.self。当你用 SomeType.self 调用该方法而不明确指定 T 时,Swift 可以推断出 T 现在将是 SomeType
  • throws -> T:这个位标志着 decode 可抛出异常,它指定 decode 将返回 T。在这个例子中,T 被推断为 SomeType
  • where T : Decodabledecode 方法签名的最后一点对 T 进行了约束。我们可以让 T 成为我们想要的任何东西,只要该对象遵守 Decodable 协议。所以在我们的例子中,只有当 SomeTypeDecodable 的时候,我们才允许使用 SomeType 作为 T 的类型。

再看一下解码的方法签名,让它沉淀一会儿。我们马上要建立自己的结构,把所有的东西放在一起,所以如果还不明白,我希望在下一节后能明白。

在你的代码中应用泛型

你已经看到 ArrayJSONDecoder.decode 如何使用泛型。让我们用一个我多年来多次遇到的例子来构建一个相对简单的东西,应用你新发现的逻辑。

想象一下,你正在构建一个在列表视图中显示项目的应用程序。因为你喜欢抽象事物和分离关注点,所以你采取了一些 UITableViewDataSource 逻辑,并将其分割成一个 ViewModel 和数据源逻辑本身。是的,我说的是 ViewModel,不,我们不打算讨论架构问题。ViewModel 只是目前练习用泛型构建东西的一个好方法。

在你的应用程序中,你可能有几个列表,它们以类似的方式暴露它们的数据,经过大量的简化,你的代码可能看起来像这样。

struct ProductsViewModel {
  private var items: [Products]
  var numberOfItems: Int { items.count }

  func item(at indexPath: IndexPath) -> Products {
    return items[indexPath.row]
  }
}

struct FavoritesViewModel {
  private var items: [Favorite]
  var numberOfItems: Int { items.count }

  func item(at indexPath: IndexPath) -> Favorite {
    return items[indexPath.row]
  }
}

这段代码真的很重复,不是吗?两个视图模型都有类似的属性和方法名称,唯一真正的区别是它们操作的对象的类型。回头看看我们的 WrappedArray 例子。你能想出如何使用泛型来使这些视图模型不那么重复吗?

如果没有,那也没关系。解决办法是这样的:

struct ListViewModel<Item> {
  private var items: [Item]
  var numberOfItems: Int { items.count }

  func item(at indexPath: IndexPath) -> Item {
    return item[indexPath.row]
  }
}

很好,对吧!相比于下面的代码:

let viewModel = FavoritesViewModel()

你现在可以写:

let viewModel = ListViewModel<Favorite>()

你的代码中的变化是最小的,但你已经删除了重复代码,这是非常好的!更少的代码重复意味着更少的着陆区域可供那些讨厌的 Bug 登陆。

这种方法的一个缺点是,你现在可以使用任何类型的对象作为 Item,而不仅仅是 FavoriteProduct。让我们通过引入一个简单的协议和约束 ListViewModel 来解决这个问题,这样它只接受有效的列表项作为 Item

protocol ListItem {}
extension Favorite: ListItem {}
extension Product: ListItem {}

struct ListViewModel<Item> where Item: ListItem {
  // implementation
}

当然,你可以决定在你的 ListItem 协议中加入某些要求,但是对于我们目前的目的来说,一个空的协议和一些扩展就可以了。类似于 decode 被限制为只接受 TDecodable 类型,我们现在将 ListViewModel 限制为只允许符合 ListItem 的类型为 Item

有时 where 被移到尖括号中:struct ListViewModel<Item: ListItem>,结果代码的功能完全相同,而且在 Swift 编译这两种符号的过程中没有任何区别。

总结

在这篇博文中,你通过研究 Swift 中的类型安全以及 Array 如何确保只包含单一类型的元素,了解了泛型的必要性。你创建了一个围绕 Array 的封装器来试验泛型,并看到泛型是类型的占位符,以后会被填入。接下来,你看到函数也可以有泛型参数,而且可以对它们进行约束,以限制可用于填入泛型的类型。

为了把这一切联系起来,你看到了如何使用泛型和泛型约束来清理一些重复的视图模型代码,实际上你的项目中可能有这些代码。

总而言之,泛型不容易上手。如果你不得不时地回过头来看一看这篇文章,以恢复你的记忆,那也没关系。最终,你会掌握它的。如果你有问题、意见或只是想与我联系,你可以在 Twitter 上找到我。