小结Swift文档-Generics

本文基于Swift5.0版本官方文档,阅读大概需要20min,可以对泛型有一个清晰的认识。

什么是泛型(Generics)

泛型:指的就是在你定义的时候放置一个占位符类型名,告诉系统使用的类型现在不确定,我先占个位置。这样编译的时候系统不会报错。然后在你使用的时候才会真正地确定类型。

语言表达起来可能不是那么直观,让我们看一段代码直观的了解一下什么是泛型。

1
2
3
4
5
6
7
//定义一个打印任意类型变量的函数
func printSomething<T>(value1: T, value2: T) {
print(value1, value2)
}
printSomething(value1: 1, value2: 2) // Int: 1 2
printSomething(value1: "a", value2: "b") //String: a b

通过上面的代码我们可以看出,通过在函数名字后面添加<T>来表明添加了一个泛型类型,<>告诉编译器T是一个占位符类型,不需要真正的查找叫做T的类型。

Note

  • <T>中的T可以是任意字符或者单词,但是要使用大写字母或者大写开头的驼峰命名法(如:VUMyTypeParameter)。
  • <>里面不止可以写一个类型占位符,也可以写多个:<T, U>

现在我们初步了解泛型是什么,那么肯定会有人问道:我们为什么要是使用泛型呢?下面我们看一下为什么要使用泛型。

为什么要用泛型

泛型类型函数

在日常工作中,我们会经常遇到在某些条件下交换两个变量的值的情况。如果需要交换两个Int的值得话,我们可以很轻易的写下下面的函数:

1
2
3
4
5
6
7
8
9
func swapTwoIntValue(_ num1: inout Int, _ num2: inout Int) {
(num1, num2) = (num2, num1)
}
var num1 = 10
var num2 = 20
swapTwoIntValue(&num1, &num2)
print(num1, num2) // 20 10

这个函数很简洁,也很正确,但是如果我们还需要交换StringDouble等等类型的变量呢,再写swapTwoStringValue(_:_:)swapTwoDoubleValue(_:_:)的函数吗?再定义两个这样的函数当然没有问题,但是我们会发现这三个函数内部实现都是一样的,区别只是参数的类型不同。这时候就轮到泛型出马了,我们可以用泛型写一个使用任意Swift基本类型的交换函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func swapTwoValue<T>(_ num1: inout T, _ num2: inout T) {
(num1, num2) = (num2, num1)
}
var num1 = 10
var num2 = 20
swapTwoValue(&num1, &num2)
print(num1, num2) // 20 10
var str1 = "hello"
var str2 = "world"
swapTwoValue(&str1, &str2)
print(str1, str2) // world hello

小结-为什么要用泛型

  • 可以写出更加灵活可复用的函数。
  • 使代码更加简洁明了。

Note

  • 上面的swapTwoValue(_:_:)函数只是举个例子说明泛型类型函数的用法,如果你想使用交换两个变量的值得功能,你可以使用官方的swap(_:_:)函数。
  • 注意交换函数两个变量的类型都是T,虽然T可以表示任意类型,但两个变量必须是同一类型,Swift不允许两个不同类型的变量交换值,因为Swift是一门类型安全的语言。

我们现在知道可以通过定义泛型类型的函数来达到减少代码冗余的问题,那么泛型的用处仅仅如此吗?作为Swift最强大的特性之一,肯定不会只是实现一个泛型类型函数这么简单的。下面让我们看一下泛型还能做什么?

我们能用它做什么

实现泛型的数据结构

我们可以通过泛型来实现一个支持多种类型的栈。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
struct Stack<Element> {
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop(_ item: Element) -> Element {
return items.removeLast()
}
}

Stack可以放入Int/String等多种类型的数据。此处有个地方要注意:在我们给Stack扩展计算属性或者方法的时候,不需要我们在声明类型参数,Stack中的泛型在extension中依然有效。具体代码如下:

1
2
3
4
5
6
// 不需要再用<Element>来声明泛型
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}

通过类型约束(Type Constraints)来实现遵守protocol的任意类型参数的函数

日常开发中,我们会经常需要实现在一个数组中查找某个值的索引的功能,如果我们将数组的元素类型写死的话,我们声明的函数只能用于某一种类型,这时候我们应该怎么办呢?对了,就是将类型声明为泛型类型。通过上面的介绍,我们可能会写下以下代码:

1
2
3
4
5
6
7
8
func findIndex<T>(_ target: T, _ array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == target {
return index
}
}
return nil
}

