【iOS】Swift系列十九 - 泛型

泛型在Java,C++等多个语言中都有,C#把泛型发挥的淋漓尽致,OC中也有泛型(比如OC中的数组,你可以限制他里面装的是NSString类型),Swift中泛型的使用范围更加多元化。

一、泛型函数

1.1. 泛型可以将类型参数化,提高代码复用率,减少代码量

示例代码:

1
2
3
4
5
6
7
var n1 = 10
var n2 = 20
func swapValues(_ a: inout Int, _ b: inout Int) {
(a, b) = (b, a)
}
swapValues(&n1, &n2)
print("a=\(n1), b=\(n2)") // 输出:a=20, b=10

如果上面示例代码中swapValues让传入的是其他类型参数就无法使用了。这时候就要考虑使用泛型。

在函数名后面加上<T>就可以表示该函数接收的是泛型参数,参数类型也是用T修饰(T不是固定写法,也可以是SABC等其他任意标识,仅仅代表不确定类型)。

泛型示例代码:

1
2
3
4
func swapValues<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
swapValues(&n1, &n2)

这时候就可以传入任意类型的参数了,但参数类型必须保持一致。

调用swapValues函数时后面不能写成swapValues<Int>(&n1, &n2),因为参数已经明确了要传入的是什么类型。

声明泛型函数时,函数后面的泛型标识也不能省略,否则就是一个普通函数,编译器无法识别泛型。

1.2. 泛型函数赋值给变量

普通函数是可以直接赋值给变量的:

1
2
3
4
5
6
7
var n1 = 10
var n2 = 20
func swapValues(_ a: inout Int, _ b: inout Int) {
(a, b) = (b, a)
}
var fn = swapValues
fn(&n1, &n2)

但是泛型函数是不能向普通函数一样直接赋值给变量的,否则直接报错。

正确做法(变量后面明确泛型的类型):

1
2
3
4
5
6
7
var n1 = 10
var n2 = 20
func swapValues<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
var fn: (inout Int, inout Int) -> () = swapValues
fn(&n1, &n2)

多个泛型示例代码:

1
2
3
4
func test<T1, T2>(_ a: T1, _ b: T2) {
print("a=\(a), b=\(b)")
}
test(10, 20.0)

二、泛型类型

结构体和类也是可以增加泛型的,这种类型叫做泛型类型。

2.1. 类

示例代码(栈:先入后出):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 栈
class Stack<E> {
var elements = [E]()
// 入栈
func push(_ element: E) {
elements.append(element)
}
// 出栈
func pop() -> E {
elements.removeLast()
}
// 栈顶元素
func top() -> E? {
elements.last
}
// 栈内元素个数
func size() -> Int {
elements.count
}
}
// 元素是Int类型
var intStack = Stack<Int>()
// 元素是String类型
var stringStack = Stack<String>()
// 元素是任意类型
var anyStack = Stack<Any>()

为什么使用类的时候就可以在类名后面加上类型,函数就不可以呢?因为函数在入参的时候,参数已经明确了泛型的类型。

2.2. 继承

继承泛型类型的类,子类也必须是泛型类型。

1
class SubStack<E>: Stack<E> {}

2.3. 结构体

如果结构体内函数需要修改结构体的内存(修改存储属性),则必须在函数前面加上mutating

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Stack<E> {
var elements = [E]()
mutating func push(_ element: E) {
elements.append(element)
}
mutating func pop() -> E {
elements.removeLast()
}
func top() -> E? {
elements.last
}
func size() -> Int {
elements.count
}
}

2.4. 枚举

1
2
3
4
5
6
7
8
9
10
enum Score<T> {
case point(T)
case grade(String)
}
// 完整写法
let score0 = Score<Int>.point(100)
// 忽略枚举后面的类型声明(参数已经明确的泛型类型)
let score1 = Score.point(100.0)
// 必须指定泛型(此时并不知道T什么类型)
let score2 = Score<Double>.grade("A")

三、泛型本质

示例代码:

1
2
3
4
5
6
7
8
9
10
func swapValues<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
var i1 = 10
var i2 = 20
swapValues(&i1, &i2)

var d1 = 10.0
var d2 = 20.0
swapValues(&d1, &d2)

猜想: 同一个函数可以传入不同类型的参数,是不是像C++一样生成了很多不同类型参数的重载函数?如果是这样的话,不同参数类型的函数内存地址肯定是不一样的,我们通过汇编看下。


通过汇编发现,两个函数地址是一样的。

疑问: 传入的参数类型不一样(内存布局不一样),是如何做到参数交互赋值呢?

通过上面汇编可以看出,函数传入的参数除了两个外界传入的参数外,还传入另外一个元类型信息参数。函数内部就是根据这个元类型参数获取参数真正的类型是什么。

四、关联类型

关联类型(Associated Type)的作用:给协议中用到的类型定义一个占位名称。

示例代码(栈):

1
2
3
4
5
6
7
protocol Stackable {
associatedtype Element
mutating func push(_ element: Element)
mutating func pop() -> Element
func top() -> Element
func size() -> Int
}

associatedtype Element代表的意思就是定义一个名称为Element的泛型。

使用关联类型时,有两种方法可以让编译器知道关联类型的确定类型:

  • 格式:typealias 关联类型 = 真实类型
    示例:typealias Element = String
  • 第二种方式就是直接把实现协议参数修改为确定类型。

使用关联类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class StringStack: Stackable {
// typealias Element = String
var elements = [String]()
func push(_ element: String) {
elements.append(element)
}
func pop() -> String {
elements.removeLast()
}
func top() -> String? {
elements.last
}
func size() -> Int {
elements.count
}
}

