1、元祖(Tuple)
把多个值合并成单一的复合型的值。
特点:
1、元祖中的值可以是任何类型,且不必是同一类型。
2、元祖中的每一个元素可以指定对应的元素名称,即使是没有指定名称的元素也可以通过下标来获取。
3、用 var 修饰的元祖是可变元祖,用 let 修饰的元祖是不可变元祖;可以修改可变元祖中的元素,但不能修改元素类型;any 类型可以改为任何类型;无论可变元祖还是不可变元祖,元祖创建后都不能增加和删除。
2、for-in
使用 stride(from:to:by:) 函数来跳过不想要的标记 (开区间)。
闭区间也同样适用,使用 stride(from:through:by:) 即可。
3、switch
1、没有隐式贯穿:switch 语句默认不会从匹配 case 的末尾贯穿到下一个 case 里。switch 语句会在匹配到第一个的 case 执行完毕之后退出,不再需要显式的 break 语句。
2、每一个 case 的函数体必须包含至少一个可执行的语句。
3、在一个 switch 的 case 中匹配多个值可以用逗号分隔,并且可以写成多行。
4、case 匹配数据类型非常广泛,可以匹配元祖,匹配一段区间,定义变量,函数语句等等。
注:如果想要实现类似 OC 的 case 贯穿效果,可以使用 fallthrough 。(功能类似 OC 中 case 语句中没有写 break)
4、数组
1、遍历
1、For-In
2、forEach方法
无法使用 break 或 continue 跳出或者跳过循环。
使用 return 只能退出当前一次循环的执行体。
3、enumerated() : 同时得到索引和值。
4、Iterator
2、索引
1、startIndex 返回第一个元素的位置,对于数组来说,永远都是 0。endIndex 返回最后一个元素索引 +1 的位置,对于数组来说,等同于 count 。如果数组为空,startIndex 等于 endIndex 。
2、indices 获取数组的索引区间。
3、查找
1、contains(:) 判断数组是否包含给定元素;contains(where:) 判断数组是否包含符合给定条件的元素。
2、allSatisfy(:) 判断数组的每一个元素都符合给定的条件。
3、常用方法
first 返回数组第一个元素(optional),如果数组为空,返回 nil ;
last 返回数组最后一个元素(optional),如果数组为空,返回 nil ;
first(where:) 返回数组第一个符合给定条件的元素(optional);
last(where:) 返回数组最后一个符合给定条件的元素(optional);
firstIndex(of:) 返回给定的元素在数组中出现的第一个位置(optional);
lastIndex(of:) 返回给定的元素在数组中出现的最后一个位置(optional);
firstIndex(where:) 返回符合条件的第一个元素的位置(optional);
lastIndex(where:) 返回符合条件的最后一个元素的位置(optional);
min() 返回数组中最小的元素;
max() 返回数组中最大的元素;
min(by:) 利用给定的方式比较元素并返回数组中的最小元素;
max(by:) 利用给定的方式比较元素并返回数组中的最大元素;
append(:) 在末尾添加一个元素;
append(contentsOf: ) 在末尾添加多个元素;
insert(:at:) 在指定的位置插入一个元素;
insert(contentsOf: at:) 在指定位置插入多个元素;
remove(at:) 移除并返回指定位置的一个元素;
removeFirst() 移除并返回数组的第一个元素;
removeLast() 移除并返回数组的最后一个元素;
popLast() 移除并返回数组的最后一个元素(optional),如果数组为空返回 nil ;
removeFirst(:) 移除数组前面多个元素;
removeLast(:) 移除数组后面多个元素;
removeSubrange(_:) 移除数组中给定范围的元素;
removeAll() 移除数组所有元素;
removeAll(keepingCapacity:) 移除数组所有元素,保留数组容量;
4、ArraySlice
ArraySlice 是数组或者其他 ArraySlice 的一段连续切片,和原数组共享内存。当要改变 ArraySlice 的时候,ArraySlice 会 copy 出来,形成单独内存。ArraySlice 拥有和 Array 基本完全类似的方法。
1、Drop 方法
dropFirst(:) “移除”原数组前面指定个数的元素得到一个 ArraySlice。
dropLast(:) “移除”原数组后面指定个数的元素得到一个 ArraySlice。
drop(:) “移除”原数组符合指定条件的元素得到一个 ArraySlice。
2、prefix方法
prefix() 获取数组前面指定个数的元素组成的 ArraySlice。
prefix(upTo: ) 获取数组到指定位置(不包含指定位置)前面的元素组成的 ArraySlice。
prefix(through: ) 获取数组到指定位置(包含指定位置)前面的元素组成的 ArraySlice。
prefix(while: ) 获取数组前面符合条件的元素(到第一个不符合条件的元素截止)组成的 ArraySlice。
3、suffix 方法
suffix() 获取数组后面指定个数的元素组成的 ArraySlice。
suffix(from: ) 获取数组从指定位置到结尾(包含指定位置)的元素组成的 ArraySlice。
4、Range 方法
可以通过对数组下标指定 Range 获取 ArraySlice,可以使用闭区间、半开半闭区间、单侧区间、甚至可以只使用 … 来获取整个数组组成的 ArraySlice 。
5、重排
1、随机排序
shuffle() 在原数组上将数组元素打乱,只能作用在数组变量上。
shuffled() 返回原数组的随机化数组,可以作用在数组变量和常量上。
2、逆序
reverse() 在原数组上将数组逆序,只能作用在数组变量上。
reversed() 返回原数组的逆序“集合表示”,可以作用在数组变量和常量上,该方法不
会分配新内存空间。
3、分组
partition(by belongsInSecondPartition: (Element) throws -> Bool) 将数组以某个条件分组,数组前半部分都是不符合条件的元素,数组后半部分都是符合条件的元素。
4、排序
sort() 在原数组上将元素排序,只能作用于数组变量。
sorted() 返回原数组的排序结果数组,可以作用在数组变量和常量上。
5、交换
swapAt(::) 交换指定位置的两个元素。
6、拼接
joined() 拼接字符串数组里的所有元素为一个字符串。
joined(separator:) 以给定的分隔符拼接字符串数组里的所有元素为一个字符串。
6、数组实现 Stack 和 Queue
5、Set
Set 是指具有某种特定性质的具体的或抽象的对象汇总而成的集体。其中,构成 Set 的这些对象则称为该 Set 的元素。
1、特性
确定性 :给定一个集合,任给一个元素,该元素或者属于或者不属于该集合,二者必居其一。
互斥性 : 一个集合中,任何两个元素都认为是不相同的,即每个元素只能出现一次。
无序性 : 一个集合中,每个元素的地位都是相同的,元素之间是无序的。
2、遍历 Set
可以使用 For-In 遍历 Set。
因为 Set 是无序的,如果要顺序遍历 Set,使用 sorted()方法。
3、访问 Set
使用 count 获取 Set 里元素个数。
使用 isEmpty 判断 Set 是否为空。
4、添加元素
insert(_:) 添加一个元素到 Set。
update(with:) 如果已经有相等的元素,替换为新元素。如果 Set 中没有,则插入。
5、移除元素
filter(:) 返回一个新的 Set,新 Set 的元素是原始 Set 符合条件的元素。
remove(:) 从 Set 当中移除一个元素,如果元素是 Set 的成员就移除它,并且返回移除的值,如果合集没有这个成员就返回 nil 。
removeAll() 移除所有元素。
removeFirst() 移除 Set 的第一个元素,因为 Set 是无序的,所以第一个元素并不是放入的第一个元素。
6、基本 Set 操作
intersection(:) 交集,由属于A且属于B的相同元素组成的集合,记作A∩B(或B∩A)。
union(:) 并集,由所有属于集合A或属于集合B的元素所组成的集合,记作A∪B(或B∪A)。
symmetricDifference(:) 对称差集,集合A与集合B的对称差集定义为集合A与集合B中所有属于A∩B的元素的集合。
subtracting(:) 相对补集,由属于A而不属于B的元素组成的集合,称为B关于A的相对补集,记作A-B或A\B。
7、Set 判断方法
isSubset(of:) 判断是否是另一个 Set 或者 Sequence 的子集。
isSuperset(of:) 判断是否是另一个 Set 或者 Sequence 的超集。
isStrictSubset(of:) 和 isStrictSuperset(of:) 判断是否是另一个 Set 的子集或者超集,但是又不等于另一个 Set 。
isDisjoint(with:) 判断两个 Set 是否有公共元素,如果没有返回 true,如果有返回 false。
8、Set 算法
给定一个集合,返回这个集合所有的子集
思路一 通过位:解这道题的思想本质上就是元素选与不选的问题,于是我们就可以想到用二进制来代表选与不选的情况。“1”代表这个元素已经选择,而“0”代表这个元素没有选择。假如三个元素 A B C ,那么 101 就代表 B 没有选择,所以 101 代表的子集为 AC 。
思路二 通过递归:如果只有一个元素,那么它的子集有两个,分别是本身和空集,然后在已经有一个元素的子集的基础上,第二个元素有两种选法,那就是加入到前面的子集里面或者不加入到前面的子集里面,也就是选与不选的问题。而前面的子集一共有两个,对每一个子集都有来自于下一个元素的加入和不加入两种选法。那么就可以得出两个元素的子集一共有四个。依次类推,就可以得出 n 的元素所有子集(这里 n 个元素的子集一共有 2n 个,非空子集一共有 2n-1 个)。
6、函数
1、默认形式参数值
通过在形式参数类型后给形式参数赋一个值来给函数的任意形式参数定义一个默认值。如果定义了默认值,你就可以在调用函数时候省略这个形式参数。
注:
省略参数标签时,需要特别注意,避免出错。
// 这里的 middle 不可以省略参数标签
func test(_ first: Int = 10, middle: Int, _ last: Int = 30) { }
test(middle: 20)
原因:
函数调用时是通过参数名获取对应的参数。
当参数含有默认值的时候,该参数可以不传递。
当参数没有默认值的时候,参数值需要传递,此时参数名省略的话要注意,传参的时候有可能会出错。
2、可变形式参数
一个可变形式参数可以接受零或者多个特定类型的值。当调用函数的时候你可以利用可变形式参数来声明形式参数可以被传入值的数量是可变的。可以通过在形式参数的类型名称后边插入三个点符号( …)来书写可变形式参数。
传入到可变参数中的值在函数的主体中被当作是对应类型的数组。
注:
1、一个函数最多只能有一个可变参数。
2、紧跟在可变参数后面的参数不能省略参数标签,避免出错。
// 参数string不能省略标签
func test(_ numbers: Int..., string: String, _ other: String) { }
test(10, 20, 30, string: "Jack", "Rose")
3、输入输出形式参数
可变形式参数只能在函数的内部做改变。如果你想函数能够修改一个形式参数的值,而且你想这些改变在函数结束之后依然生效,那么就需要将形式参数定义为输入输出形式参数。
在形式参数定义开始的时候在前边添加一个 inout 关键字可以定义一个输入输出形式参数。输入输出形式参数有一个能输入给函数的值,函数能对其进行修改,还能输出到函数外边替换原来的值。
你只能把变量作为输入输出形式参数的实际参数,在将变量作为实际参数传递给输入输出形式参数的时候,直接在它前边添加一个和符号 ( &) 来明确可以被函数修改。
inout 本质
1、如果实参有物理内存地址,且没有设置属性观察器:直接将实参的内存地址传入函数(实参进行引用传递)。
2、如果实参是计算属性 或者 设置了属性观察器:
采取了Copy In Copy Out 的做法。
-> 调用该函数时,先复制实参的值,产生副本【get】 。
-> 将副本的内存地址传入函数(副本进行引用传递),在函数内部可以修改副本的值。
-> 函数返回后,再将副本的值覆盖实参的值【set】
总结:inout 的本质就是引用传递(地址传递)。
注:
1、可变参数不能标记为 inout。
2、inout 参数不能有默认值。
3、inout 参数只能传入可以被多次赋值的。
4、函数可以作为参数,返回值,函数内部调用函数等等。
5、函数重载
函数名相同,参数个数不同或者参数类型不同或者参数标签不同。
注:
1、返回值类型和函数重载无关。
2、函数有默认参数值时和函数重载一起使用产生二义性时,编译器并不会报错。
3、可变参数、省略参数标签、函数重载一起使用产生二义性时,编译器可能会报错。
6、内联函数
如果开启了编译器优化(Release 模式默认会开启优化),编译器会自动将某些函数变成内联函数。将函数调用展开成函数体。
设置内联函数:target -> Build Settings -> 搜索 optimization -> Optimiazation level
注:
1、哪些函数不会被自动内联?
函数体比较长、包含递归调用、包含动态派发等。
2、在 Release 模式下,编译器已经开启优化,会自动决定哪些函数需要内联,因此没必要使用 @inline。
// 永远不会被内联(即使开启了编译器优化)
@inline(never) func test() {
print("test")
}
// 开启编译器优化后,即使代码很长,也会被内联(递归调用函数、动态派发的函数除外)
@inline(__always) func test() {
print("test")
}
7、闭包
1、概念
闭包是可以被传递和引用的功能性独立代码块。闭包能够捕获和存储定义在其上下文中的任何常量和变量的引用。
全局和内嵌函数,实际上是特殊的闭包。闭包符合如下三种形式中的一种:
1、全局函数是一个有名字但不会捕获任何值的闭包;
2、内嵌函数是一个有名字且能从其上层函数捕获值的闭包;
3、闭包表达式是一个轻量级语法所写的可以捕获其上下文中常量或变量值的没有名字的闭包。
2、捕获值
一个闭包能够从上下文捕获已被定义的常量和变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍能够在其函数体内引用和修改这些值。
3、闭包是引用类型
在 Swift 中,函数和闭包都是引用类型。无论什么时候赋值一个函数或者闭包给常量或者变量,实际上都是将常量和变量设置为对函数和闭包的引用。
4、闭包分类
尾随闭包:如果你需要将一个很长的闭包表达式作为函数最后一个实际参数传递给函数,使用尾随闭包将增强函数的可读性。尾随闭包是一个被书写在函数形式参数的括号外面(后面)的闭包表达式。
逃逸闭包:当闭包作为一个实际参数传递给一个函数的时候,并且它会在函数返回之后调用,我们就说这个闭包逃逸了。当声明一个接受闭包作为形式参数的函数时,可以在形式参数前写 @escaping 来明确闭包是允许逃逸的。闭包可以逃逸的一种方法是被储存在定义于函数外的变量里。
自动闭包:自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。自动闭包包含 @autoclosure 关键字。
自动闭包允许你延迟处理,因此闭包内部的代码直到你调用它的时候才会运行。对于有副作用或者占用资源的代码来说很有用,因为它可以允许你控制代码何时才进行求值。
8、枚举
1、每个枚举都定义了一个全新的类型,正如 Swift 中其它的类型一样。
2、遍历枚举的 case
可以通过在枚举名字后面写 : CaseIterable 来允许枚举被遍历。Swift 会暴露一个包含对应枚举类型所有情况的集合名为 allCases。
3、关联值
可以定义 Swift 枚举来存储任意给定类型的关联值,如果需要的话不同枚举成员关联值的类型可以不同。
注:含有关联值的枚举类型内存占用情况如下:
1、用 1 个字节用来存储成员值(即枚举中的哪个 case)。多个 case 情况下才需要这 1 个字节,如果只有一个 case 的情况下所不需要这个字节的。
2、N 个字节存储关联值(N 为 case 中需求最大内存的关联值),任何一个 case 的关联值共用这 N 个字节。
4、初始值
枚举成员可以用相同类型的默认值预先填充(称为初始值)。
注:原始值不占用枚举变量的内存。
隐式原始值
如果枚举的原始值类型是Int、String,Swift会自动分配原始值。
enum Direction : String {
case north = "north"
case south = "south"
case east = "east"
case west = "west"
}
// 等价于
enum Direction : String {
case north, south, east, west
}
print(Direction.north) // north
print(Direction.north.rawValue) // north
enum Season : Int {
case spring, summer, autumn, winter
}
print(Season.spring.rawValue) // 0
print(Season.summer.rawValue) // 1
print(Season.autumn.rawValue) // 2
print(Season.winter.rawValue) // 3
enum Season : Int {
case spring = 1, summer, autumn = 4, winter
}
print(Season.spring.rawValue) // 1
print(Season.summer.rawValue) // 2
print(Season.autumn.rawValue) // 4
print(Season.winter.rawValue) // 5
5、预设初始值
在操作存储整数或字符串原始值枚举的时候,不必显式地给每一个成员都分配一个原始值。当没有分配时,Swift 将会自动为你分配值。
6、递归枚举
递归枚举是拥有另一个枚举作为枚举成员关联值的枚举。当编译器操作递归枚举时必须插入间接寻址层。可以在声明枚举成员之前使用 indirect 关键字来明确它是递归的。
9、属性
1、属性分类:存储属性和计算属性
存储属性:是一个作为特定类和结构体实例一部分的常量或变量。
特点:
1、存储在实例对象的内存中。
2、结构体和类可以定义存储属性,但枚举不可以。
3、在创建类或者结构体时,必须为所有存储属性设置一个合适的初始值。可以在初始化器设置,可以分配一个默认值。
计算属性:它实际并不存储值,它提供一个读取器和一个可选的设置器来间接得到和设置其它的属性和值。
特点:
1、其本质就是方法/函数。
2、不占用实例内存。
3、枚举、结构体、类都可以定义计算属性。
4、定义计算属性只能用 var 不能用 let。
2、计算属性
1、简写 setter:若一个计算属性的设置器没有为将要被设置的值定义一个名字,那么它将被默认命名为 newValue 。
2、简写 getter:如果整个 getter 的函数体是一个单一的表达式,那么 getter 隐式返回这个表达式。
3、只读计算属性
一个有读取器但是没有设置器的计算属性就是所谓的只读计算属性。只读计算属性返回一个值,也可以通过点语法访问,但是不能被修改为另一个值。
3、属性观察者
willSet 会在该值被存储之前被调用。如果你实现了一个 willSet 观察者,新的属性值会以常量形式参数传递。你可以在你的 willSet 实现中为这个参数定义名字。如果你没有为它命名,那么它会使用默认的名字 newValue 。
didSet 会在一个新值被存储后被调用。如果你实现了一个 didSet 观察者,一个包含旧属性值的常量形式参数将会被传递。你可以为它命名,也可以使用默认的形式参数名 oldValue 。如果你在属性自己的 didSet 观察者里给自己赋值,你赋值的新值就会取代刚刚设置的值。
注:
1、在初始化器中设置属性值不会触发 willSet 和 didSet。
2、在属性定义时设置初始值不会触发 willSet 和 didSet。
4、类型属性
严格来说属性可以分为以下类型
实例属性:只能通过实例访问。
存储实例属性:存储在实例内存中,每个实例都有一份。
存储计算属性:
类型属性:只能通过类型访问。
存储类型属性:整个程序运行中只有一份内存。
计算类型属性:
注:
1、使用 static 关键字来定义类型属性。对于类类型的计算类型属性,你可以使用 class 关键字来允许子类重写父类的实现。
2、不同于存储实例属性,必须要给存储类型属性设置初始值。因为类型没有像实例那样的 init 初始化器来初始化属性。
3、存储类型属性默认就是 lazy 的,在第一次使用的时候才初始化。计算被多个线程访问也能保证只初始化一次。
4、存储类型属性可以是 let。
5、枚举也可以定义类型属性。
10、方法
枚举、结构体、类都可以定义实例方法和类型方法。
实例方法:通过实例对象调用。
类型方法:通过类型调用。用 static 或者 class 关键字定义。
1、mutating
结构体、枚举是值类型,默认情况下,值类型的属性不能被自身的实例方法修改,在 func 关键字前加 mutating 可以允许这种修改。
1、在实例方法中修改属性
结构体和枚举是值类型。默认情况下,值类型属性不能被自身的实例方法修改。但可以在 func 关键字前放一个 mutating 关键字来指定方可以修改属性。
2、在 mutating 方法中赋值给 self。Mutating 方法可以指定整个实例给隐含的 self 属性。
3、枚举中使用 mutating 。枚举的异变方法可以设置隐含的 self 属性为相同枚举里的不同成员。
2、类型方法
通过在 func 关键字之前使用 static 关键字来明确一个类型方法。类同样可以使用 class 关键字来允许子类重写父类对类型方法的实现。
11、下标
1、定义
类、结构体和枚举可以定义下标,它可以作为访问集合、列表或序列成员元素的快捷方式。你可使用下标通过索引值来设置或检索值而不需要使用实例方法。
可以为一个类型定义多个下标,并且下标会基于传入的索引值的类型选择合适的下标重载使用。下标没有限制单个维度,可以使用多个输入形参来定义下标以满足自定义类型的需求。
2、语法
下标脚本允许你通过在实例名后面的方括号内写一个或多个值对该类的实例进行查询。它的语法类似于实例方法和计算属性。使用关键字 subscript 来定义下标,并且指定一个或多个输入形式参数和返回类型,与实例方法一样。与实例方法不同的是,下标可以是读写也可以是只读的。
3、参数
下标可以接收任意数量的输入形式参数,并且这些输入形式参数可以是任意类型。下标也可以返回任意类型。下标可以使用变量形式参数和可变形式参数,但是不能使用输入输出形式参数或提供默认形式参数值。
4、类型下标
实例下标,是在对应类型的实例上调用下标。你同样也可以定义类型本身的下标。这类下标叫做类型下标。你可通过在 subscript 关键字前加 static 关键字来标记类型下标。在类里则使用 class 关键字,这样可以允许子类重写父类的下标实现。
12、初始化与反初始化
1、值类型的初始化器委托
为了避免多个初始化器里冗余代码,初始化器可以调用其他初始化器来执行部分实例的初始化。
2、类的继承和初始化
1、所有类的存储属性(包括从它的父类继承的所有属性)都必须在初始化期间分配初始值。
2、Swift 为类类型定义了两种初始化器以确保所有的存储属性接收一个初始值。这些就是所谓的指定初始化器和便捷初始化器。
3、指定初始化器是类的主要初始化器。指定的初始化器可以初始化所有那个类引用的属性并且调用合适的父类初始化器来继续这个初始化过程给父类链。
4、类偏向于少量指定初始化器,并且一个类通常只有一个指定初始化器。指定初始化器是初始化开始并持续初始化过程到父类链的“传送”点。
5、每个类至少得有一个指定初始化器。如同在初始化器的自动继承里描述的那样,在某些情况下,这些需求通过从父类继承一个或多个指定初始化器来满足。
6、便捷初始化器是次要的。你可以在相同的类里定义一个便捷初始化器来调用一个指定的初始化器作为便捷初始化器来给指定初始化器设置默认形式参数。你也可以为具体的使用情况或输入的值类型定义一个便捷初始化器从而创建这个类的实例。
7、如果你的类不需要便捷初始化器你可以不提供它。在为通用的初始化模式创建快捷方式以节省时间或者类的初始化更加清晰明了的时候使用便捷初始化器。
指定初始化器:用与值类型的简单初始化器相同的方式来写类的指定初始化器。
注:指定初始化器必须从它的直系父类调用指定初始化器。
便捷初始化器:用 convenience 修饰符放到 init 关键字前定义便捷初始化器。
注:便捷初始化器必须从相同的类里调用另一个初始化器。
便捷初始化器最终必须调用一个指定初始化器。
使用规则:
1、每个类至少有一个指定初始化器,指定初始化器是类的主要初始化器。
2、默认初始化器是类的指定初始化器。
3、一个类通常只有一个指定初始化器。
4、指定初始化器必须从它的直系父类调用指定初始化器。
5、便捷初始化器必须要调用自己的指定初始化器。
3、两段式初始化
定义:Swift 的类初始化是一个两段式过程。在第一个阶段,每一个存储属性被引入类分配了一个初始值。一旦每个存储属性的初始状态被确定,第二个阶段就开始了,每个类都有机会在新的实例准备使用之前来定制它的存储属性。
原因:两段式初始化过程的使用让初始化更加安全,同时在每个类的层级结构给与了完备的灵活性。两段式初始化过程可以防止属性值在初始化之前被访问,还可以防止属性值被另一个初始化器意外地赋予不同的值。
两段式初始化--第一阶段:
1、指定初始化器或便捷初始化器在类中被调用。
2、为这个类的新实例分配内存。内存还没有被初始化。
3、这个类的指定初始化器确保所有由此类引入的存储属性都有一个值。现在这些存储属性的内存被初始化了。
4、指定初始化器上交父类的初始化器为其存储属性执行相同的任务。
5、这个调用父类初始化器的过程将沿着初始化器链一直向上进行,直到到达初始化器链的最顶部。
6、一旦达了初始化器链的最顶部,在链顶部的类确保所有的存储属性都有一个值,此实例的内存被认为完全初始化了,此时第一阶段完成。
两段式初始化--第二阶段:
1、从顶部初始化器往下,链中的每一个指定初始化器都有机会进一步定制实例。初始化器现在能够访问 self 并且可以修改它的属性,调用它的实例方法等等。
2、最终,链中任何便捷初始化器都有机会定制实例以及使用 self 。
4、安全检查
1、指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
2、指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖。
3、便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
4、初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。
5、初始化器的继承和重写
1、不像在 Objective-C 中的子类,Swift 的子类不会默认继承父类的初始化器。Swift 的这种机制防止父类的简单初始化器被一个更专用的子类继承并被用来创建一个没有完全或错误初始化的新实例的情况发生。只有在特定情况下才会继承父类的初始化器。
2、如果你想自定义子类来实现一个或多个和父类相同的初始化器,你可以在子类中为那些初始化器提供定制的实现。
3、当你写的子类初始化器匹配父类指定初始化器的时候,你实际上可以重写那个初始化器。因此,在子类的初始化器定义之前你必须写 override 修饰符。如同默认初始化器所描述的那样,即使是自动提供的默认初始化器你也可以重写。
6、初始化器的自动继承
1、如果你的子类没有定义任何指定初始化器,它会自动继承父类所有的指定初始化器。
2、如果你的子类提供了所有父类指定初始化器的实现(要么是通过规则1继承来的,要么通过在定义中提供自定义实现的),那么它自动继承所有的父类便捷初始化器。
7、可失败的初始化器
1、定义类、结构体或枚举初始化时可以失败在某些情况下会管大用。这个失败可能由以下几种方式触发,包括给初始化传入无效的形式参数值,或缺少某种外部所需的资源,又或是其他阻止初始化的情况。
2、为了妥善处理这种可能失败的情况,在类、结构体或枚举中定义一个或多个可失败的初始化器。通过在 init 关键字后面添加问号( init? )来写。
3、通常来讲我们通过在 init 关键字后添加问号 ( init? )的方式来定义一个可失败初始化器以创建一个合适类型的可选项实例。另外,你也可以使用可失败初始化器创建一个隐式展开具有合适类型的可选项实例。通过在 init 后面添加惊叹号( init! )而不是问号。
8、必要初始化器
在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器。
9、反初始化
1、在类实例被释放的时候,反初始化器就会立即被调用。你可以是用 deinit 关键字来写反初始化器,就如同写初始化器要用 init 关键字一样。反初始化器只在类类型中有效。
2、反初始化器会在实例被释放之前自动被调用。你不能自行调用反初始化器。父类的反初始化器可以被子类继承,并且子类的反初始化器实现结束之后父类的反初始化器会被调用。父类的反初始化器总会被调用,就算子类没有反初始化器。
3、每个类当中只能有一个反初始化器。反初始化器不接收任何形式参数,并且不需要写圆括号。
13、扩展 -- extension
扩展为现有的类、结构体、枚举或协议添加了新功能。这也包括了为无访问权限的源代码扩展类型的能力(即所谓的逆向建模)。扩展和 Objective-C 中的 category 类似。(与 Objective-C 的分类不同的是,Swift 的扩展没有名字。)
作用:
可以添加方法,计算属性,下标,(便捷)初始化器,嵌套类型,协议等。
扩展不能做的事情:
1、不能覆盖原有功能;
2、不能添加存储属性,不能向已有的属性添加属性观察器;
3、不能添加父类;
4、不能添加指定初始化器,不能添加反初始化器。
14、协议
1、定义
自定义类型声明时,将协议名放在类型名的冒号之后来表示该类型采纳一个特定的协议。多个协议可以使用逗号分开列出。
若一个类拥有父类,则将父类名放在该类型名的冒号之后,将协议名放在父类名之后,用逗号分开。
注:
1、协议可以用来定义方法、属性、下标的声明,协议可以被枚举、结构体、类遵守。
2、协议中定义方法时不能有默认参数值。
3、默认情况下,协议中定义的内容必须全部实现。
2、属性
1、协议可以要求所有遵循该协议的类型提供特定名字和类型的实例属性或类型属性。协议并不会具体说明属性是储存型属性还是计算型属性——它只具体要求属性有特定的名称和类型。协议同时要求一个属性必须明确是可读的或可读可写的。
2、若协议要求一个属性为可读可写的,那么该属性要求不能用常量存储属性或只读计算属性来满足。若协议只要求属性为可读的,那么任何种类的属性都能满足这个要求,而且如果你的代码需要的话,该属性也可以是可写的。
3、在协议中定义类型属性时在前面添加 static 关键字。当类的实现使用 class 或 static 关键字前缀声明类型属性要求时,这个规则仍然适用。
3、方法
1、协议可以要求采纳的类型实现指定的实例方法和类方法。这些方法作为协议定义的一部分,书写方式与正常实例和类方法的方式完全相同,但是不需要大括号和方法的主体。允许变量拥有参数,与正常的方法使用同样的规则。但在协议的定义中,方法参数不能定义默认值。
2、正如类型属性要求的那样,当协议中定义类型方法时,你总要在其之前添加 static 关键字。即使在类实现时,类型方法要求使用 class 或 static 作为关键字前缀,前面的规则仍然适用。
3、若你定义了一个协议的实例方法需求,想要异变任何采用了该协议的类型实例,只需在协议里方法的定义当中使用 mutating 关键字。这允许结构体和枚举类型能采用相应协议并满足方法要求。
4、初始化
1、协议可以要求遵循协议的类型实现指定的初始化器。和一般的初始化器一样,只用将初始化器写在协议的定义当中,只是不用写大括号也就是初始化器的实体。
2、如果一个子类重写了父类指定的初始化器,并且遵循协议实现了初始化器要求,那么就要为这个初始化器的实现添加 required 和 override 两个修饰符。
5、将协议作为类型
1、在函数、方法或者初始化器里作为形式参数类型或者返回类型;
2、作为常量、变量或者属性的类型;
3、作为数组、字典或者其他存储器的元素的类型。
6、继承
协议可以继承一个或者多个其他协议并且可以在它继承的基础之上添加更多要求。协议继承的语法与类继承的语法相似,只不过可以选择列出多个继承的协议,使用逗号分隔。
重写属性
1、子类可以将父类的属性重写为计算属性,不能将父类的属性重写为存储属性。
2、只能重写 var 属性,不能重写 let 属性。
3、重写时属性名和类型要和父类保持一致。
4、子类重写后的属性权限不能小于属性在父类里的权限。即如果属性在父类中是只读的,那么子类重写后的属性可以是只读的也可以是可读可写的;如果属性在父类中是可读可写的,那么子类中重写的属性必须是可读可写的。
注意
被 class 修饰的类型方法、下标、计算属性允许被子类重写。
被 static 修饰的类型方法、下标、类型属性不允许被子类重写。
7、类专用协议
通过添加 AnyObject 关键字到协议的继承列表,你就可以限制协议只能被类类型采纳(并且不是结构体或者枚举)。
注:Swift 为不确定的类型提供了两种特殊的类型别名:
AnyObject 可以表示任何类类型的实例。
Any 可以表示任何类型,包括函数类型。
8、协议组合
可以使用协议组合来复合多个协议到一个要求里。协议组合行为就和你定义的临时局部协议一样拥有构成中所有协议的需求。协议组合不定义任何新的协议类型。
协议组合使用 ProtocolA & ProtocolB 的形式。你可以列举任意数量的协议,用和符号连接( & ),使用逗号分隔。除了协议列表,协议组合也能包含类类型,这允许你标明一个需要的父类。
9、可选协议
你可以给协议定义可选要求,这些要求不需要强制遵循协议的类型实现。可选要求使用 optional 修饰符作为前缀放在协议的定义中。可选要求允许你的代码与 Objective-C 操作。协议和可选要求必须使用 @objc 标志标记。注意 @objc 协议只能被继承自 Objective-C 类或其他 @objc 类采纳。它们不能被结构体或者枚举采纳。
15、协议和扩展
1、扩展里添加协议
你可以扩展一个已经存在的类型来采纳和遵循一个新的协议,就算是你无法访问现有类型的源代码也行。扩展可以添加新的属性、方法和下标到已经存在的类型,并且因此允许你添加协议需要的任何需要。
2、泛型
泛型类型可能只在某些情况下满足一个协议的要求,比如当类型的泛型形式参数遵循对应协议时。你可以通过在扩展类型时列出限制让泛型类型有条件地遵循某协议。在你采纳协议的名字后面写泛型 where 分句。
3、用扩展声明采纳协议
如果一个类型已经遵循了协议的所有需求,但是还没有声明它采纳了这个协议,你可以让通过一个空的扩展来让它采纳这个协议。
4、协议扩展
协议可以通过扩展来提供方法和属性的实现以遵循类型。这就允许你在协议自身定义行为,而不是在每一个遵循或者在全局函数里定义。
5、默认实现
你可以使用协议扩展来给协议的任意方法或者计算属性要求提供默认实现。如果遵循类型给这个协议的要求提供了它自己的实现,那么它就会替代扩展中提供的默认实现。
6、协议扩展添加条件
当你定义一个协议扩展,你可以明确遵循类型必须在扩展的方法和属性可用之前满足的限制。在扩展协议名字后边使用 where 分句来写这些限制。
16、面相协议编程
1、OOP 的缺陷
1、继承机制要求你在开始之前就能设计好整个程序的框架、结构、事物间的连接关系。这要求开发者必须有很好的分类设计能力,将不同的属性和方法分配的合适的层次里面去。设计清晰明了的继承体系总是很难的。 (C++标准库不是面向对象的)。
2、结构天生对改动有抵抗特性。这也是为什么OOP领域中所有程序员都对重构讳莫如深,有些框架到最后代码量急剧膨胀变得难以维护从而失控。(修改行为比修改结构简单)。
3、继承机制带来的另一个问题就:很多语言都不提供多继承,我们不得不在父类塞入更多的内容,子类中会存在无用的父类属性和方法,而这些冗余代码给子类带来的一定的风险,而且对于层级很深的代码结构来说Bug修复将会成为难题。 (组合优于继承)。
4、对象的状态不是我们的编码的好友,相反是我们的敌人。对象固有的状态在分享和传递过程中是很难追踪调试的,尤其在并行程序编码中问题就更加明显。OOP 所带来的可变、不确定、复杂等特征完全与并行编程中倡导的小型化、核心化、高效化完全背离。 (值类型优于引用类型)。
2、POP
协议(protocol)允许将相似的方法、函数、属性放到一组。Swift 中的 class、enum 和 struct 都可以遵守协议,但只有 class 支持继承。与继承相比,协议的优势在于对象可以遵守多个协议。
使用面向协议编程,代码可以更具模块化。可以将协议视为功能块,当通过遵守新的协议添加新功能时,无需创建全新的对象。创建全新的对象太耗费时间。相反,只需增加不同的功能块。
17、泛型
1、泛型函数
1、占位符类型 T 就是一个类型形式参数。类型形式参数指定并且命名一个占位符类型,紧挨着写在函数名后面的一对尖括号里(比如 <T> )。
2、一旦指定了一个类型形式参数,就可以用它定义一个函数形式参数的类型,或者用它做函数返回值类型,或者做函数体中类型标注。在不同情况下,用调用函数时的实际类型来替换类型形式参数。
3、可以通过在尖括号里写多个用逗号隔开的类型形式参数名,来提供更多类型形式参数。
2、泛型类型
1、除了泛型函数,Swift 允许定义自己的泛型类型。它们是可以用于任意类型的自定义类、结构体、枚举,和 Array 、 Dictionary 方式类似。
2、扩展泛型类型
扩展一个泛型类型时,不需要在扩展的定义中提供类型形式参数列表。原始类型定义的类型形式参数列表在扩展体里仍然有效,并且原始类型形式参数列表名称也用于扩展类型形式参数。
3、类型约束
类型约束指一个类型形式参数必须继承自特定类,或者遵循一个特定的协议、组合协议。在一个类型形式参数名称后面放置一个类或者协议作为形式参数列表的一部分,并用冒号隔开,以写出一个类型约束。
4、关联类型
1、关联类型给协议中用到的类型一个占位符名称。直到采纳协议时,才指定用于该关联类型的实际类型。关联类型通过 associatedtype 关键字指定。
2、可以在协议里给关联类型添加约束来要求遵循的类型满足约束。
3、协议可以作为它自身的要求出现。
5、where 条件子句及扩展
6、关联类型的泛型 Where 子句
7、泛型下标
8、泛型思维
1、泛型编程——编写以类型作为参数的一个模板函数,在调用时再将参数实例化为具体的数据类型。泛型编程是一种面向算法的多态技术。
2、泛型(generic)是一种允许一个值取不同数据类型(所谓多态)的技术,强调使用这种技术的编程风格被称为泛型编程。
3、泛型编程研究对软件组件的系统化组织。目标是推出一种针对算法、数据结构和内存分配机制的分类方法,以及其他能够带来高度可重用性、模块化和可用性的软件工具。
4、与针对问题和数据的面向对象的方法不同,泛型编程中强调的是算法。是一类通用的参数化算法,它们对各种数据类型和各种数据结构都能以相同的方式进行工作,从而实现源代码级的软件重用。
5、泛型编程的通用化算法,是建立在各种抽象化基础之上的:利用参数化模版来达到数据类型的抽象化、利用容器和迭代器来达到数据结构的抽象化、利用分配器和适配器来达到存储分配和界面接口的抽象化。
18、多线程
1、常用方案
Thread
Cocoa Operation(Operation和OperationQueue)
Grand Central Dispath(GCD)
注:用法和功能和 OC 中类似
2、其他方案
Promise:
就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。
流程:
1、对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中,又称Incomplete)、Resolved(已完成,又称 Fulfilled)和 Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。
2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Pipeline:
1、将一个任务分解为若干个阶段(Stage),前阶段的输出为下阶段的输入,各个阶段由不同的工作者线程负责执行。
2、各个任务的各个阶段是并行(Parallel)处理的。
3、具体任务的处理是串行的,即完成一个任务要依次执行各个阶段,但从整体任务上看,不同任务的各个阶段的执行是并行的。
Master-Slave:
将一个任务分解为若干个语义等同的子任务,并由专门的工作者线程来并行执行这些子任务,既提高计算效率,又实现了信息隐藏。
Serial Thread Confinement:
如果并发任务的执行涉及某个非线程安全对象,而很多时候我们又不希望因此而引入锁。通过将多个并发的任务存入队列实现任务的串行化,并为这些串行化任务创建唯一的工作者线程进行处理。
3、加锁
GCD 信号量
示例:
public struct Cache {
private static var data = [String: Any]()
// private static var lock = DispatchSemaphore(value: 1)
// private static var lock = NSLock()
private static var lock = NSRecursiveLock()
public static func get(_ key: String) -> Any? { data[key] }
public static func set(_ key: String, _ value: Any) {
//信号量
// lock.wait()
// defer { lock.signal() }
lock.lock()
defer { lock.unlock() }
data[key] = value
}
}
4、工具类
import Foundation
public typealias Task = () -> Void
public struct Asyncs {
///异步相关方法
public static func async(_ task: @escaping Task) {
_async(task)
}
public static func async(_ task: @escaping Task,
_ mainTask: @escaping Task) {
_async(task, mainTask)
}
private static func _async(_ task: @escaping Task,
_ mainTask: Task? = nil) {
let item = DispatchWorkItem(block: task)
DispatchQueue.global().async(execute: item)
if let main = mainTask {
item.notify(queue: DispatchQueue.main, execute: main)
}
}
///延迟相关方法
@discardableResult
public static func delay(_ seconds: Double,
_ block: @escaping Task) -> DispatchWorkItem {
let item = DispatchWorkItem(block: block)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + seconds,
execute: item)
return item
}
///异步延迟
@discardableResult
public static func asyncDelay(_ seconds: Double,
_ task: @escaping Task) -> DispatchWorkItem {
return _asyncDelay(seconds, task)
}
@discardableResult
public static func asyncDelay(_ seconds: Double,
_ task: @escaping Task,
_ mainTask: @escaping Task) -> DispatchWorkItem {
return _asyncDelay(seconds, task, mainTask)
}
private static func _asyncDelay(_ seconds: Double,
_ task: @escaping Task,
_ mainTask: Task? = nil) -> DispatchWorkItem {
let item = DispatchWorkItem(block: task)
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + seconds,
execute: item)
if let main = mainTask {
item.notify(queue: DispatchQueue.main, execute: main)
}
return item
}
}
19、运行时
1、OC 运行时
派发方式:
直接派发 (Direct Dispatch)
直接派发是最快的, 不止是因为需要调用的指令集会更少, 并且编译器还能够有很大的优化空间, 例如函数内联等, 直接派发也有人称为静态调用。然而, 对于编程来说直接调用也是最大的局限, 而且因为缺乏动态性所以没办法支持继承和多态。
函数表派发 (Table Dispatch )
函数表派发是编译型语言实现动态行为最常见的实现方式。函数表使用了一个数组来存储类声明的每一个函数的指针。每一个类都会维护一个函数表,里面记录着类所有的函数,如果父类函数被 override 的话,表里面只会保存被 override 之后的函数。一个子类新添加的函数,都会被插入到这个数组的最后。运行时会根据这一个表去决定实际要被调用的函数。
查表是一种简单、易实现,而且性能可预知的方式。然而,这种派发方式比起直接派发还是慢一点。从字节码角度来看,多了两次读和一次跳转,由此带来了性能的损耗。另一个慢的原因在于编译器可能会由于函数内执行的任务导致无法优化。
这种基于数组的实现,缺陷在于函数表无法拓展。子类会在虚数函数表的最后插入新的函数,没有位置可以让 extension 安全地插入函数。
消息机制派发 (Message Dispatch )
消息机制是调用函数最动态的方式,也是 Cocoa 的基石。这样的机制催生了 KVO,UIAppearence 和 CoreData 等功能。这种运作方式的关键在于开发者可以在运行时改变函数的行为。不止可以通过 swizzling 来改变,甚至可以用 isa-swizzling 修改对象的继承关系,可以在面向对象的基础上实现自定义派发。
2、Swift 运行时
纯 Swift 类的函数调用已经不再是 Objective-c 的运行时发消息,而是类似 C++ 的 vtable,在编译时就确定了调用哪个函数,所以没法通过 runtime 获取方法、属性。
而 Swift 为了兼容 Objective-C,凡是继承自 NSObject 的类都会保留其动态性,所以我们能通过 runtime 拿到他的方法。
不管是纯 Swift 类还是继承自 NSObject 的类只要在属性和方法前面添加 @objc 关键字就可以使用 runtime。
方法派发
1、值类型总是会使用直接派发,简单易懂。
2、协议和类的 extension 都会使用直接派发。
3、NSObject 的 extension 会使用消息机制进行派发。
4、NSObject 声明作用域里的函数都会使用函数表进行派发。
5、协议里声明的,并且带有默认实现的函数会使用函数表进行派发。
关键字
20、框架管理方式
第三方库管理方式有:CocoaPods、Carthage、Swift Package Manager
1、CocoaPods
CocoaPods 会将第三方库的源代码编译为静态库 .a 文件或者动态框架 .framework 文件的形式,并将它们添加到项目中,建立依赖关系。
2、Carthage
Carthage 是一个轻量级的项目依赖管理工具。Carthage 主张“去中心化”和“非侵入性”。CocoaPods 搭建了一个中心库,第三方库被收入到该中心库,所以没有收录的第三方库是不能使用 CocoaPods 管理的,这就是所谓的“中心化”思想。而 Carthage 没有这样的中心库,第三方库基本上都是从 GitHub 或者私有 git 库中下载的,这就是“去中心化”。另外,CocoaPods 下载第三方库后,会将其编译成静态链接库或者动态框架文件,这种做法会修改 Xcode 项目属性配置依赖关系,这就是所谓的“侵入性”。而 Carthage 下载成功后,会将第三方库编译为动态框架,由开发人员自己配置依赖关系,Carthage 不会修改 Xcode 项目属性,这就是所谓的“非侵入性”。
安装
1、使用 brew 安装 carthage。
$ brew update
$ brew install carthage
2、创建 cartfile 文件
$ touch cartfile
3、修改 cartfile 文件
github "Alamofire/Alamofire"
github "nixzhu/MonkeyKing"
github "Snapkit/Snapkit"
Cartfile - Dependency origin
Carthage 支持两种类型的源:github,git。
github 表示依赖源,告诉 Carthage 去哪里下载文件。依赖源之后跟上要下载的库,格式为Username/ProjectName。
Git 关键字后面跟的是资料库的地址,可以是远程的 URL 地址,使用 git://、http://、ssh:// 或者是本地资料库地址。
Cartfile - Dependency Version
告诉 Carthage 使用哪个版本,这是可选的,不写默认使用最新版本。
如果是 == 1.0 表示使用1.0版本;
如果是 >= 1.0 表示使用1.0或更高的版本;
如果是 ~> 1.0 表示使用版本1.0以上但是低于2.0的最新版本,如1.2,1.6。
branch名称 / tag名称 / commit名称,意思是使用特定的分支/标签/提交,比如可以是分支名
master,也可以是提交5c8a74a。
相关命令
$ carthage update
$ carthage update -- platform iOS
Cartfile resloved 文件和 Carthage 目录
Cartfile resloved 文件
这个文件是生成后的依赖关系以及各个库的版本号,不能修改。Cartfile.resolved 文件确保提交的项目可以使用完全相同的配置与方式运行启用。 跟踪项目当前所用的依赖版本号,保持多端开发一致,出于这个原因,建议提交这个文件到版本控制中。
Carthage 目录
Checkouts 保存从 git 拉取的依赖库源文件。
Build 包含编译后的文件,包含 Mac 与 iOS 对应的 .framework。
项目配置
1、项目 Target -> Build Setting -> Search Paths -> Framework Search Paths 添加 $(PROJECT_DIR)/Carthage/Build/iOS
2、项目Target -> Build Phases -> '+' -> New Run Script Phase:
添加脚本 /usr/local/bin/Carthage copy-frameworks;
添加"Input Files" $(SRCROOT)/Carthage/Build/iOS/Alamofire.framework 等等。
3、Swift Package Manager
Swift Package Manager 是苹果推出的用于管理分发 swift 代码的工具,可以用于创建使用 swift 的库和可执行程序。
能够通过命令快速创建 library 或者可执行的 swift 程序,能够跨平台使用,使开发出来的项目能够在不同平台上运行。
集成方式
21、错误处理
1、自定义错误
1、swift 可以通过 Error 协议自定义运行时错误。
enum SomeError : Error {
case illegalArg(String)
case outOfBounds(Int, Int)
case outOfMemory
}
2、函数内部通过 throw抛出自定义 Error,可能会抛出 Error 的函数必须要加上 throws 声明。
func divide(_ num1: Int, _ num2: Int) throws -> Int {
if num2 == 0 {
throw SomeError.illegalArg("0不能作为除数")
}
return num1 / num2
}
3、需要使用 try 调用可能会抛出Error 函数。
var result = try divide(20, 10)
2、do-catch
1、可以通过 do-catch捕捉 Error。
func test() {
print("1")
do {
print("2")
print(try divide(20, 0))
print("3")
} catch let SomeError.illegalArg(msg) {
print("参数异常:", msg)
} catch let SomeError.outOfBounds(size, index) {
print("下标越界:", "size=\(size)", "index=\(index)")
} catch SomeError.outOfMemory {
print("内存溢出")
} catch {
print("其他错误")
}
print("4")
}
结果:
test()
// 1
// 2
// 参数异常: 0不能作为除数
// 4
do {
try divide(20, 0)
} catch let error {
switch error {
case let SomeError.illegalArg(msg):
print("参数错误:", msg)
default:
print("其他错误") }
}
2、抛出 Error 后,try 下一句直到作用域结束的代码都将停止运行。
3、处理 Error
处理 Error 的 2 种方式:
1、通过 do-catch捕捉 Error。
2、补捕捉 Error,在当前函数增加 throws 声明,Error 将自动抛给上层函数。若最顶层函数(main 函数)没有捕捉 Error,则程序将终止运行。
do {
print(try divide(20, 0))
} catch is SomeError {
print("SomeError")
}
func test() throws {
print("1")
print(try divide(20, 0))
print("2")
}
try test()
// 1
// Fatal error: Error raised at top level
func test() throws {
print("1")
do {
print("2")
print(try divide(20, 0))
print("3")
} catch let error as SomeError {
print(error)
}
print("4")
}
try test()
// 1
// 2
// illegalArg("0不能作为除数")
// 4
4、try?,try!
可以使用 try?,try!来处理可能会抛出 Error 的函数,这样就不用处理 Error。
func test() {
print("1")
var result1 = try? divide(20, 10) // Optional(2), Int?
var result2 = try? divide(20, 0) // nil
var result3 = try! divide(20, 10) // 2, Int
print("2")
}
test()
var a = try? divide(20, 0)
var b: Int?
do {
b = try divide(20, 0)
} catch { b = nil }
注:此处的 a 和 b 写法等价。
5、rethrows
rethrows 表明:函数本省不会抛出错误,但调用闭包参数抛出错误,那么它会将错误向上抛出。
func exec(_ fn: (Int, Int) throws -> Int, _ num1: Int, _ num2: Int) rethrows {
print(try fn(num1, num2))
}
// Fatal error: Error raised at top level
try exec(divide, 20, 0)
6、defer
1、用来定义以任何方式(抛错误、return等)离开代码块前必须要执行的代码。
2、defer 语句将延迟至当前作用域结束之前执行。
3、多个 defer 语句,后加入的先执行。
7、assert(断言)
1、很多编程语言都有断言机制:不符合指定条件就抛出运行时错误,常用于调试(Debug)阶段的条件判断。
2、默认情况下,Swift 的断言只会在 Debug 模式下生效,Release 模式下会忽略。
func divide(_ v1: Int, _ v2: Int) -> Int {
assert(v2 != 0, "除数不能为0")
return v1 / v2
}
print(divide(20, 0))
3、增加 Swift Flags 修改断言的默认行为
-assert-config Release:强制关闭断言
-assert-config Debug:强制开启断言
注:build settings -->搜索 other swift flags
8、fatalError
1、如果遇到严重问题,希望结束程序运行时,可以直接使用 fatalError 函数抛出错误(这是无法通过 do-catch 捕捉的错误)。使用了 fatalError 函数,就不需要再写 return。
func test(_ num: Int) -> Int {
if num >= 0 {
return 1
}
fatalError("num不能小于0")
}
2、在某些不得不实现、但不希望别人调用的方法,可以考虑内部使用 fatalError 函数。
class Person { required init() {} }
class Student : Person {
required init() { fatalError("don't call Student.init") }
init(score: Int) {}
}
var stu1 = Student(score: 98)
var stu2 = Student()
9、局部作用域
可以使用 do 实现局部作用域。
do {
let dog1 = Dog()
dog1.age = 10
dog1.run()
}
do {
let dog2 = Dog()
dog2.age = 10
dog2.run()
}
22、关联对象
默认情况下 extention 可以增加计算属性但不可以增加存储属性,借助于关联对象可以实现
class Person {}
extension Person {
//关联对象增加存储属性
private static var AGE_KEY: Void?
var age: Int {
get { (objc_getAssociatedObject(self, &Self.AGE_KEY) as? Int) ?? 0 }
set { objc_setAssociatedObject(self, &Self.AGE_KEY,newValue,.OBJC_ASSOCIATION_ASSIGN) }
}
}
23、资源名管理
为了避免图片名称、按钮名称、标识符名称、文案等等混乱及修改麻烦的问题,建议将资源按模块、按类别存放处理。例:在首页模块创建一个图片资源管理。HomeResource.image.imageName。
优点:方便资源统一管理。
缺点:增加工作量。