原文:Swift Generics Tutorial: Getting Started
作者:Gemma Barlow
译者:kmyhy更新说明:本教程由 Gemma Barlow 更新为 Swift3。原文作者是 Mikael Konutgan。
泛型编程是一种编写函数和数据类型的方法,同时使对数据类型的预设最小化。Swift 泛型编写的代码不会指定数据的实际类型,从而允许进行更优雅的抽象,使得代码更清晰、Bug 更少——编写“适用于王和女王”的代码。我喜欢将自己的代码描述成皇室成员,你呢?
在 Swift 自身也大量使用了泛型,理解它们就等于彻底掌握了这门语言。在 Swift 中的一个泛型的例子是 Optional 类型。你可以让任意数据类型变成 Optional 的,哪怕是你自定义的类型。Optional 数据类型就是它所能包含的类型的泛型。
在本教程中,我们将在 plaground 中学习下列知识:
- 泛型是什么
- 泛型有什么用
- 如何用泛型编写泛型函数和数据结构
- 如何使用类型限制
- 如何扩展泛型
注意: 本教程使用 Xcode8 和 Swift 3。
开始
创建一个新 plaground。在 Xcode 中,打开 File\New\Playground… 菜单,取名为 Generics,platform 选择 macOS。点击 Next 保存位置,点 Create。
作为居住在很远很远的国度中的一个程序员,你被召唤进皇宫,为了帮助女王解决一个大难题。她无法算出她有多少臣民,想让你帮她计算。她需要一个函数,用于加两个整数。在这个新的 playground 中添加一个函数:
func addInts(x: Int,y: Int) -> Int {
return x + y
}
addInts(x:y:) 方法有两个 Int 参数,返回二者之和。你可以这样调用它:
let intSum = addInts(x: 1,y: 2)
这是一个简单的例子,可以说明 Swift 是类型安全的。你可以用两个整数来调用这个函数,但不能使用其他的数据类型。
女王大悦,立马让你写出另外一个函数——这次,要加两个 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 泛型的其它例子
你可能不知道,有一些常用的结构,比如数组、字典和 Optional 也是泛型的吧!
数组
在 playground 中写上这两句:
let numbers = [1,2,3]
let firstNumber = numbers[0]
这里,我们创建了一个简单数组,包含了 3 个数,然后访问它的第一个数。
现在分别在 numbers 和 firstNumber 上用 option+左键点击。看到了什么?
https://koenig-media.raywenderlich.com/uploads/2017/02/numbers.png’ width=’500’/> https://koenig-media.raywenderlich.com/uploads/2017/02/firstNumber.png’ width=’500’/>因为 Swift 的类型推断特性,你不需要显式指定两个常量的类型,但它们仍然会有一个明确的类型。numbers 是一个 [Int]——即整数数组——而 firstNumber 是一个 Int。
Swift 的 Array 类型其实是泛型。泛型需要有一个被完全指定的类型参数,这个参数指定了实例对象的具体类型。幸好有类型推断,也幸好有类型安全,numbers 数组只会存放 Int 值。当你从数组中删除全部值时,Swift——尤其是你——都无法知道它的类型是 Int。
将代码修改为稍长一点的版本,这样你就会更清晰地了解 Array 的泛型特性:
var numbersAgain: Array<Int> = []
numbersAgain.append(1)
numbersAgain.append(2)
numbersAgain.append(3)
let firstNumberAgain = numbersAgain[0]
通过 option+左键,查看 numberAgain 和 firstNumberAgain 的类型;它们的类型将和之前看到的一样。这次,我们用泛型语法显式指定了 numberAgain 的类型,在 Array 后面使用了一堆尖括号。
尝试添加其它东西到数组中,比如 String:
numbersAgain.append("All hail Lord Farquaad")
我们会看到某些错误出现, Cannot convert value of type ‘String’ to expected argument type ‘Int’。编译器告诉你,不能将一个 String 添加到一个整数数组中。append 方法是泛型 Array 中的一个泛型方法。它知道数组元素的数据类型,不允许你添加错误的类型给它。
删除这行代码。看标准库中的另外一个泛型例子。
字典
字典也是泛型,用于构造类型安全的数据结构。
在 playground 最后一行创建一个字典,并查找弗里多尼亚的代码:
let countryCodes = ["Arendelle": "AR","Genovia": "GN","Freedonia": "FD"]
let countryCode = countryCodes["Freedonia"]
查看两个字典的类型。你会看到 countryCodes 是一个键为 String 值也为 String 的字典,除此之外,不允许有其它值。这说明 Dictionary 是泛型。
Optional
在上面的代码中,注意到没有?countryCode 的类型是 Stirng?,这是
Optional 的简写形式。
看到熟悉的 < 和 > 了吧?泛型无处不在!
这里,编译器会强制你只能通过 String 类型的键来访问字典,你得到的返回值也总是 String。这里的 countryCode 用 Optional 类型表示,因为不是任何 key 都会有相应的值返回。如果你使用 “The Emerald City” 访问字典,countryCode 就可能是 nil 了。因为在字典中根本没有这个 key。
注意:关于 Optional,你可以参考本站的Beginning Swift 3 – Optionals 视频教程。
在 playground 中编写如下代码,以查看显式创建一个可空字符串的语法:
let optionalName = Optional<String>.some("Princess Moana")
if let name = optionalName {}
查看 name 的类型,你会看到它是一个 String。
可空绑定,即代码中的 if-let,是一种泛型转换。它会将泛型类型 T? 转换成泛型 T。也就是说,你可以在任意实际类型上使用 if let。
现在你已经掌握了基本的泛型概念,接下来可以学习如何编写自己的泛型数据结构和函数了。
编写泛型数据结构
队列 queue 是一种数据结构,类似于列表或堆栈,但你只能从末尾添加新值(入队操作),从前端取值(出队操作)。这就和我们曾经使用过的用于进行网络请求的 OperationQueue 类似。
女王对你之前的工作非常满意,现在想让你实现一个功能,让她的臣民排队等候召见。
在 playground 中添加下列结构:
struct Queue<Element> { }
很显然,Queue 是一个泛型类型,我们可以从它实用了泛型参数 Element 就可以看出。另外,Queue 通过 Element 来进行泛型化。例如,Queue 和 Queue 会在运行时决定 Element 所属的真正类型,这样我们就只能对整数或字符串进行入队出队操作。
为 Queue 声明一个属性:
fileprivate var elements: [Element] = []
我们用这个数组保存队列中的元素,属性初始化为一个空数组。注意,你可以把 Element 当成是真正的类型使用,当然它还需要在晚些时候才能知道真正的类型。我们用 fileprivate 进行声明,因为我们不想让调用者直接访问底层存储。我们将强制调用者通过方法来访问底层存储。同时,我们使用 fileprivate 而不是 private,是因为我们想在后面对 Queue 进行扩展。
最后,实现两个方法:
mutating func enqueue(newElement: Element) {
elements.append(newElement)
}
mutating func dequeue() -> Element? {
guard !elements.isEmpty else { return nil }
return elements.remove(at: 0)
}
在结构体(包括方法内部)中,都可以以 Element 的方式访问类型参数。将某个类型泛型化相当于让它的所有方法显式地泛型化相同的类型。我们已经实现了一个类型安全的数据结构,就像标准库中所做的一样。
接下来测试一下我们的新结构,将等待接见的臣民入队,也就是将他们的编号添加到队列中:
var q = Queue<Int>()
q.enqueue(newElement: 4)
q.enqueue(newElement: 2)
q.dequeue()
q.dequeue()
q.dequeue()
q.dequeue()
我们故意尝试制造一些泛型错误——比如,将 String 放到队列中。现在你能够看到更多的错误信息,在越大的项目中,这种错误的识别就越容易。
编写泛型函数
女王的事情太多了,她又让你写一个将由键值对组成的字典转换成列表的程序。
在 playground 中编写如下函数:
func pairs<Key,Value>(from dictionary: [Key: Value]) -> [(Key,Value)] { return Array(dictionary) }
注意函数的声明,参数列表及返回类型。
这个函数是对两个类型进行泛型化,即 Key 和 Value。函数有一个参数,是一个字典,字典的键/值类型分别为 Key 和 Value。返回值是一个元组构成的数组,你猜对了元组的类型就是 (key,Value)。
我们在任何有效的字典上使用这个 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 [(2,"Generics"),(3,"Rule"),(1,"Swift")]
当然,因为我们不能控制字典中键值对的顺序,你会发现元组是顺序会变成“Generics”,“Rule”,“Swift” !:]
在运行时,函数声明和函数体中 Key 和 Value 会分别用真正的类型替代。第一句 pairs(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,mid 函数可以使用数组作为参数,但数组的元素类型必须实现 Comparable 协议。
func mid<T: Comparable>(array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.sorted()[(array.count - 1) / 2]
}
这里,我们通过 : 语法,为泛型参数 T 添加了一个类型约束。我们只允许元素类型为 Comparable 的数组上调用 mid 函数,因此才能调用 sorted() 方法!测试一下被我们约束后的泛型函数:
mid(array: [3,5,1,2,4]) // 3
学完类型约束,我们可以在 playground 开始处创建一个 add 泛型函数——既显得优雅,又能赢得女王的欢心。在 playground 中新增一个扩展:
protocol Summable { static func +(lhs: Self,rhs: Self) -> Self }
extension Int: Summable {}
extension Double: Summable {}
首先我们创建了一个 Summable 协议,声明任何实现了加号运算符的类型都是 Summable 的。然后,指定 Int 和 Double 类型都实现了 Summable。
然后用一个泛型参数 T 和一个类型约束实现这个 add 泛型函数:
func add<T: Summable>(x: T,y: T) -> T {
return x + y
}
``
我们将两个函数(甚至更多,我们可以扩展更多的 Summable 类型)缩减为 1 个函数,从而减少了冗余代码。我们可以在整数和浮点数上使用 add 函数了:
```swift let addIntSum = add(x: 1,y: 2) // 3 let addDoubleSum = add(x: 1.0,y: 2.0) // 3 <div class="se-preview-section-delimiter"></div>
甚至在字符串上使用它:
extension String: Summable {}
let addString = add(x: "Generics",y: " are Awesome!!! :]")
<div class="se-preview-section-delimiter"></div>
我们可以让其他类型也实现 Summable 协议,这样 add 函数的用途就更广泛了,感谢它的泛型定义!由于您的贡献,女王殿下授予了你王国最高荣誉。
泛型的扩展
有一个宫廷小丑会替女王殿下监视等候接见的臣民们,并在女王正式召见之前知道下一个是谁。它通过挨个查看女王接待室的窗户来偷窥这一切。我们可以用一个扩展来模拟这种行为,使用我们的泛型队列。
extension Queue {
func peek() -> Element? {
return elements.first
}
}
<div class="se-preview-section-delimiter"></div>
peek 方法返回队列中的未出队元素的第一个。要扩展一个泛型类型非常简单!泛型参数只能够在原定义中可见。你可以用这个扩展去偷窥一个队列:
q.enqueue(newElement: 5)
q.enqueue(newElement: 3)
q.peek() // 5
<div class="se-preview-section-delimiter"></div>
你可以看到队列中的第一个元素是 5,但我们不需要进行出队操作,队列中的元素个数没有发生改变!
王室挑战:为 Queue 扩展一个函数 isHomogeneous,用它来判断是否所有元素相等。你可以在 Queue 定义中使用类型约束,以确保队列中的元素是否能够进行等于比较。
参考答案
首先让 Queue 中的元素实现 Equatable 协议:
struct Queue<Element: Equatable> {
<div class="se-preview-section-delimiter"></div>
然后实现 isHomogeneous() 方法:
extension Queue {
func isHomogeneous() -> Bool {
guard let first = elements.first else { return true }
return !elements.contains { $0 != first }
}
}
<div class="se-preview-section-delimiter"></div>
最后,进行测试:
var h = Queue<Int>()
h.enqueue(newElement: 4)
h.enqueue(newElement: 4)
h.isHomogeneous() // true
h.enqueue(newElement: 2)
h.isHomogeneous() // false
<div class="se-preview-section-delimiter"></div>
泛型的继承
Swift 允许对泛型类进行继承,在某些情况下这很有用,比如创建某个泛型类的实体子类。
在 playground 中添加下列泛型类。
class Box<T> {
// 一个简单的盒子
}
<div class="se-preview-section-delimiter"></div>
这里我们定义了一个 Box 类。Box 可以存放任意对象,因此我们将它定义为泛型类。你可以用两种方法来继承 Box 类:
- 我们可以扩展它的功能,保持它是泛型化的,这样仍然可以将任何对象放到盒子里;
- 我们也可以继承出一个具体化的子类,我们能够指明它里面放的是什么东西。
Swift 两种方法都允许。在 playground 中编写:
class Gift<T>: Box<T> {
// 默认,礼盒是用白纸进行包装
func wrap() {
print("Wrap with plain white paper.")
}
}
class Rose {
// 童话剧中常用的鲜花
}
class ValentinesBox: Gift<Rose> {
// 送给情人的玫瑰
}
class Shoe {
// 普通的鞋
}
class GlassSlipper: Shoe {
// 公主的水晶鞋
}
class ShoeBox: Box<Shoe> {
// 鞋盒
}
<div class="se-preview-section-delimiter"></div>
我们定义了两个 Box 子类:Gift 和 ShoeBox。Gift 是一种特殊的 Box,有着不同的方法和属性,比如 wrap()。但是,它仍然有一个泛型类型,这样它可以用于存放任何东西。Shoe 和特殊 Shoe GlassSlipper,可以放到 ShoeBox 中以便送出(或者送给某个追求者)。
定义几个上述子类的实例:
let Box = Box<Rose>() // 一个普通的放玫瑰的盒子
let gift = Gift<Rose>() // 一个放玫瑰的盒子
let shoeBox = ShoeBox()
<div class="se-preview-section-delimiter"></div>
注意,ShoeBox 的初始化没有使用泛型参数,因为在 ShoeBox 的声明中已经指定了类型。
然后,定义一个新的 ValentinesBox 实例 —— 一个盛放玫瑰的盒子,来自于情人节的特殊礼物。
let valentines = ValentinesBox()
普通的盒子用白纸包装,但情人节的礼盒总要漂亮点吧。为 ValentinesBox 添加如下方法:
override func wrap() {
print("Wrap with ♥♥♥ paper.")
}
<div class="se-preview-section-delimiter"></div>
最终,比较一下这两种盒子的包装:
gift.wrap() // plain white paper
valentines.wrap() // ♥♥♥ paper
<div class="se-preview-section-delimiter"></div>
ValentinesBox,虽然是通过泛型构造的,但仍然可以像正常的子类一样,可以继承、覆盖父类的方法。太贴心了。
枚举和相关值
正常的错误处理要用到一个所谓的“结果枚举”。结果枚举是一种泛型枚举,有两个相关值:一个是真正的结果值,一个是可能的错误。
这种方法允许我们编写“优雅的”错误处理,这是女王要求我们写的另一个除法方法——这是她的最后一个要求了。
在 playground 中添加下列定义:
enum Result<Value> { case success(Value),failure(Error) } <div class="se-preview-section-delimiter"></div>
这个枚举主要用于作为函数返回值,并用标准库中的 Error 类型返回特殊的错误信息,就和通常返回 Optional 的方式一样。在 playground 最后加入:
enum MathError: Error {
case divisionByZero
}
func divide(_ x: Int,by y: Int) -> Result<Int> {
guard y != 0 else {
return .failure(MathError.divisionByZero)
}
return .success(x / y)
}
<div class="se-preview-section-delimiter"></div>
这里,我们定义了一个错误枚举类型和一个对两个整数进行除法的函数。如果除法成功,我们会返回一个包含有计算结果的枚举,即 .success,否则返回一个数学错误。
let result1 = divide(42,by: 2) // .success(21)
let result2 = divide(42,by: 0) // .failure(MathError.divisionByZero)
第一句返回包含了 21 的枚举值 .success,第二句返回了包含 .divisionByZero 的枚举值 .failure。尽管这个枚举的关联值有两个泛型参数,但通过 case 语句我们指定了只会用其中一个。
结束
可以在这里下载最终的 playground 项目。
Swift 泛型是许多基础语言特性的基础,比如数组和可空。我们学习了如何用泛型编写优雅的、可重用同时 Bug 更少的代码——编写让女王满意的代码。
更多内容,可以参考苹果官方Swift 编程指南的泛型一章和 Generic Parameters and Arguments。其中你可以找到许多关于泛型的详细内容和一些有用的例子。
如果你还意犹未尽,可以阅读这里关于“Swift 4 中关于泛型的可能改变——计划中的未来特性”。
在基于本教程之后的下一个主题,是面向协议编程——请参考 Niv Yahel 写的这本书面向协议编程引述。
Swift 泛型是一个我们每天都会用到的内置特性,通过它我们可以编写强大和类型安全的代码。每当我们在改写那些常用的代码时,我们都要问一下自己:可以将它泛型化吗?
有任何问题,请在下面留言!