内建集合类型
数组
数组和可变性
- 数组是值语义
- 用 let 定义的引用类型仅代表「这个引用永远不会发生变化」,但引用的对象本身是可变的
- Swift 对标准库中的所有集合类型都使用了「写时复制」技术
数组索引
- 数组迭代
for x in array
- 迭代除了第一个元素的其余部分
for x in array.dropFirst()
- 迭代除了最后五个元素的其余部分
for x in drray.dropLast(5)
- 列举数组的元素和下标
for (index, num) in array.enumerate()
- 寻找指定元素的位置
if let index = a.firstIndex(of: Element){ print(index) }
- 对数组中所有元素做映射
array.map { $0 + 1 }
- 筛选符合条件的元素
array.filter { $0 % 2 == 0 }
数组变形
map
map 的一个可能的实现是:
1 |
|
其中,Element 是数组中包含的元素类型的占位符,T 是元素转换之后的类型占位符,但 map 并不关心 Element 和 T 分别是什么,它只关心接受一个 transform 转换函数,将 Array 中的元素做转换,并返回转换的结果。
使用函数将数组行为参数化
函数名 | 含义 |
---|---|
map & flatMap & compactMap | 对元素作变换 |
filter{} | 只包含特定元素(与 removeAll 相反) |
allSatisfy{} | 针对一个条件测试所有元素 |
reduce(0, { $0 + $1}) | 将元素聚合成一个值(前面的 0 真是脱了裤子放屁,我想加几只要在reduce 的结果上加不就可以了吗?) |
forEach | 遍历每一个值 |
sort{} | 对数组排序,可以自定义排序方法 |
firstIndex & lastIndex & first & last & contains | 数组中是否包含某一个值 |
min & max | 数组最大、最小值 |
elementsEqual & starts | 讲数组与另一数组进行比较 |
split | 将数组分割 |
prefix | 数组前 n 个元素 |
drop | 当条件为真时,丢弃元素;一旦不为真,返回其余的元素 |
removeAll | 删除特定元素 |
可变和带有状态的闭包
1 |
|
上述代码执行了一个带副作用的闭包,一般不推荐这种写法,这种情况下 for .. in 往往是更好的选择。
1 |
|
这样带有副作用的做法,和故意给闭包一个局部状态有本质不同,后者是一种非常有用的技术。上述代码使用一个
running
变量来保存闭包的状态,是故意为之,而不是副作用。
flatMap
flatMap 的函数签名看起来和 map 基本一致,只是它的变换函数返回的是一个数组。在实现中,它使用 append(contentsOf:) 代替了 append(_:),这样返回的数组是展平的了:
1 |
|
体会一下:
1 |
|
数组切片
我们不仅可以通过下标访问数组中的某个元素,也可以通过切片的方式,例如:
1 |
|
需要注意的地方有两个:
- 切片后的数组类型是 ArraySlice,而不是 Array,虽然二者所具有的方法是一致的,如果需要转换,需要
Array(slice)
方法。 - 切片和它背后的数组是使用相同的索引来引用元素的。因此,切片索引不需要从零开始。建议总是使用 startIndex做索引计算。
字典
一些有用的字典方法
-
removeValue( forKey:)
这个方法还会将被删除的值返回 (如果待删除的键不存在,则返回 nil),你同样可以通过 dic[key] = nil的方式来删除某个键值对。
-
updateValue(_:forKey:)
如果之前键已经存在的话,这个方法会返回更新前的值。类似的,也可以直接通过下标复制的方式来修改。
-
merge(_:uniquingKeysWith:)
用于合并两个字典,后面的参数用于定义如何合并两个相同的键,{ $0 }即为取前一个字典的结果。
注意事项📢:
- 字典的键必须是可哈希的,因为字典通过键的 hashValue 来为每个键在其底层作为存储的数组上指定一个位置。标准库中所有的基本数据类型都是遵守 Hashable 协议的,它们包括字符串,整数,浮点数以及布尔值。另外,像是数组,集合和可选值这些类型,如果它们的元素都是可哈希的,那么它们自动成为可哈希的。如果需要自己实现 Hashable 协议(要么因为你正在实现一个类;要么出于哈希的目的,在你自定义结构体中有几个属性需要被忽略),那么首先需要让类型实现 Equatable 协议,然后你可以实现 hash(into:) 方法来满足 Hashable 协议。
- 最好不要使用不具有值语义的对象作为字典的键,因为一旦键发生变化,带来的后果是不可预期的。
集合
- 集合通过 insert 和 remove 来插入、删除元素。
- 集合的键也必须是可哈希的。
- 可以通过 subtracting,intersection,formUnion 分别求两个集合的补集、交集、并集。
- 可以将集合和闭包结合起来,实现一些复杂功能。
Range
“范围代表的是两个值的区间,它由上下边界进行定义。你可以通过 ..< 来创建一个不包含上边界的半开范围,或者使用 … 创建同时包含上下边界的闭合范围:”
摘录来自: Chris Eidhof. “Swift 进阶。” Apple Books.
对范围最基本的操作是检测它是否包含了某些元素:
1 |
|
原来Range 可以通过 Array 的初始化方法转换成 Array! 这个感觉非常重要,Array(0..<10) 就可以非常方便的初始化一个包含 0~9的数组。
1 |
|
这种写法能够正常工作,是因为 Collection 协议里对应的下标操作符声明中,所接收的是一个实现了 RangeExpression 的类型,而不是上述五个具体的范围类型中的某一个。你甚至还可以将两个边界都省略掉,这样将会得到表示整个集合的一个切片:
1 |
|
可选值
岗哨值
首先必须阐明的是,optional 是通过枚举实现的:
1 |
|
之前一直看不懂上面这段代码里的 some 是什么意思,其实some 并不是一个函数,而是一个枚举类型,里面的 Wrapped 是他的关联值。获取枚举关联值的唯一方法是通过模式匹配,就像在 switch 或 if case let 中使用的匹配方法一样。
现在,用户就不会在没有检查的情况下,错误地使用一个值了:
1 |
|
相反,假设得到的结果不是 .none,为了使用包装在可选值中的索引,你必须对其进行“解包”:
1 |
|
一种更简明的写法是使用 ? 作为在 switch 中对 some 进行匹配时的模式后缀,另外,你还可以使用 nil 字面量来匹配 none:
1 |
|
使用 if let 来进行可选值绑定 (optional binding) 要比上面使用 switch 语句要稍好一些:
1 |
|
需要注意的是,解包后的值的作用域仅限于 if 代码块中,如果想要在代码块外也可以使用解包后的变量,请使用 guard let
可选链
“在 Objective-C 中,对 nil 发消息什么都不会发生。Swift 里,我们可以通过“可选链 (optional chaining)”来达到同样的效果:”
摘录来自: Chris Eidhof. “Swift 进阶。” Apple Books.
📚 在调用可选链时,除非方法本身返回的就是一个可选值,否则无需在后面加问号:
1 |
|
🧚♀️下面来看一个返回可选值的例子
1 |
|
因为调用 half 返回一个可选结果,因此当我们重复调用它时,需要一直添加问号。因为函数的每一步都有可能返回 nil:
1 |
|
可选值 flatMap
1 |
|
问题在于, first返回可选值,并且 Int(String) 也返回可选值 (字符串可能不是一个整数),最后 x 的结果将会是 Int??。
flatMap 可以把结果展平为单个可选值。这样一来,y 的类型将会是 Int?:
1 |
|
函数
三大特性
函数可以被赋值给变量,也可以作为参数,或者返回值
这种能力可以让我们很容易的写出高阶函数,有一个地方需要注意:
1 |
|
函数能够捕获存在于其局部作用域之外的变量
当函数引用了在其作用域之外的变量时,这个变量就被捕获了,它们将会继续存在,而不是在超过作用域后被摧毁。
1 |
|
假设这时我们定义一个新的变量
1 |
|
这并不会影响我们定义的第一个函数 a,它拥有自己的 count。
一个函数和它所捕获的变量环境组合起来被称为闭包
有两种方法可以创建函数,一种是使用 func 关键字,另一种是 { }
在 swift中,后一种方法被称为闭包表达式。
1 |
|
其实还有第三种匿名函数的用法,可以让我们在定义函数的同时对他进行调用:
1 |
|
关于基于函数的协议和回调函数,我想另写一篇文章专门探讨,这里就不做详细说明了。
inout 参数和可变方法
首先注意,inout 参数并不是传递引用,inout 实际是传递值,然后复制回来,并不是传递引用。
其次,inout 参数不允许逃逸:
1 |
|
可以这么理解,inout 的值需要在函数结束之前复制回去,如果允许逃逸,那么就意味着,在函数结束之后同样允许对某个变量进行赋值,可是,如果这个时候变量已经不存在了,那么必然会导致一个错误。
属性
属性分为两种:计算属性和存储属性,实际上,函数的计算属性值是一个方法。
计算属性和用 { } 定义的函数非常相似,区别在于没有等号:
1 |
|
属性观察器
属性观察期必须在属性生命的时候就被定义,无法在拓展里被追加,不过,可以通过在子类中重写一个属性的方式来添加观察者:
1 |
|
延迟存储属性
延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。当属性的值依赖于在实例的构造过程结束后才会知道影响值的外部因素时,或者当获得属性的初始值需要复杂或大量计算时,可以只在需要的时候计算它,也可以把延迟存储属性当成一种所谓的记忆化编程方法。
例如,在一个显示 GPSTrack 的 view controller 上,我们可能会想展示一张追踪路径的预览图像。通过延迟加载,我们可以将耗时的图像生成工作推迟到属性被首次访问的时候:
1 |
|
下标
我们可以自定义下标操作:
1 |
|
Index 后面的三个点表示可以接受多个参数。
这里让我感觉困惑的地方是,数组的 Index 和字符串的 Index 好像不是同一种东西,归根结底可能还是因为 Swift 吃了屎一样的字符串吧:
1 |
|
也就是说,Array 的Index是从0, 1, 2… 以此类推的,和字符串的Index 虽然同名但是类型其实完全不同。
自动闭包
首先介绍背景, && 是逻辑与的操作, & 是按位与(二进制),二者是完全不同的,对于逻辑与来说,只有当 && 左侧为 true 时,才会对右边的操作数求值,这种行为被叫做短路求值。
值得一提的是,值绑定也是一种短路求值:
1 |
|
那么自动闭包是什么意思呢?
1 |
|
那么该如何调用这个函数呢?
1 |
|
还有一种更加优雅的解法:
1 |
|
那么采用闭包有什么好处呢?
- 自动闭包是一种简便语法.
- 自动闭包不接受任何参数,被调用时会返回被包装在其中的表达式的值。
- 自动闭包能够延迟求值,因为代码段不会被执行直到你调用这个闭包,这样你就可以控制代码什么时候执行(这里我不理解的地方是,难道闭包不也是传递一个函数进来,然后在合适的时候进行调用吗?)
逃逸闭包
我们在使用闭包的时候,有时候编译器会强制我们在闭包中使用 self(比如网络请求),但有时则不需要这么做(比如 map、filter),这两者的不同主要在于,网络请求一般是通过多线程完成的,它无法保证闭包的生命周期短于函数的生命周期,但是如果是单线程的情况下,一定是闭包先执行完毕,然后函数执行完毕。前者就需要闭包一直持有self。
对于需要一直持有 self 的闭包,我们需要显示的使用 self,并将参数标记为@escaping:
1 |
|
结构体和类
结构体和类非常相似,都能拥有存储属性和计算属性,都可以定义方法,都有初始化方法,都可以进行扩展,以及都可以实现各种协议。但有两点不同:
- 结构体是值类型,类是引用类型
- 结构体不能拥有子类,但类可以
一般来说,除非你在定义一个类的时候就希望它被继承,否则应该使用结构体。
可变方法
在结构体中,如果某个函数想要修改某个属性的值,需要用 mutating 关键字来实现。
在可变方法中,我们可以认为 self 是一个用 var 声明的变量,所以也就可以修改那些在 self 中,用 var 声明的属性。
除此之外,我们还可以通过 inout 参数直接修改某个属性的值(感觉真没必要)
声明周期
结构体是值类型,它的生命周期是和含有结构体的变量相绑定的,变量离开作用域内存被释放,结构体实例也会被销毁。
类是引用类型,我们使用「引用计数」来管理它的生命周期,当引用计数为 0 时,释放内存。
使用引用计数时,要特别小心「循环引用」的出现,常见的容易造成循环引用的情况有:
- 闭包
- 协议
- 有相互关联的两个类
解决循环引用的方式很简单,就是将一个对象指向另一个对象的引用改为「弱引用」。
其实弱引用个人理解为 a虽然引用b,但是b的引用计数并没有加 1,这样当 b 的其他引用计数为 0 时,不会因为a 的引用而导致无法销毁。
写时复制技术
值类型中的数据,一开始是在多个变量之间共享的:只有在其中一个变量修改了它的数据时,才会产生对数据的复制操作
但是,因为实现写时复制的结构体,依赖于保存在内部的一个引用,所以这个结构体每产生一份拷贝都会增加这个内部引用的引用计数。实际上,我们是放弃了值类型不需要引用计数的这个优点,来减轻值类型的复制语义这个特性所可能带来的成本。
增加或减少一个引用计数,都是一个相对较慢的操作 (这里的慢,比较的是把一些字节复制到栈上另一个位置之类的操作)。因为这样一个操作必须是线程安全的,因此就会有锁的开销。由于标准库中所有可变长度的类型 (数组,字典,集合,字符串),内部都依赖于写时复制,所以对于含有这种类型的属性的结构体,每次复制也都会带来操作引用计数的开销。
枚举
1 |
|
这里有一个很蛋疼的地方就是,some 并不是一个方法,而是一个普通的枚举类型,Wrapped是它的关联值。
就像结构体一样,枚举也是值类型。枚举的能力几乎和结构体相同:
-
枚举可以有方法,计算属性和下标操作。
-
方法可以被声明为可变或不可变。
-
你可以为枚举实现扩展。
-
枚举可以实现各种协议。
但枚举不能拥有存储属性。一个枚举的状态完全由它的成员和成员的关联值组合起来表示。对于某个特定的成员,可以将关联值视为其存储属性。
模式匹配
我们使用模式匹配来提取某个枚举类型的关联值。
1 |
|
有几种匹配的用法需要注意:
-
可选值匹配 let value? 等价于 .some(let value),也就是说,它匹配一个不为 nil 的可选值,并把解包出来的值和一个常量绑定。
-
作为在单个模式中绑定多个值的一种简写方式,你不需要在每个绑定变量前重复的写 let,只需要在模式前加一个 let 前缀就可以了。所以模式 let (x, y) 和 (let x, let y) 是一样的。请注意在单个模式中同时使用值绑定和等式匹配时,两者的细微差别:例如,模式 (let x, y) 中把元组的第一个元素和一个新的常量做绑定,但对于第二个元素,模式只是拿它与一个现有的变量 y 做比较。
-
可以仅匹配枚举成员,忽略关联值, .success 和 .success(_)是一样的写法
-
也可以在 switch 语句中使用表达式:
1
2
3
4
5
6
7let randomNumber = Int8.random(in: .min...(.max)) switch randomNumber { case ..<0: print("\(randomNumber) is negative") case 0: print("\(randomNumber) is zero") case 1...: print("\(randomNumber) is positive") default: fatalError("Can never happen") // 注意枚举的完备性 }
我们还可以使用 if case 和 guard case 来做类似于单个 switch 语句的模式匹配:
1 |
|
Switch 语句的完备性
- 一个 switch 语句必须是完备的,也就是说,它的分支必须涵盖所有可能的输入值。
- 每次你增加一个成员到一个现有的枚举时,编译器会在所有对这个枚举使用 switch 语句的地方发出警告,提醒你需要处理这个新加的成员,这对写出安全的代码很有帮助。
- 尽可能避免使用 default 分支,尽管有时不得不这么做。
- 完备性检查的最大好处体现在如果你想让枚举和使用它的代码是同步演进的时候,也就是说,每次给枚举增加一个新的成员时,所有 switch 这个枚举代码都可以被同时更新。
下面是枚举的一个十分重要的用法:即将枚举和计算属性结合起来
1 |
|
枚举实现递归
1 |
|
indirect 告诉编译器将 node 表示成一个引用,从而递归起作用。
为了理解其中的原因,回想一下枚举是值类型这件事。值类型是不能包含自身的,因为如果允许这样的话,在计算类型大小的时候,就会创建一个无限递归。编译器必须能够为每种类型确定一个固定且有限的尺寸。将需要递归的成员作为一个引用是可以解决这个问题的,因为引用类型在其中增加了一个间接层;并且编译器知道任何引用的存储大小总是为 8 个字节 (在一个 64 位的系统上)。
indirect 本身只是一个语法糖而已并不是什么编译器的黑魔法。下面是不用 indirect 的版本,我们需要借用一个类来实现手动的装箱和拆箱:
1 |
|
使用这个类,我们就可以不用 indirect 而实现之前的 List 枚举:
1 |
|
RawRepresentable 协议
一个实现 RawRepresentable 协议的类型会获得两个新的 API:一个 rawValue 属性和一个可失败的初始化方法 (init?(rawValue:))。这两个 APi 都被声明在 RawRepresentable 协议中 (编译器自动为具有原始值的枚举实现这个协议),也就是说无需手动为有原始值的枚举手添加这个协议:
1 |
|
因为对于每个 RawValue 类型的值,有可能会存在对于实现这个协议的类型来说无效的值,所以初始化方法是可失败的。例如,只有一些整数是有效的 HTTP 状态码;对于其他所有的输入,HTTPStatus.init?(rawValue:) 必须返回 nil:
1 |
|
CaseIterable 协议
CaseIterable 协议通过添加一个静态属性 allCases 来实现这个功能 (也就是说,不是在实例上,而是在类型上调用此属性):
1 |
|
对于没有关联值的枚举,编译器会自动生成实现 CaseIterable 的代码;我们所要做的就只是在声明的时候把协议加上就可以了。
1 |
|
因为 allCases 属性的类型是 Collection,所以它具有你从数组和其他集合类型中,所知的所有常用属性和功能。在下面的示例中,我们使用 allCases 来得到所有菜单项的数量,并把它们转换为适合在用户界面中显示的字符串 (为了简单起见,我们直接使用原始值作为菜单项的标题;在一个真实的 app 中,会把原始值作为一个键,用在被存储的本地化标题的查找表上):
1 |
|
字符串
我还是保持自己的观点:swift 的字符串烂透了。
所以这里我不想写。
1 |
|
泛型
至少有四种不同的概念,可以归纳到多态编程这个范畴里:
-
我们可以定义多个同名但是类型不同的方法。这种用法叫做**重载 (overloading) **。
-
当一个函数或方法接受类 C 作为参数的时候,我们也可以给它传递 C 的派生类,这种用法叫做子类型多态 。
-
当一个函数 (通过尖括号语法) 接受泛型参数的时候,我们管这个函数叫做泛型函数 (generic function) ,类似地,还有泛型类型和泛型方法。这种用法叫做参数化多态 (parametric polymorphism)。这些泛型化的参数,叫做泛型 (generics)。
-
我们可以定义一个协议并让多个类型实现它。这是另外一种更加结构化的专属多态。
我们能编写的一个最普通的函数,就是恒等函数 (identify function)。例如,一个原封不动返回参数的函数:
1 |
|
这个恒等函数有一个泛型类型 (generic type):对于任何类型 A,这个函数的类型就是 (A) -> A。但是,这个函数却有无数多个具体类型 (concrete type),也就是不带泛型参数的类型,例如 Int,Bool 或 String。例如,A 是 Int,这个函数的具体类型就是 (Int) -> Int,如果我们让 A 是 (String -> Bool),对应的具体类型就是 ((String) -> Bool) -> (String) -> Bool。
函数和方法并不是唯一的泛型类型。我们还可以有泛型结构体,泛型类和泛型枚举。