随着第三代语言规范的崛起,Apple推出了Swift,以后也是大力支持的官方语言,作为iOS开发者,学习Swift是大势所趋,Swift相比于Obj-C,更简洁,语言自带支持的面相协议编程也是解耦的最佳实现。
常量和变量
类型注解
当你声明常量或者变量的时候可以加上类型注解(type annotation),说明常量或者变量中要存储的值的类型。如果要添加类型注解,需要在常量或者变量名后面加上一个冒号和空格,然后加上类型名称。
1 | var welcomeMessage: String |
可以在一行中定义多个同样类型的变量,用逗号分割,并在最后一个变量名之后添加类型注解:
1 | var red, green, blue: Double |
数值型字面量
一个二进制数,前缀是 0b
一个八进制数,前缀是 0o
一个十六进制数,前缀是 0x
1 | let decimalInteger = 17 |
可选类型
nil
不能用于非可选的常量和变量
声明一个可选常量或者变量但是没有赋值,它们会自动被设置为 nil
注意
Swift 的
nil
和 Objective-C 中的nil
并不一样。在 Objective-C 中,nil
是一个指向不存在对象的指针。在 Swift 中,nil
不是指针——它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为nil
,不只是对象类型
可选绑定
使用可选绑定(optional binding)来判断可选类型是否包含值,如果包含就把值赋给一个临时常量或者变量
1 | if let constantName = someOptional { |
可以包含多个可选绑定或多个布尔条件在一个 if
语句中,只要使用逗号
分开就行。只要有任意一个可选绑定的值为 nil
,或者任意一个布尔条件为 false
,则整个 if
条件判断为 false
。下面的两个 if 语句是等价的:
1 | if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 { |
注意
在
if
条件语句中使用常量和变量来创建一个可选绑定,仅在if
语句的句中(body)中才能获取到值。相反,在guard
语句中使用常量和变量来创建一个可选绑定,仅在guard
语句外且在语句后才能获取到值
简单值
把值转换成字符串的方法:把值写到括号中,并且在括号之前写一个反斜杠(\)
1 | let apples = 3 |
使用三个双引号("""
)来包含多行字符串内容。每行行首的缩进会被去除,只要和结尾引号的缩进相匹配。举个例子:
1 | let quotation = """ |
使用方括号 [] 来创建数组和字典,并使用下标或者键(key)来访问元素。最后一个元素后面允许有个逗号。
1 | var shoppingList = ["catfish", "water", "tulips", "blue paint"] |
使用初始化语法来创建一个空数组或者空字典
1 | let emptyArray: [String] = [] |
控制流
使用 if
和 switch
来进行条件操作,使用 for-in
、while
和 repeat-while
来进行循环。包裹条件和循环变量的括号可以省略,但是语句体的大括号是必须的。
1 | let individualScores = [75, 43, 103, 87, 12] |
条件
if
在 if
语句中,条件必须是一个布尔表达式——这意味着像if score { ... }
这样的代码将报错,而不会隐形地与 0 做对比。
可以一起使用 if
和 let
一起来处理值缺失的情况。这些值可由可选值来代表。一个可选的值是一个具体的值或者是 nil
以表示值缺失。在类型后面加一个问号(?
)来标记这个变量的值是可选的。
1 | var optionalString: String? = "Hello" |
提前退出
使用 guard
语句来要求条件必须为真时,以执行 guard
语句后的代码,一个 guard
语句总是有一个 else
从句,如果条件不为真则执行 else
从句中的代码
1 | func greet(person: [String: String]) { |
switch
switch
支持任意类型的数据以及各种比较操作——不仅仅是整数以及测试相等
1 | let vegetable = "red pepper" |
运行 switch
中匹配到的 case
语句之后,程序会退出 switch
语句,并不会继续向下运行,所以不需要在每个子句结尾写 break
每一个 case
分支都必须包含至少一条语句。像下面这样书写代码是无效的,因为第一个 case
分支是空的:
1 | let anotherCharacter: Character = "a" |
为了让单个 case 同时匹配 a 和 A,可以将这个两个值组合成一个复合匹配,并且用逗号分开:
1 | let anotherCharacter: Character = "a" |
case 分支的模式可以使用 where 语句来判断额外的条件
1 | let yetAnotherPoint = (1, -1) |
需要 C 风格的贯穿的特性,你可以在每个需要该特性的 case 分支中使用 fallthrough 关键字
1 | let integerToDescribe = 5 |
fallthrough
关键字不会检查它下一个将会落入执行的 case 中的匹配条件。fallthrough
简单地使代码继续连接到下一个case
中的代码,这和 C 语言标准中的switch
语句特性是一样的
循环
可以使用 for-in
来遍历字典,需要一对儿变量来表示每个键值对。
1 | let interestingNumbers = [ |
每 5 分钟作为一个刻度。使用 stride(from:to:by:)
函数跳过不需要的标记
1 | let minutes = 60 |
使用 while
来重复运行一段代码直到条件改变。循环条件也可以在结尾,保证能至少循环一次
1 | var n = 2 |
可以在循环中使用 ..<
来表示下标范围
1 | var total = 0 |
使用 ..<
创建的范围不包含上界,如果想包含的话需要使用 ...
函数和闭包
函数
使用 func
来声明一个函数,使用名字和参数来调用函数。使用 ->
来指定函数返回值的类型
1 | func greet(person: String, day: String) -> String { |
默认情况下,函数使用它们的参数名称作为它们参数的标签,在参数名称前可以自定义参数标签,或者使用_
表示不使用参数标签
1 | func greet(_ person: String, on day: String) -> String { |
使用元组来生成复合值,比如让一个函数返回多个值。该元组的元素可以用名称或数字来获取
1 | func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) { |
隐式返回的函数
如果一个函数的整个函数体是一个单行表达式,这个函数可以隐式地返回这个表达式。举个例子,以下的函数有着同样的作用:
1 | func greeting(for person: String) -> String { |
何一个可以被写成一行 return
语句的函数都可以忽略 return
,一个属性的 getter
也可以使用隐式返回的形式
参数标签和参数名称
每个函数参数都有一个参数标签(argument label
)以及一个参数名称(parameter name
)。参数标签在调用函数的时候使用;调用的时候需要将函数的参数标签写在对应的参数前面。参数名称在函数的实现中使用。默认情况下,函数参数使用参数名称来作为它们的参数标签
1 | func greet(person: String, from hometown: String) -> String { |
参数标签的使用能够让一个函数在调用时更有表达力,更类似自然语言,并且仍保持了函数内部的可读性以及清晰的意图。
忽略参数标签
不希望为某个参数添加一个标签,可以使用一个下划线(_
)来代替一个明确的参数标签
1 | func someFunction(_ firstParameterName: Int, secondParameterName: Int) { |
如果一个参数有一个标签,那么在调用的时候必须使用标签来标记这个参数。
默认参数值
可以在函数体中通过给参数赋值来为任意一个参数定义默认值(Deafult Value
)。当默认值被定义后,调用这个函数时可以忽略这个参数。
1 | func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) { |
将不带有默认值的参数放在函数参数列表的最前。
输入输出参数
函数参数默认是常量。如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters),定义一个输入输出参数时,在参数定义前加 inout
关键字。
当传入的参数作为输入输出参数时,需要在参数名前加 &
符,表示这个值可以被函数修改
输入输出参数不能有默认值,而且可变参数不能用 inout 标记
函数类型
在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:
1 | var mathFunction: (Int, Int) -> Int = addTwoInts |
”定义一个叫做 mathFunction
的变量,类型是‘一个有两个 Int 型的参数并返回一个 Int 型的值的函数’,并让这个新变量指向 addTwoInts
函数”。
函数类型作为参数类型
可以用(Int, Int) -> Int
这样的函数类型作为另一个函数的参数类型。这样你可以将函数的一部分实现留给函数的调用者来提供
1 | func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) { |
函数可以嵌套,可以作为另一个函数的返回值,可以当做参数传入另一个函数
闭包
函数实际上是一种特殊的闭包:它是一段能之后被调取的代码。闭包中的代码能访问闭包作用域中的变量和函数,即使闭包是在一个不同的作用域被执行的——你已经在嵌套函数的例子中看过了。你可以使用 {}
来创建一个匿名闭包。使用 in
将参数和返回值类型的声明与闭包函数体进行分离。
1 | numbers.map({ |
如果一个闭包的类型已知,比如作为一个代理的回调,你可以忽略参数,返回值,甚至两个都忽略。单个语句闭包会把它语句的值当做结果返回。
1 | let mappedNumbers = numbers.map({ number in 3 * number }) |
可以通过参数位置而不是参数名字来引用参数——这个方法在非常短的闭包中非常有用。当一个闭包作为最后一个参数传给一个函数的时候,它可以直接跟在圆括号后面。当一个闭包是传给函数的唯一参数,你可以完全忽略圆括号
1 | let sortedNumbers = numbers.sorted { $0 > $1 } |
闭包表达式
排序方法
使用 sorted(by:)
方法对一个 String 类型的数组进行字母逆序排序
1 | let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] |
sorted(by:)
方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true
,反之返回 false
。
1 | func backward(_ s1: String, _ s2: String) -> Bool { |
闭包表达式语法
闭包表达式语法有如下的一般形式:
1 | { (parameters) -> return type in |
闭包表达式参数 可以是in-out
参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值。
1 | reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in |
精简后
1 | reversedNames = names.sorted(by: { $0 > $1 } ) |
闭包的函数体部分由关键字 in
引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
尾随闭包
将一个很长的闭包表达式作为最后一个参数传递给函数,将这个闭包替换成为尾随闭包的形式很有用。尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:
1 | func someFunctionThatTakesAClosure(closure: () -> Void) { |
上节字符串排序闭包可以作为尾随包的形式改写在 sorted(by:)
方法圆括号的外面:
1 | reversedNames = names.sorted() { $0 > $1 } |
如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 ()
省略掉:
1 | reversedNames = names.sorted { $0 > $1 } |
当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。
1 | let digitNames = [ |
函数和闭包都是引用类型
逃逸闭包
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。在参数名之前标注 @escaping
,用来指明这个闭包是允许“逃逸”出这个函数的
一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:
1 | var completionHandlers: [() -> Void] = [] |
将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self,非逃逸闭包,这意味着它可以隐式引用 self
1 | func someFunctionWithNonescapingClosure(closure: () -> Void) { |
自动闭包
自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式,这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。
1 | var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] |
尽管在闭包的代码中,customersInLine
的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,customerProvider
的类型不是 String
,而是 () -> String
,一个没有参数且返回值为 String
的函数。
将闭包作为参数传递给函数时,你能获得同样的延时求值行为。
1 | // customersInLine is ["Alex", "Ewa", "Barry", "Daniella"] |
上面的 serve(customer:)
函数接受一个返回顾客名字的显式的闭包。下面这个版本的 serve(customer:)
完成了相同的操作,不过它并没有接受一个显式的闭包,而是通过将参数标记为 @autoclosure
来接收一个自动闭包。现在你可以将该函数当作接受 String 类型参数(而非闭包)的函数来调用。customerProvider 参数将自动转化为一个闭包,因为该参数被标记了 @autoclosure
特性
1 | // customersInLine is ["Ewa", "Barry", "Daniella"] |
想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure
和 @escaping
属性。
1 | // customersInLine i= ["Barry", "Daniella"] |
对象和类
getter 和 setter 的计算属性
1 | class EquilateralTriangle: NamedShape { |
如果你不需要计算属性,但是仍然需要在设置一个新值之前或者之后运行代码,使用 willSet 和 didSet。写入的代码会在属性值发生改变时调用,但不包含构造器中发生值改变的情况。比如,下面的类确保三角形的边长总是和正方形的边长相同
1 | class TriangleAndSquare { |
处理变量的可选值时,你可以在操作(比如方法、属性和子脚本)之前加 ?
。如果 ?
之前的值是 nil
,?
后面的东西都会被忽略,并且整个表达式返回 nil
。否则,可选值会被解包,之后的所有代码都会按照解包后的值运行。在这两种情况下,整个表达式的值也是一个可选值。
1 | let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square") |
方法
方法是与某些特定类型相关联的函数。类、结构体、枚举都可以定义实例方法;实例方法为给定类型的实例封装了具体的任务与功能。类、结构体、枚举也可以定义类型方法;类型方法与类型本身相关联。类型方法与 Objective-C 中的类方法(class methods)相似。
结构体和枚举是值类型,默认情况下,值类型的属性不能在它的实例方法中被修改。可以为这个方法选择可变mutating
行为,然后就可以从其方法内部改变它的属性。并且这个方法做的任何改变都会在方法执行结束时写回到原始结构中。
1 | struct Point { |
不能在结构体类型的常量(a constant of structure type)上调用可变方法,因为其属性不能被改变,即使属性是变量属性
1 | let fixedPoint = Point(x: 3.0, y: 3.0) |
在可变方法中给self赋值
可变方法能够赋给隐含属性 self 一个全新的实例。上面 Point 的例子可以用下面的方式改写:
1 | struct Point { |
枚举的可变方法可以把 self
设置为同一枚举类型中不同的成员:
1 | enum TriStateSwitch { |
类型方法
定义在类型本身上调用的方法,这种方法就叫做类型方法。在方法的 func
关键字之前加上关键字 static
,来指定类型方法。类还可以用关键字 class
来指定,从而允许子类重写父类该方法的实现。
在
Swift
中,你可以为所有的类、结构体和枚举定义类型方法。每一个类型方法都被它所支持的类型显式包含。
1 | class SomeClass { |
下面的例子定义了一个名为 LevelTracker
结构体。它监测玩家的游戏发展情况(游戏的不同层次或阶段)。这是一个单人游戏,但也可以存储多个玩家在同一设备上的游戏信息。
1 | struct LevelTracker { |
允许在调用 advance(to:)
时候忽略返回值,不会产生编译警告,所以函数被标注为 @discardableResult
属性
下标
下标可以定义在类、结构体和枚举中,一个类型可以定义多个下标,通过不同索引类型进行对应的重载。
下标语法
下标允许你通过在实例名称后面的方括号中传入一个或者多个索引值来对实例进行查询。定义下标使用 subscript
关键字
1 | subscript(index: Int) -> Int { |
newValue
的类型和下标操作的返回类型相同
下标用法
1 | var numberOfLegs = ["spider": 8, "ant": 6, "cat": 4] |
注意
Swift 的 Dictionary 类型的下标接受并返回可选类型的值。上例中的 numberOfLegs 字典通过下标返回的是一个 Int? 或者说“可选的 int”。Dictionary 类型之所以如此实现下标,是因为不是每个键都有对应的值,同时这也提供了一种通过键删除对应值的方式,只需将键对应的值赋值为 nil 即可
下标不能使用 in-out 参数,支持重载
定义了一个 Matrix 结构体,用于表示一个 Double 类型的二维矩阵。Matrix 结构体的下标接受两个整型参数:
1 | struct Matrix { |
通过传入合适的 row 和 column 数值来构造一个新的 Matrix 实例:
1 | var matrix = Matrix(rows: 2, columns: 2) |
将 row 和 column 的值传入下标来为矩阵设值,下标的入参使用逗号分隔:
1 | matrix[0, 1] = 1.5 |
断言在下标越界时触发:
1 | let someValue = matrix[2, 2] |
类型下标
通过在 subscript
关键字之前写下 static
关键字的方式来表示一个类型下标。类类型可以使用 class
关键字来代替 static,它允许子类重写父类中对那个下标的实现。下面的例子展示了如何定义和调用一个类型下标:
1 | enum Planet: Int { |
继承
重写属性
可以将一个继承来的只读属性重写为一个读写属性,只需要在重写版本的属性里提供 getter
和 setter
即可。但是,你不可以将一个继承来的读写属性重写为一个只读属性
如果你在重写属性中提供了
setter
,那么你也一定要提供getter
。如果你不想在重写版本中的getter
里修改继承来的属性值,你可以直接通过super.someProperty
来返回继承来的值,其中someProperty
是你要重写的属性的名字
重写属性观察器
可以通过重写属性为一个继承来的属性添加属性观察器
不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。这些属性的值是不可以被设置的,所以,为它们提供
willSet
或didSet
实现也是不恰当。 此外还要注意,你不可以同时提供重写的setter
和重写的属性观察器。如果你想观察属性值的变化,并且你已经为那个属性提供了定制的setter
,那么你在 setter 中就可以观察到任何值变化了
构造过程
构造过程是使用类、结构体或枚举类型的实例之前的准备过程。包括设置实例中每个存储属性的初始值和执行其他必须的设置或构造过程。Swift 的构造器没有返回值。它们的主要任务是保证某种类型的新实例在第一次使用前完成正确的初始化
构造器并不像函数和方法那样在括号前有一个可辨别的方法名。因此在调用构造器时,主要通过构造器中形参命名和类型来确定应该被调用的构造器。
可选属性类型
如果你自定义的类型有一个逻辑上允许值为空的存储型属性,无论是因为它无法在初始化时赋值,还是因为它在之后某个时机可以赋值为空——都需要将它声明为 可选类型。可选类型的属性将自动初始化为 nil
,表示这个属性是特意在构造过程设置为空。
1 | class SurveyQuestion { |
调查问题的答案在询问前是无法确定的,因此我们将属性 response
声明为 String?
类型,或者说是 “可选类型 String
“。当 SurveyQuestion
的实例初始化时,它将自动赋值为 nil
,表明“暂时还没有字符“。
构造过程中常量属性的赋值
可以在构造过程中的任意时间点给常量属性赋值,只要在构造过程结束时它设置成确定的值。一旦常量属性被赋值,它将永远不可更改。
对于类的实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。
1 | class SurveyQuestion { |
默认构造器
如果结构体或类为所有属性提供了默认值,又没有提供任何自定义的构造器,那么 Swift
会给这些结构体或类提供一个默认构造器。这个默认构造器将简单地创建一个所有属性值都设置为它们默认值的实例。
1 | class ShoppingListItem { |
由于 ShoppingListItem
类中的所有属性都有默认值,且它是没有父类的基类,它将自动获得一个将为所有属性设置默认值的并创建实例的默认构造器(由于 name
属性是可选 String
类型,它将接收一个默认 nil
的默认值,尽管代码中没有写出这个值)
结构体的逐一成员构造器
结构体如果没有定义任何自定义构造器,它们将自动获得一个逐一成员构造器(memberwise initializer)。不像默认构造器,即使存储型属性没有默认值,结构体也能会获得逐一成员构造器。
结构体 Size
自动获得了一个逐一成员构造器 init(width:height:)
。你可以用它来创建新的 Size 实例:
1 | struct Size { |
调用一个逐一成员构造器(memberwise initializer)时,可以省略任何一个有默认值的属性。
1 | let zeroByTwo = Size(height: 2.0) |
值类型的构造器代理
构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理
值类型(结构体和枚举类型)不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给自己的其它构造器。类则不同,它可以继承自其它类。这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。
如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)。这种限制避免了在一个更复杂的构造器中做了额外的重要设置,但有人不小心使用自动生成的构造器而导致错误的情况。
假如你希望默认构造器、逐一成员构造器以及你自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中。
1 | struct Size { |
类的继承和构造过程
类里面的所有存储型属性——包括所有继承自父类的属性——都必须在构造过程中设置初始值。
Swift 为类类型提供了两种构造器来确保实例中所有存储型属性都能获得初始值,它们被称为指定构造器
和便利构造器
。
指定构造器是类中最主要的构造器,一个指定构造器将初始化类中提供的所有属性,并调用合适的父类构造器让构造过程沿着父类链继续网上进行。
每一个类都必须至少拥有一个指定构造器
便利构造器是类中比较次要的,辅助型的构造器,你可以定义便利构造器来调用同一个类中的指定构造器,并为部分形参提供默认值。
类类型的构造器代理
为了简化指定构造器和便利构造器之间的调用关系,Swift 构造器之间的代理调用遵循以下三条规则:
规则 1
指定构造器必须调用其直接父类的的指定构造器。
规则 2
便利构造器必须调用同类中定义的其它构造器。
规则 3
便利构造器最后必须调用指定构造器。
一个更方便记忆的方法是:
指定构造器必须总是向上代理
便利构造器必须总是横向代理
下面图例中展示了一种涉及四个类的更复杂的类层级结构。它演示了指定构造器是如何在类层级中充当“漏斗”的作用,在类的构造器链上简化了类之间的相互关系。
两段式构造过程
Swift
中类的构造过程包含两个阶段。第一个阶段,类中的每个存储型属性赋一个初始值。当每个存储型属性的初始值被赋值后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步自定义它们的存储型属性。
Swift
的两段式构造过程跟Objective-C
中的构造过程类似。最主要的区别在于阶段 1,Objective-C
给每一个属性赋值 0 或空值(比如说 0 或 nil)。Swift
的构造流程则更加灵活,它允许你设置定制的初始值,并自如应对某些属性不能以0
或nil
作为合法默认值的情况。
4种安全检查,以确保两段式构造过程
- 指定构造器必须保证它所在类的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器
一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类的属性在它往上代理之前先完成初始化。
2.指定构造器必须在为继承的属性设置新值之前向上代理调用父类构造器。否则,指定构造器赋予新值将被父类中的构造器所覆盖。
3.便利构造器必须为任意属性(包括所有同类中定义的)赋新值之前代理调用其它构造器。否则便利构造器赋予的新值将被该类指定构造器所覆盖
4.构造器在第一阶段完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能饮用self
作为一个值。
类的实例在第一阶段结束以前并不是完全有效的,只有第一阶段完成之后,类的实例才是有效的,才能访问属性和调用方法。
两段式构造过程展示:
阶段 1
- 类的某个指定构造器或便利构造器被调用。
- 完成类的新实例内存的分配,但此时内存还没有被初始化。
- 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
- 指定构造器切换到父类的构造器,对其存储属性完成相同的任务。
- 这个过程沿着类的继承链一直往上执行,直到到达继承链的最顶部。
- 当到达了继承链最顶部,而且继承链的最后一个类已确保所有的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段 1 完成。
阶段 2
- 从继承链顶部往下,继承链中每个类的指定构造器都有机会进一步自定义实例。构造器此时可以访问
self
、修改它的属性并调用实例方法等等。 - 最终,继承链中任意的便利构造器有机会自定义实例和使用
self
。
构造器的继承和重写
跟 Objective-C
中的子类不同,Swift 中的子类默认情况下不会继承父类的构造器,这种机制可以防止一个父类的简单构造器被一个更精细的子类继承,而在用来创建子类时的新实例时没有完全或错误被初始化。
重写父类的指定构造器,必须在定义子类构造器时带上 override
修饰符。即使你重写的是系统自动提供的默认构造器,也需要带上 override
修饰符。
重写属性,方法或者是下标,override
修饰符会让编译器去检查父类中是否有相匹配的指定构造器,并验证构造器参数是否被按预想中被指定。
子类不能直接调用父类的便利构造器(每个规则都在上文 类的构造器代理规则 有所描述)因此,在子类中“重写”一个父类便利构造器时,不需要加 override
修饰符
如果子类的构造器没有在阶段2过程中做自定义操作,并且父类有一个无参数的指定构造器,可以在所有子类的存储属性赋值之后省略super.init()
的调用
1 | class Vehicle { |
子类可以在构造过程修改继承来的变量属性,但是不能修改继承来的常量属性。
构造器的自动继承
子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。
假设你为子类中引入的所有新属性都提供了默认值,以下 2 个规则将适用:
规则 1
如果子类没有定义任何指定构造器,它将自动继承父类所有的指定构造器。
规则 2
如果子类提供了所有父类指定构造器的实现——无论是通过规则 1 继承过来的,还是提供了自定义实现——它将自动继承父类所有的便利构造器。
子类可以将父类的指定构造器实现为便利构造器来满足规则 2
指定构造器和便利构造器实践
例子定义了包含三个类 Food
、RecipeIngredient
以及 ShoppingListItem
的层级结构,并将演示它们的构造器是如何相互作用的。
1 | class Food { |
层级中的第二个类是 Food
的子类 RecipeIngredient
。RecipeIngredient
类用来表示食谱中的一项原料。它引入了 Int
类型的属性 quantity(以及从 Food 继承过来的 name 属性),并且定义了两个构造器来创建 RecipeIngredient
实例:
1 | class RecipeIngredient: Food { |
类层级中第三个也是最后一个类是 RecipeIngredient
的子类,叫做 ShoppingListItem
。这个类构建了购物单中出现的某一种食谱原料。
1 | class ShoppingListItem: RecipeIngredient { |
因为它为自己引入的所有属性都提供了默认值,并且自己没有定义任何构造器,ShoppingListItem 将自动继承所有父类中的指定构造器和便利构造器。
下图展示了这三个类的构造器链:
可以使用三个继承来的构造器来创建 ShoppingListItem 的新实例:
1 | var breakfastList = [ |
可失败构造器
处理这种构造过程中可能会失败的情况。可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在 init 关键字后面添加问号(init?
)。
可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名,及其参数类型相同。
注意
严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用 return nil 表明可失败构造器构造失败,而不要用关键字 return 来表明构造成功。
1 | let wholeNumber: Double = 12345.0 |
枚举类型的可失败构造器
1 | enum TemperatureUnit { |
带原始值的枚举类型的可失败构造器
带原始值的枚举类型会自带一个可失败构造器 init?(rawValue:)
,该可失败构造器有一个合适的原始值类型的 rawValue
形参,选择找到的相匹配的枚举成员,找不到则构造失败。
上面的 TemperatureUnit 的例子可以用原始值类型的 Character 和进阶的 init?(rawValue:) 构造器重写为:
1 | enum TemperatureUnit: Character { |
重写可失败构造器
以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。
当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。
可以用非可失败构造器重写可失败构造器,但反过来却不行。
必要构造器
在类的构造器前添加 required
修饰符表明所有该类的子类都必须实现该构造器:
1 | class SomeClass { |
在子类重写父类的必要构造器时,必须在子类的构造器前也添加 required 修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加 override 修饰符:
1 | class SomeSubclass: SomeClass { |
如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现
通过闭包或函数设置属性的默认值
如果某个存储型属性的默认值需要一些自定义或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被构造时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。
这种类型的闭包或函数通常会创建一个跟属性类型相同的临时变量,然后修改它的值以满足预期的初始状态,最后返回这个临时变量,作为属性的默认值。
如何用闭包为属性提供默认值
1 | class SomeClass { |
闭包结尾的花括号后面接了一对空的小括号。这用来告诉 Swift 立即执行此闭包。如果你忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。
如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能在闭包里访问其它属性,即使这些属性有默认值。同样,你也不能使用隐式的 self 属性,或者调用任何实例方法。
下面例子中定义了一个结构体 Chessboard
,它构建了西洋跳棋游戏的棋盘,西洋跳棋游戏在一副黑白格交替的 8 x 8 的棋盘中进行的:
为了呈现这副游戏棋盘,Chessboard 结构体定义了一个属性 boardColors
,它是一个包含 64 个 Bool
值的数组。在数组中,值为 true
的元素表示一个黑格,值为 false
的元素表示一个白格。数组中第一个元素代表棋盘上左上角的格子,最后一个元素代表棋盘上右下角的格子。
boardColors
数组是通过一个闭包来初始化并设置颜色值的:
1 | struct Chessboard { |
每当一个新的 Chessboard 实例被创建时,赋值闭包则会被执行,boardColors 的默认值会被计算出来并返回。上面例子中描述的闭包将计算出棋盘中每个格子对应的颜色,并将这些值保存到一个临时数组 temporaryBoard 中,最后在构建完成时将此数组作为闭包返回值返回。这个返回的数组会保存到 boardColors 中,并可以通过工具函数 squareIsBlackAtRow 来查询:
1 | let board = Chessboard() |
析构过程
析构器只适用于类类型,当一个类的实例被释放之前,析构器会被立即调用。析构器用关键字 deinit
来标示
每个类最多只能有一个析构器,而且析构器不带任何参数和圆括号
1 | deinit { |
析构器实践
这个例子描述了一个简单的游戏,这里定义了两种新类型,分别是 Bank 和 Player。Bank 类管理一种虚拟硬币,确保流通的硬币数量永远不可能超过 10,000。在游戏中有且只能有一个 Bank 存在,因此 Bank 用类来实现,并使用类型属性和类型方法来存储和管理其当前状态。
1 | class Bank { |
Bank
使用 coinsInBank
属性来跟踪它当前拥有的硬币数量。Bank
还提供了两个方法,distribute(coins:)
和 receive(coins:)
,分别用来处理硬币的分发和收集
Player 类描述了游戏中的一个玩家。每一个玩家在任意时间都有一定数量的硬币存储在他们的钱包中。这通过玩家的 coinsInPurse 属性来表示:
1 | class Player { |
每个 Player 实例在初始化的过程中,都从 Bank 对象获取指定数量的硬币。如果没有足够的硬币可用,Player 实例可能会收到比指定数量少的硬币。
Player 类定义了一个 win(coins:) 方法,该方法从 Bank 对象获取一定数量的硬币,并把它们添加到玩家的钱包。Player 类还实现了一个析构器,这个析构器在 Player 实例释放前被调用。在这里,析构器的作用只是将玩家的所有硬币都返还给 Bank 对象:
1 | var playerOne: Player? = Player(coins: 100) |
创建一个 Player 实例的时候,会向 Bank 对象申请得到 100 个硬币,前提是有足够的硬币可用。这个 Player 实例存储在一个名为 playerOne 的可选类型的变量中。这里使用了一个可选类型的变量,是因为玩家可以随时离开游戏,设置为可选使你可以追踪玩家当前是否在游戏中。
因为 playerOne 是可选的,所以在访问其 coinsInPurse 属性来打印钱包中的硬币数量和调用 win(coins:) 方法时,使用感叹号(!)强制解包:
1 | playerOne!.win(coins: 2_000) |
1 | playerOne = nil |
玩家现在已经离开了游戏。这通过将可选类型的 playerOne 变量设置为 nil 来表示,意味着“没有 Player 实例”。当这一切发生时,playerOne 变量对 Player 实例的引用被破坏了。没有其它属性或者变量引用 Player 实例,因此该实例会被释放,以便回收内存。在这之前,该实例的析构器被自动调用,玩家的硬币被返还给银行。
可选链式调用
可选链式调用是一种可以在当前值可能为 nil
的可选值上请求和调用属性、方法及下标的方法。如果可选值有值,那么调用就会成功;如果可选值是 nil,那么调用将返回 nil。多个调用可以连接在一起形成一个调用链,如果其中任何一个节点为 nil,整个调用链都会失败,即返回 nil。
注意
Swift 的可选链式调用和 Objective-C 中向 nil 发送消息有些相像,但是 Swift 的可选链式调用可以应用于任意类型,并且能检查调用是否成功。
可选链式调用代替强制解包
通过在想调用的属性、方法,或下标的可选值后面放一个问号(?),可以定义一个可选链。这一点很像在可选值后面放一个叹号(!)来强制解包它的值。主要区别在于当可选值为空时可选链式调用只会调用失败,然而强制解包将会触发运行时错误
为可选链式调用定义模型类
1 | class Person { |
现在 Residence
有了一个存储 Room
实例的数组,numberOfRooms
属性被实现为计算型属性,而不是存储型属性。numberOfRooms
属性简单地返回 rooms
数组的 count
属性的值。
在可选值上通过可选链式调用来调用这个方法,该方法的返回类型会是
Void?
,而不是Void
,因为通过可选链式调用得到的返回值都是可选的。
通过判断返回值是否为nil
可以判断方法调用是否成功:
1 | if john.residence?.printNumberOfRooms() != nil { |
同样的,可以据此判断通过可选链式调用为属性赋值是否成功。
1 | if (john.residence?.address = someAddress) != nil { |
可选链调用下标
注意
通过可选链式调用访问可选值的下标时,应该将问号放在下标方括号的前面而不是后面。可选链式调用的问号一般直接跟在可选表达式的后面。
1 | if let firstRoomName = john.residence?[0].name { |
访问可选类型的下标
如果下标返回可选类型值,比如 Swift 中 Dictionary
类型的键的下标,可以在下标的结尾括号后面放一个问号来在其可选返回值上进行可选链式调用:
1 | var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]] |
多层可选链式调用
多层可选链式调用不会增加返回值的可选层级。
如果你访问的值不是可选的,可选链式调用将会返回可选值。
如果你访问的值就是可选的,可选链式调用不会让可选返回值变得“更可选”。
通过可选链式调用访问一个
Int
值,将会返回Int?
,无论使用了多少层可选链式调用。类似的,通过可选链式调用访问
Int?
值,依旧会返回Int?
值,并不会返回Int??
。
1 | if let johnsStreet = john.residence?.address?.street { |
在方法的可选返回值上进行可选链式调用
通过可选链式调用来调用 Address 的 buildingIdentifier() 方法。这个方法返回 String? 类型的值。如上所述,通过可选链式调用来调用该方法,最终的返回值依旧会是 String?
类型:
1 | if let buildingIdentifier = john.residence?.address?.buildingIdentifier() { |
如果要在该方法的返回值上进行可选链式调用,在方法的圆括号后面加上问号即可:
1 | if let beginsWithThe = |
注意
在上面的例子中,在方法的圆括号后面加上问号是因为你要在 buildingIdentifier() 方法的可选返回值上进行可选链式调用,而不是 buildingIdentifier() 方法本身
类型转换
类型转换在 Swift
中使用 is
和 as
操作符实现。
用类型检查操作符(is
)来检查一个实例是否属于特定子类型。若实例属于那个子类型,类型检查操作符返回 true
,否则返回 false
。
向下转型
某类型的一个常量或变量可能在幕后实际上属于一个子类。当确定是这种情况时,你可以尝试用类型转换操作符(as?
或 as!
)向下转到它的子类型。
因为向下转型可能会失败,类型转型操作符带有两种不同形式。条件形式 as?
返回一个你试图向下转成的类型的可选值。强制形式 as!
把试图向下转型和强制解包转换结果结合为一个操作。
只有你可以确定向下转型一定会成功时,才使用强制形式(as!)。当你试图向下转型为一个不正确的类型时,强制形式的类型转换会触发一个运行时错误。
Any和AnyObject的类型转换
Swift 为不确定类型提供了两种特殊的类型别名:
Any
可以表示任何类型,包括函数类型。AnyObject
可以表示任何类类型的实例。
示例
使用 Any 类型来和混合的不同类型一起工作,包括函数类型和非类类型。它创建了一个可以存储 Any 类型的数组 things:
1 | var things: [Any] = [] |
可以在 switch
表达式的 case
中使用 is
和 as
操作符来找出只知道是 Any
或 AnyObject
类型的常量或变量的具体类型。下面的示例迭代 things
数组中的每一项,并用 switch
语句查找每一项的类型。有几个 switch
语句的 case
绑定它们匹配到的值到一个指定类型的常量,从而可以打印这些值:
1 | for thing in things { |
注意
Any 类型可以表示所有类型的值,包括可选类型。Swift 会在你用 Any 类型来表示一个可选值的时候,给你一个警告。如果你确实想使用 Any 类型来承载可选值,你可以使用 as 操作符显式转换为 Any,如下所示:
1 | let optionalNumber: Int? = 3 |
嵌套类型
嵌套类型,可以在支持的类型中定义嵌套的枚举、类和结构体。
要在一个类型中嵌套另一个类型,将嵌套类型的定义写在其外部类型的{}
内,而且可以根据需要定义多级嵌套。
例子
定义了一个结构体 BlackjackCard
(二十一点),用来模拟 BlackjackCard
中的扑克牌点数。BlackjackCard
结构体包含两个嵌套定义的枚举类型 Suit
和 Rank
。
在 BlackjackCard
中,Ace
牌可以表示 1 或者 11,Ace
牌的这一特征通过一个嵌套在 Rank
枚举中的结构体 Values
来表示:
1 | struct BlackjackCard { |
因为 BlackjackCard 是一个没有自定义构造器的结构体,所以结构体有默认的成员构造器,所以你可以用默认的构造器去初始化新常量 theAceOfSpades
:
1 | let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades) |
引用嵌套类型
在外部引用嵌套类型时,在嵌套类型的类型名前加上其外部类型的类型名作为前缀:
1 | let heartsSymbol = BlackjackCard.Suit.hearts.rawValue |
扩展
扩展可以给一个现有的类,结构体,枚举,还有协议添加新的功能。它还拥有不需要访问被扩展类型源代码就能完成扩展的能力(即逆向建模)
Swift 中的扩展可以:
- 添加计算型实例属性和计算型类属性
- 定义实例方法和类方法
- 提供新的构造器
- 定义下标
- 定义和使用新的嵌套类型
- 使已经存在的类型遵循(conform)一个协议
在 Swift 中,你甚至可以扩展协议以提供其需要的实现,或者添加额外功能给遵循的类型所使用。
注意
扩展可以给一个类型添加新的功能,但是不能重写已经存在的功能。
使用 extension
关键字声明扩展:
1 | extension SomeType { |
扩充一个现有的类型,给它添加一个或多个协议。协议名称的写法和类或者结构体一样:
1 | extension SomeType: SomeProtocol, AnotherProtocol { |
计算型属性
扩展可以给现有类型添加计算型实例属性和计算型类属性。
这个例子给 Swift 内建的 Double
类型添加了五个计算型实例属性,从而提供与距离单位相关工作的基本支持:
1 | extension Double { |
这些计算型属性表示的含义是把一个 Double
值看作是某单位下的长度值。即使它们被实现为计算型属性,但这些属性的名字仍可紧接一个浮点型字面值,从而通过点语法来使用,并以此实现距离转换。
这些属性都是只读的计算型属性,所以为了简便,它们的表达式里面都不包含 get 关键字。它们使用 Double 作为返回值类型,并可用于所有接受 Double 类型的数学计算中:
1 | let aMarathon = 42.km + 195.m |
注意
扩展可以添加新的计算属性,但是它们不能添加存储属性,或向现有的属性添加属性观察者。
构造器
扩展可以给现有的类型添加新的构造器。它使你可以把自定义类型作为参数来供其他类型的构造器使用,或者在类型的原始实现上添加额外的构造选项。
扩展可以给一个类添加新的便利构造器
,但是它们不能给类添加新的指定构造器
或者析构器
。指定构造器和析构器必须始终由类的原始实现提供。
如果你使用扩展给另一个模块中定义的结构体添加构造器,那么新的构造器直到定义模块中使用一个构造器之前,不能访问 self
。
注意
如果你通过扩展提供一个新的构造器,你有责任确保每个通过该构造器创建的实例都是初始化完整的。
方法
扩展可以给现有类型添加新的实例方法和类方法。在下面的例子中,给 Int 类型添加了一个新的实例方法叫做 repetitions:
1 | extension Int { |
repetitions(task:)
方法仅接收一个 () -> Void
类型的参数,它表示一个没有参数没有返回值的方法。
以对任意整形数值调用 repetitions(task:) 方法,来执行对应次数的任务:
1 | 3.repetitions { |
可变实例方法
通过扩展添加的实例方法同样也可以修改(或 mutating
(改变))实例本身。结构体和枚举的方法,若是可以修改 self 或者它自己的属性,则必须将这个实例方法标记为 mutating
,就像是改变了方法的原始实现。
1 | extension Int { |
下标
展可以给现有的类型添加新的下标。下面的例子中,对 Swift 的 Int 类型添加了一个整数类型的下标。下标 [n] 从数字右侧开始,返回小数点后的第 n 位:
- 123456789[0] 返回 9
- 123456789[1] 返回 8
1 | extension Int { |
如果操作的 Int 值没有足够的位数满足所请求的下标,那么下标的现实将返回 0,将好像在数字的左边补上了 0:
1 | 746381295[9] |
嵌套类型
扩展可以给现有的类,结构体,还有枚举添加新的嵌套类型:
1 | extension Int { |
1 | func printIntegerKinds(_ numbers: [Int]) { |
注意
number.kind 已经被认为是 Int.Kind 类型。所以,在 switch 语句中所有的 Int.Kind case 分支可以被缩写,就像使用 .negative 替代 Int.Kind.negative.。
协议
协议 定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。
除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。
1 | protocol SomeProtocol { |
属性要求
协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型。此外,协议还指定属性是可读的还是可读可写的。
协议总是用 var 关键字来声明变量属性,在类型声明后加上{ set get }
来表示属性是可读可写的,可读属性则用{ get }
来表示:
1 | protocol SomeProtocol { |
方法要求
协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。不支持为协议中的方法提供默认参数。
异变方法要求
有时需要在方法中改变(或异变)方法所属的实例。例如,在值类型(即结构体和枚举)的实例方法中,将 mutating
关键字作为方法的前缀,写在 func
关键字之前,表示可以在该方法中修改它所属的实例以及实例的任意属性的值。
注意
实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写 mutating 关键字。
1 | protocol Togglable { |
Togglable
协议只定义了一个名为 toggle
的实例方法。顾名思义,toggle()
方法将改变实例属性,从而切换遵循该协议类型的实例的状态。
构造器要求
协议可以要求遵循协议的类型实现指定的构造器。
1 | protocol SomeProtocol { |
协议构造器要求的类实现
可以在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,你都必须为构造器实现标上 required
修饰符:
1 | class SomeClass: SomeProtocol { |
使用 required
修饰符可以确保所有子类也必须提供此构造器实现,从而也能遵循协议。
注意
如果类已经被标记为final
,那么不需要在协议构造器的实现中使用required
修饰符,因为final
类不能有子类。
如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 required 和 override 修饰符:
1 | protocol SomeProtocol { |
协议作为类型
协议作为类型使用,有时被称作「存在类型」,这个名词来自「存在着一个类型 T,该类型遵循协议 T」。
协议可以像其他普通类型一样使用,使用场景如下:
- 作为函数、方法或构造器中的参数类型或返回值类型
- 作为常量、变量或属性的类型
- 作为数组、字典或其他容器中的元素类型
委托
允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。委托模式的实现很简单:定义协议来封装那些需要被委托的功能,这样就能确保遵循协议的类型能提供这些功能。委托模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。
两个基于骰子游戏的协议:
1 | protocol DiceGame { |
DiceGame 协议可以被任意涉及骰子的游戏遵循。DiceGameDelegate 协议可以被任意类型遵循,用来追踪 DiceGame 的游戏过程。
1 | class SnakesAndLadders: DiceGame { |
1 | class DiceGameTracker: DiceGameDelegate { |
DiceGameTracker
的运行情况如下所示:
1 | let tracker = DiceGameTracker() |
有条件地遵循协议
泛型类型可能只在某些情况下满足一个协议的要求,比如当类型的泛型形式参数遵循对应协议时。你可以通过在扩展类型时列出限制让泛型类型有条件地遵循某协议。在你采纳协议的名字后面写泛型 where 分句。
1 | extension Array: TextRepresentable where Element: TextRepresentable { |
让 Array 类型只要在存储遵循 TextRepresentable 协议的元素时就遵循 TextRepresentable 协议
在扩展里声明采纳协议
当一个类型已经遵循了某个协议中的所有要求,却还没有声明采纳该协议时,可以通过空的扩展来让它采纳该协议:
1 | struct Hamster { |
注意
即使满足了协议的所有要求,类型也不会自动遵循协议,必须显式地遵循协议。
使用合成实现来采纳协议
Swift 可以自动提供一些简单场景下遵循 Equatable、Hashable 和 Comparable 协议的实现。在使用这些合成实现之后,无需再编写重复的代码来实现这些协议所要求的方法。
Swift 为以下几种自定义类型提供了 Equatable
协议的合成实现:
- 遵循 Equatable 协议且只有存储属性的结构体。
- 遵循 Equatable 协议且只有关联类型的枚举
- 没有任何关联类型的枚举
在包含类型原始声明的文件中声明对 Equatable 协议的遵循,可以得到 ==
操作符的合成实现,且无需自己编写任何关于 == 的实现代码。Equatable 协议同时包含 !=
操作符的默认实现。
Swift 为以下几种自定义类型提供了 Hashable
协议的合成实现:
- 遵循 Hashable 协议且只有存储属性的结构体。
- 遵循 Hashable 协议且只有关联类型的枚举
- 没有任何关联类型的枚举
在包含类型原始声明的文件中声明对 Hashable 协议的遵循,可以得到 hash(into:) 的合成实现,且无需自己编写任何关于 hash(into:) 的实现代码。
Swift
为没有原始值的枚举类型提供了 Comparable
协议的合成实现。如果枚举类型包含关联类型,那这些关联类型也必须同时遵循 Comparable
协议。在包含原始枚举类型声明的文件中声明其对 Comparable
协议的遵循,可以得到 <
操作符的合成实现,且无需自己编写任何关于 <
的实现代码。Comparable
协议同时包含 <=
、>
和 >=
操作符的默认实现。
1 | enum SkillLevel: Comparable { |
类专属的协议
通过添加 AnyObject 关键字到协议的继承列表,就可以限制协议只能被类类型采纳(以及非结构体或者非枚举的类型)
1 | protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol { |
注意
当协议定义的要求需要遵循协议的类型必须是引用语义而非值语义时,应该采用类类型专属协议
协议合成
可以使用协议组合来复合多个协议到一个要求里。协议组合行为就和你定义的临时局部协议一样拥有构成中所有协议的需求。协议组合不定义任何新的协议类型。
协议组合使用 SomeProtocol
& AnotherProtocol
的形式。你可以列举任意数量的协议,用和符号(&
)分开。
1 | protocol Named { |
检查协议一致性
可以使用 类型转换 中描述的 is 和 as 操作符来检查协议一致性,即是否遵循某协议,并且可以转换到指定的协议类型。检查和转换协议的语法与检查和转换类型是完全一样的:
- is 用来检查实例是否遵循某个协议,若遵循则返回 true,否则返回 false;
- as? 返回一个可选值,当实例遵循某个协议时,返回类型为协议类型的可选值,否则返回 nil;
- as! 将实例强制向下转换到某个协议类型,如果强转失败,将触发运行时错误
可选的协议要求
在协议中使用 optional
关键字作为前缀来定义可选要求。可选要求用在你需要和 Objective-C
打交道的代码中。协议和可选要求都必须带上 @objc
属性。标记 @objc
特性的协议只能被继承自 Objective-C
类的类或者 @objc
类遵循,其他类以及结构体和枚举均不能遵循这种协议。
使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。
协议扩展
协议可以通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个遵循协议的类型中都重复同样的实现,也无需使用全局函数。
提供默认实现
可以通过协议扩展来为协议要求的方法、计算属性提供默认的实现。如果遵循协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。
注意
通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,遵循协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。
1 | extension PrettyTextRepresentable { |
为协议扩展添加限制条件
在扩展协议的时候,可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用 where 子句来描述
1 | extension Collection where Element: Equatable { |
范型
范型函数
交换两个变量的范型版本
1 | func swapTwoValues<T>(_ a: inout T, _ b: inout T) { |
泛型版本的函数使用占位符类型名(这里叫做 T ),而不是 实际类型名(例如 Int
、String
或 Double
),占位符类型名并不关心 T 具体的类型,但它要求 a 和 b 必须是相同的类型,T 的实际类型由每次调用 swapTwoValues(_:_:)
来决定,这个尖括号告诉 Swift 那个 T 是 swapTwoValues(_:_:)
函数定义内的一个占位类型名,因此 Swift 不会去查找名为 T 的实际类型。
1 | var someInt = 3 |
类型参数
占位类型 T 是一个类型参数的例子,类型参数指定并命名一个占位类型,并且紧随在函数名后面,使用一对尖括号括起来(例如
字典 Dictionary<Key, Value>
中的 Key
和 Value
及数组 Array<Element>
中的 Element
,这能告诉阅读代码的人这些参数类型与泛型类型或函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字符来表示,例如 T
、U
、V
,例如上面演示函数 swapTwoValues(_:_:)
中的 T
范型类型
Swift 还允许自定义泛型类型。这些自定义类、结构体和枚举可以适用于任意类型,类似于 Array
和 Dictionary
1 | struct Stack<Element> { |
类型约束
对泛型函数或泛型类型中添加特定的类型约束,这将在某些情况下非常有用。类型约束指定类型参数必须继承自指定类、遵循特定的协议或协议组合
类型约束语法
在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束。
1 | func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) { |
上面这个函数有两个类型参数。第一个类型参数 T 必须是 SomeClass
子类;第二个类型参数 U 必须符合 SomeProtocol
协议。
1 | func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? { |
Swift 标准库中定义了一个 Equatable
协议,该协议要求任何遵循该协议的类型必须实现等式符(==
)及不等符(!=
),从而能对该类型的任意两个值进行比较。所有的 Swift 标准类型自动支持 Equatable
协议。
1 | let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25]) |
关联类型
定义一个协议时
,声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位符名称,其代表的实际类型在协议被遵循时才会被指定。关联类型通过 associatedtype
关键字来指定。
实战
定义了一个 Container
协议,该协议定义了一个关联类型 Item
:
1 | protocol Container { |
让泛型 Stack 结构体遵循 Container 协议:
1 | struct Stack<Element>: Container { |
给关联类型添加约束
可以在协议里给关联类型添加约束来要求遵循的类型满足约束。例如,下面的代码定义了 Container
协议, 要求关联类型 Item
必须遵循 Equatable
协议:
1 | protocol Container { |
在关联类型约束里使用协议
有一个协议细化了 Container
协议,添加了一个 suffix(_:)
方法。suffix(_:)
方法返回容器中从后往前给定数量的元素,并把它们存储在一个 Suffix
类型的实例里
1 | protocol SuffixableContainer: Container { |
Suffix 是一个关联类型,Suffix 拥有两个约束:它必须遵循 SuffixableContainer 协议(就是当前定义的协议),以及它的 Item 类型必须是和容器里的 Item 类型相同。
Stack 类型的扩展,它遵循了 SuffixableContainer 协议:
1 | extension Stack: SuffixableContainer { |
范型where语句
通过泛型 where
子句让关联类型遵从某个特定的协议,以及某个特定的类型参数和关联类型必须类型相同。
1 | func allItemsMatch<C1: Container, C2: Container> |
定义了一个名为 allItemsMatch
的泛型函数,用来检查两个 Container
实例是否包含相同顺序的相同元素。
这个函数的类型参数列表还定义了对两个类型参数的要求:
- C1 必须符合 Container 协议(写作 C1: Container)。
- C2 必须符合 Container 协议(写作 C2: Container)。
- C1 的 Item 必须和 C2 的 Item 类型相同(写作 C1.Item == C2.Item)。
- C1 的 Item 必须符合 Equatable 协议(写作 C1.Item: Equatable)。
这些要求意味着:
- someContainer 是一个 C1 类型的容器。
- anotherContainer 是一个 C2 类型的容器。
- someContainer 和 anotherContainer 包含相同类型的元素。
- someContainer 中的元素可以通过不等于操作符(!=)来检查它们是否相同。
1 | var stackOfStrings = Stack<String>() |
范型where子句的扩展
可以使用泛型 where 子句作为扩展的一部分。
1 | extension Stack where Element: Equatable { |
可以使用泛型 where 子句去扩展一个协议
1 | extension Container where Item: Equatable { |
这个 startsWith(_:)
方法首先确保容器至少有一个元素,然后检查容器中的第一个元素是否与给定的元素相等。
1 | if [9, 9, 9].startsWith(42) { |
可以编写一个泛型 where 子句去要求 Item 为特定类型
1 | extension Container where Item == Double { |
包含上下文关系的where分句
当你使用泛型时,可以为没有独立类型约束的声明添加 where 分句。例如,你可以使用 where 分句为泛型添加下标,或为扩展方法添加泛型约束。
1 | extension Container { |
下面的例子和上面的具有相同效果。
1 | extension Container where Item == Int { |
在包含上下文关系的 where 分句的例子中,由于每个方法的 where 分句各自声明了需要满足的条件,因此 average()
和 endsWith(_:)
的实现能放在同一个扩展里。而将 where 分句放在扩展进行声明也能起到同样的效果,但每一个扩展只能有一个必备条件。
具有范型where子句的关联类型
可以在关联类型后面加上具有泛型 where 的子句。
1 | protocol Container { |
迭代器(Iterator)的泛型 where 子句要求:无论迭代器是什么类型,迭代器中的元素类型,必须和容器项目的类型保持一致。makeIterator() 则提供了容器的迭代器的访问接口。
一个协议继承了另一个协议,你通过在协议声明的时候,包含泛型 where 子句,来添加了一个约束到被继承协议的关联类型。
1 | protocol ComparableContainer: Container where Item: Comparable { } |
范型下标
下标可以是泛型,它们能够包含泛型 where 子句。你可以在 subscript 后用尖括号来写占位符类型,你还可以在下标代码块花括号前写 where 子句。
1 | extension Container { |
这个 Container 协议的扩展添加了一个下标方法,接收一个索引的集合,返回每一个索引所在的值的数组。这个泛型下标的约束如下:
- 在尖括号中的泛型参数 Indices,必须是符合标准库中的 Sequence 协议的类型。
- 下标使用的单一的参数,indices,必须是 Indices 的实例。
- 泛型 where 子句要求 Sequence(Indices)的迭代器,其所有的元素都是 Int 类型。这样就能确保在序列(Sequence)中的索引和容器(Container)里面的索引类型是一致的。
综合一下,这些约束意味着,传入到 indices 下标,是一个整型的序列。
枚举
使用 enum
来创建一个枚举。
1 | enum CompassPoint { |
与 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的 CompassPoint 例子中,north,south,east 和 west 不会被隐式地赋值为 0,1,2 和 3。相反,这些枚举成员本身就是完备的值,这些值的类型是已经明确定义好的 CompassPoint 类型。
枚举成员的遍历
令枚举遵循 CaseIterable
协议。Swift
会生成一个 allCases
属性,用于表示一个包含枚举所有成员的集合。
1 | enum Beverage: CaseIterable { |
关联值
把其他类型的值和成员值一起存储起来会很有用。这额外的信息称为关联值,并且你每次在代码中使用该枚举成员时,还可以修改这个关联值
有些商品上标有使用 0 到 9 的数字的 UPC 格式的一维条形码。每一个条形码都有一个代表数字系统的数字,该数字后接五位代表厂商代码的数字,接下来是五位代表“产品代码”的数字。最后一个数字是检查位,用来验证代码是否被正确扫描
其他商品上标有 QR 码格式的二维码,它可以使用任何 ISO 8859-1 字符,并且可以编码一个最多拥有 2,953 个字符的字符串
1 | enum Barcode { |
可以使用任意一种条形码类型创建新的条形码
1 | var productBarcode = Barcode.upc(8, 85909, 51226, 3) |
Barcode
类型的常量和变量可以存储一个 .upc
或者一个 .qrCode
(连同它们的关联值),但是在同一时间只能存储这两个值中的一个。
1 | switch productBarcode { |
如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,你可以只在成员名称前标注一个 let 或者 var:
1 | switch productBarcode { |
原始值
枚举成员可以被默认值(称为原始值)预填充,这些原始值的类型必须相同
1 | enum ASCIIControlCharacter: Character { |
原始值可以是字符串、字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。
注意
原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值,像上述三个 ASCII 码。对于一个特定的枚举成员,它的原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。
原始值的隐式赋值
在使用原始值为整数或者字符串类型的枚举时,不需要显式地为每一个枚举成员设置原始值,Swift 将会自动为你赋值。
当使用整数作为原始值时,隐式赋值的值依次递增 1
当使用字符串作为枚举类型的原始值时,每个枚举成员的隐式原始值为该枚举成员的名称。
1 | enum CompassPoint: String { |
CompassPoint.south
拥有隐式原始值 south
,依次类推
使用枚举成员的 rawValue 属性可以访问该枚举成员的原始值:
1 | let earthsOrder = Planet.earth.rawValue |
原始值初始化枚举实例
如果在定义枚举类型的时候使用了原始值,那么将会自动获得一个初始化方法,这个方法接收一个叫做 rawValue 的参数,参数类型即为原始值类型,返回值则是枚举成员或 nil
1 | let possiblePlanet = Planet(rawValue: 7) |
注意
原始值构造器是一个可失败构造器,因为并不是每一个原始值都有与之对应的枚举成员。
就像类和其他所有命名类型一样,枚举可以包含方法
1 | enum Rank: Int { |
可以使用字符串或者浮点数作为枚举的原始值。使用 rawValue
属性来访问一个枚举成员的原始值。
使用 init?(rawValue:)
初始化构造器来从原始值创建一个枚举实例。如果存在与原始值相应的枚举成员就返回该枚举成员,否则就返回 nil
1 | if let convertedRank = Rank(rawValue: 3) { |
如果枚举成员的实例有原始值,那么这些值是在声明的时候就已经决定了,这意味着不同枚举实例的枚举成员总会有一个相同的原始值。当然我们也可以为枚举成员设定关联值,关联值是在创建实例时决定的。这意味着同一枚举成员不同实例的关联值可以不相同。你可以把关联值想象成枚举成员实例的存储属性。例如,考虑从服务器获取日出和日落的时间的情况。服务器会返回正常结果或者错误信息。
1 | enum ServerResponse { |
注意 ServerResponse
的值在与 switch
的分支匹配时,日升和日落时间是如何从该值中提取出来的
结构体和类
一个类的实例被称为对象
与结构体相比,类还有如下的附加功能:
- 继承允许一个类继承另一个类的特征
- 类型转换允许在运行时检查和解释一个类实例的类型
- 析构器允许一个类实例释放任何其所被分配的资源
- 引用计数允许对一个类的多次引用
所有结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。与结构体不同,类实例没有默认的成员逐一构造器。
1 | struct Resolution { |
结构体和枚举是值类型
,值类型是这样一种类型,当它被赋值给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝。
类是引用类型,与值类型不同,引用类型在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,使用的是已存在实例的引用,而不是其拷贝。
判定两个常量或者变量是否引用同一个类实例有时很有用,恒等运算符:
相同(===
)
不相同(!==
)
使用这两个运算符检测两个常量或者变量是否引用了同一个实例:
1 | if tenEighty === alsoTenEighty { |
“相同”表示两个类类型(class type)的常量或者变量引用同一个类实例。“等于”表示两个实例的值“相等”或“等价”
属性
属性将值与特定的类、结构体或枚举关联。存储属性会将常量和变量存储为实例的一部分,而计算属性则是直接计算(而不是存储)值。计算属性
可以用于类、结构体和枚举,而存储属性
只能用于类和结构体。
存储属性
一个存储属性就是存储在特定类或结构体实例里的一个常量或变量
延时加载存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy
来标示一个延时加载存储属性。
必须将延时加载属性声明成变量(使用
var
关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延时加载。
延时加载属性使用场景
当属性的值依赖于一些外部因素且这些外部因素只有在构造过程结束之后才会知道的时候
当获得属性的值因为需要复杂或者大量的计算,而需要采用需要的时候再计算的方式
1 | class DataImporter { |
DataManager
管理数据时也可能不从文件中导入数据。所以当 DataManager
的实例被创建时,没必要创建一个 DataImporter
的实例,更明智的做法是第一次用到 DataImporter
的时候才去创建它。
1 | print(manager.importer.fileName) |
如果一个被标记为
lazy
的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。
计算属性
类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个 getter
和一个可选的 setter
,来间接获取和设置其他属性或变量的值。
1 | struct Point { |
简化 Setter , Getter 声明
如果计算属性的 setter
没有定义表示新值的参数名,则可以使用默认名称 newValue
,如果整个 getter
是单一表达式,getter
会隐式地返回这个表达式结果
1 | struct CompactRect { |
必须使用
var
关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let 关键字只用来声明常量属性,表示初始化后再也无法修改的值。
只读计算属性
只有 getter
没有 setter
的计算属性叫只读计算属性。只读计算属性的声明可以去掉 get 关键字和花括号:
1 | struct Cuboid { |
属性观察器
注意
在父类初始化方法调用之后,在子类构造器中给父类的属性赋值时,会调用父类属性的 willSet 和 didSet 观察器。而在父类初始化方法调用之前,给子类的属性赋值时不会调用子类属性的观察器。
注意
如果将带有观察器的属性通过 in-out 方式传入函数,willSet 和 didSet 也会调用。这是因为 in-out 参数采用了拷入拷出内存模式:即在函数内部使用的是参数的 copy,函数结束后,又对参数重新赋值。
属性包装器
属性包装器在管理属性如何存储和定义属性的代码之间添加了一个分隔层。举例来说,如果你的属性需要线程安全性检查或者需要在数据库中存储它们的基本数据,那么必须给每个属性添加同样的逻辑代码。当使用属性包装器时,你只需在定义属性包装器时编写一次管理代码,然后应用到多个属性上来进行复用。
1 | @propertyWrapper |
TwelveOrLess
结构体确保它包装的值始终是小于等于 12 的数字
1 | struct SmallRectangle { |
通过 TwelveOrLess
属性包装器来确保它的长宽均小于等于 12
当你把一个包装器应用到一个属性上时,编译器将合成提供包装器存储空间和通过包装器访问属性的代码。(属性包装器只负责存储被包装值,所以没有合成这些代码。)不利用这个特性语法的情况下,你可以写出使用属性包装器行为的代码。
1 | struct SmallRectangle { |
_height
和 _width
属性存着这个属性包装器的一个实例,即 TwelveOrLess
。height
和 width
的 getter
和 setter
把对 wrappedValue
属性的访问包装起来。
- 设置被包装属性的初始值
能设置被包装值和最大值的构造器:
1 | @propertyWrapper |
当你把包装器应用于属性且没有设定初始值时,Swift 使用init()
构造器来设置包装器。
1 | struct ZeroRectangle { |
当你为属性指定初始值时,Swift 使用 init(wrappedValue:) 构造器来设置包装器。
1 | struct UnitRectangle { |
当你在自定义特性后面把实参写在括号里时,Swift 使用接受这些实参的构造器来设置包装器。
1 | struct NarrowRectangle { |
- 从属性包装器中呈现一个值
属性包装器可以通过定义被呈现值暴露出其他功能。
1 | @propertyWrapper |
写下 someStructure.$someNumber
即可访问包装器的被呈现值。属性包装器可以返回任何类型的值作为它的被呈现值。
用 $height
和 $width
引用包装器 height
和 width
的被呈现值:
1 | enum Size { |
属性包装器语法只是具有 getter 和 setter 的属性的语法糖,所以访问 height 和 width 的行为与访问任何其他属性的行为相同。
类型属性
实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。
可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性。
注意
跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。
存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用 lazy 修饰符。
使用关键字 static
来定义类型属性。在为类定义计算型类型属性时,可以改用关键字 class
来支持子类对父类的实现进行重写。
1 | struct SomeStructure { |
类型属性是通过类型本身来访问,而不是通过实例。比如:
1 | print(SomeStructure.storedTypeProperty) |
协议和扩展
协议
使用 protocol
来声明一个协议
1 | protocol ExampleProtocol { |
类、枚举和结构体都可以遵循协议
1 | class SimpleClass: ExampleProtocol { |
注意声明 SimpleStructure
时候 mutating
关键字用来标记一个会修改结构体的方法。SimpleClass
的声明不需要标记任何方法,因为类中的方法通常可以修改类属性(类的性质)
扩展
使用 extension
来为现有的类型添加功能,比如新的方法和计算属性。你可以使用扩展让某个在别处声明的类型来遵守某个协议,这同样适用于从外部库或者框架引入的类型。
1 | extension Int: ExampleProtocol { |
可以像使用其他命名类型一样使用协议名——例如,创建一个有不同类型但是都实现一个协议的对象集合。当你处理类型是协议的值时,协议外定义的方法不可用
1 | let protocolValue: ExampleProtocol = a |
即使 protocolValue
变量运行时的类型是 simpleClass
,编译器还是会把它的类型当做 ExampleProtocol
。这表示你不能调用在协议之外的方法或者属性。
错误处理
使用采用 Error
协议的类型来表示错误
1 | enum PrinterError: Error { |
使用 throw
来抛出一个错误和使用 throws
来表示一个可以抛出错误的函数。如果在函数中抛出一个错误,这个函数会立刻返回并且调用该函数的代码会进行错误处理。
1 | func send(job: Int, toPrinter printerName: String) throws -> String { |
有多种方式可以用来进行错误处理。一种方式是使用 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 |
范型
在尖括号里写一个名字来创建一个泛型函数或者类型
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 |
断言和先决条件
断言和先决条件是在运行时所做的检查。如果断言或者先决条件中的布尔条件评估的结果为 true(真),则代码像往常一样继续执行。如果布尔条件评估结果为 false(假),程序的当前状态是无效的,则代码执行结束,应用程序中止。
1 | let age = -3 |
基本运算符
Swift 的赋值操作并不返回任何值,所以下面语句是无效的:
1 | if x = y { |
能帮你避免把 (==
)错写成(=
)这类错误的出现
Swift 也提供恒等(===
)和不恒等(!==
)这两个比较符来判断两个对象是否引用同一个对象实例。
如果两个元组的元素相同,且长度相同的话,元组就可以被比较。比较元组大小会按照从左到右、逐值比较的方式,直到发现有两个值不等时停止。如果所有的值都相等,那么这一对元组我们就称它们是相等的。例如:
1 | (1, "zebra") < (2, "apple") // true,因为 1 小于 2 |
空合运算符
空合运算符(a ?? b
)将对可选类型 a
进行空判断,如果 a
包含一个值就进行解包,否则就返回一个默认值 b
如果 a 为非空值(non-nil),那么值 b 将不会被计算。这也就是所谓的短路求值
区间运算符
闭区间运算符(a...b
)定义一个包含从a
到 b
(包括 a
和 b
)的所有值的区间。a
的值不能超过 b
闭区间运算符在迭代一个区间的所有值时是非常有用的,如在 for-in 循环中:
1 | for index in 1...5 { |
半开区间运算符(a..<b
)定义一个从 a
到 b
但不包括 b
的区间。 之所以称为半开区间,是因为该区间包含第一个值而不包括最后的值。
半开区间的实用性在于当你使用一个从 0 开始的列表(如数组)时,非常方便地从0数到列表的长度
1 | let names = ["Anna", "Alex", "Brian", "Jack"] |
单侧区间,表达往一侧无限延伸的区间,一个包含了数组从索引 2 到结尾的所有值的区间
1 | for name in names[2...] { |
半开区间操作符也有单侧表达形式,附带上它的最终值。就像你使用区间去包含一个值,最终值并不会落在区间内。例如:
1 | for name in names[..<2] { |
字符串和字符
在 Swift 中 String
类型是值类型 ,Swift 的 String
和 Character
类型是完全兼容 Unicode 标准的。
Unicode是一个用于在不同书写系统中对文本进行编码、表示和处理的国际标准。
每一个 Swift 的 Character 类型代表一个可扩展的字形群,而一个可扩展的字形群构成了人类可读的单个字符,它由一个或多个(当组合时) Unicode 标量的序列组成。
1 | let eAcute: Character = "\u{E9}" // é |
可扩展的字形集是一个将许多复杂的脚本字符表示为单个字符值的灵活方式。
获得一个字符串中 Character 值的数量,可以使用 count
属性
1 | let unusualMenagerie = "Koala 🐨, Snail 🐌, Penguin 🐧, Dromedary 🐪" |
通过 count 属性返回的字符数量并不总是与包含相同字符的 NSString 的 length 属性相同。NSString 的 length 属性是利用 UTF-16 表示的十六位代码单元数字,而不是 Unicode 可扩展的字符群集
每一个 String 值都有一个关联的索引(index)类型,String.Index,它对应着字符串中的每一个 Character 的位置
使用 indices 属性会创建一个包含全部索引的范围(Range),用来在一个字符串中访问单个字符。
1 | let greeting = "Guten Tag!" |
调用 remove(at:) 方法可以在一个字符串的指定索引删除一个字符,调用 removeSubrange(_:) 方法可以在一个字符串的指定索引删除一个子字符串。
1 | var welcome = "hello" |
子字符串
使用下标或者 prefix(_:)
之类的方法 —— 就可以得到一个 Substring
的实例
1 | let greeting = "Hello, world!" |
比较字符串
字符串/字符可以用等于操作符(==
)和不等于操作符(!=
)
调用字符串的 hasPrefix(_:)/hasSuffix(_:)
方法来检查字符串是否拥有特定前缀/后缀,两个方法均接收一个 String 类型的参数,并返回一个布尔值。
集合类型
集合
一个类型为了存储在集合中,该类型必须是可哈希化的——也就是说,该类型必须提供一个方法来计算它的哈希值。一个哈希值是 Int
类型的,相等的对象哈希值必须相同
,比如 a == b
,因此必须 a.hashValue == b.hashValue
Swift 的所有基本类型(比如 String
、Int
、Double
和 Bool
)默认都是可哈希化的,可以作为集合值的类型或者字典键的类型。没有关联值的枚举成员值(在 枚举 有讲述)默认也是可哈希化的。
集合操作
使用 intersection(_:)
方法根据两个集合的交集创建一个新的集合。
使用 symmetricDifference(_:)
方法根据两个集合不相交的值创建一个新的集合。
使用 union(_:)
方法根据两个集合的所有值创建一个新的集合。
使用 subtracting(_:)
方法根据不在另一个集合中的值创建一个新的集合
1 | let oddDigits: Set = [1, 3, 5, 7, 9] |
集合成员关系和相等
使用“是否相等”运算符(==
)来判断两个集合包含的值是否全部相同。
使用 isSubset(of:)
方法来判断一个集合中的所有值是否也被包含在另外一个集合中。
使用 isSuperset(of:)
方法来判断一个集合是否包含另一个集合中所有的值。
使用 isStrictSubset(of:)
或者 isStrictSuperset(of:)
方法来判断一个集合是否是另外一个集合的子集合或者父集合并且两个集合并不相等。
使用 isDisjoint(with:)
方法来判断两个集合是否不含有相同的值(是否没有交集)。
1 | let houseAnimals: Set = ["🐶", "🐱"] |