原文:Swift Generics Tutorial: Getting Started
💡 学习编写函数和数据类型,同时做出最小的假设。Swift 泛型可以使代码更简洁,错误更少。
泛型编程(Generic programming)是一种编写函数和数据类型的方法,同时对所使用的数据类型做出最小的假设。Swift 泛型编程创建的代码不会对底层数据类型进行具体化,允许进行优雅的抽象,产生更简洁的代码,减少错误。它允许你编写一次函数,并在不同的类型上使用(简而言之,支持函数在不同的类型上复用)。
你会发现泛型在整个 Swift 中使用,这使得理解它们对于完全掌握该语言至关重要。你在 Swift 中已经遇到的一个泛型的例子是 Optional 类型。你可以拥有一个你想要的任何数据类型的可选类型,当然,即使是那些你自己创建的类型。换句话说,Optional 数据类型对于它可能包含的值的类型是通用的。
在本教程中,你将在 Swift playground 上进行实验,学习:
- 泛型到底是什么?
- 为什么它们是有用的。
- 如何编写泛型函数和数据结构。
- 如何使用类型约束。
- 如何扩展泛型类型。
开始
首先,创建一个新的 playground。在 Xcode 中,转到 File ▸ New ▸ Playground.... 选择 macOS ▸ Blank 模板。点击 "下一步",并将 playground 命名为 "Generics"。最后,点击 "创建"。
作为居住在一个遥远的王国的少数程序员之一,你被召唤到皇家城堡,帮助女王处理一件非常重要的事情。她已经不知道自己有多少个王室臣民,需要有人帮助她进行计算。
她要求写一个函数,将两个整数相加。将以下内容添加到你新创建的 playground:
func addInts(x: Int, y: Int) -> Int {
return x + y
}
addInts(x:y:) 接收两个 Int 值并返回其总和。你可以通过在 playground 上添加以下代码来试一试:
let intSum = addInts(x: 1, y: 2)
这是一个简单的例子,展示了 Swift 的类型安全。你可以用两个整数类型来调用这个函数,但不能用任何其他类型。
女王很高兴,并立即要求编写另一个 add 函数来计算她的财富 -- 这次是添加 Double 值。创建第二个函数 addDoubles(x:y:):
func addDoubles(x: Double, y: Double) -> Double {
return x + y
}
let doubleSum = addDoubles(x: 1.0, y: 2.0)
addInts 和 addDoubles 的函数签名是不同的,但函数体是相同的。你不仅有两个函数,而且里面的代码也是重复的。可以使用泛型来将这两个函数减少到一个,并删除多余的代码。
然而,首先,你会看到日常 Swift 中泛型编程的一些其他常见情况。
Swift 泛型编程的其他例子
你可能没有意识到,你最常使用的一些结构,比如 Array、Dictionary、Optional 和 Result,都是泛型类型!
Array
在你的 playground 上添加以下内容:
let numbers = [1, 2, 3]
let firstNumber = numbers[0]
在这里,你创建了一个由三个数字组成的简单数组,然后从该数组中取出第一个数字。
现在 Option-click,先点击 numbers,再点击 firstNumber。你看到了什么?
因为 Swift 有类型推断(type inference),你不必明确定义常量的类型,但它们都有一个确切的类型。 numbers 是一个 [Int]-- 也就是一个包含整数的数组 -- 而 firstNumber 是一个 Int。
Swift Array 类型是一个泛型类型。泛型类型都有至少一个 type 参数,是一个尚未指定的其他类型的占位符。你需要指定这个其他类型,以便将泛型类型明确化(specialize),并实际创建它的一个实例。
例如,Array 的类型参数决定了数组中的内容。你的数组是被明确的,所以它只能包含 Int 值。这支持了 Swift 的类型安全。当你从数组中移除任何东西时,Swift-- 更重要的是你 -- 知道它必须是一个 Int。
你可以通过在 playground 上添加一个稍长版本的相同代码来更好地看到 Array 的泛型性质:
var numbersAgain: Array<Int> = []
numbersAgain.append(1)
numbersAgain.append(2)
numbersAgain.append(3)
let firstNumberAgain = numbersAgain[0]
通过 Option-click 检查 numbersAgain 和 firstNumberAgain 的类型;这些类型将与之前的值完全相同。在这里,你使用显式的泛型语法指定 numbersAgain 的类型,将 Int 放在 Array 后面的尖括号中。你已经提供了 Int 作为 type 参数的显式类型参数。
试着在数组中追加一些其他的东西,比如说一个字符串:
numbersAgain.append("All hail Lord Farquaad")
你会得到一个错误 -- 类似的错误:Cannot convert value of type ‘String’ to expected argument type ‘Int’。编译器告诉你,你不能把一个 String 添加到一个 Int 数组中。作为泛型数组上的一个方法,append 是一个所谓的泛型方法。因为这个数组实例是专门的 Array<Int> 类型,它的 append 方法现在也被专门定为 append(_ newElement:Int)。它不会让你添加一个不匹配的类型。
删除导致错误的那一行。接下来你将看到标准库中泛型的另一个例子。
Dictionary
Dictionary 也是泛型类型,会产生类型安全的数据结构。
在你的 playground 末尾创建以下魔法王国的字典,然后查找 Freedonia 的国家代码。
let countryCodes = ["Arendelle": "AR", "Genovia": "GN", "Freedonia": "FD"]
let countryCode = countryCodes["Freedonia"]
检查两个声明的类型。你会看到 countryCodes 是一个由 String 键和 String 值组成的字典 -- 这个字典里不可能有其他东西。正式的泛型类型是 Dictionary。
Optional
在上面的例子中,注意 countryCode 的类型是 String? 这实际上只是 Optional 的一个缩写。
如果 <和> 看起来很熟悉,那是因为即使是 Optional 也是一个泛型类型。泛型无处不在!
在这里,编译器强制要求你只能用 String 键来访问字典,并且你总是得到String 值。一个可选的类型被用来表示 countryCode,因为可能没有一个值对应于这个键。例如,如果你试图查找 "The Emerald City",countryCode 的值将是 nil,因为它不存在于你的魔法王国的字典中。
💡 注:关于 Optionals 的更详细介绍,请查看本网站上的 Swift 编程视频。
在你的 playground 添加以下内容,以查看创建一个可选字符串的完整显式语法:
let optionalName = Optional<String>.some("Princess Moana")
if let name = optionalName {
print(name + " is a string")
}
检查 name 的类型,你会看到它是 String。
可选绑定(Optional binding),也就是 if-let 结构,是一种泛型转换。它接收一个类型为 T? 的泛型值,并给你一个类型为 T 的泛型值。这意味着你可以对任何具体类型使用 if let。
T 时间到了!
Results
Result 是 Swift 5 中的一个新类型。和 Optional 一样,它是一个有两种情况的泛型枚举。结果不是有或无,而是成功(success)或失败(failure)。每种情况都有自己相关的泛型类型,成功有一个值,失败有一个 Error。
考虑这个情况,皇家魔术师招募你去施展一些法术。已知的法术产生一个符号,但未知的法术失败。这个函数看起来会是这样的:
enum MagicError: Error {
case spellFailure
}
func cast(_ spell: String) -> Result<String, MagicError> {
switch spell {
case "flowers":
return .success("💐")
case "stars":
return .success("✨")
default:
return .failure(.spellFailure)
}
}
Result 允许你编写返回一个值或一个错误的函数,而不必使用 try 语法。作为额外的奖励,失败情况的泛型规范意味着你不需要像使用 catch 块那样检查类型。如果有一个错误,你可以确定在与.failation case 相关的值中会有一个 MagicError。
试着用一些法术来看看 Result 的作用:
let result1 = cast("flowers") // .success("💐")
let result2 = cast("avada kedavra") // .failure(.spellFailure)
在掌握了泛型的基础知识后,你可以学习编写自己的泛型数据结构和函数。
编写一个泛型数据结构
队列(queue)是一种类似于列表或堆栈的数据结构,但你只能在其末端添加新的值(enqueue them),并且只能从前面取值(dequeue them)。如果你曾经使用过 OperationQueue-- 也许是在做网络请求时,这个概念可能很熟悉。
女王对你在本教程中的努力很满意,现在她希望你能写一些功能来帮助跟踪排队等候与她谈话的皇家臣民。
在你的 playground 的末尾添加以下 struct 声明:
struct Queue<Element> {
}
Queue 是一个泛型类型,在其泛型参数子句中有一个类型参数 Element。另一种说法是,Queue 是对 Element 类型的泛型。例如,Queue<Int> 和 Queue<String> 在运行时将成为它们自己的具体类型,它们分别只能对字符串和整数进行入队和出列。
给队列添加以下属性:
private var elements: [Element] = []
你将使用这个数组来保存元素,你将其初始化为一个空数组。注意,你可以使用 Element,就像它是一个真实的类型一样,尽管它将在以后被填入。你把它标记为 private,因为你不想让 Queue 的使用者直接访问 elements 属性。你想强迫他们使用方法来访问后援存储。
最后,实现两个主要的队列方法:
mutating func enqueue(newElement: Element) {
elements.append(newElement)
}
mutating func dequeue() -> Element? {
guard !elements.isEmpty else { return nil }
return elements.remove(at: 0)
}
同样,类型参数 Element 在结构体的任何地方都是可用的,包括方法内部。让一个类型成为泛型就像让它的每一个方法都隐含在同一类型上的泛型。你已经实现了一个类型安全的泛型数据结构,就像标准库中的结构一样。
在 playground 的底部玩一下你的新数据结构,通过向队列中添加王室 ID 来排队等待:
var q = Queue<Int>()
q.enqueue(newElement: 4)
q.enqueue(newElement: 2)
q.dequeue() // 4
q.dequeue() // 2
q.dequeue() // nil
q.dequeue() // nil
通过故意制造尽可能多的错误来触发与泛型相关的不同错误信息来获得一些乐趣 -- 例如,向你的队列中添加一个字符串。你现在对这些错误了解得越多,在更复杂的项目中识别和处理它们就越容易。
编写一个泛型函数
女王有很多数据需要处理,她要求你写的下一段代码将获取一个包含键和值的字典,并将其转换为一个列表。
在 playground 的底部添加以下函数:
func pairs<Key, Value>(from dictionary: [Key: Value]) -> [(Key, Value)] {
return Array(dictionary)
}
好好看看这个函数的声明、参数列表和返回类型。
这个函数在两种类型上是泛型的,你把它们命名为 Key 和 Value。唯一的参数是一个字典,里面有一个键值对,类型为 Key 和 Value。返回值是一个形式为 - 你猜对了 - (Key, Value) 的 tuple 数组。
你可以在任何有效的 dictionary 上使用 pairs(from:) ,它将工作,这要感谢泛型:
let somePairs = pairs(from: ["minimum": 199, "maximum": 299])
// result is [("maximum", 299), ("minimum", 199)]
let morePairs = pairs(from: [1: "Swift", 2: "Generics", 3: "Rule"])
// result is [(1, "Swift"), (2, "Generics"), (3, "Rule")]
当然,由于你不能控制字典元素进入数组的顺序,你可能会在你的 playground 上看到元组值的顺序更像 "Generics"、"Rule"、"Swift",而事实上,它们确实有点像!:]
在运行时,每个可能的 Key 和 Value 将作为一个单独的函数,在函数声明和主体中填写具体类型。对 pair(from:) 的第一次调用返回一个 (String, Int) 元组数组。第二次调用使用元组中类型的翻转顺序,并返回一个 (Int, String) 元组数组。
你仅仅创建了一个函数,可以通过不同的调用返回不同的类型。你可以看到把你的逻辑放在一个地方可以简化你的代码。你没有必要使用两个不同的函数,而是用一个函数来处理这两个调用。
现在你已经知道了创建和使用泛型类型和函数的基本知识,现在是时候进入一些更高级的功能了。你已经看到了泛型是如何通过类型来限制事物的,但你可以添加额外的约束,以及扩展你的泛型,使它们更加有用。
限制泛型类型
女王希望分析她最忠诚的一小群臣民的年龄,她要求一个函数来对一个数组进行排序并找到中间值。
当你把下面这个函数添加到你的 playground 时:
func mid<T>(array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.sorted()[(array.count - 1) / 2]
}
你会得到一个编译器错误。问题是,为了让 sorted() 工作,数组中的元素必须是 Comparable 的。你需要以某种方式告诉 Swift,只要元素类型实现了 Comparable,mid 就可以接受任何数组。
将函数声明改为如下:
func mid<T: Comparable>(array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.sorted()[(array.count - 1) / 2]
}
在这里,你使用 : 语法为通用类型参数 T 添加类型约束,现在你只能用一个 Comparable 元素的数组来调用该函数,这样 sorted() 就会一直工作下去!通过添加来尝试一下这个受约束的函数:
mid(array: [3, 5, 1, 2, 4]) // 3
其实,你已经在使用 Result 时看到了这一点。Failure 类型被约束为 Error。
清理 Add 函数
现在你知道了类型约束,你可以从 playground 的一开始就创建一个泛型版本的 add 函数 -- 这将更加优雅,并且让女王非常满意。将以下协议和扩展添加到你的 playground:
protocol Summable { static func +(lhs: Self, rhs: Self) -> Self }
extension Int: Summable {}
extension Double: Summable {}
首先,你创建一个 Summable 协议,说任何遵守该协议的类型必须有可用的加法运算符 +。然后,你指定 Int 和 Double 类型遵守该协议。
现在使用一个泛型参数 T 和一个类型约束,你可以创建一个泛型函数 add。
func add<T: Summable>(x: T, y: T) -> T {
return x + y
}
你已经将你的两个函数(实际上更多,因为你需要更多的函数来处理其他的 Summable 类型)减少到一个,并删除了多余的代码。你可以在 Int 和 Double 上使用这个新函数:
let addIntSum = add(x: 1, y: 2) // 3
let addDoubleSum = add(x: 1.0, y: 2.0) // 3.0
你也可以在其他类型上使用它,比如字符串:
extension String: Summable {}
let addString = add(x: "Generics", y: " are Awesome!!! :]")
通过添加其他符合要求的类型到 Summable,你的 add(x:y:) 函数变得更加广泛有用,这要归功于它的泛型驱动的定义!公主殿下为你的努力授予你王国的最高荣誉。
扩展一个泛型
一个宫廷小丑一直在协助女王,监视着等待的皇室臣民,并在正式迎接他们之前,让女王知道下一个臣民是谁。他通过女王起居室的窗户偷看,以达到这个目的。你可以使用一个扩展来模拟他的行为,应用于本教程前面的泛型队列类型。
扩展 Queue 类型,并在 Queue 定义下面添加以下方法:
extension Queue {
func peek() -> Element? {
return elements.first
}
}
peek 返回第一个元素,而不对其进行出列。扩展一个泛型类型是很容易的!泛型类型的参数就像在原始定义的主体中一样可见。你可以使用你的扩展来偷看一个队列:
q.enqueue(newElement: 5)
q.enqueue(newElement: 3)
q.peek()
你会看到值 5 是队列中的第一个元素,但没有任何东西被取消,队列中的元素数量与之前一样。
皇家挑战:扩展队列类型,实现一个函数 isHomogeneous,检查队列中的所有元素是否相等。你需要在队列声明中添加一个类型约束,以确保其元素可以被检查为彼此相等。
struct Queue<Element: **Equatable**> {
fileprivate var elements: [Element] = []
mutating func enqueue(newElement: Element) {
elements.append(newElement)
}
mutating func dequeue() -> Element? {
guard !elements.isEmpty else { return nil }
return elements.remove(at: 0)
}
}
extension Queue {
func isHomogeneous() -> Bool {
guard let first = elements.first else { return true }
return !elements.contains { $0 != first }
}
}
var h = Queue<Int>()
h.enqueue(newElement: 4)
h.enqueue(newElement: 4)
h.isHomogeneous() // true
h.enqueue(newElement: 2)
h.isHomogeneous() // false
子类化泛型类型
Swift 有能力对泛型类进行子类化。这在某些情况下很有用,比如创建一个泛型类的具体子类。
把下面这个泛型类添加到 playground:
class Box<T> {
// Just a plain old box.
}
这里你定义了一个 Box 类。这个盒子可以包含任何东西,这就是为什么它是一个泛型类。你有两种方法可以对 Box 进行子类化:
- 你可能想扩展盒子的功能和工作方式,但要保持它的通用性,所以你仍然可以在盒子里放任何东西。
- 你可能想有一个专门的子类,总是知道里面有什么。
Swift 允许这两种情况。把这个添加到你的 playground 上:
class Gift<T>: Box<T> {
// By default, a gift box is wrapped with plain white paper
func wrap() {
print("Wrap with plain white paper.")
}
}
class Rose {
// Flower of choice for fairytale dramas
}
class ValentinesBox: Gift<Rose> {
// A rose for your valentine
override func wrap() {
print("Wrap with ♥♥♥ paper.")
}
}
class Shoe {
// Just regular footwear
}
class GlassSlipper: Shoe {
// A single shoe, destined for a princess
}
class ShoeBox: Box<Shoe> {
// A box that can contain shoes
}
你在这里定义了两个 Box 子类。Gift 和 ShoeBox。Gift 是一种特殊的 Box,被分离出来,所以你可以在它上面定义不同的方法和属性,比如 wrap()。然而,它在类型上仍有一个泛型,意味着它可以包含任何东西。Shoe 和 GlassSlipper,一种非常特殊的鞋,已经被声明了,并且可以放在 ShoeBox 的一个实例中,以便交付(或介绍给合适的追求者)。
在子类声明下声明每个类的实例:
let box = Box<Rose>() // A regular box that can contain a rose
let gift = Gift<Rose>() // A gift box that can contain a rose
let shoeBox = ShoeBox()
注意,ShoeBox 的初始化器不需要再接受泛型类型的参数,因为它在 ShoeBox 的声明中已经固定了。
接下来,声明一个子类 ValentinesBox 的新实例 -- 一个装着玫瑰的盒子,一个专门用于情人节的神奇礼物:
let valentines = ValentinesBox()
虽然标准的盒子是用白纸包装的,但你希望你的节日礼物能更高级一点。在 ValentinesBox 中添加以下方法:
class ValentinesBox: Gift<Rose> {
// A rose for your valentine
override func wrap() {
print("Wrap with ♥♥♥ paper.")
}
}
最后,通过在你的 playground 上添加以下代码来比较包装这两种类型的结果:
gift.wrap() // plain white paper
valentines.wrap() // ♥♥♥ paper
ValentinesBox,虽然是用泛型构造的,但作为一个标准的子类,其方法可以从父类继承和重写。多么优雅啊!
带有关联值的枚举
女王对你的工作很满意,想给你一个奖励。你可以选择一个普通的宝物或一枚奖章。
在你的 playground 的末尾添加以下声明:
enum Reward<T> {
case treasureChest(T)
case medal
var message: String {
switch self {
case .treasureChest(let treasure):
return "You got a chest filled with \(treasure)."
case .medal:
return "Stand proud, you earned a medal!"
}
}
}
这种语法允许你写一个枚举类型,其中至少有一个情况是一个泛型框。有了消息 var,你就可以把值拿回来了。在上面说明的 Result 例子中,成功和失败的情况都是具有不同类型的泛型。
为了得到相关的值,可以像这样使用它:
let message = Reward.treasureChest("💰").message
print(message) // You got a chest filled with 💰.
恭喜你,享受你的奖励!
何去何从
Swift 泛型是许多常用语言功能的核心,比如数组和可选类型。你已经看到了如何使用它们来构建优雅、可重用的代码,从而减少错误 -- 适合皇家的代码。
欲了解更多信息,请阅读 Apple 的指南《Swift 编程语言》中的泛型一章和泛型参数和参数语言参考章节。你会发现关于 Swift 中泛型的更多详细信息,以及一些方便的例子。
在本教程所学的基础上,下一个主题是面向协议编程 -- 详见面向协议的编程介绍。
Swift 中的泛型是一个不可或缺的功能,你每天都会使用它来编写强大而类型安全的抽象。通过记住问 "我可以泛型化这个吗?" 来改进你常用的代码。