这个函数创建的很不错,但是很可惜它编译会报错:Binary operator '==' cannot be applied to two 'T' operands,该报错表示我们声明的类型占位符T不能使用==运算符。那么如何正确的实现该函数呢?这就要使用类型约束(Type Constraints)来实现了。具体的做法就是将findIndex<T>改为<T: Equatable>,这句话的意思是T只支持实现了Equatable协议的类型使用。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
func findIndex<T: Equatable>(_ target: T, _ array: [T]) -> Int? {
for (index, value) in array.enumerated() {
if value == target {
return index
}
}
return nil
}
if let index = findIndex("person0", arr) {
print("person0 index is \(index)") //person0 index is 0
}

Protocol中的联合类型

现在我们知道可以在函数参数中使用泛型,那么我能在protocol中实现类似的功能吗?答案是:当然可以。我们可以用associatedtype关键字来告诉编译器该类型为泛型,在真正使用的时候再检查它的类型。

假如我们要实现一个Container的Protocol,该协议包含了append(_)添加元素的函数、获取长度的计算属性count、根据下标获取元素的函数subscript(_)。这时候如果我们将Item的类型写死的话就说明了只有这一种类型能够遵守该Protocol,那么如何让更多的类型能够遵守呢?这时候就轮到associatedtype出场了。下面为具体代码:

1
2
3
4
5
6
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
}

我们可以使Stack遵守该协议,看一下具体使用。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extension Stack: Container {
mutating func append(_ item: Element) {
push(item)
}
var count: Int {return items.count }
subscript (_ i: Int) -> Element {
return items[i]
}
}
var s1 = Stack(items: [1,2,3,4])
var s2 = Stack(items: ["a", "b", "c", "d"])
s1.append(5)
print(s1.items) //[1,2,3,4,5]
print(s1.count) //5
print(s1[2]) // 3
s2.append("f")
print(s2.items) //["a", "b", "c", "d", "f"]
print(s2.count) //5
print(s2[2]) //"f"

Protocol中的联合类型添加类型约束

在上面我们看到Protocol中可以使用联合类型来实现泛型,那么我们也可以给联合类型添加类型约束来实现泛型遵守某个Protocol、或者遵守某种条件(比如类型相同等)。具体代码如下:

1
2
3
4
5
6
7
protocol Container {
//该行代码表示Item必须是遵守Equatable的类型
associatedtype Item: Equatable
mutating func append(_ item: Item)
var count: Int { get }
subscript(_ i: Int) -> Item { get }
}

Protocol中的联合类型添加多个类型约束

我们知道可以给联合类型添加类型约束,可以用associatedtype Item: Equatable来使Item遵守Equatable协议,那如果我想让Item遵守Equatable的同时,又约束它必须是某一种类型呢?这时候我们可以使用where语句来实现。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protocol SuffixableContainer: Container {
//该行代码表示Suffix必须遵守SuffixableContainer,并且它的Item类型必须和Container的Item类型一致
associatedtype Suffix: SuffixableContainer where Suffix.Item == Item
func suffix(_ size: Int) -> Suffix
}
//Stack相当于上面的Suffix,它遵守了SuffixableContainer协议
extension Stack: SuffixableContainer {
func suffix(_ size: Int) -> Stack {
var result = Stack()
for index in (count-size)..<count {
result.append(self[index])
}
return result
}
}
var stackOfInts = Stack<Int>()
stackOfInts.append(10)
stackOfInts.append(20)
stackOfInts.append(30)
let suffix = stackOfInts.suffix(2)
// suffix 包含 20 和 30

上面的代码SuffixableContainer实现了一个获取某个位置到最后的一段数据。

Extension中使用类型约束

同Protocol,我们也可以在Extension中通过where来实现类型约束。
如果我们不让Element遵守Equatable协议的话,是会编译错误的,因为在该函数中我们使用了 == 操作符。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// Prints "Top element is tres."

当然,我们也可以在扩展Protocol的时候来使用类型约束。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
extension Array: Container where Element: Equatable { }
// 扩展Container,并且Item是遵守Equatable协议的
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
//"Starts with something else."

除了强制泛型元素遵守某个协议外,我们也可以强制泛型元素为特定的某个类型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
extension Array: Container where Element: Equatable { }
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += self[index]
}
return sum / Double(count)
}
}
print([1260.0, 1200.0, 98.6, 37.0].average()) //648.9

总结

上面就是关于泛型的讲解,下面来看一下关于泛型的总结。

  • 类型占位符要使用大写的字母或者大写开头的驼峰命名法。
  • 泛型使代码更加灵活可复用、更加简洁明了。
  • 类型参数有类型约束的参数可以在泛型函数、泛型下标、泛型类型中使用。
  • 泛型的where语句可以使你的联合类型必须遵守某个协议或者满足某些条件。

到这里,关于泛型的讲解就结束了。希望大家通过本文能对泛型有一个全新的、深刻的认识。让我们在项目中愉快的使用泛型吧!

参考