当实现协议的类也无法确定关联类型时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Stack<E>: Stackable {
// typealias Element = E
var elements = [E]()
func push(_ element: E) {
elements.append(element)
}
func pop() -> E {
elements.removeLast()
}
func top() -> E? {
elements.last
}
func size() -> Int {
elements.count
}
}

协议中可以拥有多个关联类型:

1
2
3
4
5
6
7
8
9
protocol Stackable {
associatedtype Element
associatedtype Element2
associatedtype Element3
mutating func push(_ element: Element)
mutating func pop() -> Element
func top() -> Element
func size() -> Int
}

不能把关联类型拼接在一起声明,否则报错。

五、类型约束

可以对泛型进行限制/约束。

示例代码一:

1
2
3
4
5
protocol Runnable { }
class Person { }
func swapValues<T: Person & Runnable>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}

上面的示例代码中T就被PersonRunnable做了约束,意思是T只能是Person类并且需要遵守Runnable协议。

示例代码二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protocol Stackable {
associatedtype Element: Equatable
}
class Stack<E: Equatable> : Stackable {
typealias Element = E
}
func equal<S1: Stackable, S2: Stackable>(_ s1: S1, _ s2: S2) -> Bool
where S1.Element == S2.Element, S1.Element: Hashable {
return false
}
var s1 = Stack<Int>()
var s2 = Stack<Int>()
var s3 = Stack<String>()
equal(s1, s2)
equal(s2, s3)

示例代码二就稍微有点复杂了,约束泛型后还添加了其他条件。S1S2不仅要遵守Stackable协议,而且S1S2还要相等,S1必须遵守Hashable协议(可哈希)。

equal(s1, s2)可以编译通过,因为泛型都是Int类型。但是equal(s2, s3)就会编译报错,因为s2泛型类型是Int类型,而s3泛型类型是String类型,不符合条件,所以编译报错。

扩展:IntString都是遵守Hashable协议的。

注意点:

示例代码:

1
2
3
4
5
6
7
8
9
10
11
protocol Runnable { }
class Person: Runnable { }
class Car: Runnable { }
func get(_ type: Int) -> Runnable {
if type == 0 {
return Person()
}
return Car()
}
var r1 = get(0)
var r2 = get(1)

当使用r1的时候,编译器会认为r1Runnable类型。因为返回的具体类型是程序运行中才知道的。

如果协议中有associatedtype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protocol Runnable {
associatedtype Speed
var speed: Speed { get }
}
class Person: Runnable {
var speed: Double { 0.0 }
}
class Car: Runnable {
var speed: Int { 0 }
}
func get(_ type: Int) -> Runnable {
if type == 0 {
return Person()
}
return Car()
}

编译就会报错:
协议Runnable不能用作泛型约束,因为他有Self或关联类型约束

报错的原因就是程序在编译期间并不知道协议中的关联类型Speed具体是什么类型。

解决方案有两种:
方案一:使用泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protocol Runnable {
associatedtype Speed
var speed: Speed { get }
}
class Person: Runnable {
var speed: Double { 0.0 }
}
class Car: Runnable {
var speed: Int { 0 }
}

func get<T: Runnable>(_ type: Int) -> T {
if type == 0 {
return Person() as! T
}
return Car() as! T
}

var r1: Person = get(0)
var r2: Car = get(1)

返回值是泛型,定义变量的时候把变量类型确定好,所以函数返回值就随之确定。

方案二:不透明类型
使用some关键字声明一个不透明类型。

即使在函数返回值前面限定了不透明类型还是报错,那是因为不透明类型限制函数只能返回一种类型

思考一:为什么只需要限定不透明类型就可以了?

因为不透明类型限制函数只能返回一种类型,所以函数内部已经知道返回的类型是什么。

思考二:既然只能返回一种类型,为什么不直接返回具体类型?

可以避免外界知道返回值的具体类型(即屏蔽真实类型)。

应用场景: 当需要返回一个遵守某个协议的对象时,如果不希望外界知道返回的具体对象类型,仅对外公开协议中定义的接口时,可以使用不透明类型。

some除了用在返回值类型上,一般还可以用在属性类型上。

1
2
3
4
5
6
7
8
9
10
11
protocol Runnable {
associatedtype Speed
}
class Dog: Runnable {
typealias Speed = Double
}
class Person {
var pet: some Runnable {
return Dog()
}
}

上面代码中属性pet就对外隐藏了返回值真实类型(如果没有关联类型,就可以不加some,也能够达到隐藏真实类型的目的)。

六、可选项的本质

可选项的本质是枚举类型。

枚举定义:

1
2
3
4
5
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
public init(_ some: Wrapped)
}

示例代码:

1
2
3
var age: Int? = 10
age = 20
age = nil

上面示例代码的完整代码形式:

1
2
3
4
var age: Optional<Int> = Optional(10)
// var age: Optional<Int> = .some(10)
age = .some(20)
age = .none

?其实就是可选项的语法糖。

可选项在switch中的注意点:

1
2
3
4
5
6
7
8
9
10
var age: Int? = 10
age = 20
age = nil

switch age {
case let v?:
print("1", v)
case nil:
print("2")
}

正常情况下,case let vv一定是一个可选类型,和if不一样(if会自动解包)。

如果把v后面加上?,最终得到的vInt类型:

等价代码:

1
2
3
4
5
if let v = age {
print("1", v)
} else {
print("2")
}

多重可选性的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 示例一
var age_: Int? = 10
var age: Int?? = age_
age = nil

// 示例一本质
// 写法一:
var age0 = Optional.some(Optional.some(10))
age0 = .none
// 写法二:
var age1: Optional<Optional> = .some(.some(10))
age1 = .none


// 示例二
//var age: Int?? = 10
//// 示例二本质
//var age0: Optional<Optional> = 10