1.关于Swift
Swift是一门满足工业标准的编程语言,但又有着脚本语言般的表达力和可玩性。
支持代码预览(playgrounds)。
Swift 通过采用现代编程模式来避免大量常见编程错误:
- 变量始终在使用前初始化。
- 检查数组索引超出范围的错误。
- 检查整数是否溢出。
- 可选值确保明确处理
nil
值。 - 内存被自动管理。
- 错误处理允许从意外故障控制恢复。
2.Swift初见
2.1 简单值
let
声明常量,常量在初始不一定赋值,但只能赋值一次。var
声明变量
当你通过一个值来声明变量和常量时,编译器会自动推断其类型。如果初始值没有提供足够的信息(或者没有初始值),那你需要在变量后面声明类型,用冒号分割。
1 | let a: Float = 4 |
- 值永远不会被隐式转换为其他类型。如果你需要把一个值转换成其他类型,请显式转换。
把值转换成字符串的方法:把值写到括号中,并且在括号之前写一个反斜杠(\)。
1 | print("hello ltp \(4) 号") |
使用三个双引号(
"""
)来包含多行字符串内容。每行行首的缩进会被去除,直到和结尾引号的缩进相匹配。使用方括号
[]
来创建数组和字典,并使用下标或者键(key)来访问元素。
最后一个元素后面允许有个逗号。
数组在添加元素时会自动变大。
使用初始化语法来创建一个空数组或者空字典。如果类型信息可以被推断出来,你可以用[]
和[:]
来创建空数组和空字典。
1 | let emptyArray = [String]() |
2.2 控制流
使用 if
和 switch
来进行条件操作,使用 for-in
、while
和 repeat-while
来进行循环。
包裹条件和循环变量的小括号可以省略,但是语句体的大括号是必须的。
1 | for score in individualScores { |
- if条件不会隐形地与 0 做对比。
可以一起使用if
和let
一起来处理值缺失的情况。
这些值可由可选值来代表。一个可选的值是一个具体的值或者是nil
以表示值缺失。在类型后面加一个问号(?
)来标记这个变量的值是可选的。
另一种处理可选值的方法是通过使用??
操作符来提供一个默认值。如果可选值缺失的话,可以使用默认值来代替。
1 | let nickName: String? = nil //nickName是可选值,设为nil |
switch
支持任意类型的数据以及各种比较操作——不仅仅是整数以及测试相等。
运行 switch 中匹配到的 case 语句之后,程序会退出 switch 语句,并不会继续向下运行,所以不需要在每个子句结尾写break
。
在switch
中使用case let,是将匹配到的结果赋值给let常量。
1 | let vegetable = "red pepper" |
for-in
来遍历字典,需要一对变量来表示每个键值对。- 使用
while
来重复运行一段代码直到条件改变。
循环条件也可以在结尾,保证能至少循环一次。
你可以在循环中使用..<
来表示下标范围,不包含上界。...
包含上界。
2.3 函数和闭包
- 使用
func
来声明一个函数,使用名字和参数来调用函数。使用->
来指定函数返回值的类型。
默认情况下,函数使用它们的参数名称作为它们参数的标签,在参数名称前可以自定义参数标签,或者使用_
表示不使用参数标签。
1 | func greet(person: String, day: String) -> String { |
- 使用元组
(···)
来生成复合值,比如让一个函数返回多个值。该元组的元素可以用名称或数字来获取。
1 | func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {···} |
- 函数可以嵌套。被嵌套的函数可以访问外侧函数的变量,可以使用嵌套函数来重构一个太长或者太复杂的函数。
- 函数是第一等类型,这意味着函数可以作为另一个函数的返回值。函数也可以当做参数传入另一个函数。
- 可以使用
{}
来创建一个匿名闭包。使用in
将参数和返回值类型的声明与闭包函数体进行分离。
1 | numbers.map({ |
- 如果一个闭包的类型已知,比如作为一个代理的回调,可以忽略参数,返回值,甚至两个都忽略。
单个语句闭包会把它语句的值当做结果返回。
你可以通过参数位置而不是参数名字来引用参数——这个方法在非常短的闭包中非常有用。
当一个闭包作为最后一个参数传给一个函数的时候,它可以直接跟在圆括号后面。
当一个闭包是传给函数的唯一参数,你可以完全忽略圆括号。
1 | let sortedNumbers = numbers.sorted { $0 > $1 } |
2.4 对象和类
- 使用 class 和类名来创建一个类。
- 要创建一个类的实例,在类名后面加上括号。使用点语法来访问实例的属性和方法。
- 用
init
创建一个构造函数。 - 用
deinit
创建一个析构函数。 - 子类的定义方法是在它们的类名后面加上父类的名字,用冒号分割。
- 子类如果要重写父类的方法的话,在
func
前面需要用override
标记。 - 使用 getter 和 setter 的计算属性。
1 | class EquilateralTriangle: NamedShape { |
- 如果你不需要计算属性,但是仍然需要在设置一个新值之前或者之后运行代码,使用
willSet
和didSet
。
写入的代码会在属性值发生改变时调用,但不包含构造器中发生值改变的情况。
比如,下面的类确保三角形的边长总是和正方形的边长相同。
1 | class TriangleAndSquare { |
- 处理变量的可选值时,可以在操作(比如方法、属性和子脚本)之前加
?
。
如果?
之前的值是nil
,那么?
后面的东西都会被忽略,并且整个表达式返回nil
。
否则,可选值会被解包,之后的所有代码都会按照解包后的值运行。
在这两种情况下,整个表达式的值也是一个可选值。
1 | let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square") |
2.5 枚举和结构体
- 使用
enum
来创建一个枚举。就像类和其他所有命名类型一样,枚举可以包含方法。
默认情况下,Swift 按照从 0 开始每次加 1 的方式为原始值进行赋值,不过你可以通过显式赋值进行改变。
在下面的例子中,Ace 被显式赋值为 1,并且剩下的原始值会按照顺序赋值。
你也可以使用字符串或者浮点数作为枚举的原始值。
使用rawValue
属性来访问一个枚举成员的原始值。
1 | enum Rank: Int { |
- 使用
init?(rawValue:)
初始化构造器来从原始值创建一个枚举实例。
如果存在与原始值相应的枚举成员就返回该枚举成员,否则就返回nil
。
1 | if let convertedRank = Rank(rawValue: 3) { |
- 枚举的关联值是实际值,并不是原始值的另一种表达方法。实际上,如果没有比较有意义的原始值,你就不需要提供原始值。
- 关联值是在创建实例时决定的。这意味着同一枚举成员不同实例的关联值可以不相同。
1 | //从服务器获取日出和日落的时间的情况。 |
- 使用
struct
来创建一个结构体。结构体和类有很多相同的地方,包括方法和构造器。它们之间最大的一个区别就是结构体是传值,类是传引用。
2.6 协议和扩展
- 使用
protocol
来声明一个协议。
1 | protocol ExampleProtocol { |
2.** 类、枚举和结构体**都可以遵循协议。
1 | //类遵循协议 |
- 使用
extension
来为现有的类型添加功能,比如新的方法和计算属性。
你可以使用扩展让某个在别处声明的类型来遵守某个协议,这同样适用于从外部库或者框架引入的类型。
1 | extension Int: ExampleProtocol { |
- 你可以像使用其他命名类型一样使用协议名。
例如,创建一个有不同类型但是都实现一个协议的对象集合。当你处理类型是协议的值时,协议外定义的方法不可用。
1 | let protocolValue: ExampleProtocol = a |
2.7 错误处理
- 使用采用
Error
协议的类型来表示错误。
1 | enum PrinterError: Error { //PrinterError表示错误 |
- 使用
throw
来抛出一个错误。 - 使用
throws
来表示一个可以抛出错误的函数。 - 如果在函数中抛出一个错误,这个函数会立刻返回并且调用该函数的代码会进行错误处理。
1 | //send函数可能抛出错误 |
do-catch
处理错误。
在do
代码块中,使用try
来标记可以抛出错误的代码。
在catch
代码块中,除非你另外命名,否则错误会自动命名为error
。
1 | do { |
- 可以使用多个 catch 块来处理特定的错误。参照 switch 中的 case 风格来写 catch。
1 | do { |
- 使用
try?
将结果转换为可选的来处理错误
如果函数抛出错误,该错误会被抛弃并且结果为 nil。
否则,结果会是一个包含函数返回值的可选值。
1 | let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler") |
- 使用
defer
代码块来表示在函数返回前,函数中最后执行的代码。
无论函数是否会抛出错误,这段代码都将执行。
使用 defer,可以把函数调用之初就要执行的代码和函数调用结束时的扫尾代码写在一起,虽然这两者的执行时机截然不同。
1 | var fridgeIsOpen = false |
2.8 泛型
- 在尖括号里写一个名字来创建一个泛型函数或者类型。
1 | func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] { |
- 也可以创建泛型函数、方法、类、枚举和结构体。
1 | // 重新实现 Swift 标准库中的可选类型 |
- 在类型名后面使用
where
来指定对类型的一系列需求。
比如,限定类型实现某一个协议,限定两个类型是相同的,或者限定某个类必须有一个特定的父类。
1 | func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool |
1、基本运算符
- 赋值运算符无返回值
- %运算符为求余,注意负数的运算
- 元组的元素和长度相同时可以进行笔记(swift标准库只能实现七个元素以内的元组的比较)
- ??:空合运算符,为可选类型提供默认值
- !a:a为可选类型,对其进行解封,访问其值
a...b
a到b内的所有值,包括a和ba..<b
不包括ba...
a 到结尾,一般用于访问下标...b
开头到b- 逻辑运算(&& 和 ||)是左结合
2、字符串String和字符Character
- String支持“ + ”连接
- “”“ ”“” 多行字符串字面量
- 在每一行的结尾加上 \ 标识续行符,此处不换行
- 开头处或结尾处手动换行,标识在开头或结尾有个换行符
- 用
\
转义其他字符 - 扩展字符串分隔符:
# #
- 其中的特殊字符不需要转义
- 空字符串的
isempty
返回true - 将字符串常量赋值给var变量,则该变量可进行更改
- 字符串是值类型,在对其进行赋值时,会创建副本进行值拷贝
+
和+=
和append()
均可连接字符串- 连接时,多行字符串的结尾没有换行
"\(variable)"
:字符串插值- 会在字符串中插入variable变量
- String和character完全兼容Unicode标准
count
可以获取String的字符数- 必须遍历全部的 Unicode 标量,才能确定字符串的字符数量。
- 字符串索引
- 不同的字符会占用不同的空间,因此索引不能用整数
startIndex
是String的第一个字符的索引endIndex
是String最后一个字符的下一个位置的索引,不是String的有效下标- 空字符串的
startIndex
和endIndex
是相等的 index(before:)
获取前一个索引index(after:)
获取后一个索引index(_:offsetBy:)
获取一定偏移量的索引indices
属性:获取String的全部索引的range
insert(_:at:)
插入字符remove(at:)
删除字符removeSubrange(_:)
删除子串Substring
实例:从String中截取的用于短时处理的子串,- Substring和原String共用一段内存空间
- Substring和String都遵循
StringProtocol
- 字符串比较
- == 和 != 只要两个字符串的可扩展字型群集相等,他们就像等,即使字符标量不同
hasPrefix(_:)
和hasSuffix(_:)
是否拥有指定的前缀或后缀
- 字符串的Unicode表现形式
utf8
属性:访问一个String的UTF-8表示,是UInt8的值的集合utf16
属性:unicodeScalars
属性:每个unicodeScalar有一个value属性,可以返回21位的数值,用UInt32表示
3、集合类型
3.1 集合类型
数组Array:有序
集合Set:无序无重复
字典Dictionary:无序键值对
必须明确其类型
var声明的集合是可变的
let声明的集合不可变
3.2 数组(Array)
1)构建方法
1 | [Int]() //空数组 |
2)访问和修改
1 | // 属性 |
3)数组遍历
1 | for value in arr1 |
3.3 集合(Sets)
1)集合类型的哈希值
集合中的类型必须是可哈希化的(哈希值是Int类型,相等的对象哈希值一定相同)
所有基本类型(String、Int、Double、Bool)都是可哈希化的
没有关联值的枚举成员值是可哈希化的
自定义的类型要想作为集合和字典的类型,要遵循Hashable
协议,提供一个类型为Int
的可读属性hashValue
,和==
的实现(满足自反性、对称性、传递性)
2)创建集合
1 | Set<Int>() //空集合 |
3)访问和修改
1 | // 属性 |
4)集合遍历
1 | for value in set1 |
5)集合操作
1 | intersection(_:) //返回交集 |
3.4 字典(Dictionary)
Key必须遵循Hashable
协议
1)创建字典
1 | [Int: String]() // |
2)访问和修改
1 | count |
3)字典遍历
1 | for (key, value) in dic //遍历每对键值对 |
4、控制流
4.1 for-in
可遍历集合、range中的元素
若不需要每个元素值,可用_
写在for后面,使得遍历时不访问值
1 | stride(from:to:by:) //提供 [from, to) 区间中每隔by的数字组成的range |
4.2 while
while
:可以和let
一起用,来判断某个可选值是否为nil
repeat { } while
4.3 条件语句
1)if
可以和let
一起用,来判断某个可选值是否为nil
2)switch
不用对每个case手动添加break,若想显式贯穿case分支,可用fallthrough
,但他不会检查下一条匹配条件
每个分支不能为空,可用break
来忽略该分支
case可匹配区间,判断值是否落入区间
可匹配元组
值绑定:case分支可以将匹配的值赋值给临时变量或常量
while:case let (x, y) while x==y
复合型cases:匹配多个可能的值,用逗号隔开
复合匹配的值绑定:必须赋值给相同的变量,且绑定值的类型要相同
4.4 控制转移
- conginue
- break
- 可用来忽略case分支,或者终止循环
- fallthrough
- 不会检查下一个匹配条件,简单地使代码继续连接到下一个 case 中的代码
- return
- throw
1)带标签的语句
可以给循环体添加标签,然后用break
或者continue
来结束或者继续被标记的语句
1 | label name: while condition { |
2)guard
guard和if的功能一样,不同的是guard后面一定要有else,且else中要有控制转移语句。
将可选值的绑定作为guard的条件,可以保护guard后面的代码。
可以在紧邻条件判断的地方处理违规的情况。
4.5 检测API可用性(#available)
1 | // 可用在if或者guard语句中。*一定要有,指定在更高版本也适用 |
5、函数
5.1 可变参数(…)
参数的数量不确定
在参数的类型后面加上...
例如:numbers: Double...
在函数中会变为数组[Double]
一个函数最多允许有一个可变参数
5.2 输入输出参数(inout)
在函数中可被更改,且函数返回后仍然存在
在参数类型前加inout
,调用时要在变量前面加上&
1 | func testInout(_ a: inout Int){ a += 1 } // 函数定义 |
5.3 函数类型
函数类型可像基本类型一样使用
1)作为参数类型
可以将函数的部分实现留给调用者实现
1 | fucn printMathResult(_ addTwoInt: (Int, Int) -> Int, _ a: Int, _ b: Int){ |
2)作为返回值类型
5.4 嵌套函数
6、闭包
函数代码块,类似于C++的Lambda和OC的Block
可以捕获和存储上下文的变量的引用
闭包有三种形式:
- 全局函数:有名字,但不会捕获任何值
- 嵌套函数:有名字,可以捕获所在函数内的值
- 闭包表达式:匿名,可以捕获上下文的变量,不能设定默认值
1 | { (parameters) -> returnType in |
6.1 优化
names.sorted(by: { (s1: String, s2: String) -> Bool in return s1>s2 })
优化:
利用上下文推断参数和返回类型(可省略参数和返回类型)
names.sorted(by: { s1, s2 in return s1>s2 })
隐式返回单表达式(单表达式可省略 return)
names.sorted(by: { s1, s2 in s1>s2 })
参数名称缩写(可使用$0, $1)
names.sorted(by: { $0>$1 })
names.sorted(by: { > })
String的>
方法是个接收两个String并返回Bool的函数尾随闭包语法
是个写在函数的
()
之后的闭包表达式,函数可将其作为最后一个参数调用,且不用写出他的参数标签:names.sorted() { $0 > $1 }
若只有该闭包一个参数,可省掉
()
:names.sorted { $0 > $1 }
6.2 值捕获
闭包捕获的是值的引用
若某值不会被闭包改变,swift会为捕获的值创建一份值拷贝
若一个类实例的属性是闭包,且该闭包通过访问该实例而捕获了该实例,会导致循环引用。可用捕获列表解决。
6.3 闭包是引用类型
将函数或闭包赋值给一个常量,实际是将该常量的值设置为函数或闭包的引用,常量的值不能改变,但是函数或闭包可以改变
6.4 逃逸闭包(@escaping)
当一个闭包作为参数传入一个函数中,但是这个闭包在函数返回之后才被执行,则该闭包是逃逸闭包
使闭包逃逸:将闭包保存在一个函数外部定义的变量中。
在定义函数时,可在接收闭包的参数前加上@escaping
,指明这个闭包允许逃逸。
将闭包标记为@escaping
那么在闭包中必须显式引用self
举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。
6.5 自动闭包(autoclosure)
自动创建的闭包,用于包装传递给函数作为参数的表达式
在定义函数时,可在接收闭包的参数前加上autoclosure
,指明该参数会自动包装为闭包。
可以省略花括号,但是导致代码难以理解。
7、枚举
7.1 语法
枚举类型首字母大写
1 | enum EnumName { |
若一个enum变量的类型已知,对该变量再次赋值时可以省略enum类型,直接用.a
对其赋值
7.2 用Switch语句匹配枚举值
1 | EnumName enum1 = EnumName.a |
7.3 enum成员的遍历(.allClass)
令该enum遵循CaseIterable
协议,则可用unum1.allClass
得到所有成员,每个成员都是enum1类型的实例。
7.4 关联值
在7.1节中的a、b、c、d称为成员值,此外还可把成员值和其他类型的值(关联值)一起存储。
1 | enum BarCode { |
在switch语句中也可把每个关联值提取出来
1 | switch productCode { |
7.5 原始值/默认值(rawValue)
可为每个成员值声明一个默认值(不可改变)
要在声明的enum名字后面加上: <rawType>
来声明原始值的类型
使用成员的rawValue
属性可访问该成员的原始值
1)原始值的隐式赋值
原始值为整型或者String类型时,swift会自动赋值
整数时:第一个成员默认为0或者显式设置,之后每个原始值递增1
String时:默认为成员的名字
2)使用原始值初始化枚举实例
若在定义enum类型的时候使用了原始值,则会自动获得一个接受rawValue
参数的初始化方法返回枚举类型或者nil。
若没有对应的原始值则返回nil
7.6 递归枚举(indirect)
是一种枚举类型,存在成员使用该枚举类型作为关联值。
可在成员前加上indirect
表示该成员可递归,或者在枚举类型开头加上indirect
表示所有成员均可递归。
1 | indirect enum ArithmeticExpression { |
8、类和结构体
- Swift不用把类和结构体的定义与实现分开写
- 使用首字母大写的驼峰命名法(UpperCamelCase)命名类和结构体
- 使用lowerCamelCase命名属性和方法
8.1 类和结构体的对比
1)共同点
- 属性
- 方法
- 下标操作
- 构造器
- 可以扩展
- 遵循协议
2)类的特点
- 继承:允许一个类继承另一个类的特性
- 类型转换:允许在运行时检查和解释一个实例的类型
- 析构器:允许一个实例可以释放其拥有的资源
- 引用计数:允许对一个类多次饮用
3)类的缺点
类的上述附加功能,增加了其复杂性
4)结构体的逐一构造器
结构体有个自动生成的成员逐一构造器
类没有
5)总结
优先使用结构体,必要时(使用了OC的API、需要Control Identity)才用类。
8.2 结构体和枚举是值类型
值类型:被赋值或传递给函数时会被拷贝
8.3 类是引用类型
赋值时不会拷贝
1)恒等运算符===
和!==
用来判断两个变量是否引用同一个类实例
2) 指针
9、属性
存储属性会将变量存储为实例的一部分,可用于类和结构体
计算属性只是计算值,可用于类、结构体和枚举
属性观察器可用来检测属性值的变化,来触发自定义操作
属性包装器可用来复用多个属性的getter和setter中的代码
9.1 存储属性
1)常量结构体实例的存储属性
若一个结构体实例被声明为常量后,因为它是值类型,所以他的属性无论是不是可变属性,都不能改变。
2)延时加载存储属性(lazy)
在属性声明前用lazy
标识,且必须时var
变量。
第一次被调用时才会计算其初始值。
可以避免类中不必要的初始化工作。
适用于:a)属性的值依赖于外部因素,且这些外部因素在构造结束之后才会知道的时候。b)属性的值需要大量的计算,因此要在需要的时候再计算。
若lazy
属性在没有初始化时被多个线程访问,则无法保证该属性只会被初始化一次。
9.2 计算属性(getter/setter)
提供一个getter和可选的setter,来间接获取或设置其他属性的值
1)简化Setter声明
若没有定义新值的参数名,可使用默认名称newValue
2)简化Getter声明
若整个getter表达式是单一表达式,则可省略return
3)只读计算属性
只有getter没有setter,可以去掉get
关键字和花括号
该属性必须使用var定义
9.3 属性观察器(willset/didset)
可为自定义的存储属性以及继承的存储属性和计算属性添加属性观察器。
对于继承的属性,通过重写属性来添加属性观察器
对于自定义的属性,通过setter来添加属性观察器
willset
新值设置前调用,会将新值作为参数传入,默认名称为newValue
didset
新值设置后调用,会将旧值作为参数传入,默认名称为oldValue
父类初始化完成之后,在子类中给父类属性赋值,会调用父类属性的willset
和didset
父类初始化之前,给子类属性赋值,不会调用子类属性的观察器
带有观察器的属性,通过in-out方式传入参数,也会调用观察器
9.4 属性包装器(@propertyWrapper)
使得所有使用同一个属性包装器的属性,均满足同一个规则
被包装值对外展示的变量名一定是
wrappedValue
1 | // 定义属性包装器,使属性值小于等于12 |
1)设置被包装属性的初始值
通过将实参传递给包装器,可设置被包装属性的初始化状态,并为其提供所需的其他参数,且能传到构造器中。
1 |
|
2)从属性包装器中呈现一个值
被呈现值的参数名一定是
projectedValue
在调用包装器的结构体中,使用被呈现值时可省略
self.
1 |
|
9.5 全局变量和局部变量
也可对全局变量和局部变量定义计算属性以及观察属性。
全局变量都是延迟计算,局部变量从不延迟计算。
9.6 类型属性(static)
上述的属性是实例属性,每个实例的属性之间是独立的。
类型属性是指,每个实例的类型属性之间是共享的。用于定义所有实例共享的数据,类似于静态变量。
存储型类型属性可以是常量或者变量,必须指定默认值,且是延迟初始化的。
计算型类型属性只能是变量。
1)类型属性语法
1 | // 在`var`前加上`static`表示该变量是类型属性 |
2)获取和设置类型属性的值
通过类名来获取的
10、方法
类、结构体、枚举都能定义方法
10.1 实例方法(Instance Methods)
实例方法可以访问该类型的方法和属性。
实例方法只能被一个具体的实例调用。
1)self 属性
self完全等同于该实例本身。
在方法的参数名与某个属性名相同时,参数名享有优先权,可用self来标识属性名。
2)在实例方法中修改值类型
结构体和枚举是值类型,其属性不能在方法中被修改。
若在方法定义的func
前附加mutating
,表面该方法是可变方法,则该方法可以修改属性。
但是常量结构体实例则不能调用可变方法。
在可变方法中也可以给self
赋值。
10.2 类型方法(static)
在func
前加上static
,表明该方法是类型方法。
和类型属于一样,也可用class
表示该方法可被子类重写。
类型方法中的self
指的是类型本身,而不是某实例。
在类型方法中,可以直接通过其他类型方法名来调用,而不需要附加类型名。
11、下标(subscript)
使用下标的索引代替存取方法来设置和获取值。
11.1 下标语法
可以设为可读可写,或者只读
1 | subscript(index: Int) -> Int { |
11.2 类型下标
类似于类型属性和类型方法,可用static
和calss
12、继承
只有类可以继承。
子类可以调用、访问、重写父类的方法、属性、下标。
为子类中继承而来的属性添加观察器可以得到他被修改的通知。
12.1 基类
只要一个类不继承其他的类,那他就是基类。
12.2 重写
在重写的方法、属性、下标前要加上override
关键字
1)访问超类的方法,属性及下标
在子类重写的方法、属性的setter/getter、下标中,使用super
调用对应的方法即可访问父类的方法。
2)重写属性的setter和getter
不管父类是存储属性还是计算属性,子类都可定制setter和getter
可以将继承来的只读属性重写为读写属性,但不能将继承来的读写属性重写为只读属性。
若在重写的属性中提供了setter,那么一定也要提供getter。
3)重写属性观察器
不能为继承来的常量存储属性或只读计算属性添加观察器。
不能同时为属性提供重写的setter和观察器。
12.4 final防止重写或继承
在定义前附加final
可以防止方法、属性、下标被重写,或者防止类被继承
编译器会自动对final标识的类、方法、属性、下标进行优化。
13、构造过程
13.1 存储属性的初始赋值
若一个属性总是有相同的初始值,最好在定义时为其设置一个默认值,而不是在构造器中赋值
13.2 自定义构造过程
形参名字及类型,实参标签
构造过程中,常量属性可在任何时间初始化,单初始化之后不可更改
13.3 默认构造器
若所有属性都有默认值,且没有自定义构造器,swift会自动为其提供一个默认构造器
若在结构体中没有自定义构造器,swift会自动生成一个逐一成员构造器,不管属性有没有默认值。
13.4 值类型的构造器代理
构造器代理:在构造器中调用其他构造器完成部分功能
值类型的构造器内部,可用self.init
调用该类型中的其他构造器
若为值类型定义了自定义构造器,那么swift就不会为其生成默认构造器。若想同时使用自定义构造器和默认构造器,可将自定义构造器写在extension
中
13.5 类的继承和构造过程
类中所有继承自父类的存储属性都要在构造过程中设置初始值
1)指定构造器(init)
指定构造器初始化类中的所有属性,并调用父类的构造一直向上进行。
一个类必须有至少一个指定构造器。
1 | init(parameters){ |
2)便利构造器(convenience init)
在需要时才为一个类提供便利构造器。
用来调用同一个类中的指定构造器,并为部分形参提供默认值。
1 | convenience init(parameters){ |
3)构造器代理
构造器之间的代理调用应遵循以下规则:
- 指定构造器必须调用父类的指定构造器
- 便利构造器必须调用同类中的其他构造器
- 便利构造器最后必须调用指定构造器
即:
- 指定构造器必须总是向上代理
- 便利构造器必须总是横向代理
4)两段式构造过程
两段式构造过程:
- 阶段一:向上(初始化构造器所在类的存储属性)
- 调用某个指定构造器或者便利构造器
- 完成新实例的内存分配
- 完成当前构造器所在类的所有存储属性的初始化
- 切换到父类的构造器,对其完成所有存储属性的初始化
- 向上一直传递到最顶部,在完成最顶部的所有存储属性的初始化后,认为该实例内存完全初始化
- 阶段二:向下(对构造器所在类的父类的属性重新赋值)
- 从继承链顶部向下,每个类的指定构造器都可进行进一步自定义实例(访问self、修改属性、调用实例方法)
- 最终,继承链中的任意便利构造器都可进一步自定义实例
在调用父类构造器
super.init
之前,要初始化本类的存储属性(一阶段)在调用父类构造器
super.init
之后,可以修改任何存储属性值(二阶段)
优点:
- 可以防止属性值在初始化之前被访问
- 可以防止属性被另一个构造器意外地赋予不同的值
为保证两段式构造正确完成,编译器会执行四种安全检查:
- 指定构造器必须保证它所在类的所有属性初始化完成之后,才将其他构造任务向上代理给父类
- 指定构造器必须在为继承的属性设置新值之前,向上代理调用父类构造器
- 便利构造器必须在为任意属性赋新值之前,代理调用其他构造器
- 构造器在第一阶段完成之前,不能调用任何方法、属性,不能引用``self`作为一个值
5)构造器的继承和重写
swift中子类默认不继承父类的构造器
子类重写父类的指定构造器,要加上
override
。‘重写’便利构造器,不用加,因为无法访问父类的便利构造器,不算‘重写’若在子类的二阶段没有自定义操作,且父类有无参构造函数
init()
,那么在子类中可省略super.init()
子类在构造过程中,可以修改继承过来的变量属性,不能修改常量属性。
6)构造器的自动继承
- 若子类没有定义任何指定构造器,他会自动继承父类所有的指定构造器。
- 若子类中提供了所有父类的指定构造器(包括上条规则获得的指定构造器),他会自动继承父类所有的便利构造器。
7)指定构造器和便利构造器实践
1 | class Food { |
1 | class RecipeIngredient: Food { |
1 | class ShoppingListItem: RecipeIngredient { |
13.6 可失败构造器(init?)
可失败构造器不能和非可失败构造器有相同的参数名和参数类型
只要return nil
来表面创建失败即可,不需要用return表明成功
1)枚举类型的可失败构造器
可根据传入的参数,判断获取哪个枚举成员,匹配不到则返回nil
2)带原始值的枚举类型的可失败构造器
自带init?(rawValue:)
构造器
3)构造失败的传递
可失败构造器,可横向代理或向上代理其他可失败构造器。
可失败构造器可以被代理到非可失败构造器中。
若在代理过程中构造失败,构造过程会立即终止。
4)重写一个可失败构造器
子类的非可失败构造器或者可失败构造器都可以重写父类的可失败构造器。
可失败构造器不能重写非可失败构造器。
子类的不可失败构造器可用
!
强制解包来调用父类的可失败构造器。
5)init! 可失败构造器(尽量不用)
可以得到一个隐式解包的可选类型对象,一旦init!
构造失败,会触发断言
init?
和init!
可相互代理
13.7 必要构造器(required)
在类的构造器前加上required
表明所有的子类都要实现该构造器,且子类前也要加上required
,不用加override
13.8 通过闭包或函数设置属性的默认值
通过闭包或者全局函数,可以为某个属性提供定制的默认值。
但是闭包在执行时,实例的其他部分还未完成初始化,闭包无法访问实例的其他部分。
1 | class SomeClass { |
14、析构过程(deinit)
析构器只能用于类,用
deinit
来标识,不带任何参数和括号
1 | deinit { |
用来清理自己定义的资源
析构器可以访问实例的所有属性,并对其进行修改
15、可选链
是一种可以在可选值上调用方法、属性、下标的方法
多个调用可以连接在一起形成调用链,链中一个节点为nil,就会返回nil
15.1 使用可选链式调用代替强制展开
在可选值后加上?
可以定义一个可选链。
可用来代替!
的强制展开
15.2 通过可选链式调用来调用方法、属性、下标
通过可选链调用方法时,返回的是个可选值,void方法会返回void?,通过判断方法返回是否时nil即可判断方法是否被调用:
1 | if john.residence?.printNumberOfRooms() != nil {} else {} //调用方法 |
16、错误处理
16.1 表示与抛出错误(Error和throw)
错误要遵循
Error
协议枚举类型尤其适合构建一组相关的错误状态,关联值还能提供额外的信息。
用
throw
抛出一个错误。
16.2 处理错误
1)用 throwing 函数传递错误
- 在函数的括号后面加上
throws
表示该函数可以抛出错误,该函数被称为throwing函数 - throwing函数可以传递错误,非throwing函数只能在函数内部处理错误,不能传递
- 调用throwing函数的方法,要么使用
do-catch、try?、try!
来处理错误,要么将错误继续传递下去
2)用 Do-Catch 处理错误
1 | do { |
3)将错误转换成可选值(try?)
1 | let x = try? someThrowingFunction() //若someThrowingFunction()抛出错误,则x为nil |
4)禁用错误传递(try!)
若出现错误,会得到运行时错误
16.3 指定清理操作(defer)
defer
语句能保证语句中到代码在当前代码块的最后执行,不管是抛出错误、return、break语句中不能有任何控制转移语句
语句中的执行顺序是从后往前
17、类型转换(is和as)
17.1 检查类型(is)
用is
来检查一个实例是否是某个特定的子类型:if item is Movie {}
17.2 向下转型(as?和as!)
if let movie = item as? Movie {}
转换并没有改变实例,只是把它当成被转换成的类型来使用
17.3 不确定类型(Any和AnyObject)
Any
表示任何类型
AnyObject
表示任何实例
18、嵌套类型
枚举、类、结构体均可嵌套
19、扩展(extension)
给现有的类、结构体、枚举、协议添加新功能,但是不能重写已经存在的功能。
扩展的功能:
- 添加计算型的实例属性和类属性
- 添加实例方法和类方法
- 添加新的构造器
- 添加新的下标
- 添加新的嵌套类型
- 使已经存在的类型遵循一个协议
19.1 扩展的语法
1 | extension SomeType {} //给SomeType添加新功能 |
19.2 计算型属性
只能添加计算属性,不能添加存储属性和观察器
19.3 构造器
- 可以给类添加便利构造器,不能添加指定构造器和析构器。
- 若一个值类型的所有存储属性都有默认值,且没有自定义构造器,那么在扩展中可以使用默认构造器和成员添加新的构造器。
- 若值类型已经有了自定义的构造器,则无法为其添加新的构造器。
- 若在扩展中给另一个模块中定义的结构体添加构造器,那么在调用那个结构体的构造器之前不能访问self。
19.4 方法
通过扩展添加的方法也可以改变实例本身,若是结构体和枚举类型,要在方法前加上mutating
20、协议(protocol)
20.1 协议语法
1 | protocol SomeProtocol { //定义协议 |
20.2 协议作为类型
协议可以作为类型使用,若某变量被声明为某个协议,则该变量可接收任何遵循该协议的类型。
- 作为参数类型、返回类型
- 作为常量、变量、属性的类型
- 作为容器中的元素类型(表示该容器的元素可以时遵循该协议的任何类型)
20.3 委托
是一种设计模式,将一些需要自己负责的功能委托给其他类型的实例
实现:将要被委托的功能定义为协议。
20.4 协议应用于类型的扩展中
1)让一个已存在类型遵循某协议
若一个类已经满足了某协议的要求,但是并没有遵循该协议,可以通过增加一个空的扩展,让该类遵循该协议。
1 | protocol TextRExpresentable {...} //定义新的协议 |
2)为一个已存在类型添加新的协议
1 | extension Dice: TextRExpresentable {...} //为Dice增加TextRExpresentable |
20.5 协议的继承
通过协议的继承,可以增加新的功能
1 | protocol PrettyTextRepresentable: TextRepresentable {...} //定义新的协议 |
20.6 有条件地遵循协议(where)
泛型协议可能在某些类型的参数下才遵循一个协议。
1 | //在array的元素遵循SomeProtocol时,Array才遵循SomeProtocol协议 |
20.7 类专属的协议(:AnyObject)
当某协议要求遵循他的类型必须时引用类型时,可以添加AnyObject
到它的继承列表,即可限制该协议只能被类类型遵循.
在某协议继承AnyObject
之后,即可用遵循他的类型来定义一个弱引用,以此解决循环引用问题。
20.8 协议合成(&)
将多个协议组合到一个要求中。
1 | //任何遵循来Named和Aged协议的类型都可作为参数传入wishHappyBirthday函数 |
20.9 检查协议一致性(is、as)
is
:检查某个实例是否遵循某协议
as?
:将一个实例转为某协议类型
20.10 协议的可选方法和可选属性(optional)
optional
标识的要求可以不被实现
用于和OC打交道的代码,协议和要求都要加上@objc
@objc
的协议只能被其他的@objc
协议或者继承自OC类的类遵循
可选要求的类型会自动变为可选的(整个函数类型是可选的,标识该函数可能不被实现)。
20.11 协议扩展
通过扩展协议,来为遵循协议的类型提供新功能,无需在每个遵循该协议的类型中都实现该功能。
但是不能声明该协议继承另一个协议。
1)提供默认实现
通过协议扩展可为一个协议提供默认实现。
若遵循该协议的类型定义了自己的实现,那么将会覆盖默认实现。
2)为协议扩展添加限制条件
扩展协议时可以增加限制,使得满足限制条件的类型才能使用默认实现
1 | //只要集合中的元素遵循Equatable协议,集合才能使用此扩展 |
21、泛型
1 | func swapTwoValues<T>(_ a: inout T, _ b: inout T){} |
T是类型参数,类型参数的命名也是首字母大写的驼峰命名法。
21.1 泛型扩展
对泛型进行扩展时,不需要再次提供类型参数,可直接使用原始类型的类型参数
21.2 类型约束
限制指定的类型必须要继承某些类,或者遵循某些协议。
1 | // T要是SomeClass的子类,U要是遵循SomeProtocol的类型 |
例如:Dictionary
的key必须是遵循Hashable
协议的
21.3 关联类型(associatedtype)–只有协议有关联类型
1)关联类型实践
1 | protocol Container { |
2)给关联类型添加约束
1 | protocol Container { |
3)在关联类型约束里使用协议
1 | protocol SuffixableContainer: Container { |
21.4 泛型 Where 语句
可以约束多个类型参数之间的关系。
1 | func allItemsMatch<C1: Container, C2: Container> |
泛型where语句可加在:
- 泛型函数的大括号前
- 扩展的大括号前
- 关联类型之后
- 继承的协议名之后
21.5 泛型下标
1 | protocol Container { |
22、不透明类型(some)
泛型函数允许调用方在调用泛型方法时,为形参和返回值指定一个与实现无关的类型。
返回不透明类型的函数允许函数在实现时,选择与调用方无关的类型。
22.1 不透明类型解决的问题
1 | protocol Shape { |
暴露出了返回的具体类型是JoinedShape<Triangle, FlippedShape<Triangle>>
,调用方就要声明一个同样完整的类型来接收
使用不透明类型就可以隐藏函数返回的具体类型,只要返回shape
即可
22.2 返回不透明类型
1 | struct Square: Shape { //矩形 |
若函数中多个地方都返回不透明类型,要保证所有的返回值都是同一类型。
在返回的不透明类型中使用泛型是可以的。
22.3 不透明类型和协议类型的区别
区别在于是否保证类型一致性:
一个不透明类型只能对应一个类型,而协议可以对应多个类型。
协议更灵活,底层可以存储更多样的值。但是返回类型的不确定性,也意味着很多依赖返回类型信息的操作也无法执行。比如下述函数的返回值无法用==来比较,因为==依赖于具体的类型信息,协议类型无法提供。(使用关联类型的协议不能作为返回类型)
不透明类型则保留了底层类型的唯一性。
1 | //只要是遵循Shape协议的类型都可以作为返回类型 |
23、自动引用计数
仅适用于类的实例
23.1 自动引用计数的工作机制
当创建一个实例时,ARC会为其分配一块内存,存储他的类型信息、存储属性的值。
当把它赋值给其他变量时,会创建它的强引用。当将该变量赋值nil时,会断开强引用。
若一个实例,没有强引用指向它,他就会立即被销毁。
23.2 类实例之间的循环强引用
1 | class Person { |
23.3 解决实例之间的循环强引用
【*】项目中尽量使用weak而不是unowned
当其他的实例有更短的生命周期时,使用弱引用。当其他实例有相同的或者更长生命周期时,使用无主引用。
1)弱引用(weak)
适用于两个属性都允许为nil
如:“人-公寓”,够可以为nil,人存在的周期更短,所以设置指向人的属性使用weak。
弱引用变量要声明为可选类型。
2)无主引用(unowned)
适用于有一个属性不允许为nil
要确保无主引用始终指向一个未销毁的实例
如:“人-银行卡”,银行卡对应的人不能为nil,人存在的周期更长,所以指向人的属性使用unowned
无主引用通常都希望拥有值,因此不会将其设为可选类型。
3)无主引用和隐式解包可选值属性
适用于两个属性都必须有值,这时要一个类使用无主属性,而另外一个类使用隐式解包可选值属性
如:“首都-国家”,
1 | class Country { |
23.4 闭包的循环强引用
将闭包赋值给某个类实例的属性时,若闭包中使用了该类实例的属性或方法,也会发生循环引用。
1)解决闭包的循环强引用(定义捕获列表)
1 | lazy var someClosure = { |
2)弱引用和无主引用
当闭包和捕获的实例互相引用且同时销毁时,将捕获定义为无主引用。
被捕获的引用可能是nil时,将捕获定义为弱引用。
24、内存安全
24.1 理解内存访问冲突
内存冲突主要出现在使用in-out参数的函数或者结构体的mutating方法中。
24.2 In-Out 参数的访问冲突
1 | //情况1.函数中访问的全局变量作为参数传入函数 |
24.3 方法里 self 的访问冲突
1 | //mutating方法会对self进行写访问 |
24.4 属性的访问冲突
结构体、元组、枚举等值类型,若同时访问其中对多个属性,就会产生冲突。
因此访问一个属性就相当于访问整个实例。
1 | var playerInformation = (health: 10, energy: 20) //元组 |
遵循以下原则时即可保证对结构体属性的重叠访问是安全的:
- 访问的实例的存储属性,而不是计算属性或者类属性
- 访问的局部变量,而不是全局变量
- 结构体要么没被闭包捕获,要么被非逃逸闭包捕获
25、访问控制
25.1 模块和源文件
swift中的访问控制是基于模块和源文件的。
模块:用来构建和发布的独立代码单元、框架、应用程序。(模块之间可以使用import
导入)
源文件:一个源代码文件
25.2 访问级别
open、public
:可以被同一模块中的所有实体访问,模块外也可以通过import访问,用来指定框架的外部接口。区别在于open只能作用于类和类成员,且限定的类和类成员在模块外能被继承和重写。internal
:可以被同一模块中的所有实体访问,模块外不能访问。fileprivate
:只能在其定义的文件内访问。private
:只能在其定义的作用域内访问。
访问级别基本原则:
实体不能定义在更低访问级别的实体中。
默认访问级别:
internal。
单 target 应用程序的访问级别:
使用默认的internal即可,也可使用更严格的。
框架的访问级别:
对外接口使用public或者open,内部可使用internal及更严格的权限。
单元测试 target 的访问级别:
在单测目标中导入应用程序的语句前加上
@testable
,可以使得该单测目标访问应用程序中的所有级别的实体。
25.4 自定义类型
若将一个类型的访问级别指定为public,他的所有属性会默认为internal,而不是public。
元组类型
元组的访问类型由元组中级别最严格的元素来决定,而不是显式指定。
函数类型
函数的访问级别由最严格的参数或者返回类型的级别来决定。
若这种级别不符合函数所处环境的默认级别,就需要显式指定。
1
2//函数返回的是private,但是默认的是internal,此时需要手动为其指定private级别
func someFunc() -> (SomeInternalClass, SomePrivateClass) {}枚举类型
枚举成员的访问级别和枚举类型相同,不能对枚举成员单独指定。
原始值和关联值的级别不能低于枚举类型的级别。
嵌套类型
嵌套类型的级别和包含他的级别相同,public除外。
25.5 子类
子类的访问级别不能高于父类
可以通过重写给继承的类的成员提供更高的访问级别。
子类成员可以访问父类中级别更低的成员。
25.6 常量、变量、属性、下标
- 不能拥有比他们的类型更高的访问级别。
- 他们的setter和getter的级别和类型的级别相同,且setter的级别可以低于getter的级别。
- 使用
fileprivate(set),private(set)
等可以改变存储属性的setter的访问级别。 public private(set) var name = ""
:设定name的getter为public,setter为private
25.7 构造器
- 自定义构造器的访问级别可以低于或等于其所属类型的级别。
- 必要构造器的级别一定等于和其所属类型的访问级别。
- 默认构造器的访问级别等于其所属类型的访问级别,除非所属类型是
public
。 - 若结构体存在
private
存储属性,则他默认的成员逐一构造器的访问级别就是private
。
25.8 协议
- 在定义协议时为其指定访问级别,将限制该协议只能在该访问级别范围内被遵循,且协议中的方法和属性的访问级别和该级别相等,不能指定其他级别。
public
协议的方法和属性的级别也是public
。- 若一个协议继承了其他协议,则该协议的访问级别不能比它继承的协议的访问级别要高。
- 一个类可以遵循比它级别低的协议,遵循协议时的上下文级别是类型和协议中最小的那个。
25.9 Extension
- 用扩展增加新成员时,新成员的访问级别和原始类型一致。
- 给extension指定访问级别,则扩展中的所有成员都是指定的级别。
- 使用扩展来遵循协议时,不能给扩展指定访问级别。
25.10 泛型
泛型类型、函数的访问级别取决于函数本身的级别、类型参数的类型的级别的最低级别
25.11 类型别名
类型别名会被当作不同的类型,访问级别不能高于其表示的类型的访问级别。
但是public级别的类型别名只能作为public类型的别名。
26、高级运算符
26.1 位运算符
按位取反运算符
~
按位与运算符
&
按位或运算符
/
按位异或运算符
^
按位左移
<<
、右移运算符>>
无符号数填充0,有符号数填充符号位
26.2 溢出运算符
允许溢出的运算符都是&
加上基本运算符&+ &- &*
。
26.3 运算符重载
1 | static func + (left: Vector2D, right: Vector2D) -> Vector2D {} //中缀运算符 |
26.4 自定义运算符
新运算符要用operator
在全局定义,同时要指定prefix
、infix
或者 postfix
修饰符
26.5 自定义中缀运算符的优先级
1 | infix operator +-: AdditionPrecedence //声明+-运算符属于AdditionPrecedence优先级组 |
对同一个值同时使用前缀和后缀运算符,则后缀运算符会优先参与运算。