基础部分
数值型类型转换(Numeric Type Conversion)
和c语言的强制类型转换类似,利用括号进行数值型类型转换:
1 | let two: UInt16 = 2000 |
注意:
SomeType(ofInitialValue) 是调用 Swift 构造器并传入一个初始值的默认方法。在语言内部, UInt16 有一个构造器,可以接受一个 UInt8 类型的值,所以这个构造器可以用现有的 UInt8 来创建一个新的 UInt16 。
所以说他不能传入任意类型的值,只能传入 UInt16 内部有对应构造器的值。不过你可以扩展现有的类型来让它可以接收其他类型的值(包括自定义类型)
当用这种方式来初始化一个新的整数值时,浮点值会被截断。也就是说 4.75 会变成 4 , -3.9 会变成 -3 ,如下:
1 | let three = 1.99 |
注意:
结合数字类常量和变量不同于结合数字类字面量。字面量 3 可以直接和字面量 0.14159 相加,因为数字字面量本身没有明确的类型。它们的类型只在编译器需要求值的时候被推测。
类型别名(type aliases)
使用 typealias 关键字来定义类型别名
1 | typealias Sample = UInt16 |
布尔类型(bool)
如果你在需要使用 Bool 类型的地方使用了非布尔值,Swift 的类型安全机制会报错。
1 | let i = 1 |
但是可以这样做:
1 | let i = 1 |
元组(tuples)
元组(tuples)把多个值组合成一个复合值。元组内的值可以是任意类型,并不要求是相同类型。
分解的时候可以把要忽略的部分用下划线( _ )标记,还可以通过下标来访问元组中的单个元素:
可以在定义元组的时候给单个元素命名,然后通过名字来获取元素的值:
1 |
|
可选类型(optionals)
下面的例子使用 toInt 方法来尝试将一个 String 转换成 Int :
1 | let possibleNumber = "123" |
因为 toInt 方法可能会失败,所以它返回一个_可选类型(optional) Int ,而不是一个 Int 。一个可选的 Int 被写作 Int? 而不是 Int 。问号暗示包含的值是可选类型,也就是说可能包含 Int 值也可能不包含值。(不能包含其他任何值比如 Bool 值或者 String 值。只能是 Int 或者什么都没有。)
强制解析(forced unwrapping)
当确定可选类型包含值的时候,就可以加个感叹号来获取值。
1 | if convertedNumber != nil { |
可选绑定(optional binding)
可选绑定可以用在 if 和 while 语句中来对可选类型的值进行判断并把值赋给一个常量或者变量,然后在内部可以使用这个值
1 | //如果这里不加?声明 optionalString 是一个可选值的话,第二行就会报错,提示变量没有初始化 |
多个可选绑定可以用逗号区分成一列表达式出现在一个if语句中。
1 | if let constantName = someOptional, anotherConstantName = someOtherOptional { |
我觉得这个很有用,利用if避免了错误的发生,又实现了类型强制解析的目的,当然用 空合运算符 也很好
隐式解析可选类型(implicitly unwrapped optionals)
把想要用作可选的类型的后面的问号(String?)改成感叹号(String!)来声明一个隐式解析可选类型。
1 | let possibleString: String? = "An optional string." |
我的理解就是加了感叹号就直接认为他是有值的,需要的时候就把它传递出去,不管他是不是空的。这个就和强制解析一样的东西。如果没有值的话就会直接出错。
基本运算符
空合运算符(Nil Coalescing Operator)
空合运算符( a ?? b )将对可选类型 a 进行空判断,如果 a 包含一个值就进行解封,否则就返回一个默认值 b .这个运算符有两个条件:
- 表达式 a 必须是Optional类型
- 默认值 b 的类型必须要和 a 存储值的类型保持一致
空合并运算符是对以下代码的简短表达方法
1 | a != nil ? a! : b |
注意:
如果 a 为非空值( non-nil ),那么值 b 将不会被估值。这也就是所谓的短路求值。
但是我测试如果b也为可选类型,那么会直接出现一个nil
区间运算符
- 闭区间运算符( a…b )定义一个包含从 a 到 b (包括 a 和 b )的所有值的区间, b 必须大于等于 a
- 半开区间( a..< b )定义一个从 a 到 b 但不包括 b 的区间
字符串和字符
字符串字面量(String Literals)
字符串字面量是由双引号 (“”) 包裹着的具有固定顺序的文本字符集。 字符串字面量可以用于为常量和变量提供初始值。
字符串是值类型(Strings Are Value Types)
Swift 的 String 类型是值类型。 如果您创建了一个新的字符串,那么当其进行常量、变量赋值操作,或在函数/方法中传递时,会进行值拷贝。
意思就是他们传递的时候也都是值传递,会比较安全,在传递的过程中不用担心会被更改
字符串插值 (String Interpolation)
- 插值字符串中写在括号中的表达式不能包含非转义双引号 ( “ ) 和反斜杠 ( \ ),并且不能包含回车或换行符。
Todo:还有一些字符串操作的函数没有看
集合类型
数组(Array)
创建一个带有默认值的数组
Swift 中的 Array 类型还提供一个可以创建特定大小并且所有数据都被默认的构造方法。我们可以把准备加入新数组的数据项数量( count )和适当类型的初始值( repeatedValue )传入数组构造函数:
1 | var threeDoubles = [Double](count: 3, repeatedValue:0.0) |
通过两个数组相加创建一个数组
我们可以使用加法操作符( + )来组合两种已存在的相同类型数组。新数组的数据类型会被从两个数组的数据类型中推断出来:
1 | var anotherThreeDoubles = Array(count: 3, repeatedValue: 2.5) |
使用加法赋值运算符( += )添加数据项
使用加法赋值运算符( += )也可以直接在数组后面添加一个或多个拥有相同类型的数据项:
1 | shoppingList += ["Baking Powder"] |
使用下标访问数组
可以利用下标来一次改变一系列数据值,即使新数据和原有数据的数量是不一样的。下面的例子把 “Chocolate Spread” , “Cheese” ,和 “Butter” 替换为 “Bananas” 和 “Apples” :
1 | shoppingList[4...6] = ["Bananas", "Apples"] |
集合(Set)
基本集合操作

字典
Swift 的字典类型是无序集合类型。为了以特定的顺序遍历字典的键或值,可以对字典的 keys 或 values 属性使用 sort() 方法。
控制流
if
在 if 语句中,条件必须是一个布尔表达式,这意味着像 if score { … } 这样的代码将报错,而不会隐形地与 0 做对比。 你可以一起使用 if 和 let 来处理值缺失的情况,这些值可由可选值来代表。
一个可选的值是一个具体的值或者是 nil 以表示值缺失,在类型后面加一个问号来标记这个变量的值是可选的。
如果不加这个问号的话就会导致比较报错,无法进行对比。
1 | //如果这里不加?声明 optionalString 是一个可选值的话,第二行就会报错,提示变量没有初始化 |
if let 就是把 ptionalName 值直接给一个临时常量,Swift会自动检测 optionalName 是否包含值,如果包含值,会隐式的拆包并给那个临时常量,在接下来的上下文中就能直接使用这个临时常量,这种方式称为**可选绑定(optional binding)**。 如果想要在后面操作可选值,可以定义为 if var 变量名,这样可选类型包含的值就会赋值给一个变量。
switch
- 运行 switch 中匹配到的子句之后,程序会退出 switch 语句,并不会继续向下运行,所以不需要在每个子句结尾写 break 。
区间匹配
可以使用…还有..<来进行区间匹配。
元组(Tuple)
使用下划线( _ )来匹配所有可能的值,如下:
1 | ... |
值绑定(Value Bindings)
case 分支的模式允许将匹配的值绑定到一个临时的常量或变量,这些常量或变量在该 case 分支里就可以被引用了——这种行为被称为_值绑定_(value binding)。
下面的例子展示了如何在一个 (Int, Int) 类型的元组中使用值绑定来分类下图中的点(x, y):
1 | let anotherPoint = (2, 0) |
where
还可以在do catch / for / 范型 / 协议 结合使用
在类型名后面使用 where 来指定对类型的需求,比如,限定类型实现某一个协议,限定两个类型是相同的,或者限定某个类必须有一个特定的父类
case 分支的模式可以使用 where 语句来判断额外的条件。
下面的例子把下图中的点(x, y)进行了分类:
1 | let yetAnotherPoint = (1, -1) |

贯穿(Fallthrough)
Swift 中的 switch不会从上一个 case 分支落入到下一个 case 分支中。相反,只要第一个匹配到的 case 分支完成了它需要执行的语句,整个 switch 代码块完成了它的执行。相比之下,C 语言要求你显示的插入 break 语句到每个 switch 分支的末尾来阻止自动落入到下一个 case 分支中。Swift 的这种避免默认落入到下一个分支中的特性意味着它的 switch 功能要比 C 语言的更加清晰和可预测,可以避免无意识地执行多个 case 分支从而引发的错误。
如果你确实需要 C 风格的贯穿的特性,你可以在每个需要该特性的 case 分支中使用 fallthrough 关键字。下面的例子使用 fallthrough 来创建一个数字的描述语句。
1 | let integerToDescribe = 5 |
注意: fallthrough 关键字不会检查它下一个将会落入执行的 case 中的匹配条件。 fallthrough 简单地使代码执行继续连接到下一个 case 中的执行代码,这和 C 语言标准中的 switch 语句特性是一样的。
for
可以使用 for-in 来遍历字典,需要两个变量来表示每个键值对。
字典是一个无序的集合,所以他们的键和值以任意顺序迭代结束。
1 | let interestingNumbers = [ |
while
- while 循环的另外一种形式是 repeat-while ,它和 while 的区别是在判断循环条件之前,先执行一次循环的代码块,然后重复循环直到条件为 false 。这样可以保证一定可以执行一次循环
guard
像 if 语句一样,guard 的执行取决于一个表达式的布尔值。我们可以使用 guard 语句来要求条件必须为真时,以执行 guard 语句后的代码。不同于 if 语句,一个 guard 语句总是有一个 else 从句,如果条件不为真则执行 else 从句中的代码。
1 | guard let name = person["name"] else { |
检测 API 可用性
Swift 内置支持检查 API 可用性,这可以确保我们不会在当前部署机器上,不小心地使用了不可用的 API。
我们在 if 或 guard 语句中使用 可用性条件(availability condition)去有条件的执行一段代码,来在运行时判断调用的 API 是否可用。编译器使用从可用性条件语句中获取的信息去验证,在这个代码块中调用的 API 是否可用。
1 | if #available(iOS 10, macOS 10.12, *) { |
模式和模式匹配
模式代表单个值或者复合值的结构。
例如,元组 (1, 2) 的结构是由逗号分隔的,包含两个元素的列表。因为模式代表一种值的结 构,而不是特定的某个值,你可以利用模式来匹配各种各样的值。比如,(x, y) 可以匹配元 组 (1, 2),以及任何含两个元素的元组。除了利用模式匹配一个值以外,你可以从复合值中提 取出部分或全部值,然后分别把各个部分的值和一个常量或变量绑定起来。
表达式模式(Expression Pattern)
重载 ~= 运算符
函数//todo
1 | func greet(person: String) -> String { |
- 使用元组类型来实现多返回值的函数
- 还可以使用 (Int, Int)? 类似的来实现可选元组的返回类型。
- 如果整个函数体是一个单一表达式,那么函数隐式返回这个表达式,如下:
1 | func greet(person: String) -> String { |
函数实际参数标签和形式参数名
实参标签和形参名
Swift 引进参数标签(Argument Label)这个概念,主要应用在调用函数的情况,使得函数的实参与真实命名相关联,更加容易理解实参的意义
- 在提供形式参数名之前写实际参数标签,用空格分隔。
- 如果你为一个形式参数提供了实际参数标签,那么这个实际参数就必须在调用函数的时候使用标签。
- 实际参数标签的使用能够让函数的调用更加明确,更像是自然语句,同时还能提供更可读的函数体并更清晰地表达你的意图
1 | func greet(person: String, from hometown: String) -> String { |
可变参数
一个可变参数(variadic parameter)可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入(...)的方式来定义可变参数。
可变参数的传入值在函数体中变为此类型的一个数组。例如,一个叫做 numbers 的 Double... 型可变参数,在函数体内可以当做一个叫 numbers 的 [Double] 型的数组常量。
下面的这个函数用来计算一组任意长度数字的 *算术平均数(arithmetic mean)*:
1 | func arithmeticMean(_ numbers: Double...) -> Double { |
一个函数能拥有多个可变参数。可变参数后的第一个行参前必须加上实参标签。实参标签用于区分实参是传递给可变参数,还是后面的行参。
输入输出参数
函数参数默认是常量。试图在函数体中更改参数值将会导致编译错误。这意味着你不能错误地更改参数值。如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters)。
定义一个输入输出参数时,在参数定义前加 inout 关键字。一个 输入输出参数有传入函数的值,这个值被函数修改,然后被传出函数,替换原来的值。想获取更多的关于输入输出参数的细节和相关的编译器优化,请查看 输入输出参数 一节。
你只能传递变量给输入输出参数。你不能传入常量或者字面量,因为这些量是不能被修改的。当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。
注意
输入输出参数不能有默认值,而且可变参数不能用
inout标记。
下例中,swapTwoInts(_:_:) 函数有两个分别叫做 a 和 b 的输入输出参数:
1 | func swapTwoInts(_ a: inout Int, _ b: inout Int) { |
swapTwoInts(_:_:) 函数简单地交换 a 与 b 的值。该函数先将 a 的值存到一个临时常量 temporaryA 中,然后将 b 的值赋给 a,最后将 temporaryA 赋值给 b。
你可以用两个 Int 型的变量来调用 swapTwoInts(_:_:)。需要注意的是,someInt 和 anotherInt 在传入 swapTwoInts(_:_:) 函数前,都加了 & 的前缀:
1 | var someInt = 3 |
从上面这个例子中,我们可以看到 someInt 和 anotherInt 的原始值在 swapTwoInts(_:_:) 函数中被修改,尽管它们的定义在函数体外。
注意
输入输出参数和返回值是不一样的。上面的
swapTwoInts函数并没有定义任何返回值,但仍然修改了someInt和anotherInt的值。输入输出参数是函数对函数体外产生影响的另一种方式。
高阶函数
map
filter
reduce
对于原始集合的每个元素,作用于当前累积的结果上
flatmap
对于元素是集合的集合,可以得到单级的集合
(flat 水平的)
compactmap
过滤空值
1 | let numbers = [1, 2, 4, 6, 10] |
函数式编程
有一个名字列表,其中一些条目由单个字符构成。现在任务是,将除去单字符条目之外的列表内容,放在一个逗号分隔的字符串里返回,且每个名字的首字母都要大写。
1 | let employee = ["wss", "lxy", "xx", "xy", "gg", "z", "niuniu", "m", "bob", "jack", "x"] |
闭包//todo
在 函数章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采用如下三种形式之一:
- 全局函数是一个有名字但不会捕获任何值的闭包
- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
- 利用上下文推断参数和返回值类型
- 隐式返回单表达式闭包,即单表达式闭包可以省略
return关键字 - 参数名称缩写
- 尾随闭包语法
闭包表达式
下面的闭包表达式示例使用 sorted(by:) 方法对一个 String 类型的数组进行字母逆序排序。以下是初始数组:
1 | let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] |
sorted(by:) 方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false。
该例子对一个 String 类型的数组进行排序,因此排序闭包函数类型需为 (String, String) -> Bool。
提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为 sorted(by:) 方法的参数传入:
1 | func backward(_ s1: String, _ s2: String) -> Bool { |
闭包表达式语法
闭包表达式语法有如下的一般形式:
1 | { (parameters) -> return type in |
闭包表达式参数 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。
下面的例子展示了之前 backward(_:_:) 函数对应的闭包表达式版本的代码:
1 | reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in |
内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外,改写为一行代码:
1 | reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } ) |
根据上下文推断类型
因为排序闭包函数是作为 sorted(by:) 方法的参数传入的,Swift 可以推断其参数和返回值的类型。
1 | reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } ) |
单表达式闭包的隐式返回
1 | reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } ) |
参数名称缩写
1 | reversedNames = names.sorted(by: { $0 > $1 } ) |
运算符方法
1 | reversedNames = names.sorted(by: >) |
尾随闭包
尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
1 | func someFunctionThatTakesAClosure(closure: () -> Void) { |
在 闭包表达式语法 上章节中的字符串排序闭包可以作为尾随包的形式改写在 sorted(by:) 方法圆括号的外面:
1 | reversedNames = names.sorted() { $0 > $1 } |
如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉:
1 | reversedNames = names.sorted { $0 > $1 } |
当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。
捕获列表
默认情况下,闭包会捕获附近作用域的常量和变量,并使用强引用指向他们。可以用一个捕获列表来显示的指定这个捕获行为。
一旦使用了捕获列表,就必须要用**in**关键字,即使已经省略了参数名和返回类型。
1 | var a = 0 |
在示例中,变量 b 只有一个,然而,变量 a 有两个,一个在闭包外,一个在闭包内。闭包内的变量 a 会在闭包创建时用闭包外的变量 a 的值来初始化,除此之外它们并无其他联系。
但是下面这个x,虽然有两个,但是因为他们是引用类型,都指向了同一个实例。
1 | class SimpleClass { |
如果捕获列表中的值是类类型,你可以使用 weak 或者 unowned 来修饰它,闭包会分别用弱引用和无主引用来捕获该值。
1 | myFunction { print(self.title) } // 隐式强引用捕获 |
在捕获列表中,也可以将任意表达式的值绑定到一个常量上。该表达式会在闭包被创建时进行求值,闭包会按照指定的引用类型来捕获表达式的值。例如:
1 | // 以弱引用捕获 self.parent 并赋值给 parent |
枚举
语法
每个枚举都定义了一个全新的类型。正如 Swift 中其它的类型那样,它们的名称(例如: CompassPoint和 Planet)需要首字母大写。给枚举类型起一个单数的而不是复数的名字,从 而使得它们能够顾名思义。
遍历枚举的 case
对于某些枚举来说,如果能有一个集合包含了枚举的所有情况就好了。你可以通过在枚举名字后面写 : CaseIterable 来允许枚举被遍历。Swift 会暴露一个包含对应枚举类型所有情况的集合名为 allCases。
1 | enum CompassPoint : CaseIterable { |
原始值
枚举成员可以用相同类型的默认值预先填充(称为原始值)。
当你在操作存储整数或字符串原始值枚举的时候,你不必显式地给每一个成员都分配一个原始值。当你没有分配时,Swift 将会自动为你分配值。
1 | enum CompassPoint : Int { |
从原始值初始化
如果用原始值类型来定义一个枚举,那么枚举就会自动收到一个可以接受原始值类型的值的构造器(叫做 rawValue的形式参数)然后返回一个枚举成员或者 nil
1 | let direction = CompassPoint(rawValue: 4) |
关联值
可以定义 Swift 枚举来存储任意给定类型的关联值,如果需要的话不同枚举成员关联值的类型 可以不同。
1 | enum Barcode { |
添加属性
存储属性
存储属性要么是变量存储属性(由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。
常量结构体实例的存储属性:如果创建了一个结构体的实例并且把这个实例赋给常量,你不能修改这个实例的属性,即使是声明为变量的属性。
延迟存储属性
延迟存储属性的初始值在其第一次使用时才进行计算。可以通过在其声明前标注 lazy 修饰语来表示一个延迟存储属性。如果被标记为 lazy 修饰符的属性同时被多个线程访问并且属性还没有被初始化,则无法保证属性只初始化一次。
计算属性
类,结构体和枚举也能够定义计算属性,计算属性不存储值,它提供一个getter和一个setter
1 | struct Point { |
简写setter
如果一个计算属性的setter没有为将要被设置的值定义一个名字,那么它将被默认命名为newValue
简写 getter
如果整个 getter 的函数体是一个单一的表达式,那么 getter 隐式返回这个表达式。
1 | struct Rect { |
只读计算属性
一个有 getter 但是没有 setter 的计算属性就是所谓的只读计算属性。只读计算属性返回一个值,也可以通过点语法访问,但是不能被修改为另一个值。所以必须要用var关键字来定义计算属性(包括只读计算属性)因为它们的值不是固定的。 let 关键字只用于常量属性,用于明确那些值一旦作为实例初始化就不能更改。
1 | var center: Point { |
属性观察者
willSet 会在该值被存储之前被调用。 didSet 会在一个新值被存储之后被调用。
如果你现了一个 willSet 观察者,新的属性值会以常量形式参数传递。可以在 willSet 实现中为这个参数定义名字。如果没有为它命名,那么它会使用默认的名字 newValue 。
如果实现了一个 didSet观察者,一个包含旧属性值的常量形式参数将会被传递。可以为它命名,也可以使用默认的形式参数名 oldValue 。
观察属性的能力同样对全局变量和局部变量有效。全局变量是定义在任何函数、方法、闭包或者类型环境之外的变量。局部变量是定义在函数、方法或者闭包环境之中的变量。
类型属性
可以给类型本身也定义属性,和实例的存储属性不同,实例之间的属性相互独立,而类型属性则只有一份,无论创建了多少个该类新的实例,都会是这个。
类型属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量(就像 C 语言中的静态常量),或者所有实例都能访问的一个变量(就像 C 语言中的静态变量)。
存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算型属性一样只能定义成变量属性。
注意:
跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。
存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。
使用 static 关键字来定义类型属性。对于类类型的计算类型属性,你可以使用 class 关键字 来允许子类重写父类的实现。
1 | class SomeClass { |
添加方法
实例方法
不用显式的写出self,swift会假定调用了当前实例中的属性或方法
但是如果一个实例方法的形式参数名与实例中某个属性拥有相同的名字的时候,这个时候形式参数名具有优先权,所以要加上self来区分形式参数名和属性名。
1 | struct Point { |
在实例方法中修改属性
结构体和枚举是值类型,默认情况下,值类型属性不能被自身的实例方法修改。
但是可以在 func 关键字前放一个 mutating 关键字来指定方可以修改属性。
关于mutating 可以看看这篇
1 | struct Point { |
在 mutating 方法中赋值给 self
Mutating 方法可以指定整个实例给隐含的 self 属性
1 | struct Point { |
枚举的 mutating 方法
枚举的 mutating 方法可以设置隐含的 self 属性为相同枚举里的不同成员。
1 | enum TriSwitch { |
类型方法
通过在 func关键字之前使用 static关键字来明确一个类型方法。类同样可以使用 class关键 字来允许子类重写父类对类型方法的实现。
下标
类、结构体和枚举可以定义下标,它可以作为访问集合、列表或序列成员元素的快捷方式。
可以为一个类型定义多个下标,并且下标会基于传入的索引值的类型选择合适的下标重载使用。下标没有限制单个维度,你可以使用多个输入形参来定义下标以满足自定义类型的需求。
下标语法
使用关键字 subscript 来定义下标,并且指定一个或多个 输入形式参数和返回类型,与实例方法一样。与实例方法不同的是,下标可以是读写也可以 是只读的。
下标可以接收任意数量的输入形式参数,并且这些输入形式参数可以是任意类型。下标也可以返回任意类型。下标可以使用变量形式参数和可变形式参数,但是不能使用输入输出形式参数或提供默认形式参数值。
1 | struct Matrix { |
类型下标
通过在 subscript 关键字前加 static 关键字来标 记类型下标。在类里则使用 class 关键字,这样可以允许子类重写父类的下标实现。
类的继承
在 Swift 中,类可以调用和访问超类的方法、属性和下标,并且可以重写这些方法,属性和下标来优化或修改它们的行为。Swift 会检查你的重写定义在超类中是否有匹配的定义,以此确保你的重写行为是正确的。
可以为类中继承来的属性添加属性观察器,这样一来,当属性值改变时,类就会被通知到。可以为任何属性添加属性观察器,无论它原本被定义为存储型属性还是计算型属性。
定义一个基类
不继承于其它类的类,称之为基类。
子类生成
子类生成指的是在一个已有类的基础上创建一个新的类。子类继承超类的特性,并且可以进一步完善。你还可以为子类添加新的特性。
重写
子类可以为继承来的实例方法,类方法,实例属性,类属性,或下标提供自己定制的实现。我们把这种行为叫重写。
重写属性
重写属性的 getters 和 setters
你可以提供定制的 getter(或 setter)来重写任何一个继承来的属性,无论这个属性是存储型还是计算型属性。子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。你在重写一个属性时,必须将它的名字和类型都写出来。这样才能使编译器去检查你重写的属性是与超类中同名同类型的属性相匹配的。
你可以将一个继承来的只读属性重写为一个读写属性,只需要在重写版本的属性里提供 getter 和 setter 即可。但是,不可以将一个继承来的读写属性重写为一个只读属性。
注意
如果你在重写属性中提供了 setter,那么你也一定要提供 getter。如果你不想在重写版本中的 getter 里修改继承来的属性值,你可以直接通过
super.someProperty来返回继承来的值,其中someProperty是你要重写的属性的名字。
以下的例子定义了一个新类,叫 Car,它是 Vehicle 的子类。这个类引入了一个新的存储型属性叫做 gear,默认值为整数 1。Car 类重写了继承自 Vehicle 的 description 属性,提供包含当前档位的自定义描述:
1 | class Car: Vehicle { |
重写的 description 属性首先要调用 super.description 返回 Vehicle 类的 description 属性。之后,Car 类版本的 description 在末尾增加了一些额外的文本来提供关于当前档位的信息。
如果你创建了 Car 的实例并且设置了它的 gear 和 currentSpeed 属性,你可以看到它的 description 返回了 Car 中的自定义描述:
1 | let car = Car() |
重写属性观察器
可以通过重写属性为一个继承来的属性添加属性观察器。这样一来,无论被继承属性原本是如何实现的,当其属性值发生改变时,你就会被通知到。关于属性观察器的更多内容,请看 属性观察器。
注意
不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。这些属性的值是不可以被设置的,所以,为它们提供
willSet或didSet实现也是不恰当。
注意
不可以同时提供重写的 setter 和重写的属性观察者。如果你想观察属性值的变化,并且你已经为那个属性提供了定制的 setter,那么你在 setter 中就可以观察到任何值变化了。
下面的例子定义了一个新类叫 AutomaticCar,它是 Car 的子类。AutomaticCar 表示自动档汽车,它可以根据当前的速度自动选择合适的档位:
1 | class AutomaticCar: Car { |
当你设置 AutomaticCar 的 currentSpeed 属性,属性的 didSet 观察器就会自动地设置 gear 属性,为新的速度选择一个合适的档位。具体来说就是,属性观察器将新的速度值除以 10,然后向下取得最接近的整数值,最后加 1 来得到档位 gear 的值。例如,速度为 35.0 时,档位为 4:
1 | let automatic = AutomaticCar() |
防止重写
可以通过把方法,属性或下标标记为 final 来防止它们被重写,只需要在声明关键字前加上 final 修饰符即可
可以通过在关键字 class 前添加 final 修饰符(final class)来将整个类标记为 final 。这样的类是不可被继承的,试图继承这样的类会导致编译报错。
类的构造过程
构造器
构造器在创建特定类型的实例时被调用。
默认的属性值和构造器
可以指定一个默认属性值作为属性声明的一部分。当属性被定义的时候你可以通过为这个属性分配一个初始值来指定默认的属性值。
Swift 为所有没有提供构造器的结构体或类提供了一个默认的构造器来给所有的属性提供了默认值。这个默认的构造器只是简单地创建了一个所有属性都有默认值的新实例。
1 | class ShoppingListItem { |
自定义构造
可以提供构造形式参数作为构造器的一部分,来定义构造过程中的类型和值的名称。
构造形式参数与函数和方法的形式参数具有相同的功能和语法。
1 | struct Celsius { |
在初始化中分配常量属性
在初始化的任意时刻,你都可以给常量属性赋值,只要它在初始化结束是设置了确定的值即可。
一旦为常量属性被赋值,它就不能再被修改了。
结构体的成员初始化构造器
如果结构体类型中没有定义任何自定义构造器,它会自动获得一个成员构造器。不同于默认构造器,结构体会接收成员构造器即使它的存储属性没有默认值。
值类型的构造器委托
就是值类型的构造器中可以调用其他构造器来执行部分实例的初始化。这个过程,就是所谓的构造器委托,避免了多个构造器里冗余代码。
1 | struct Size { |
类的构造过程
- 所有类的存储属性(包括从它的父类继承的所有属性)都必须在初始化期间分配初始值。
- Swift 为类类型定义了两种构造器以确保所有的存储属性接收一个初始值。
- 指定构造器是类的主要构造器。指定的构造器可以初始化所有那个类引用的属性并且调用合适的父类构造器来继续这个初始化过程给父类链。
- 类偏向于少量指定构造器,并且一个类通常只有一个指定构造器。指定构造器是初始化开始并持续初始化过程到父类链的“传送”点。
- 每个类至少得有一个指定构造器。如同在构造器的自动继承里描述的那样,在某些情况下,这些需求通过从父类继承一个或多个指定构造器来满足。
- 便捷构造器是次要的。可以在相同的类里定义一个便捷构造器来调用一个指定的构造器作为便捷构造器来给指定构造器设置默认形式参数。你也可以为具体的使用情况或输入的值类型定义一个便捷构造器从而创建这个类的实例。
- 如果类不需要便捷构造器你可以不提供它。在为通用的初始化模式创建快捷方式以节省时间或者类的初始化更加清晰明了的时候使用便捷构造器。
指定构造器
用与值类型的简单构造器相同的方式来写类的指定构造器。
1 | init(parameters) { |
便捷构造器
用convenicence修饰符放到init关键字前定义便捷构造器
1 | convenicence init(paramenters) { |
类的初始化委托
- 指定构造器必须从它的直系父类调用指定构造器。
- 便捷构造器必须从相同的类里调用另一个构造器。(便捷构造器没办法直接调用父类的构造器
- 便捷构造器最终必须调用一个指定构造器。
一个更方便记忆的方法是:
- 指定构造器必须总是向上代理
- 便利构造器必须总是横向代理
两段式构造过程
Swift 中类的构造过程包含两个阶段。第一个阶段,类中的每个存储型属性赋一个初始值。当每个存储型属性的初始值被赋值后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储型属性。
两段式初始化过程的使用让初始化更加安全,同时在每个类的层级结构给与了完备的灵活性。两段式初始化过程可以防止属性值在初始化之前被访问,还可以防止属性值被另一个构造器意外地赋予不同的值。
安全检查
Swift 编译器将执行 4 种有效的安全检查,以确保两段式构造过程不出错地完成:
- 指定构造器必须保证在向上委托给父类构造器之前,其所在类引入的所有属性都要初始化完成。
- 指定构造器必须先向上委托父类构造器,然后才能为继承的属性设置新值。如果不这样做, 指定构造器赋予的新值将被父类中的构造器所覆盖。
- 便捷构造器必须先委托同类中的其它构造器,然后再为任意属性赋新值(包括同类里定义的 属性)。如果没这么做,便捷构构造器赋予的新值将被自己类中其它指定构造器所覆盖。(其实也就是
self.init必须位于属性访问,属性赋值之前,通常位于第一行) - 构造器在第一阶段构造完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也 不能引用 self 作为值。
阶段1
类的某个指定构造器或便利构造器被调用。
完成类的新实例内存的分配,但此时内存还没有被初始化。
指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。
阶段2
从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。构造器此时可以访问 self、修改它的属性并调用实例方法等等。
最终,继承链中任意的便利构造器有机会自定义实例和使用 self。
下图展示了在假定的子类和父类之间的构造阶段 1:

在这个例子中,构造过程从对子类中一个便利构造器的调用开始。这个便利构造器此时还不能修改任何属性,它会代理到该类中的指定构造器。
如安全检查 1 所示,指定构造器将确保所有子类的属性都有值。然后它将调用父类的指定构造器,并沿着继承链一直往上完成父类的构造过程。
父类中的指定构造器确保所有父类的属性都有值。由于没有更多的父类需要初始化,也就无需继续向上代理。
一旦父类中所有属性都有了初始值,实例的内存被认为是完全初始化,阶段 1 完成。
以下展示了相同构造过程的阶段 2:

父类中的指定构造器现在有机会进一步自定义实例(尽管这不是必须的)。
一旦父类中的指定构造器完成调用,子类中的指定构造器可以执行更多的自定义操作(这也不是必须的)。
最终,一旦子类的指定构造器完成调用,最开始被调用的便利构造器可以执行更多的自定义操作。
构造器的自动继承
子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。事实上,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。
假设你为子类中引入的所有新属性都提供了默认值,以下 2 个规则将适用:
- 如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。
- 如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。
注意!
子类可以将父类的指定构造器实现为便利构造器来满足规则2
即使在子类中添加了更多的便利构造器,这两条规则仍然适用。
可失败构造器
为了妥善处理这种构造过程中可能会失败的情况。你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在 init 关键字后面添加问号(init?)。
可失败构造器会创建一个类型为自身类型的可选类型的对象。通过 return nil 语句来表明可失败构造器在何种情况下应该 “失败”。
通常来说我们通过在 init 关键字后添加问号的方式(init?)来定义一个可失败构造器,但你也可以通过在 init 后面添加感叹号的方式来定义一个可失败构造器(init!),该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。
你可以在 init? 中代理到 init!,反之亦然。你也可以用 init? 重写 init!,反之亦然。你还可以用 init 代理到 init!,不过,一旦 init! 构造失败,则会触发一个断言。
必要构造器
在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器:
1 | class SomeClass { |
在子类重写父类的必要构造器时,必须在子类的构造器前也添加 required 修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加 override 修饰符:
1 | class SomeSubclass: SomeClass { |
注意:
如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。
通过闭包或函数设置属性的默认值
如果某个存储型属性的默认值需要一些自定义或设置,可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被构造时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。
注意:
如果使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着不能在闭包里访问其它属性,即使这些属性有默认值。同样,也不能使用隐式的
self属性,或者调用任何实例方法。
类的析构过程
析构器只适用于类类型,当一个类的实例被释放之前,析构器会被立即调用。析构器用关键字 deinit 来标示,类似于构造器要用 init 来标示。
析构过程原理
Swift 会自动释放不再需要的实例以释放资源。如 自动引用计数 章节中所讲述,Swift 通过自动引用计数(ARC) 处理实例的内存管理。通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。例如,如果创建了一个自定义的类来打开一个文件,并写入一些数据,你可能需要在类实例被释放之前手动去关闭该文件。
在类的定义中,每个类最多只能有一个析构器,而且析构器不带任何参数和圆括号,如下所示:
1 | deinit { |
析构器是在实例释放发生前被自动调用的。你不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。
因为直到实例的析构器被调用后,实例才会被释放,所以析构器可以访问实例的所有属性,并且可以根据那些属性可以修改它的行为(比如查找一个需要被关闭的文件)。
可选链
错误处理
类型转换
类型转换在 Swift 中使用 is 和 as 操作符实现。
先为下面的实例代码定义类层次:
1 | class MediaItem { |
检查类型
用类型检查操作符(is)来检查一个实例是否属于特定子类型。若实例属于那个子类型,类型检查操作符返回 true,否则返回 false。
1 | var movieCount = 0 |
向下转型
某类型的一个常量或变量可能在幕后实际上属于一个子类。当确定是这种情况时,可以尝试用类型转换操作符(as? 或 as!)向下转到它的子类型。
因为向下转型可能会失败,类型转型操作符带有两种不同形式。条件形式 as? 返回一个你试图向下转成的类型的可选值,如果不能向下转型,那么这个可选值将是nil。强制形式 as! 把试图向下转型和强制解包转换结果结合为一个操作,如果向下转型为一个不正确的类型时,强制形式的类型转换会触发一个运行时错误。
1 | for item in library { |
注意
转换没有真的改变实例或它的值。根本的实例保持不变;只是简单地把它作为它被转换成的类型来使用。
Any 和 AnyObject 的类型转换
Swift 为不确定类型提供了两种特殊的类型别名:
Any可以表示任何类型,包括函数类型。AnyObject可以表示任何类类型的实例。
嵌套类型(Nested Types)
就是一个类型里面可以套其他类型,Swift 允许你定义嵌套类型,可以在支持的类型中定义嵌套的枚举、类和结构体。要在一个类型中嵌套另一个类型,将嵌套类型的定义写在其外部类型的 {} 内,而且可以根据需要定义多级嵌套。
扩展(extension)
扩展可以给一个现有的类,结构体,枚举,还有协议添加新的功能。它还拥有不需要访问被扩展类型源代码就能完成扩展的能力(即逆向建模)。
Swift 中的扩展可以:
- 添加计算型实例属性(computed instance properties )和计算型类属性(computed instance properties )
- 定义实例方法和类方法
- 提供新的构造器
- 定义下标
- 定义和使用新的嵌套类型
- 使已经存在的类型遵循(conform)一个协议
在 Swift 中,你甚至可以扩展协议以提供其需要的实现(implementations),或者添加额外功能给遵循的类型所使用。
注意
扩展可以给一个类型添加新的功能,但是不能重写已经存在的功能。
拓展的语法
使用 extension 关键字声明扩展
注意
对一个现有的类型,如果你定义了一个扩展来添加新的功能,那么这个类型的所有实例都可以使用这个新功能,包括那些在扩展定义之前就存在的实例。
计算型属性
扩展可以给现有类型添加计算型实例属性和计算型类属性。这个例子给 Swift 内建的 Double 类型添加了五个计算型实例属性,从而提供与距离单位相关工作的基本支持:
1 | extension Double { |
注意
扩展可以添加新的计算属性,但是它们不能添加存储属性,或向现有的属性添加属性观察者。
构造器
扩展可以给现有的类型添加新的构造器。对于值类型,他可以添加一个简单构造器,但是对于类类型,只能添加便利构造器,不能添加新的指定构造器或者析构器,指定构造器和析构器必须始终由类的原始实现提供。
方法
扩展可以给现有类型添加新的实例方法和类方法。
在下面的例子中,给 Int 类型添加了一个新的实例方法叫做 repetitions:
1 | extension Int { |
repetitions(task:) 方法仅接收一个 () -> Void 类型的参数,它表示一个没有参数没有返回值的方法。
定义了这个扩展之后,你可以对任意整形数值调用 repetitions(task:) 方法,来执行对应次数的任务:
1 | 3.repetitions { |
可变实例方法
将这个实例方法标记为 mutating
1 | extension Int { |
下标
1 | extension Int { |
嵌套类型
扩展可以给现有的类,结构体,还有枚举添加新的嵌套类型
协议
属性要求
协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型,还有这个属性是可读的还是可读可写的。
如果协议要求这个属性是可读可写的,那遵循这个协议的时候这个属性就不能是常量属性或者只读的计算属性。
如果协议只要求这个属性是可读的,那这个属性其实不止可读,如果代码需要的话它还可以可写,这也就是为什么不说只读而说可读的原因。
协议总是用 var 关键字来声明变量属性,在类型声明后加上 { set get } 来表示属性是可读可写的,可读属性则用 { get } 来表示:
1 | protocol SomeProtocol { |
在协议中定义类型属性时,总是使用 static 关键字作为前缀。当类类型遵循协议时,除了 static 关键字,还可以使用 class 关键字来声明类型属性:
1 | protocol AnotherProtocol { |
方法要求
协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。但是不支持为协议中的方法指定默认参数。
1 | protocol SomeProtocol { |
和属性要求一样,在协议中定义方法时,总是使用 static 关键字作为前缀,在类实现的时候,可以使用class来作为关键字前缀。
异变方法要求
实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写 mutating 关键字。(结构体和枚举是值类型,默认情况下,值类型属性不能被自身的实例方法修改。)
构造器要求
可以像编写普通构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体:
1 | protocol SomeProtocol { |
协议构造器要求的类实现
你可以在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,你都必须为构造器实现标上 required 修饰符:
1 | class SomeClass: SomeProtocol { |
使用 required 修饰符可以确保所有子类也必须提供此构造器实现,从而也能遵循协议。
注意
如果类已经被标记为
final,那么不需要在协议构造器的实现中使用required修饰符,因为final类不能有子类。
如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 required 和 override 修饰符:
1 | protocol SomeProtocol { |
可失败构造器要求
协议还可以为遵循协议的类型定义可失败构造器要求,详见 可失败构造器。
遵循协议的类型可以通过可失败构造器(init?)或非可失败构造器(init)来满足协议中定义的可失败构造器要求。协议中定义的非可失败构造器要求可以通过非可失败构造器(init)或隐式解包可失败构造器(init!)来满足。
协议作为类型
尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。协议作为类型使用,有时被称作「存在类型」,这个名词来自「存在着一个类型 T,该类型遵循协议 T」。
协议可以像其他普通类型一样使用,使用场景如下:
- 作为函数、方法或构造器中的参数类型或返回值类型
- 作为常量、变量或属性的类型
- 作为数组、字典或其他容器中的元素类型
注意
协议是一种类型,因此协议类型的名称应与其他类型(例如
Int,Double,String)的写法相同,使用大写字母开头的驼峰式写法,例如(FullyNamed和RandomNumberGenerator)。
委托
委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。委托模式的实现很简单:定义协议来封装那些需要被委托的功能,这样就能确保遵循协议的类型能提供这些功能。委托模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。
在扩展里添加协议遵循
通过扩展令已有类型遵循并符合协议时,该类型的所有实例也会随之获得协议中定义的各项功能。
在扩展里声明采纳协议(Declaring Protocol Adoption with an Extension)
当一个类型已经遵循了某个协议中的所有要求,却还没有声明采纳该协议时,可以通过空的扩展来让它采纳该协议:
1 | struct Hamster { |
从现在起,Hamster 的实例可以作为 TextRepresentable 类型使用:
1 | let simonTheHamster = Hamster(name: "Simon") |
注意
即使满足了协议的所有要求,类型也不会自动遵循协议,必须显式地遵循协议。
有条件地遵循协议
泛型类型可能只在某些情况下满足一个协议的要求,比如当类型的泛型形式参数遵循对应协议时。你可以通过在扩展类型时列出限制让泛型类型有条件地遵循某协议。在你采纳协议的名字后面写泛型 where 分句。更多关于泛型 where 分句,见 泛型 Where 分句。
下面的扩展让 Array 类型只要在存储遵循 TextRepresentable 协议的元素时就遵循 TextRepresentable 协议。
1 | extension Array: TextRepresentable where Element: TextRepresentable { |
使用合成实现来采纳协议(Adopting a Protocol Using a Synthesized Implementation)
Swift 可以自动提供一些简单场景下遵循 Equatable、Hashable 和 Comparable 协议的实现。在使用这些合成实现之后,无需再编写重复的代码来实现这些协议所要求的方法。
Swift 为以下几种自定义类型提供了 Equatable 协议的合成实现:
- 遵循
Equatable协议且只有存储属性的结构体。 - 遵循
Equatable协议且只有关联类型的枚举 - 没有任何关联类型的枚举
在包含类型原始声明的文件中声明对 Equatable 协议的遵循,可以得到 == 操作符的合成实现,且无需自己编写任何关于 == 的实现代码。Equatable 协议同时包含 != 操作符的默认实现。
下面的例子中定义了一个 Vector3D 结构体来表示一个类似 Vector2D 的三维向量 (x, y, z)。由于 x、y 和 z 都是满足 Equatable 的类型,Vector3D 可以得到连等判断的合成实现。
1 | struct Vector3D: Equatable { |
Swift 为以下几种自定义类型提供了 Hashable 协议的合成实现:
- 遵循
Hashable协议且只有存储属性的结构体。 - 遵循
Hashable协议且只有关联类型的枚举 - 没有任何关联类型的枚举
在包含类型原始声明的文件中声明对 Hashable 协议的遵循,可以得到 hash(into:) 的合成实现,且无需自己编写任何关于 hash(into:) 的实现代码。
Swift 为没有原始值的枚举类型提供了 Comparable 协议的合成实现。如果枚举类型包含关联类型,那这些关联类型也必须同时遵循 Comparable 协议。在包含原始枚举类型声明的文件中声明其对 Comparable 协议的遵循,可以得到 < 操作符的合成实现,且无需自己编写任何关于 < 的实现代码。Comparable 协议同时包含 <=、> 和 >= 操作符的默认实现。
下面的例子中定义了 SkillLevel 枚举类型,其中定义了初学者(beginner)、中级(intermediate)和专家(expert)三种等级,专家等级会由额外的星级(stars)来进行排名。
1 | enum SkillLevel: Comparable { |
类专属的协议
通过添加 AnyObject 关键字到协议的继承列表,就可以限制协议只能被类类型采纳(以及非结构体或者非枚举的类型)。
1 | protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol { |
在以上例子中,协议 SomeClassOnlyProtocol 只能被类类型采纳。如果尝试让结构体或枚举类型采纳 SomeClassOnlyProtocol,则会导致编译时错误。
Swift 为不确定类型提供了两种特殊的类型别名:
Any可以表示任何类型,包括函数类型。AnyObject可以表示任何类类型的实例。
注意
当协议定义的要求需要遵循协议的类型必须是引用语义而非值语义时,应该采用类类型专属协议。
协议组合(Composition),协议合成
协议组合就是使用&来组合多个协议到一个要求里,它还可以使用 typealias 关键字来定义别名,可以不定义新的协议类型的同时,拥有构成中所有协议的需求。
详见协议组合小章节
检查协议一致性(Checking for Protocol Conformance)
可以使用 类型转换 中描述的 is 和 as 操作符来检查协议一致性,即是否遵循某协议,并且可以转换到指定的协议类型。检查和转换协议的语法与检查和转换类型是完全一样的:
is用来检查实例是否遵循某个协议,若遵循则返回true,否则返回false;as?返回一个可选值,当实例遵循某个协议时,返回类型为协议类型的可选值,否则返回nil;as!将实例强制向下转换到某个协议类型,如果强转失败,将触发运行时错误。
1 | class test: A, B { |
可选的协议要求//todo
协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在你需要和 Objective-C 打交道的代码中。协议和可选要求都必须带上 @objc 属性。
标记 @objc 特性的协议只能被继承自 Objective-C 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。
使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。
协议中的可选要求可通过可选链式调用来使用,因为遵循协议的类型可能没有实现这些可选要求。类似 someOptionalMethod?(someArgument) 这样,你可以在可选方法名称后加上 ? 来调用可选方法。详细内容可在 可选链式调用 章节中查看。
下面的例子定义了一个名为 Counter 的用于整数计数的类,它使用外部的数据源来提供每次的增量。数据源由 CounterDataSource 协议定义,它包含两个可选要求:
1 | @objc protocol CounterDataSource { |
CounterDataSource 协议定义了一个可选方法 increment(forCount:) 和一个可选属性 fiexdIncrement,它们使用了不同的方法来从数据源中获取适当的增量值。
注意
严格来讲,
CounterDataSource协议中的方法和属性都是可选的,因此遵循协议的类可以不实现这些要求,尽管技术上允许这样做,不过最好不要这样写。
协议扩展
协议可以通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个遵循协议的类型中都重复同样的实现,也无需使用全局函数。
通过协议扩展,所有遵循协议的类型,都能自动获得这个扩展所增加的方法实现而无需任何额外修改
协议扩展可以为遵循协议的类型增加实现,但不能声明该协议继承自另一个协议。
协议的继承只能在协议声明处进行指定。
提供默认实现
可以通过协议扩展来为协议要求的方法、计算属性提供默认的实现。如果遵循协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。
注意
通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,遵循协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。
为协议扩展添加限制条件
举个例子,可以扩展 Collection 协议,适用于集合中的元素遵循了 Equatable 协议的情况。通过限制集合元素遵循 Equatable 协议, 作为标准库的一部分, 你可以使用 == 和 != 操作符来检查两个元素的等价性和非等价性。
1 | extension Collection where Element: Equatable { |
如果集合中的所有元素都一致,allEqual() 方法才返回 true。
看看两个整数数组,一个数组的所有元素都是一样的,另一个不一样:
1 | print(equalNumbers.allEqual()) |
注意
如果一个遵循的类型满足了为同一方法或属性提供实现的多个限制型扩展的要求, Swift 会使用最匹配限制的实现。
泛型
不透明类型
自动引用计数
内存安全
访问控制
高级运算符
可选链
错误处理
并发
类型转换