2016年5月Swift 2 学习 --- 117个注意事项与要点

这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的,为了不辜负它的好意,也因为姻缘巧合,那就发布出来吧


改一下工时!!!!!!!!!!!!!

Extention

try catch

rxSwift

internalpublicprivate

varlet

asas?强转

? !

didSet

#selector

约束问题

var myLabel : UILable ?//声明全局变量myLabel

计算属性/存储属性

基础部分

1.可选类型

2.if语句以及强制解析

3.隐式解析可选类型

隐式解析可选类型

如上所述,可选类型暗示了常量或者变量可以“没有值”。可选可以通过if语句来判断是否有值,如果有值的话可以通过可选绑定来解析值。

有时候在程序架构中,第一次被赋值之后,可以确定一个可选类型_总会_有值。在这种情况下,每次都要判断和解析可选值是非常低效的,因为可以确定它总会有值。

这种类型的可选状态被定义为隐式解析可选类型(implicitly unwrapped optionals。把想要用作可选的类型的后面的问号(String?)改成感叹号(String!)来声明一个隐式解析可选类型。

当可选类型被第一次赋值之后就可以确定之后一直有值的时候,隐式解析可选类型非常有用。隐式解析可选类型主要被用在Swift中类的构造过程中,请参考无主引用以及隐式解析可选属性

一个隐式解析可选类型其实就是一个普通的可选类型,但是可以被当做非可选类型来使用,并不需要每次都使用解析来获取可选值。下面的例子展示了可选类型String和隐式解析可选类型String之间的区别:

let possibleString: String? = "An optional string."

let forcedString: String = possibleString! //需要惊叹号来获取值

let assumedString: String! = "An implicitly unwrapped optional string."

let implicitString: String = assumedString//不需要感叹号

你可以把隐式解析可选类型当做一个可以自动解析的可选类型。你要做的只是声明的时候把感叹号放到类型的结尾,而不是每次取值的可选名字的结尾。

注意:

如果你在隐式解析可选类型没有值的时候尝试取值,会触发运行时错误。和你在没有值的普通可选类型后面加一个惊叹号一样。

你仍然可以把隐式解析可选类型当做普通可选类型来判断它是否包含值:

if assumedString != nil {

print(assumedString)

}

//输出"An implicitly unwrapped optional string."

你也可以在可选绑定中使用隐式解析可选类型来检查并解析它的值:

if let definiteString = assumedString {

print(definiteString)

}

//输出"An implicitly unwrapped optional string."

注意:

如果一个变量之后可能变成nil的话请不要使用隐式解析可选类型。如果你需要在变量的生命周期中判断是否是nil的话,请使用普通可选类型。

4.错误处理 添加throws关键词来抛出错误消息

5.使用断言进行调试let age = -3

assert(age >= 0, "A person's age cannot be less than zero")

基本运算符

6.空合运算符(Nil Coalescing Operator)a ?? b—>>a != nil ? a! : b

空合运算符(a ?? b)将对可选类型a进行空判断,如果a包含一个值就进行解封,否则就返回一个默认值b表达式a必须是Optional类型。默认值b的类型必须要和a存储值的类型保持一致。

空合运算符是对以下代码的简短表达方法:

a != nil ? a! : b

上述代码使用了三目运算符。当可选类型a的值不为空时,进行强制解封(a!),访问a中的值;反之返回默认值b。无疑空合运算符(??)提供了一种更为优雅的方式去封装条件判断和解封两种行为,显得简洁以及更具可读性。

注意:如果a为非空值(non-nil),那么值b将不会被计算。这也就是所谓的短路求值。

下文例子采用空合运算符,实现了在默认颜色名和可选自定义颜色名之间抉择:

let defaultColorName = "red"

var userDefinedColorName: String?//默认值为nil

var colorNameToUse = userDefinedColorName ?? defaultColorName

// userDefinedColorName的值为空,所以colorNameToUse的值为"red"

userDefinedColorName变量被定义为一个可选的String类型,默认值为nil。由于userDefinedColorName是一个可选类型,我们可以使用空合运算符去判断其值。在上一个例子中,通过空合运算符为一个名为colorNameToUse的变量赋予一个字符串类型初始值。由于userDefinedColorName值为空,因此表达式userDefinedColorName ?? defaultColorName返回defaultColorName的值,即red。

另一种情况,分配一个非空值(non-nil)给userDefinedColorName,再次执行空合运算,运算结果为封包在userDefaultColorName中的值,而非默认值。

userDefinedColorName = "green"

colorNameToUse = userDefinedColorName ?? defaultColorName

// userDefinedColorName非空,因此colorNameToUse的值为"green"

7.闭区间运算符

8.逻辑运算符组合计算加括号更清晰

9.字符串是值类型(Strings Are Value Types)

10.使用字符(Working with Characters)let catCharacters: [Character] = ["C", "a", "t", "!", "🐱"]

11.连接字符串和字符(Concatenating Strings and Characters)—->>+

12.字符串插值(String Interpolation)

13.字符串字面量的特殊字符(Special Characters in String Literals)

转义字符\0(空字符)、\\(反斜线)、\t(水平制表符)、\n(换行符)、\r(回车符)、\"(双引号)、\'(单引号)。

14.字符串索引(String Indices)

15.数组(Arrays)是有序数据的集。集合(Sets)是无序无重复数据的集。字典(Dictionaries)是无序的键值对的集。

16.访问和修改数组shoppingList[4...6] = ["Bananas", "Apples"]

17.遍历:同时需要每个数据项的值和索引值,可以使用enumerate()方法来进行数组遍历

集合(Sets

集合(Set)用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。

注意:

Swift的Set类型被桥接到Foundation中的NSSet类。

18.集合类型语法

Swift中的Set类型被写为Set,这里的Element表示Set中允许存储的类型,和数组不同的是,集合没有等价的简化形式。

19.创建和构造一个空的集合var letters = Set()

20.用数组字面量创建集合var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]

21.创建一个空字典var namesOfIntegers = [Int: String]()

22.namesOfIntegers[16] = "sixteen"

// namesOfIntegers现在包含一个键值对

namesOfIntegers = [:]

// namesOfIntegers又成为了一个[Int: String]类型的空字典

如果上下文已经提供了类型信息,我们可以使用空字典字面量来创建一个空字典,记作[:](中括号中放一个冒号):

23.用字典字面量创建字典var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]

24.访问和修改字典在字典中使用下标语法来添加新的数据项airports["LHR"] = "London"

25.While循环

元组(Tuple)

我们可以使用元组在同一个switch语句中测试多个值。元组中的元素可以是值,也可以是区间。另外,使用下划线(_)来匹配所有可能的值。

let somePoint = (1, 1)

switch somePoint {

case (0, 0):

print("(0, 0) is at the origin")

case (_, 0):

print("(\(somePoint.0), 0) is on the x-axis")

case (0, _):

print("(0, \(somePoint.1)) is on the y-axis")

case (-2...2, -2...2):

print("(\(somePoint.0), \(somePoint.1)) is inside the box")

default:

print("(\(somePoint.0), \(somePoint.1)) is outside of the box")

}

//输出"(1, 1) is inside the box"

Where

case分支的模式可以使用where语句来判断额外的条件。

26.控制转移语句(Control Transfer Statements

控制转移语句改变你代码的执行顺序,通过它你可以实现代码的跳转。Swift有五种控制转移语句:

continue

break

fallthrough

return

throw

27.带标签的语句

提前退出

像if语句一样,guard的执行取决于一个表达式的布尔值。我们可以使用guard语句来要求条件必须为真时,以执行guard语句后的代码。不同于if语句,一个guard语句总是有一个else分句,如果条件不为真则执行else分句中的代码。

func greet(person: [String: String]) {

guard let name = person["name"] else {

return

}

print("Hello \(name)")

guard let location = person["location"] else {

print("I hope the weather is nice near you.")

return

}

print("I hope the weather is nice in \(location).")

}

greet(["name": "John"])

//输出"Hello John!"

//输出"I hope the weather is nice near you."

greet(["name": "Jane", "location": "Cupertino"])

//输出"Hello Jane!"

//输出"I hope the weather is nice in Cupertino."

28.函数(Functions)

函数定义与调用(Defining and Calling Functions)

函数参数与返回值(Function Parameters and Return Values)

函数参数名称(Function Parameter Names)

函数类型(Function Types)

嵌套函数(Nested Functions)

无返回值函数(Functions Without Return Values)

严格上来说,虽然没有返回值被定义,sayGoodbye(_:)函数依然返回了值。没有定义返回类型的函数会返回特殊的值,叫Void。它其实是一个空的元组(tuple),没有任何元素,可以写成()。

29.多重返回值函数(Functions with Multiple Return Values

你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。

30.可选元组返回类型(Optional Tuple Return Types)

如果函数返回的元组类型有可能整个元组都“没有值”,你可以使用可选的(Optional)元组返回类型反映整个元组可以是nil的事实。你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如(Int, Int)?或(String, Int, Bool)?

func minMax(array: [Int]) -> (min: Int, max: Int)? {

if array.isEmpty { return nil }

var currentMin = array[0]

var currentMax = array[0]

for value in array[1..

if value < currentMin {

currentMin = value

} else if value > currentMax {

currentMax = value

}

}

return (currentMin, currentMax)

}

31.函数参数名称(Function Parameter Names)

一般情况下,第一个参数省略其外部参数名,第二个以及随后的参数使用其局部参数名作为外部参数名。所有参数必须有独一无二的局部参数名。尽管多个参数可以有相同的外部参数名,但不同的外部参数名能让你的代码更有可读性。

32.指定外部参数名(Specifying External Parameter Names)

你可以在局部参数名前指定外部参数名,中间以空格分隔:

func someFunction(externalParameterName localParameterName: Int) {

// function body goes here, and can use localParameterName

// to refer to the argument value for that parameter

}

注意

如果你提供了外部参数名,那么函数在被调用时,必须使用外部参数名。

这个版本的sayHello(_:)函数,接收两个人的名字,会同时返回对他俩的问候:

func sayHello(to person: String, and anotherPerson: String) -> String {

return "Hello \(person) and \(anotherPerson)!"

}

print(sayHello(to: "Bill", and: "Ted"))

// prints "Hello Bill and Ted!"

为每个参数指定外部参数名后,在你调用sayHello(to:and:)函数时两个外部参数名都必须写出来。

使用外部函数名可以使函数以一种更富有表达性的类似句子的方式调用,并使函数体意图清晰,更具可读性。

33.忽略外部参数名(Omitting External Parameter Names

如果你不想为第二个及后续的参数设置外部参数名,用一个下划线(_)代替一个明确的参数名。

func someFunction(firstParameterName: Int, _ secondParameterName: Int) {

// function body goes here

// firstParameterName and secondParameterName refer to

// the argument values for the first and second parameters

}

someFunction(1, 2)

注意

因为第一个参数默认忽略其外部参数名称,显式地写下划线是多余的。

34.默认参数值(Default Parameter Values

你可以在函数体中为每个参数定义默认值(Deafult Values)。当默认值被定义后,调用这个函数时可以忽略这个参数。

35.可变参数(Variadic Parameters

一个可变参数(variadic parameter)可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数可以被传入不确定数量的输入值。通过在变量类型名后面加入(...)的方式来定义可变参数。

可变参数的传入值在函数体中变为此类型的一个数组。例如,一个叫做numbers的Double...型可变参数,在函数体内可以当做一个叫numbers的[Double]型的数组常量。

下面的这个函数用来计算一组任意长度数字的算术平均数(arithmetic mean):

func arithmeticMean(numbers: Double...) -> Double {

var total: Double = 0

for number in numbers {

total += number

}

return total / Double(numbers.count)

}

arithmeticMean(1, 2, 3, 4, 5)

// returns 3.0, which is the arithmetic mean of these five numbers

arithmeticMean(3, 8.25, 18.75)

// returns 10.0, which is the arithmetic mean of these three numbers

注意

一个函数最多只能有一个可变参数。

35.常量参数和变量参数(Constant and Variable Parameters

函数参数默认是常量。试图在函数体中更改参数值将会导致编译错误。这意味着你不能错误地更改参数值。

但是,有时候,如果函数中有传入参数的变量值副本将是很有用的。你可以通过指定一个或多个参数为变量参数,从而避免自己在函数中定义新的变量。变量参数不是常量,你可以在函数中把它当做新的可修改副本来使用。

通过在参数名前加关键字var来定义变量参数:

func alignRight(var string: String, totalLength: Int, pad: Character) -> String {

let amountToPad = totalLength - string.characters.count

if amountToPad < 1 {

return string

}

let padString = String(pad)

for _ in 1...amountToPad {

string = padString + string

}

return string

}

let originalString = "hello"

let paddedString = alignRight(originalString, totalLength: 10, pad: "-")

// paddedString is equal to "-----hello"

// originalString is still equal to "hello"

这个例子中定义了一个叫做alignRight(_:totalLength:pad:)的新函数,用来将输入的字符串对齐到更长的输出字符串的右边缘。左侧空余的地方用指定的填充字符填充。这个例子中,字符串"hello"被转换成了"-----hello"。

alignRight(_:totalLength:pad:)函数将输入参数string定义为变量参数。这意味着string现在可以作为一个局部变量,被传入的字符串值初始化,并且可以在函数体中进行操作。

函数首先计算出有多少字符需要被添加到string的左边,从而将其在整个字符串中右对齐。这个值存储在一个称为amountToPad的本地常量。如果不需要填充(也就是说,如果amountToPad小于1),该函数简单地返回没有任何填充的输入值string。

否则,该函数用pad字符创建一个叫做padString的临时String常量,并将amountToPad个padString添加到现有字符串的左边。(一个String值不能被添加到一个Character值上,所以padString常量用于确保+操作符两侧都是String值)。

注意

对变量参数所进行的修改在函数调用结束后便消失了,并且对于函数体外是不可见的。变量参数仅仅存在于函数调用的生命周期中。

36.输入输出参数(In-Out Parameters

变量参数,正如上面所述,仅仅能在函数体内被更改。如果你想要一个函数可以修改参数的值,并且想要在这些修改在函数调用结束后仍然存在,那么就应该把这个参数定义为输入输出参数(In-Out Parameters)。

定义一个输入输出参数时,在参数定义前加inout关键字。一个输入输出参数有传入函数的值,这个值被函数修改,然后被传出函数,替换原来的值。想获取更多的关于输入输出参数的细节和相关的编译器优化,请查看输入输出参数一节。

你只能传递变量给输入输出参数。你不能传入常量或者字面量(literal value),因为这些量是不能被修改的。当传入的参数作为输入输出参数时,需要在参数名前加&符,表示这个值可以被函数修改。

注意

输入输出参数不能有默认值,而且可变参数不能用inout标记。如果你用inout标记一个参数,这个参数不能被var或者let标记。

下面是例子,swapTwoInts(_:_:)函数,有两个分别叫做a和b的输入输出参数:

func swapTwoInts(inout a: Int, inout _ b: Int) {

let temporaryA = a

a = b

b = temporaryA

}

这个swapTwoInts(_:_:)函数简单地交换a与b的值。该函数先将a的值存到一个临时常量temporaryA中,然后将b的值赋给a,最后将temporaryA赋值给b。

你可以用两个Int型的变量来调用swapTwoInts(_:_:)。需要注意的是,someInt和anotherInt在传入swapTwoInts(_:_:)函数前,都加了&的前缀:

var someInt = 3

var anotherInt = 107

swapTwoInts(&someInt, &anotherInt)

print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")

// prints "someInt is now 107, and anotherInt is now 3"

从上面这个例子中,我们可以看到someInt和anotherInt的原始值在swapTwoInts(_:_:)函数中被修改,尽管它们的定义在函数体外。

注意

输入输出参数和返回值是不一样的。上面的swapTwoInts函数并没有定义任何返回值,但仍然修改了someInt和anotherInt的值。输入输出参数是函数对函数体外产生影响的另一种方式。

37.使用函数类型(Using Function Types

在Swift中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:

var mathFunction: (Int, Int) -> Int = addTwoInts

这个可以解读为:

“定义一个叫做mathFunction的变量,类型是‘一个有两个Int型的参数并返回一个Int型的值的函数’,并让这个新变量指向addTwoInts函数”。

addTwoInts和mathFunction有同样的类型,所以这个赋值过程在Swift类型检查中是允许的。

现在,你可以用mathFunction来调用被赋值的函数了:

print("Result: \(mathFunction(2, 3))")

// prints "Result: 5"

有相同匹配类型的不同函数可以被赋值给同一个变量,就像非函数类型的变量一样:

mathFunction = multiplyTwoInts

print("Result: \(mathFunction(2, 3))")

// prints "Result: 6"

就像其他类型一样,当赋值一个函数给常量或变量时,你可以让Swift来推断其函数类型:

let anotherMathFunction = addTwoInts

// anotherMathFunction is inferred to be of type (Int, Int) -> Int

38.函数类型作为参数类型(Function Types as Parameter Types

你可以用(Int, Int) -> Int这样的函数类型作为另一个函数的参数类型。这样你可以将函数的一部分实现留给函数的调用者来提供。

下面是另一个例子,正如上面的函数一样,同样是输出某种数学运算结果:

func printMathResult(mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {

print("Result: \(mathFunction(a, b))")

}

printMathResult(addTwoInts, 3, 5)

// prints "Result: 8"

printMathResult(_:_:_:)函数的作用就是输出另一个适当类型的数学函数的调用结果。它不关心传入函数是如何实现的,它只关心这个传入的函数类型是正确的。这使得printMathResult(_:_:_:)能以一种类型安全(type-safe)的方式将一部分功能转给调用者实现。

39.函数类型作为返回类型(Function Types as Return Types

你可以用函数类型作为另一个函数的返回类型。你需要做的是在返回箭头(->)后写一个完整的函数类型。

func stepForward(input: Int) -> Int {

return input + 1

}

func stepBackward(input: Int) -> Int {

return input - 1

}

func chooseStepFunction(backwards: Bool) -> (Int) -> Int {

return backwards ? stepBackward : stepForward

}

你现在可以用chooseStepFunction(_:)来获得两个函数其中的一个:

var currentValue = 3

let moveNearerToZero = chooseStepFunction(currentValue > 0)

// moveNearerToZero now refers to the stepBackward() function

一个指向返回的函数的引用保存在了moveNearerToZero常量中。

40.嵌套函数(Nested Functions

这章中你所见到的所有函数都叫全局函数(global functions),它们定义在全局域中。你也可以把函数定义在别的函数体中,称作嵌套函数(nested functions)。

默认情况下,嵌套函数是对外界不可见的,但是可以被它们的外围函数(enclosing function)调用。一个外围函数也可以返回它的某一个嵌套函数,使得这个函数可以在其他域中被使用。

func chooseStepFunction(backwards: Bool) -> (Int) -> Int {

func stepForward(input: Int) -> Int { return input + 1 }

func stepBackward(input: Int) -> Int { return input - 1 }

return backwards ? stepBackward : stepForward

}

41.闭包表达式语法(Closure Expression Syntax

闭包表达式语法有如下一般形式:

{ (parameters) -> returnType in

statements

}

闭包表达式语法可以使用常量、变量和inout类型作为参数,不能提供默认值。也可以在参数列表的最后使用可变参数。元组也可以作为参数和返回值。

下面的例子展示了之前backwards(_:_:)函数对应的闭包表达式版本的代码:

reversed = names.sort({ (s1: String, s2: String) -> Bool in

return s1 > s2

})

需要注意的是内联闭包参数和返回值类型声明与backwards(_:_:)函数类型声明相同。在这两种方式中,都写成了(s1: String, s2: String) -> Bool。然而在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。

闭包的函数体部分由关键字in引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。

由于这个闭包的函数体部分如此短,以至于可以将其改写成一行代码:

reversed = names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 } )

该例中sort(_:)方法的整体调用保持不变,一对圆括号仍然包裹住了方法的整个参数。然而,参数现在变成了内联闭包。

42.根据上下文推断类型(Inferring Type From Context

因为排序闭包函数是作为sort(_:)方法的参数传入的,Swift可以推断其参数和返回值的类型。sort(_:)方法被一个字符串数组调用,因此其参数必须是(String, String) -> Bool类型的函数。这意味着(String, String)和Bool类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:

reversed = names.sort( { s1, s2 in return s1 > s2 } )

实际上任何情况下,通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,都可以推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,您几乎不需要利用完整格式构造内联闭包。

尽管如此,您仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则可以采用完整格式的闭包。而在sort(_:)方法这个例子里,闭包的目的就是排序。由于这个闭包是为了处理字符串数组的排序,因此读者能够推测出这个闭包是用于字符串处理的。

43.参数名称缩写(Shorthand Argument Names

Swift自动为内联闭包提供了参数名称缩写功能,您可以直接通过$0,$1,$2来顺序调用闭包的参数,以此类推。

如果您在闭包表达式中使用参数名称缩写,您可以在闭包参数列表中省略对其的定义,并且对应参数名称缩写的类型会通过函数类型进行推断。in关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:

reversed = names.sort( { $0 > $1 } )

在这个例子中,$0和$1表示闭包中第一个和第二个String类型的参数。

44.运算符函数(Operator Functions

实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。Swift的String类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。而这正好与sort(_:)方法的第二个参数需要的函数类型相符合。因此,您可以简单地传递一个大于号,Swift可以自动推断出您想使用大于号的字符串函数实现:

reversed = names.sort(>)

45.尾随闭包(Trailing Closures

如果您需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用:

func someFunctionThatTakesAClosure(closure: () -> Void) {

//函数体部分

}

//以下是不使用尾随闭包进行函数调用

someFunctionThatTakesAClosure({

//闭包主体部分

})

//以下是使用尾随闭包进行函数调用

someFunctionThatTakesAClosure() {

//闭包主体部分

}

闭包表达式语法一节中作为sort(_:)方法参数的字符串排序闭包可以改写为:

reversed = names.sort() { $0 > $1 }

如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉:

reversed = names.sort { $0 > $1 }

let digitNames = [

0: "Zero", 1: "One", 2: "Two",3: "Three", 4: "Four",

5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"

]

let numbers = [16, 58, 510]

如上代码创建了一个数字位和它们英文版本名字相映射的字典。同时还定义了一个准备转换为字符串数组的整型数组。

您现在可以通过传递一个尾随闭包给numbers的map(_:)方法来创建对应的字符串版本数组:

let strings = numbers.map {

(var number) -> String in

var output = ""

while number > 0 {

output = digitNames[number % 10]! + output

number /= 10

}

return output

}

// strings常量被推断为字符串类型数组,即[String]

//其值为["OneSix", "FiveEight", "FiveOneZero"]

46.解决闭包引起的循环强引用:解决闭包引起的循环强引用

定义捕获列表

捕获列表中的每一项都由一对元素组成,一个元素是weak或unowned关键字,另一个元素是类实例的引用(例如self)或初始化过的变量(如delegate = self.delegate!)。这些项在方括号中用逗号分开。

如果闭包有参数列表和返回类型,把捕获列表放在它们前面:

lazy var someClosure: (Int, String) -> String = {

[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in

//这里是闭包的函数体

}

如果闭包没有指明参数列表或者返回类型,即它们会通过上下文推断,那么可以把捕获列表和关键字in放在闭包最开始的地方:

lazy var someClosure: Void -> String = {

[unowned self, weak delegate = self.delegate!] in

//这里是闭包的函数体

}

弱引用和无主引用

47.

在闭包和捕获的实例总是互相引用并且总是同时销毁时,将闭包内的捕获定义为无主引用。

相反的,在被捕获的引用可能会变为nil时,将闭包内的捕获定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包体内检查它们是否存在。

注意

如果被捕获的引用绝对不会变为nil,应该用无主引用,而不是弱引用。

前面的HTMLElement例子中,无主引用是正确的解决循环强引用的方法。这样编写HTMLElement类来避免循环强引用:

class HTMLElement {

let name: String

let text: String?

lazy var asHTML: Void -> String = {

[unowned self] in

if let text = self.text {

return "<\(self.name)>\(text)"

} else {

return "<\(self.name) />"

}

}

init(name: String, text: String? = nil) {

self.name = name

self.text = text

}

deinit {

print("\(name) is being deinitialized")

}

}

上面的HTMLElement实现和之前的实现一致,除了在asHTML闭包中多了一个捕获列表。这里,捕获列表是[unowned self],表示“将self捕获为无主引用而不是强引用”。

和之前一样,我们可以创建并打印HTMLElement实例:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")

print(paragraph!.asHTML())

//打印

hello, world

使用捕获列表后引用关系如下图所示:

这一次,闭包以无主引用的形式捕获self,并不会持有HTMLElement实例的强引用。如果将paragraph赋值为nil,HTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息:

paragraph = nil

//打印“p is being deinitialized”

48.闭包是引用类型(Closures Are Reference Types

上面的例子中,incrementBySeven和incrementByTen是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型

无论您将函数或闭包赋值给一个常量还是变量,您实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用incrementByTen是一个常量,而并非闭包内容本身。

这也意味着如果您将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:

let alsoIncrementByTen = incrementByTen

alsoIncrementByTen()

//返回的值为50

49.枚举语法

使用enum关键词来创建枚举并且把它们的整个定义放在一对大括号内:

enum CompassPoint {

case North

case South

case East

case West

}

与C和Objective-C不同,Swift的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的CompassPoint例子中,North,South,East和West不会被隐式地赋值为0,1,2和3。相反,这些枚举成员本身就是完备的值,这些值的类型是已经明确定义好的CompassPoint类型。

多个成员值可以出现在同一行上,用逗号隔开:

enum Planet {

case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune

}

每个枚举定义了一个全新的类型。像Swift中其他类型一样,它们的名字(例如CompassPoint和Planet)应该以一个大写字母开头。给枚举类型起一个单数名字而不是复数名字,以便于读起来更加

容易理解:

var directionToHead = CompassPoint.West

directionToHead的类型可以在它被CompassPoint的某个值初始化时推断出来。一旦directionToHead

被声明为CompassPoint类型,你可以使用更简短的点语法将其设置为另一个CompassPoint的值:

directionToHead = .East

当directionToHead的类型已知时,再次为其赋值可以省略枚举类型名。在使用具有显式类型的枚举值时,这种写法让代码具有更好的可读性。

switch语句必须穷举所有情况。如果忽略了.West这种情况,上面那段代码将无法通过编译,因为它没有考虑到CompassPoint的全部成员。强制穷举确保了枚举成员不会被意外遗漏。

当不需要匹配每个枚举成员的时候,你可以提供一个default分支来涵盖所有未明确处理的枚举成员:

let somePlanet = Planet.Earth

switch somePlanet {

case .Earth:

print("Mostly harmless")

default:

print("Not a safe place for humans")

}

//输出"Mostly harmless”

50.关联值(Associated Values)

在Swift中,使用如下方式定义表示两种商品条形码的枚举:

enum Barcode {

case UPCA(Int, Int, Int, Int)

case QRCode(String)

}

定义一个名为Barcode的枚举类型,它的一个成员值是具有(IntIntIntInt)类型关联值的UPCA,另一个成员值是具有String类型关联值的QRCode

这个定义不提供任何IntString类型的关联值,它只是定义了,当Barcode常量和变量等于Barcode.UPCABarcode.QRCode时,可以存储的关联值的类型。

switch productBarcode {

case .UPCA(let numberSystem, let manufacturer, let product, let check):

print("UPC-A: \(numberSystem), \(manufacturer), \(product), \(check).")

case .QRCode(let productCode):

print("QR code: \(productCode).")

}

//输出"QR code: ABCDEFGHIJKLMNOP."

如果一个枚举成员的所有关联值都被提取为常量,或者都被提取为变量,为了简洁,你可以只在成员名称前标注一个let或者var:

switch productBarcode {

case let .UPCA(numberSystem, manufacturer, product, check):

print("UPC-A: \(numberSystem), \(manufacturer), \(product), \(check).")

case let .QRCode(productCode):

print("QR code: \(productCode).")

}

//输出"QR code: ABCDEFGHIJKLMNOP."

51.原始值(Raw Values

关联值小节的条形码例子中,演示了如何声明存储不同类型关联值的枚举成员。作为关联值的替代选择,枚举成员可以被默认值(称为原始值)预填充,这些原始值的类型必须相同。

这是一个使用ASCII码作为原始值的枚举:

enum ASCIIControlCharacter: Character {

case Tab = "\t"

case LineFeed = "\n"

case CarriageReturn = "\r"

}

枚举类型ASCIIControlCharacter的原始值类型被定义为Character,并设置了一些比较常见的ASCII控制字符。Character的描述详见字符串和字符部分。

原始值可以是字符串,字符,或者任意整型值或浮点型值。每个原始值在枚举声明中必须是唯一的。

注意

原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值,像上述三个ASCII码。对于一个特定的枚举成员,它的原始值始终不变。关联值是创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值可以变化。

52.原始值的隐式赋值(Implicitly Assigned Raw Values

在使用原始值为整数或者字符串类型的枚举时,不需要显式地为每一个枚举成员设置原始值,Swift将会自动为你赋值。

例如,当使用整数作为原始值时,隐式赋值的值依次递增1。如果第一个枚举成员没有设置原始值,其原始值将为0。

下面的枚举是对之前Planet这个枚举的一个细化,利用整型的原始值来表示每个行星在太阳系中的顺序:

enum Planet: Int {

case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune

}

在上面的例子中,Plant.Mercury的显式原始值为1,Planet.Venus的隐式原始值为2,依次类推。

当使用字符串作为枚举类型的原始值时,每个枚举成员的隐式原始值为该枚举成员的名称。

下面的例子是CompassPoint枚举的细化,使用字符串类型的原始值来表示各个方向的名称:

enum CompassPoint: String {

case North, South, East, West

}

上面例子中,CompassPoint.South拥有隐式原始值South,依次类推。

使用枚举成员的rawValue属性可以访问该枚举成员的原始值:

let earthsOrder = Planet.Earth.rawValue

// earthsOrder值为3

let sunsetDirection = CompassPoint.West.rawValue

// sunsetDirection值为"West"

53.使用原始值初始化枚举实例(Initializing from a Raw Value

如果在定义枚举类型的时候使用了原始值,那么将会自动获得一个初始化方法,这个方法接收一个叫做rawValue的参数,参数类型即为原始值类型,返回值则是枚举成员或nil。你可以使用这个初始化方法来创建一个新的枚举实例。

这个例子利用原始值7创建了枚举成员Uranus:

let possiblePlanet = Planet(rawValue: 7)

// possiblePlanet类型为Planet?值为Planet.Uranus

然而,并非所有Int值都可以找到一个匹配的行星。因此,原始值构造器总是返回一个可选的枚举成员。在上面的例子中,possiblePlanet是Planet?类型,或者说“可选的Planet”。

注意

原始值构造器是一个可失败构造器,因为并不是每一个原始值都有与之对应的枚举成员。更多信息请参见可失败构造器

如果你试图寻找一个位置为9的行星,通过原始值构造器返回的可选Planet值将是nil:

let positionToFind = 9

if let somePlanet = Planet(rawValue: positionToFind) {

switch somePlanet {

case .Earth:

print("Mostly harmless")

default:

print("Not a safe place for humans")

}

} else {

print("There isn't a planet at position \(positionToFind)")

}

//输出"There isn't a planet at position 9

这个例子使用了可选绑定(optional binding),试图通过原始值9来访问一个行星。if let somePlanet = Planet(rawValue: 9)语句创建了一个可选Planet,如果可选Planet的值存在,就会赋值给somePlanet。在这个例子中,无法检索到位置为9的行星,所以else分支被执行。

54.递归枚举(Recursive Enumerations

当各种可能的情况可以被穷举时,非常适合使用枚举进行数据建模,例如可以用枚举来表示用于简单整数运算的操作符。这些操作符让你可以将简单的算术表达式,例如整数5,结合为更为复杂的表达式,例如5 + 4。

算术表达式的一个重要特性是,表达式可以嵌套使用。例如,表达式(5 + 4) * 2,乘号右边是一个数字,左边则是另一个表达式。因为数据是嵌套的,因而用来存储数据的枚举类型也需要支持这种嵌套——这意味着枚举类型需要支持递归。

递归枚举(recursive enumeration)是一种枚举类型,它有一个或多个枚举成员使用该枚举类型的实例作为关联值。使用递归枚举时,编译器会插入一个间接层。你可以在枚举成员前加上indirect来表示该成员可递归。

例如,下面的例子中,枚举类型存储了简单的算术表达式:

enum ArithmeticExpression {

case Number(Int)

indirect case Addition(ArithmeticExpression, ArithmeticExpression)

indirect case Multiplication(ArithmeticExpression, ArithmeticExpression)

}

你也可以在枚举类型开头加上indirect关键字来表明它的所有成员都是可递归的:

indirect enum ArithmeticExpression {

case Number(Int)

case Addition(ArithmeticExpression, ArithmeticExpression)

case Multiplication(ArithmeticExpression, ArithmeticExpression)

}

上面定义的枚举类型可以存储三种算术表达式:纯数字、两个表达式相加、两个表达式相乘。枚举成员Addition和Multiplication的关联值也是算术表达式——这些关联值使得嵌套表达式成为可能。

要操作具有递归性质的数据结构,使用递归函数是一种直截了当的方式。例如,下面是一个对算术表达式求值的函数:

func evaluate(expression: ArithmeticExpression) -> Int {

switch expression {

case .Number(let value):

return value

case .Addition(let left, let right):

return evaluate(left) + evaluate(right)

case .Multiplication(let left, let right):

return evaluate(left) * evaluate(right)

}

}

//计算(5 + 4) * 2

let five = ArithmeticExpression.Number(5)

let four = ArithmeticExpression.Number(4)

let sum = ArithmeticExpression.Addition(five, four)

let product = ArithmeticExpression.Multiplication(sum, ArithmeticExpression.Number(2))

print(evaluate(product))

//输出"18"

该函数如果遇到纯数字,就直接返回该数字的值。如果遇到的是加法或乘法运算,则分别计算左边表达式和右边表达式的值,然后相加或相乘。

55.类和结构体(Classes and Structures)

与其他编程语言所不同的是,Swift并不要求你为自定义类和结构去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类或者结构体,系统将会自动生成面向其它代码的外部接口。

56.类和结构体对比

Swift中类和结构体有很多共同点。共同处在于:

定义属性用于存储值

定义方法用于提供功能

定义附属脚本用于访问值

定义构造器用于生成初始化值

通过扩展以增加默认实现的功能

实现协议以提供某种标准功能

与结构体相比,类还有如下的附加功能:

继承允许一个类继承另一个类的特征

类型转换允许在运行时检查和解释一个类实例的类型

析构器允许一个类实例释放任何其所被分配的资源

引用计数允许对一个类的多次引用

注意

结构体总是通过被复制的方式在代码中传递,不使用引用计数。

57.结构体类型的成员逐一构造器(Memberwise Initializers for Structure Types

所有结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:

let vga = Resolution(width:640, height: 480)

与结构体不同,类实例没有默认的成员逐一构造器。

58.结构体和枚举是值类型

值类型被赋予给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝。

在之前的章节中,我们已经大量使用了值类型。实际上,在Swift中,所有的基本类型:整数(Integer)、浮点数(floating-point)、布尔值(Boolean)、字符串(string)、数组(array)和字典(dictionary),都是值类型,并且在底层都是以结构体的形式所实现。

Swift中,所有的结构体和枚举类型都是值类型。这意味着它们的实例,以及实例中所包含的任何值类型属性,在代码中传递的时候都会被复制。

请看下面这个示例,其使用了前一个示例中的Resolution结构体:

let hd = Resolution(width: 1920, height: 1080)

var cinema = hd

在以上示例中,声明了一个名为hd的常量,其值为一个初始化为全高清视频分辨率(1920像素宽,1080像素高)的Resolution实例。

然后示例中又声明了一个名为cinema的变量,并将hd赋值给它。因为Resolution是一个结构体,所以cinema的值其实是hd的一个拷贝副本,而不是hd本身。尽管hd和cinema有着相同的宽(width)和高(height),但是在幕后它们是两个完全不同的实例。

下面,为了符合数码影院放映的需求(2048像素宽,1080像素高),cinema的width属性需要作如下修改:

cinema.width = 2048

这里,将会显示cinema的width属性确已改为了2048:

print("cinema is now\(cinema.width) pixels wide")

//输出"cinema is now 2048 pixels wide"

然而,初始的hd实例中width属性还是1920:

print("hd is still \(hd.width) pixels wide")

//输出"hd is still 1920 pixels wide"

在将hd赋予给cinema的时候,实际上是将hd中所存储的值进行拷贝,然后将拷贝的数据存储到新的cinema实例中。结果就是两个完全独立的实例碰巧包含有相同的数值。由于两者相互独立,因此将cinema的width修改为2048并不会影响hd中的width的值。

枚举也遵循相同的行为准则:

enum CompassPoint {

case North, South, East, West

}

var currentDirection = CompassPoint.West

let rememberedDirection = currentDirection

currentDirection = .East

if rememberedDirection == .West {

print("The remembered direction is still .West")

}

//输出"The remembered direction is still .West"

上例中rememberedDirection被赋予了currentDirection的值,实际上它被赋予的是值的一个拷贝。赋值过程结束后再修改currentDirection的值并不影响rememberedDirection所储存的原始值的拷贝。

59.类是引用类型

与值类型不同,引用类型在被赋予到一个变量、常量或者被传递到一个函数时,其值不会被拷贝。因此,引用的是已存在的实例本身而不是其拷贝。

let tenEighty = VideoMode()

tenEighty.resolution = hd

tenEighty.interlaced = true

tenEighty.name = "1080i"

tenEighty.frameRate = 25.0

let alsoTenEighty = tenEighty

alsoTenEighty.frameRate = 30.0

需要注意的是tenEighty和alsoTenEighty被声明为常量而不是变量。然而你依然可以改变tenEighty.frameRate和alsoTenEighty.frameRate,因为tenEighty和alsoTenEighty这两个常量的值并未改变。它们并不存储这个VideoMode实例,而仅仅是对VideoMode实例的引用。所以,改变的是被引用的VideoModeframeRate属性,而不是引用VideoMode的常量的值。

60.恒等运算符

因为类是引用类型,有可能有多个常量和变量在幕后同时引用同一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予到常量、变量或者传递到函数时,其值总是会被拷贝。)

如果能够判定两个常量或者变量是否引用同一个类实例将会很有帮助。为了达到这个目的,Swift内建了两个恒等运算符:

等价于(===

不等价于(!==

运用这两个运算符检测两个常量或者变量是否引用同一个实例:

if tenEighty === alsoTenEighty {

print("tenEighty and alsoTenEighty refer to the same Resolution instance.")

}

//输出"tenEighty and alsoTenEighty refer to the same Resolution instance."

请注意,等价于(用三个等号表示,===)与等于(用两个等号表示,==)的不同:

等价于表示两个类类型(class type)的常量或者变量引用同一个类实例。

等于表示两个实例的值相等相同,判定时要遵照设计者定义的评判标准,因此相对于相等来说,这是一种更加合适的叫法。

61.指针

如果你有C,C++或者Objective-C语言的经验,那么你也许会知道这些语言使用指针来引用内存中的地址。一个引用某个引用类型实例的Swift常量或者变量,与C语言中的指针类似,但是并不直接指向某个内存地址,也不要求你使用星号(*)来表明你在创建一个引用。Swift中的这些引用与其它的常量或变量的定义方式相同

62.类和结构体的选择

按照通用的准则,当符合一条或多条以下条件时,请考虑构建结构体:

该数据结构的主要目的是用来封装少量相关简单数据值。

有理由预计该数据结构的实例在被赋值或传递时,封装的数据将会被拷贝而不是被引用。

该数据结构中储存的值类型属性,也应该被拷贝,而不是被引用。

该数据结构不需要去继承另一个既有类型的属性或者行为。

举例来说,以下情境中适合使用结构体:

几何形状的大小,封装一个width属性和height属性,两者均为Double类型。

一定范围内的路径,封装一个start属性和length属性,两者均为Int类型。

三维坐标系内一点,封装x,y和z属性,三者均为Double类型。

在所有其它案例中,定义一个类,生成一个它的实例,并通过引用来管理和传递。实际中,这意味着绝大部分的自定义数据构造都应该是类,而非结构体。

63.字符串(String)、数组(Array)、和字典(Dictionary)类型的赋值与复制行为

Swift中,许多基本类型,诸如String,Array和Dictionary类型均以结构体的形式实现。这意味着被赋值给新的常量或变量,或者被传入函数或方法中时,它们的值会被拷贝。

Objective-C中NSString,NSArray和NSDictionary类型均以类的形式实现,而并非结构体。它们在被赋值或者被传入函数或方法时,不会发生值拷贝,而是传递现有实例的引用。

注意

以上是对字符串、数组、字典的“拷贝”行为的描述。在你的代码中,拷贝行为看起来似乎总会发生。然而,Swift在幕后只在绝对必要时才执行实际的拷贝。Swift管理所有的值拷贝以确保性能最优化,所以你没必要去回避赋值来保证性能最优化。

64.存储属性

简单来说,一个存储属性就是存储在特定类或结构体实例里的一个常量或变量。存储属性可以是变量存储属性(用关键字var定义),也可以是常量存储属性(用关键字let定义)。

65.常量结构体的存储属性

如果创建了一个结构体的实例并将其赋值给一个常量,则无法修改该实例的任何属性,即使有属性被声明为变量也不行:

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)

//该区间表示整数0,1,2,3

rangeOfFourItems.firstValue = 6

//尽管firstValue是个变量属性,这里还是会报错

因为rangeOfFourItems被声明成了常量(用let关键字),即使firstValue是一个变量属性,也无法再修改它了。

这种行为是由于结构体(struct)属于值类型。当值类型的实例被声明为常量的时候,它的所有属性也就成了常量。

属于引用类型的类(class)则不一样。把一个引用类型的实例赋给一个常量后,仍然可以修改该实例的变量属性。

66.延迟存储属性

延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用lazy来标示一个延迟存储属性。

注意

必须将延迟存储属性声明成变量(使用var关键字),因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造过程完成之前必须要有初始值,因此无法声明成延迟属性。

延迟属性很有用,当属性的值依赖于在实例的构造过程结束后才会知道影响值的外部因素时,或者当获得属性的初始值需要复杂或大量计算时,可以只在需要的时候计算它。

下面的例子使用了延迟存储属性来避免复杂类中不必要的初始化。例子中定义了DataImporter和DataManager两个类,下面是部分代码:

class DataImporter {

/*

DataImporter是一个负责将外部文件中的数据导入的类。

这个类的初始化会消耗不少时间。

*/

var fileName = "data.txt"

//这里会提供数据导入功能

}

class DataManager {

lazy var importer = DataImporter()

var data = [String]()

//这里会提供数据管理功能

}

let manager = DataManager()

manager.data.append("Some data")

manager.data.append("Some more data")

// DataImporter实例的importer属性还没有被创建

DataManager类包含一个名为data的存储属性,初始值是一个空的字符串(String)数组。这里没有给出全部代码,只需知道DataManager类的目的是管理和提供对这个字符串数组的访问即可。

DataManager的一个功能是从文件导入数据。该功能由DataImporter类提供,DataImporter完成初始化需要消耗不少时间:因为它的实例在初始化时可能要打开文件,还要读取文件内容到内存。

DataManager管理数据时也可能不从文件中导入数据。所以当DataManager的实例被创建时,没必要创建一个DataImporter的实例,更明智的做法是第一次用到DataImporter的时候才去创建它。

由于使用了lazy,importer属性只有在第一次被访问的时候才被创建。比如访问它的属性fileName时:

print(manager.importer.fileName)

// DataImporter实例的importer属性现在被创建了

//输出"data.txt”

注意

如果一个被标记为lazy的属性在没有初始化时就同时被多个线程访问,则无法保证该属性只会被初始化一次。

67.存储属性和实例变量

如果您有过Objective-C经验,应该知道Objective-C为类实例存储值和引用提供两种方法。除了属性之外,还可以使用实例变量作为属性值的后端存储。

Swift编程语言中把这些理论统一用属性来实现。Swift中的属性没有对应的实例变量,属性的后端存储也无法直接访问。这就避免了不同场景下访问方式的困扰,同时也将属性的定义简化成一个语句。属性的全部信息——包括命名、类型和内存管理特征——都在唯一一个地方(类型定义中)定义。

68.计算属性

除存储属性外,类、结构体和枚举可以定义计算属性。计算属性不直接存储值,而是提供一个getter和一个可选的setter,来间接获取和设置其他属性或变量的值。

struct Point {

var x = 0.0, y = 0.0

}

struct Size {

var width = 0.0, height = 0.0

}

struct Rect {

var origin = Point()

var size = Size()

var center: Point {

get {

let centerX = origin.x + (size.width / 2)

let centerY = origin.y + (size.height / 2)

return Point(x: centerX, y: centerY)

}

set(newCenter) {

origin.x = newCenter.x - (size.width / 2)

origin.y = newCenter.y - (size.height / 2)

}

}

}

var square = Rect(origin: Point(x: 0.0, y: 0.0),

size: Size(width: 10.0, height: 10.0))

let initialSquareCenter = square.center

square.center = Point(x: 15.0, y: 15.0)

print("square.origin is now at (\(square.origin.x), \(square.origin.y))")

//输出"square.origin is now at (10.0, 10.0)”

69.便捷setter声明

如果计算属性的setter没有定义表示新值的参数名,则可以使用默认名称newValue。下面是使用了便捷setter声明的Rect结构体代码:

struct AlternativeRect {

var origin = Point()

var size = Size()

var center: Point {

get {

let centerX = origin.x + (size.width / 2)

let centerY = origin.y + (size.height / 2)

return Point(x: centerX, y: centerY)

}

set {

origin.x = newValue.x - (size.width / 2)

origin.y = newValue.y - (size.height / 2)

}

}

}

70.只读计算属性

只有getter没有setter的计算属性就是只读计算属性。只读计算属性总是返回一个值,可以通过点运算符访问,但不能设置新的值。

注意

必须使用var关键字定义计算属性,包括只读计算属性,因为它们的值不是固定的。let关键字只用来声明常量属性,表示初始化后再也无法修改的值。

只读计算属性的声明可以去掉get关键字和花括号

struct Cuboid {

var width = 0.0, height = 0.0, depth = 0.0

var volume: Double {

return width * height * depth

}

}

let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)

print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")

//输出"the volume of fourByFiveByTwo is 40.0"

这个例子定义了一个名为Cuboid的结构体,表示三维空间的立方体,包含width、height和depth属性。结构体还有一个名为volume的只读计算属性用来返回立方体的体积。为volume提供setter毫无意义,因为无法确定如何修改width、height和depth三者的值来匹配新的volume。然而,Cuboid提供一个只读计算属性来让外部用户直接获取体积是很有用的。

71.属性观察器

属性观察器监控和响应属性值的变化,每次属性被设置值的时候都会调用属性观察器,即使新值和当前值相同的时候也不例外。

可以为除了延迟存储属性之外的其他存储属性添加属性观察器,也可以通过重写属性的方式为继承的属性(包括存储属性和计算属性)添加属性观察器。你不必为非重写的计算属性添加属性观察器,因为可以通过它的setter直接监控和响应值的变化。属性重写请参考重写

可以为属性添加如下的一个或全部观察器:

willSet在新的值被设置之前调用

didSet在新的值被设置之后立即调用

willSet观察器会将新的属性值作为常量参数传入,在willSet的实现代码中可以为这个参数指定一个名称,如果不指定则参数仍然可用,这时使用默认名称newValue表示。

同样,didSet观察器会将旧的属性值作为参数传入,可以为该参数命名或者使用默认参数名oldValue。如果在didSet方法中再次对该属性赋值,那么新值会覆盖旧的值。

例子:

class StepCounter {

var totalSteps: Int = 0 {

willSet(newTotalSteps) {

print("About to set totalSteps to \(newTotalSteps)")

}

didSet {

if totalSteps > oldValue{

print("Added \(totalSteps - oldValue) steps")

}

}

}

}

let stepCounter = StepCounter()

stepCounter.totalSteps = 200

// About to set totalSteps to 200

// Added 200 steps

stepCounter.totalSteps = 360

// About to set totalSteps to 360

// Added 160 steps

stepCounter.totalSteps = 896

// About to set totalSteps to 896

// Added 536 steps

注意

父类的属性在子类的构造器中被赋值时,它在父类中的willSet和didSet观察器会被调用,随后才会调用子类的观察器。在父类书初始化方法调用之前,子类给属性赋值时,观察器不会被调用。

注意

如果将属性通过in-out方式传入函数,willSet和didSet也会调用。这是因为in-out参数采用了拷入拷出模式:即在函数内部使用的是参数的copy,函数结束后,又对参数重新赋值

72.全局变量和局部变量

计算属性和属性观察器所描述的功能也可以用于全局变量和局部变量。全局变量是在函数、方法、闭包或任何类型之外定义的变量。局部变量是在函数、方法或闭包内部定义的变量。

注意

全局的常量或变量都是延迟计算的,跟延迟存储属性相似,不同的地方在于,全局的常量或变量不需要标记lazy修饰符。

局部范围的常量或变量从不延迟计算。

73.类型属性

实例属性属于一个特定类型的实例,每创建一个实例,实例都拥有属于自己的一套属性值,实例之间的属性相互独立。

也可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一一份。这种属性就是类型属性

类型属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量(就像C语言中的静态常量),或者所有实例都能访问的一个变量(就像C语言中的静态变量)。

存储型类型属性可以是变量或常量,计算型类型属性跟实例的计算型属性一样只能定义成变量属性。

注意

跟实例的存储型属性不同,必须给存储型类型属性指定默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。

存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才会被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,并且不需要对其使用lazy修饰符。

74.类型属性语法

在C或Objective-C中,与某个类型关联的静态常量和静态变量,是作为全局(global)静态变量定义的。但是在Swift中,类型属性是作为类型定义的一部分写在类型最外层的花括号内,因此它的作用范围也就在类型支持的范围内。

使用关键字static来定义类型属性。在为类定义计算型类型属性时,可以改用关键字class来支持子类对父类的实现进行重写。下面的例子演示了存储型和计算型类型属性的语法:

struct SomeStructure {

static var storedTypeProperty = "Some value."

static var computedTypeProperty: Int {

return 1

}

}

enum SomeEnumeration {

static var storedTypeProperty = "Some value."

static var computedTypeProperty: Int {

return 6

}

}

class SomeClass {

static var storedTypeProperty = "Some value."

static var computedTypeProperty: Int {

return 27

}

class var overrideableComputedTypeProperty: Int {

return 107

}

}

75.获取和设置类型属性的值

跟实例属性一样,类型属性也是通过点运算符来访问。但是,类型属性是通过类型本身来访问,而不是通过实例。比如:

print(SomeStructure.storedTypeProperty)

//输出"Some value."

SomeStructure.storedTypeProperty = "Another value."

print(SomeStructure.storedTypeProperty)

//输出"Another value.”

print(SomeEnumeration.computedTypeProperty)

//输出"6"

print(SomeClass.computedTypeProperty)

//输出"27"

struct AudioChannel {

static let thresholdLevel = 10

static var maxInputLevelForAllChannels = 0

var currentLevel: Int = 0 {

didSet {

if currentLevel > AudioChannel.thresholdLevel {

//将当前音量限制在阀值之内

currentLevel = AudioChannel.thresholdLevel

}

if currentLevel > AudioChannel.maxInputLevelForAllChannels {

//存储当前音量作为新的最大输入音量

AudioChannel.maxInputLevelForAllChannels = currentLevel

}

}

}

}

76.方法(Methods)

方法是与某些特定类型相关联的函数。类、结构体、枚举都可以定义实例方法;实例方法为给定类型的实例封装了具体的任务与功能。类、结构体、枚举也可以定义类型方法;类型方法与类型本身相关联。类型方法与Objective-C中的类方法(class methods)相似。

结构体和枚举能够定义方法是Swift与C/Objective-C的主要区别之一。在Objective-C中,类是唯一能定义方法的类型。但Swift中,你不仅能选择是否要定义一个类/结构体/枚举,还能灵活地在你创建的类型(类/结构体/枚举)上定义方法。

77.修改方法的外部参数名称(Modifying External Parameter Name Behavior for Methods)

有时为方法的第一个参数提供一个外部参数名称是非常有用的,尽管这不是默认的行为。你自己可以为第一个参数添加一个显式的外部名称。

相反,如果你不想为方法的第二个及后续的参数提供一个外部名称,可以通过使用下划线(_)作为该参数的显式外部名称,这样做将覆盖默认行为。

78.实例方法(Instance Methods)

实例方法是属于某个特定类、结构体或者枚举类型实例的方法。实例方法提供访问和修改实例属性的方法或提供与实例目的相关的功能,并以此来支撑实例的功能。实例方法的语法与函数完全一致,详情参见函数

class Counter {

var count = 0

func increment() {

++count

}

func incrementBy(amount: Int) {

count += amount

}

func reset() {

count = 0

}

}

Counter类定义了三个实例方法:

increment让计数器按一递增;

incrementBy(amount: Int)让计数器按一个指定的整数值递增;

reset将计数器重置为0。

Counter这个类还声明了一个可变属性count,用它来保持对当前计数器值的追踪。

和调用属性一样,用点语法(dot syntax)调用实例方法:

let counter = Counter()

//初始计数值是0

counter.increment()

//计数值现在是1

counter.incrementBy(5)

//计数值现在是6

counter.reset()

//计数值现在是0

79.self属性(The self Property)

类型的每一个实例都有一个隐含属性叫做selfself完全等同于该实例本身。你可以在一个实例的实例方法中使用这个隐含的self属性来引用当前实例。

上面例子中的increment方法还可以这样写:

func increment() {

self.count++

}

实际上,你不必在你的代码里面经常写self。不论何时,只要在一个方法中使用一个已知的属性或者方法名称,如果你没有明确地写self,Swift假定你是指当前实例的属性或者方法。这种假定在上面的Counter中已经示范了:Counter中的三个实例方法中都使用的是count(而不是self.count)。

使用这条规则的主要场景是实例方法的某个参数名称与实例的某个属性名称相同的时候。在这种情况下,参数名称享有优先权,并且在引用属性时必须使用一种更严格的方式。这时你可以使用self属性来区分参数名称和属性名称。

下面的例子中,self消除方法参数x和实例属性x之间的歧义:

struct Point {

var x = 0.0, y = 0.0

func isToTheRightOfX(x: Double) -> Bool {

return self.x > x

}

}

let somePoint = Point(x: 4.0, y: 5.0)

if somePoint.isToTheRightOfX(1.0) {

print("This point is to the right of the line where x == 1.0")

}

//打印输出: This point is to the right of the line where x == 1.0

如果不使用self前缀,Swift就认为两次使用的x都指的是名称为x的函数参数。

80.在实例方法中修改值类型(Modifying Value Types from Within Instance Methods)

结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法中被修改。

但是,如果你确实需要在某个特定的方法中修改结构体或者枚举的属性,你可以为这个方法选择可变(mutating)行为,然后就可以从其方法内部改变它的属性;并且这个方法做的任何改变都会在方法执行结束时写回到原始结构中。方法还可以给它隐含的self属性赋予一个全新的实例,这个新实例在方法结束时会替换现存实例。

要使用可变方法,将关键字mutating放到方法的func关键字之前就可以了:

struct Point {

var x = 0.0, y = 0.0

mutating func moveByX(deltaX: Double, y deltaY: Double) {

x += deltaX

y += deltaY

}

}

var somePoint = Point(x: 1.0, y: 1.0)

somePoint.moveByX(2.0, y: 3.0)

print("The point is now at (\(somePoint.x), \(somePoint.y))")

//打印输出: "The point is now at (3.0, 4.0)"

上面的Point结构体定义了一个可变方法moveByX(_:y:)来移动Point实例到给定的位置。该方法被调用时修改了这个点,而不是返回一个新的点。方法定义时加上了mutating关键字,从而允许修改属性。

注意,不能在结构体类型的常量(a constant of structure type)上调用可变方法,因为其属性不能被改变,即使属性是变量属性,详情参见常量结构体的存储属性

let fixedPoint = Point(x: 3.0, y: 3.0)

fixedPoint.moveByX(2.0, y: 3.0)

//这里将会报告一个错误

81.在可变方法中给self赋值(Assigning to self Within a Mutating Method)

可变方法能够赋给隐含属性self一个全新的实例。上面Point的例子可以用下面的方式改写:

struct Point {

var x = 0.0, y = 0.0

mutating func moveByX(deltaX: Double, y deltaY: Double) {

self = Point(x: x + deltaX, y: y + deltaY)

}

}

新版的可变方法moveByX(_:y:)创建了一个新的结构体实例,它的x和y的值都被设定为目标值。调用这个版本的方法和调用上个版本的最终结果是一样的。

枚举的可变方法可以把self设置为同一枚举类型中不同的成员:

enum TriStateSwitch {

case Off, Low, High

mutating func next() {

switch self {

case Off:

self = Low

case Low:

self = High

case High:

self = Off

}

}

}

var ovenLight = TriStateSwitch.Low

ovenLight.next()

// ovenLight现在等于.High

ovenLight.next()

// ovenLight现在等于.Off

上面的例子中定义了一个三态开关的枚举。每次调用next()方法时,开关在不同的电源状态(Off,Low,High)之间循环切换。

82.类型方法(Type Methods)

在方法的func关键字之前加上关键字static,来指定类型方法。类还可以用关键字class来允许子类重写父类的方法实现。

注意

在Objective-C中,你只能为Objective-C的类类型(classes)定义类型方法(type-level methods)。在Swift中,你可以为所有的类、结构体和枚举定义类型方法。每一个类型方法都被它所支持的类型显式包含。

类型方法和实例方法一样用点语法调用。但是,你是在类型上调用这个方法,而不是在实例上调用。下面是如何在SomeClass类上调用类型方法的例子:

class SomeClass {

class func someTypeMethod() {

// type method implementation goes here

}

}

SomeClass.someTypeMethod()

在类型方法的方法体(body)中,self指向这个类型本身,而不是类型的某个实例。这意味着你可以用self来消除类型属性和类型方法参数之间的歧义(类似于我们在前面处理实例属性和实例方法参数时做的那样)。

一般来说,在类型方法的方法体中,任何未限定的方法和属性名称,可以被本类中其他的类型方法和类型属性引用。一个类型方法可以直接通过类型方法的名称调用本类中的其它类型方法,而无需在方法名称前面加上类型名称。类似地,在结构体和枚举中,也能够直接通过类型属性的名称访问本类中的类型属性,而不需要前面加上类型名称。

下面的例子定义了一个名为LevelTracker结构体。它监测玩家的游戏发展情况(游戏的不同层次或阶段)。这是一个单人游戏,但也可以存储多个玩家在同一设备上的游戏信息。

游戏初始时,所有的游戏等级(除了等级1)都被锁定。每次有玩家完成一个等级,这个等级就对这个设备上的所有玩家解锁。LevelTracker结构体用类型属性和方法监测游戏的哪个等级已经被解锁。它还监测每个玩家的当前等级。

struct LevelTracker {

static var highestUnlockedLevel = 1

static func unlockLevel(level: Int) {

if level > highestUnlockedLevel { highestUnlockedLevel = level }

}

static func levelIsUnlocked(level: Int) -> Bool {

return level <= highestUnlockedLevel

}

var currentLevel = 1

mutating func advanceToLevel(level: Int) -> Bool {

if LevelTracker.levelIsUnlocked(level) {

currentLevel = level

return true

} else {

return false

}

}

}

LevelTracker监测玩家已解锁的最高等级。这个值被存储在类型属性highestUnlockedLevel中。

LevelTracker还定义了两个类型方法与highestUnlockedLevel配合工作。第一个类型方法是unlockLevel,一旦新等级被解锁,它会更新highestUnlockedLevel的值。第二个类型方法是levelIsUnlocked,如果某个给定的等级已经被解锁,它将返回true。(注意,尽管我们没有使用类似LevelTracker.highestUnlockedLevel的写法,这个类型方法还是能够访问类型属性highestUnlockedLevel)

除了类型属性和类型方法,LevelTracker还监测每个玩家的进度。它用实例属性currentLevel来监测每个玩家当前的等级。

为了便于管理currentLevel属性,LevelTracker定义了实例方法advanceToLevel。这个方法会在更新currentLevel之前检查所请求的新等级是否已经解锁。advanceToLevel方法返回布尔值以指示是否能够设置currentLevel。

下面,Player类使用LevelTracker来监测和更新每个玩家的发展进度:

class Player {

var tracker = LevelTracker()

let playerName: String

func completedLevel(level: Int) {

LevelTracker.unlockLevel(level + 1)

tracker.advanceToLevel(level + 1)

}

init(name: String) {

playerName = name

}

}

Player类创建一个新的LevelTracker实例来监测这个用户的进度。它提供了completedLevel方法,一旦玩家完成某个指定等级就调用它。这个方法为所有玩家解锁下一等级,并且将当前玩家的进度更新为下一等级。(我们忽略了advanceToLevel返回的布尔值,因为之前调用LevelTracker.unlockLevel时就知道了这个等级已经被解锁了)。

你还可以为一个新的玩家创建一个Player的实例,然后看这个玩家完成等级一时发生了什么:

var player = Player(name: "Argyrios")

player.completedLevel(1)

print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")

//打印输出:highest unlocked level is now 2

如果你创建了第二个玩家,并尝试让他开始一个没有被任何玩家解锁的等级,那么试图设置玩家当前等级将会失败:

player = Player(name: "Beto")

if player.tracker.advanceToLevel(6) {

print("player is now on level 6")

} else {

print("level 6 has not yet been unlocked")

}

//打印输出:level 6 has not yet been unlocked

83.下标语法

下标允许你通过在实例名称后面的方括号中传入一个或者多个索引值来对实例进行存取。语法类似于实例方法语法和计算型属性语法的混合。与定义实例方法类似,定义下标使用subscript关键字,指定一个或多个输入参数和返回类型;与实例方法不同的是,下标可以设定为读写或只读。这种行为由getter和setter实现,有点类似计算型属性:

subscript(index: Int) -> Int {

get {

//返回一个适当的Int类型的值

}

set(newValue) {

//执行适当的赋值操作

}

}

newValue的类型和下标的返回类型相同。如同计算型属性,可以不指定setter的参数(newValue)。如果不指定参数,setter会提供一个名为newValue的默认参数。

如同只读计算型属性,可以省略只读下标的get关键字:

subscript(index: Int) -> Int {

//返回一个适当的Int类型的值

}

下面代码演示了只读下标的实现,这里定义了一个TimesTable结构体,用来表示传入整数的乘法表:

struct TimesTable {

let multiplier: Int

subscript(index: Int) -> Int {

return multiplier * index

}

}

let threeTimesTable = TimesTable(multiplier: 3)

print("six times three is \(threeTimesTable[6])")

//输出"six times three is 18"

在上例中,创建了一个TimesTable实例,用来表示整数3的乘法表。数值3被传递给结构体的构造函数,作为实例成员multiplier的值。

你可以通过下标访问threeTimesTable实例,例如上面演示的threeTimesTable[6]。这条语句查询了3的乘法表中的第六个元素,返回3的6倍即18。

注意

TimesTable例子基于一个固定的数学公式,对threeTimesTable[someIndex]进行赋值操作并不合适,因此下标定义为只读的。

84.下标用法

注意

Swift的Dictionary类型的下标接受并返回可选类型的值。上例中的numberOfLegs字典通过下标返回的是一个Int?或者说“可选的int”。Dictionary类型之所以如此实现下标,是因为不是每个键都有个对应的值,同时这也提供了一种通过键删除对应值的方式,只需将键对应的值赋值为nil即可。

85.下标选项

下标可以接受任意数量的入参,并且这些入参可以是任意类型。下标的返回值也可以是任意类型。下标可以使用变量参数和可变参数,但不能使用输入输出参数,也不能给参数设置默认值。

一个类或结构体可以根据自身需要提供多个下标实现,使用下标时将通过入参的数量和类型进行区分,自动匹配合适的下标,这就是下标的重载。

虽然接受单一入参的下标是最常见的,但也可以根据情况定义接受多个入参的下标。例如下例定义了一个Matrix结构体,用于表示一个Double类型的二维矩阵。Matrix结构体的下标接受两个整型参数:

struct Matrix {

let rows: Int, columns: Int

var grid: [Double]

init(rows: Int, columns: Int) {

self.rows = rows

self.columns = columns

grid = Array(count: rows * columns, repeatedValue: 0.0)

}

func indexIsValidForRow(row: Int, column: Int) -> Bool {

return row >= 0 && row < rows && column >= 0 && column < columns

}

subscript(row: Int, column: Int) -> Double {

get {

assert(indexIsValidForRow(row, column: column), "Index out of range")

return grid[(row * columns) + column]

}

set {

assert(indexIsValidForRow(row, column: column), "Index out of range")

grid[(row * columns) + column] = newValue

}

}

}

Matrix提供了一个接受两个入参的构造方法,入参分别是rows和columns,创建了一个足够容纳rows * columns个Double类型的值的数组。通过传入数组长度和初始值0.0到数组的构造器,将矩阵中每个位置的值初始化为0.0。关于数组的这种构造方法请参考创建一个空数组

你可以通过传入合适的row和column的数量来构造一个新的Matrix实例:

var matrix = Matrix(rows: 2, columns: 2)

上例中创建了一个Matrix实例来表示两行两列的矩阵。该Matrix实例的grid数组按照从左上到右下的阅读顺序将矩阵扁平化存储:

将row和column的值传入下标来为矩阵设值,下标的入参使用逗号分隔:

matrix[0, 1] = 1.5

matrix[1, 0] = 3.2

上面两条语句分别调用下标的setter将矩阵右上角位置(即row为0、column为1的位置)的值设置为1.5,将矩阵左下角位置(即row为1、column为0的位置)的值设置为3.2:

Matrix下标的getter和setter中都含有断言,用来检查下标入参row和column的值是否有效。为了方便进行断言,Matrix包含了一个名为indexIsValidForRow(_:column:)的便利方法,用来检查入参row和column的值是否在矩阵范围内:

func indexIsValidForRow(row: Int, column: Int) -> Bool {

return row >= 0 && row < rows && column >= 0 && column < columns

}

断言在下标越界时触发:

let someValue = matrix[2, 2]

//断言将会触发,因为[2, 2]已经超过了matrix的范围

86.重写属性

你可以重写继承来的实例属性或类型属性,提供自己定制的getter和setter,或添加属性观察器使重写的属性可以观察属性值什么时候发生改变。

重写属性的Getters和Setters

你可以提供定制的getter(或setter)来重写任意继承来的属性,无论继承来的属性是存储型的还是计算型的属性。子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。你在重写一个属性时,必需将它的名字和类型都写出来。这样才能使编译器去检查你重写的属性是与超类中同名同类型的属性相匹配的。

你可以将一个继承来的只读属性重写为一个读写属性,只需要在重写版本的属性里提供getter和setter即可。但是,你不可以将一个继承来的读写属性重写为一个只读属性

注意

如果你在重写属性中提供了setter,那么你也一定要提供getter。如果你不想在重写版本中的getter里修改继承来的属性值,你可以直接通过super.someProperty来返回继承来的值,其中someProperty是你要重写的属性的名字。

重写属性观察器(Property Observer)

你可以通过重写属性为一个继承来的属性添加属性观察器。这样一来,当继承来的属性值发生改变时,你就会被通知到,无论那个属性原本是如何实现的。关于属性观察器的更多内容,请看属性观察器

注意

你不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。这些属性的值是不可以被设置的,所以,为它们提供willSet或didSet实现是不恰当。

此外还要注意,你不可以同时提供重写的setter和重写的属性观察器。如果你想观察属性值的变化,并且你已经为那个属性提供了定制的setter,那么你在setter中就可以观察到任何值变化了。

class AutomaticCar: Car {

override var currentSpeed: Double {

didSet {

gear = Int(currentSpeed / 10.0) + 1

}

}

}

class AutomaticCar: Car {

override var currentSpeed: Double {

didSet {

gear = Int(currentSpeed / 10.0) + 1

}

}

}

87.防止重写

你可以通过把方法,属性或下标标记为final来防止它们被重写,只需要在声明关键字前加上final修饰符即可(例如:final var,final func,final class func,以及final subscript)。

如果你重写了final方法,属性或下标,在编译时会报错。在类扩展中的方法,属性或下标也可以在扩展的定义里标记为final的。

你可以通过在关键字class前添加final修饰符final class来将整个类标记为final的。这样的类是不可被继承的,试图继承这样的类会导致编译报错。

88.存储属性的初始赋值

类和结构体在创建实例时,必须为所有存储型属性设置合适的初始值。存储型属性的值不能处于一个未知的状态。

当你为存储型属性设置默认值或者在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察者(property observers)。

89.构造器

构造器在创建某个特定类型的新实例时被调用。它的最简形式类似于一个不带任何参数的实例方法,以关键字init命名:

init() {

//在此处执行构造过程

}

下面例子中定义了一个用来保存华氏温度的结构体Fahrenheit,它拥有一个Double类型的存储型属性temperature:

struct Fahrenheit {

var temperature: Double

init() {

temperature = 32.0

}

}

var f = Fahrenheit()

print("The default temperature is \(f.temperature)° Fahrenheit")

//输出"The default temperature is 32.0° Fahrenheit”

这个结构体定义了一个不带参数的构造器init,并在里面将存储型属性temperature的值初始化为32.0(华氏温度下水的冰点)。

90.构造参数

自定义构造过程时,可以在定义中提供构造参数,指定所需值的类型和名字。构造参数的功能和语法跟函数和方法的参数相同。

下面例子中定义了一个包含摄氏度温度的结构体Celsius。它定义了两个不同的构造器:init(fromFahrenheit:)和init(fromKelvin:),二者分别通过接受不同温标下的温度值来创建新的实例:

struct Celsius {

var temperatureInCelsius: Double

init(fromFahrenheit fahrenheit: Double) {

temperatureInCelsius = (fahrenheit - 32.0) / 1.8

}

init(fromKelvin kelvin: Double) {

temperatureInCelsius = kelvin - 273.15

}

}

let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)

// boilingPointOfWater.temperatureInCelsius是100.0

let freezingPointOfWater = Celsius(fromKelvin: 273.15)

// freezingPointOfWater.temperatureInCelsius是0.0”

第一个构造器拥有一个构造参数,其外部名字为fromFahrenheit,内部名字为fahrenheit;第二个构造器也拥有一个构造参数,其外部名字为fromKelvin,内部名字为kelvin。这两个构造器都将唯一的参数值转换成摄氏温度值,并保存在属性temperatureInCelsius中。

91.不带外部名的构造器参数

如果你不希望为构造器的某个参数提供外部名字,你可以使用下划线(_)来显式描述它的外部名,以此重写上面所说的默认行为。

下面是之前Celsius例子的扩展,跟之前相比添加了一个带有Double类型参数的构造器,其外部名用_代替:

struct Celsius {

var temperatureInCelsius: Double

init(fromFahrenheit fahrenheit: Double) {

temperatureInCelsius = (fahrenheit - 32.0) / 1.8

}

init(fromKelvin kelvin: Double) {

temperatureInCelsius = kelvin - 273.15

}

init(_ celsius: Double){

temperatureInCelsius = celsius

}

}

let bodyTemperature = Celsius(37.0)

// bodyTemperature.temperatureInCelsius为37.0

调用Celsius(37.0)意图明确,不需要外部参数名称。因此适合使用init(_ celsius: Double)这样的构造器,从而可以通过提供Double类型的参数值调用构造器,而不需要加上外部名。

92.可选属性类型

如果你定制的类型包含一个逻辑上允许取值为空的存储型属性——无论是因为它无法在初始化时赋值,还是因为它在之后某个时间点可以赋值为空——你都需要将它定义为可选类型(optional type)。可选类型的属性将自动初始化为nil,表示这个属性是有意在初始化时设置为空的。

下面例子中定义了类SurveyQuestion,它包含一个可选字符串属性response:

class SurveyQuestion {

var text: String

var response: String?

init(text: String) {

self.text = text

}

func ask() {

print(text)

}

}

let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")

cheeseQuestion.ask()

//输出"Do you like cheese?"

cheeseQuestion.response = "Yes, I do like cheese."

调查问题的答案在回答前是无法确定的,因此我们将属性response声明为String?类型,或者说是可选字符串类型(optional String)。当SurveyQuestion实例化时,它将自动赋值为nil,表明此字符串暂时还没有值。

93.构造过程中常量属性的修改

你可以在构造过程中的任意时间点给常量属性指定一个值,只要在构造过程结束时是一个确定的值。一旦常量属性被赋值,它将永远不可更改。

注意

对于类的实例来说,它的常量属性只能在定义它的类的构造过程中修改;不能在子类中修改。

你可以修改上面的SurveyQuestion示例,用常量属性替代变量属性text,表示问题内容text在SurveyQuestion的实例被创建之后不会再被修改。尽管text属性现在是常量,我们仍然可以在类的构造器中设置它的值:

class SurveyQuestion {

let text: String

var response: String?

init(text: String) {

self.text = text

}

func ask() {

print(text)

}

}

let beetsQuestion = SurveyQuestion(text: "How about beets?")

beetsQuestion.ask()

//输出"How about beets?"

beetsQuestion.response = "I also like beets. (But not with cheese.)"

94.默认构造器

如果结构体或类的所有属性都有默认值,同时没有自定义的构造器,那么Swift会给这些结构体或类提供一个默认构造器(default initializers。这个默认构造器将简单地创建一个所有属性值都设置为默认值的实例。

下面例子中创建了一个类ShoppingListItem,它封装了购物清单中的某一物品的属性:名字(name)、数量(quantity)和购买状态purchase state

class ShoppingListItem {

var name: String?

var quantity = 1

var purchased = false

}

var item = ShoppingListItem()

由于ShoppingListItem类中的所有属性都有默认值,且它是没有父类的基类,它将自动获得一个可以为所有属性设置默认值的默认构造器(尽管代码中没有显式为name属性设置默认值,但由于name是可选字符串类型,它将默认设置为nil)。上面例子中使用默认构造器创造了一个ShoppingListItem类的实例(使用ShoppingListItem()形式的构造器语法),并将其赋值给变量item。

95.结构体的逐一成员构造器

除了上面提到的默认构造器,如果结构体没有提供自定义的构造器,它们将自动获得一个逐一成员构造器,即使结构体的存储型属性没有默认值。

逐一成员构造器是用来初始化结构体新实例里成员属性的快捷方法。我们在调用逐一成员构造器时,通过与成员属性名相同的参数名进行传值来完成对成员属性的初始赋值。

下面例子中定义了一个结构体Size,它包含两个属性width和height。Swift可以根据这两个属性的初始赋值0.0自动推导出它们的类型为Double。

结构体Size自动获得了一个逐一成员构造器init(width:height:)。你可以用它来为Size创建新的实例:

struct Size {

var width = 0.0, height = 0.0

}

let twoByTwo = Size(width: 2.0, height: 2.0)

96.值类型的构造器代理

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间的代码重复。

构造器代理的实现规则和形式在值类型和类类型中有所不同。值类型(结构体和枚举类型)不支持继承,所以构造器代理的过程相对简单,因为它们只能代理给自己的其它构造器。类则不同,它可以继承自其它类(请参考继承),这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的初始化。这些责任将在后续章节类的继承和构造过程中介绍。

对于值类型,你可以使用self.init在自定义的构造器中引用类型中的其它构造器。并且你只能在构造器内部调用self.init

如果你为某个值类型定义了一个自定义的构造器,你将无法访问到默认构造器(如果是结构体,还将无法访问逐一成员构造器)。这个限制可以防止你为值类型定义了一个进行额外必要设置的复杂构造器之后,别人还是错误地使用了一个自动生成的构造器。

注意

假如你希望默认构造器、逐一成员构造器以及你自己的自定义构造器都能用来创建实例,可以将自定义的构造器写到扩展(extension)中,而不是写在值类型的原始定义中。想查看更多内容,请查看扩展章节。

下面例子将定义一个结构体Rect,用来代表几何矩形。这个例子需要两个辅助的结构体Size和Point,它们各自为其所有的属性提供了初始值0.0。

struct Size {

var width = 0.0, height = 0.0

}

struct Point {

var x = 0.0, y = 0.0

}

你可以通过以下三种方式为Rect创建实例——使用被初始化为默认值的origin和size属性来初始化;提供指定的origin和size实例来初始化;提供指定的center和size来初始化。在下面Rect结构体定义中,我们为这三种方式提供了三个自定义的构造器:

struct Rect {

var origin = Point()

var size = Size()

init() {}

init(origin: Point, size: Size) {

self.origin = origin

self.size = size

}

init(center: Point, size: Size) {

let originX = center.x - (size.width / 2)

let originY = center.y - (size.height / 2)

self.init(origin: Point(x: originX, y: originY), size: size)

}

}

第一个Rect构造器init(),在功能上跟没有自定义构造器时自动获得的默认构造器是一样的。这个构造器是一个空函数,使用一对大括号{}来表示,它没有执行任何构造过程。调用这个构造器将返回一个Rect实例,它的origin和size属性都使用定义时的默认值Point(x: 0.0, y: 0.0)和Size(width: 0.0, height: 0.0):

let basicRect = Rect()

// basicRect的origin是(0.0, 0.0),size是(0.0, 0.0)

第二个Rect构造器init(origin:size:),在功能上跟结构体在没有自定义构造器时获得的逐一成员构造器是一样的。这个构造器只是简单地将origin和size的参数值赋给对应的存储型属性:

let originRect = Rect(origin: Point(x: 2.0, y: 2.0),

size: Size(width: 5.0, height: 5.0))

// originRect的origin是(2.0, 2.0),size是(5.0, 5.0)

第三个Rect构造器init(center:size:)稍微复杂一点。它先通过center和size的值计算出origin的坐标,然后再调用(或者说代理给)init(origin:size:)构造器来将新的origin和size值赋值到对应的属性中:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),

size: Size(width: 3.0, height: 3.0))

// centerRect的origin是(2.5, 2.5),size是(3.0, 3.0)

构造器init(center:size:)可以直接将origin和size的新值赋值到对应的属性中。然而,利用恰好提供了相关功能的现有构造器会更为方便,构造器init(center:size:)的意图也会更加清晰。

注意

如果你想用另外一种不需要自己定义init()和init(origin:size:)的方式来实现这个例子

97.指定构造器和便利构造器的语法

类的指定构造器的写法跟值类型简单构造器一样:

init(parameters) {

statements

}

便利构造器也采用相同样式的写法,但需要在init关键字之前放置convenience关键字,并使用空格将它们俩分开:

convenience init(parameters) {

statements

}

98.类的构造器代理规则

为了简化指定构造器和便利构造器之间的调用关系,Swift采用以下三条规则来限制构造器之间的代理调用:

规则1

指定构造器必须调用其直接父类的的指定构造器。

规则2

便利构造器必须调用同一类中定义的其它构造器。

规则3

便利构造器必须最终导致一个指定构造器被调用。

一个更方便记忆的方法是:

指定构造器必须总是向上代理

便利构造器必须总是横向代理

这些规则可以通过下面图例来说明:

如图所示,父类中包含一个指定构造器和两个便利构造器。其中一个便利构造器调用了另外一个便利构造器,而后者又调用了唯一的指定构造器。这满足了上面提到的规则2和3。这个父类没有自己的父类,所以规则1没有用到。

子类中包含两个指定构造器和一个便利构造器。便利构造器必须调用两个指定构造器中的任意一个,因为它只能调用同一个类里的其他构造器。这满足了上面提到的规则2和3。而两个指定构造器必须调用父类中唯一的指定构造器,这满足了规则1。

注意

这些规则不会影响类的实例如何创建。任何上图中展示的构造器都可以用来创建完全初始化的实例。这些规则只影响类定义如何实现。

下面图例中展示了一种涉及四个类的更复杂的类层级结构。它演示了指定构造器是如何在类层级中充当“管道”的作用,在类的构造器链上简化了类之间的相互关系。

两段式构造过程

Swift中类的构造过程包含两个阶段。第一个阶段,每个存储型属性被引入它们的类指定一个初始值。当每个存储型属性的初始值被确定后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步定制它们的存储型属性。

两段式构造过程的使用让构造过程更安全,同时在整个类层级结构中给予了每个类完全的灵活性。两段式构造过程可以防止属性值在初始化之前被访问,也可以防止属性被另外一个构造器意外地赋予不同的值。

注意

Swift的两段式构造过程跟Objective-C中的构造过程类似。最主要的区别在于阶段1,Objective-C给每一个属性赋值0或空值(比如说0或nil)。Swift的构造流程则更加灵活,它允许你设置定制的初始值,并自如应对某些属性不能以0或nil作为合法默认值的情况。

Swift编译器将执行4种有效的安全检查,以确保两段式构造过程能不出错地完成:

安全检查1

指定构造器必须保证它所在类引入的所有属性都必须先初始化完成,之后才能将其它构造任务向上代理给父类中的构造器。

如上所述,一个对象的内存只有在其所有存储型属性确定之后才能完全初始化。为了满足这一规则,指定构造器必须保证它所在类引入的属性在它往上代理之前先完成初始化。

安全检查2

指定构造器必须先向上代理调用父类构造器,然后再为继承的属性设置新值。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。

安全检查3

便利构造器必须先代理调用同一类中的其它构造器,然后再为任意属性赋新值。如果没这么做,便利构造器赋予的新值将被同一类中其它指定构造器所覆盖。

安全检查4

构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用self作为一个值。

类实例在第一阶段结束以前并不是完全有效的。只有第一阶段完成后,该实例才会成为有效实例,才能访问属性和调用方法。

以下是两段式构造过程中基于上述安全检查的构造流程展示:

阶段1

某个指定构造器或便利构造器被调用。

完成新实例内存的分配,但此时内存还没有被初始化。

指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。

指定构造器将调用父类的构造器,完成父类属性的初始化。

这个调用父类构造器的过程沿着构造器链一直往上执行,直到到达构造器链的最顶部。

当到达了构造器链最顶部,且已确保所有实例包含的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段1完成。

阶段2

从顶部构造器链一直往下,每个构造器链中类的指定构造器都有机会进一步定制实例。构造器此时可以访问self、修改它的属性并调用实例方法等等。

最终,任意构造器链中的便利构造器可以有机会定制实例和使用self。

下图展示了在假定的子类和父类之间的构造阶段1:

在这个例子中,构造过程从对子类中一个便利构造器的调用开始。这个便利构造器此时没法修改任何属性,它把构造任务代理给同一类中的指定构造器。

如安全检查1所示,指定构造器将确保所有子类的属性都有值。然后它将调用父类的指定构造器,并沿着构造器链一直往上完成父类的构造过程。

父类中的指定构造器确保所有父类的属性都有值。由于没有更多的父类需要初始化,也就无需继续向上代理。

一旦父类中所有属性都有了初始值,实例的内存被认为是完全初始化,阶段1完成。

以下展示了相同构造过程的阶段2:

父类中的指定构造器现在有机会进一步来定制实例(尽管这不是必须的)。

一旦父类中的指定构造器完成调用,子类中的指定构造器可以执行更多的定制操作(这也不是必须的)。

最终,一旦子类的指定构造器完成调用,最开始被调用的便利构造器可以执行更多的定制操作。

99.构造器的继承和重写

跟Objective-C中的子类不同,Swift中的子类默认情况下不会继承父类的构造器。Swift的这种机制可以防止一个父类的简单构造器被一个更专业的子类继承,并被错误地用来创建子类的实例。

注意

父类的构造器仅会在安全和适当的情况下被继承。具体内容请参考后续章节构造器的自动继承

假如你希望自定义的子类中能提供一个或多个跟父类相同的构造器,你可以在子类中提供这些构造器的自定义实现。

当你在编写一个和父类中指定构造器相匹配的子类构造器时,你实际上是在重写父类的这个指定构造器。因此,你必须在定义子类构造器时带上override修饰符。即使你重写的是系统自动提供的默认构造器,也需要带上override修饰符,具体内容请参考默认构造器

正如重写属性,方法或者是下标,override修饰符会让编译器去检查父类中是否有相匹配的指定构造器,并验证构造器参数是否正确。

注意

当你重写一个父类的指定构造器时,你总是需要写override修饰符,即使你的子类将父类的指定构造器重写为了便利构造器。

相反,如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器(每个规则都在上文类的构造器代理规则有所描述),因此,严格意义上来讲,你的子类并未对一个父类构造器提供重写。最后的结果就是,你在子类中“重写”一个父类便利构造器时,不需要加override前缀。

在下面的例子中定义了一个叫Vehicle的基类。基类中声明了一个存储型属性numberOfWheels,它是值为0的Int类型的存储型属性。numberOfWheels属性用于创建名为descrpiption的String类型的计算型属性:

class Vehicle {

var numberOfWheels = 0

var description: String {

return "\(numberOfWheels) wheel(s)"

}

}

Vehicle类只为存储型属性提供默认值,而不自定义构造器。因此,它会自动获得一个默认构造器,具体内容请参考默认构造器。自动获得的默认构造器总会是类中的指定构造器,它可以用于创建numberOfWheels为0的Vehicle实例:

let vehicle = Vehicle()

print("Vehicle: \(vehicle.description)")

// Vehicle: 0 wheel(s)

下面例子中定义了一个Vehicle的子类Bicycle:

class Bicycle: Vehicle {

override init() {

super.init()

numberOfWheels = 2

}

}

子类Bicycle定义了一个自定义指定构造器init()。这个指定构造器和父类的指定构造器相匹配,所以Bicycle中的指定构造器需要带上override修饰符。

Bicycle的构造器init()以调用super.init()方法开始,这个方法的作用是调用Bicycle的父类Vehicle的默认构造器。这样可以确保Bicycle在修改属性之前,它所继承的属性numberOfWheels能被Vehicle类初始化。在调用super.init()之后,属性numberOfWheels的原值被新值2替换。

如果你创建一个Bicycle实例,你可以调用继承的description计算型属性去查看属性numberOfWheels是否有改变:

let bicycle = Bicycle()

print("Bicycle: \(bicycle.description)")

// Bicycle: 2 wheel(s)

注意

子类可以在初始化时修改继承来的变量属性,但是不能修改继承来的常量属性。

100.构造器的自动继承

如上所述,子类在默认情况下不会继承父类的构造器。但是如果满足特定条件,父类构造器是可以被自动继承的。在实践中,这意味着对于许多常见场景你不必重写父类的构造器,并且可以在安全的情况下以最小的代价继承父类的构造器。

假设你为子类中引入的所有新属性都提供了默认值,以下2个规则适用:

规则1

如果子类没有定义任何指定构造器,它将自动继承所有父类的指定构造器。

规则2

如果子类提供了所有父类指定构造器的实现——无论是通过规则1继承过来的,还是提供了自定义实现——它将自动继承所有父类的便利构造器。

即使你在子类中添加了更多的便利构造器,这两条规则仍然适用。

注意

对于规则2,子类可以将父类的指定构造器实现为便利构造器。

指定构造器和便利构造器实践

接下来的例子将在实践中展示指定构造器、便利构造器以及构造器的自动继承。这个例子定义了包含三个类Food、RecipeIngredient以及ShoppingListItem的类层次结构,并将演示它们的构造器是如何相互作用的。

类层次中的基类是Food,它是一个简单的用来封装食物名字的类。Food类引入了一个叫做name的String类型的属性,并且提供了两个构造器来创建Food实例:

class Food {

var name: String

init(name: String) {

self.name = name

}

convenience init() {

self.init(name: "[Unnamed]")

}

}

下图中展示了Food的构造器链:

类类型没有默认的逐一成员构造器,所以Food类提供了一个接受单一参数name的指定构造器。这个构造器可以使用一个特定的名字来创建新的Food实例:

let namedMeat = Food(name: "Bacon")

// namedMeat的名字是"Bacon”

Food类中的构造器init(name: String)被定义为一个指定构造器,因为它能确保Food实例的所有存储型属性都被初始化。Food类没有父类,所以init(name: String)构造器不需要调用super.init()来完成构造过程。

Food类同样提供了一个没有参数的便利构造器init()。这个init()构造器为新食物提供了一个默认的占位名字,通过横向代理到指定构造器init(name: String)并给参数name传值[Unnamed]来实现:

let mysteryMeat = Food()

// mysteryMeat的名字是[Unnamed]

类层级中的第二个类是Food的子类RecipeIngredient。RecipeIngredient类构建了食谱中的一味调味剂。它引入了Int类型的属性quantity(以及从Food继承过来的name属性),并且定义了两个构造器来创建RecipeIngredient实例:

class RecipeIngredient: Food {

var quantity: Int

init(name: String, quantity: Int) {

self.quantity = quantity

super.init(name: name)

}

override convenience init(name: String) {

self.init(name: name, quantity: 1)

}

}

下图中展示了RecipeIngredient类的构造器链:

RecipeIngredient类拥有一个指定构造器init(name: String, quantity: Int),它可以用来填充RecipeIngredient实例的所有属性值。这个构造器一开始先将传入的quantity参数赋值给quantity属性,这个属性也是唯一在RecipeIngredient中新引入的属性。随后,构造器向上代理到父类Food的init(name: String)。这个过程满足两段式构造过程中的安全检查1。

RecipeIngredient还定义了一个便利构造器init(name: String),它只通过name来创建RecipeIngredient的实例。这个便利构造器假设任意RecipeIngredient实例的quantity为1,所以不需要显式指明数量即可创建出实例。这个便利构造器的定义可以更加方便和快捷地创建实例,并且避免了创建多个quantity为1的RecipeIngredient实例时的代码重复。这个便利构造器只是简单地横向代理到类中的指定构造器,并为quantity参数传递1。

注意,RecipeIngredient的便利构造器init(name: String)使用了跟Food中指定构造器init(name: String)相同的参数。由于这个便利构造器重写了父类的指定构造器init(name: String),因此必须在前面使用override修饰符(参见构造器的继承和重写)。

尽管RecipeIngredient将父类的指定构造器重写为了便利构造器,它依然提供了父类的所有指定构造器的实现。因此,RecipeIngredient会自动继承父类的所有便利构造器。

在这个例子中,RecipeIngredient的父类是Food,它有一个便利构造器init()。这个便利构造器会被RecipeIngredient继承。这个继承版本的init()在功能上跟Food提供的版本是一样的,只是它会代理到RecipeIngredient版本的init(name: String)而不是Food提供的版本。

所有的这三种构造器都可以用来创建新的RecipeIngredient实例:

let oneMysteryItem = RecipeIngredient()

let oneBacon = RecipeIngredient(name: "Bacon")

let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

类层级中第三个也是最后一个类是RecipeIngredient的子类,叫做ShoppingListItem。这个类构建了购物单中出现的某一种调味料。

购物单中的每一项总是从未购买状态开始的。为了呈现这一事实,ShoppingListItem引入了一个布尔类型的属性purchased,它的默认值是false。ShoppingListItem还添加了一个计算型属性description,它提供了关于ShoppingListItem实例的一些文字描述:

class ShoppingListItem: RecipeIngredient {

var purchased = false

var description: String {

var output = "\(quantity) x \(name)"

output += purchased ? "" : ""

return output

}

}

注意

ShoppingListItem没有定义构造器来为purchased提供初始值,因为添加到购物单的物品的初始状态总是未购买。

由于它为自己引入的所有属性都提供了默认值,并且自己没有定义任何构造器,ShoppingListItem将自动继承所有父类中的指定构造器和便利构造器。

下图展示了这三个类的构造器链:

你可以使用全部三个继承来的构造器来创建ShoppingListItem的新实例:

var breakfastList = [

ShoppingListItem(),

ShoppingListItem(name: "Bacon"),

ShoppingListItem(name: "Eggs", quantity: 6),

]

breakfastList[0].name = "Orange juice"

breakfastList[0].purchased = true

for item in breakfastList {

print(item.description)

}

// 1 x orange juice✔

// 1 x bacon✘

// 6 x eggs✘

如上所述,例子中通过字面量方式创建了一个数组breakfastList,它包含了三个ShoppingListItem实例,因此数组的类型也能被自动推导为[ShoppingListItem]。在数组创建完之后,数组中第一个ShoppingListItem实例的名字从[Unnamed]更改为Orange juice,并标记为已购买。打印数组中每个元素的描述显示了它们都已按照预期被赋值。

101.可失败构造器

如果一个类、结构体或枚举类型的对象,在构造过程中有可能失败,则为其定义一个可失败构造器。这里所指的“失败”是指,如给构造器传入无效的参数值,或缺少某种所需的外部资源,又或是不满足某种必要的条件等。

为了妥善处理这种构造过程中可能会失败的情况。你可以在一个类,结构体或是枚举类型的定义中,添加一个或多个可失败构造器。其语法为在init关键字后面添加问号(init?)

注意

可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名,及其参数类型相同。

可失败构造器会创建一个类型为自身类型的可选类型的对象。你通过return nil语句来表明可失败构造器在何种情况下应该“失败”。

注意

严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用return nil表明可失败构造器构造失败,而不要用关键字return来表明构造成功。

下例中,定义了一个名为Animal的结构体,其中有一个名为species的String类型的常量属性。同时该结构体还定义了一个接受一个名为species的String类型参数的可失败构造器。这个可失败构造器检查传入的参数是否为一个空字符串。如果为空字符串,则构造失败。否则,species属性被赋值,构造成功。

struct Animal {

let species: String

init?(species: String) {

if species.isEmpty { return nil }

self.species = species

}

}

你可以通过该可失败构造器来构建一个Animal的实例,并检查构造过程是否成功:

let someCreature = Animal(species: "Giraffe")

// someCreature的类型是Animal?而不是Animal

if let giraffe = someCreature {

print("An animal was initialized with a species of \(giraffe.species)")

}

//打印"An animal was initialized with a species of Giraffe"

如果你给该可失败构造器传入一个空字符串作为其参数,则会导致构造失败:

let anonymousCreature = Animal(species: "")

// anonymousCreature的类型是Animal?,而不是Animal

if anonymousCreature == nil {

print("The anonymous creature could not be initialized")

}

//打印"The anonymous creature could not be initialized"

注意

空字符串(如"",而不是"Giraffe")和一个值为nil的可选类型的字符串是两个完全不同的概念。上例中的空字符串("")其实是一个有效的,非可选类型的字符串。这里我们之所以让Animal的可失败构造器构造失败,只是因为对于Animal这个类的species属性来说,它更适合有一个具体的值,而不是空字符串。

102.带原始值的枚举类型的可失败构造器

带原始值的枚举类型会自带一个可失败构造器init?(rawValue:),该可失败构造器有一个名为rawValue的参数,其类型和枚举类型的原始值类型一致,如果该参数的值能够和某个枚举成员的原始值匹配,则该构造器会构造相应的枚举成员,否则构造失败。

因此上面的TemperatureUnit的例子可以重写为:

enum TemperatureUnit: Character {

case Kelvin = "K", Celsius = "C", Fahrenheit = "F"

}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")

if fahrenheitUnit != nil {

print("This is a defined temperature unit, so initialization succeeded.")

}

//打印"This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(rawValue: "X")

if unknownUnit == nil {

print("This is not a defined temperature unit, so initialization failed.")

}

//打印"This is not a defined temperature unit, so initialization failed."

103.构造失败的传递

类,结构体,枚举的可失败构造器可以横向代理到类型中的其他可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。

无论是向上代理还是横向代理,如果你代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。

注意

可失败构造器也可以代理到其它的非可失败构造器。通过这种方式,你可以增加一个可能的失败状态到现有的构造过程中。

下面这个例子,定义了一个名为CartItem的Product类的子类。这个类建立了一个在线购物车中的物品的模型,它有一个名为quantity的常量存储型属性,并确保该属性的值至少为1:

class Product {

let name: String

init?(name: String) {

if name.isEmpty { return nil }

self.name = name

}

}

class CartItem: Product {

let quantity: Int

init?(name: String, quantity: Int) {

if quantity < 1 { return nil }

self.quantity = quantity

super.init(name: name)

}

}

CartItem可失败构造器首先验证接收的quantity值是否大于等于1。倘若quantity值无效,则立即终止整个构造过程,返回失败结果,且不再执行余下代码。同样地,Product的可失败构造器首先检查name值,假如name值为空字符串,则构造器立即执行失败。

如果你通过传入一个非空字符串name以及一个值大于等于1的quantity来创建一个CartItem实例,那么构造方法能够成功被执行:

if let twoSocks = CartItem(name: "sock", quantity: 2) {

print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")

}

//打印"Item: sock, quantity: 2”

倘若你以一个值为0的quantity来创建一个CartItem实例,那么将导致CartItem构造器失败:

if let zeroShirts = CartItem(name: "shirt", quantity: 0) {

print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")

} else {

print("Unable to initialize zero shirts")

}

//打印"Unable to initialize zero shirts”

同样地,如果你尝试传入一个值为空字符串的name来创建一个CartItem实例,那么将导致父类Product的构造过程失败:

if let oneUnnamed = CartItem(name: "", quantity: 1) {

print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")

} else {

print("Unable to initialize one unnamed product")

}

//打印"Unable to initialize one unnamed product”

104.重写一个可失败构造器

如同其它的构造器,你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。

注意,当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。

注意

你可以用非可失败构造器重写可失败构造器,但反过来却不行。

下例定义了一个名为Document的类,name属性的值必须为一个非空字符串或nil,但不能是一个空字符串:

class Document {

var name: String?

//该构造器创建了一个name属性的值为nildocument实例

init() {}

//该构造器创建了一个name属性的值为非空字符串的document实例

init?(name: String) {

self.name = name

if name.isEmpty { return nil }

}

}

下面这个例子,定义了一个Document类的子类AutomaticallyNamedDocument。这个子类重写了父类的两个指定构造器,确保了无论是使用init()构造器,还是使用init(name:)构造器并为参数传递空字符串,生成的实例中的name属性总有初始"[Untitled]":

class AutomaticallyNamedDocument: Document {

override init() {

super.init()

self.name = "[Untitled]"

}

override init(name: String) {

super.init()

if name.isEmpty {

self.name = "[Untitled]"

} else {

self.name = name

}

}

}

AutomaticallyNamedDocument用一个非可失败构造器init(name:)重写了父类的可失败构造器init?(name:)。因为子类用另一种方式处理了空字符串的情况,所以不再需要一个可失败构造器,因此子类用一个非可失败构造器代替了父类的可失败构造器。

你可以在子类的非可失败构造器中使用强制解包来调用父类的可失败构造器。比如,下面的UntitledDocument子类的name属性的值总是"[Untitled]",它在构造过程中使用了父类的可失败构造器init?(name:):

class UntitledDocument: Document {

override init() {

super.init(name: "[Untitled]")!

}

}

在这个例子中,如果在调用父类的可失败构造器init?(name:)时传入的是空字符串,那么强制解包操作会引发运行时错误。不过,因为这里是通过非空的字符串常量来调用它,所以并不会发生运行时错误。

105.可失败构造器init!

通常来说我们通过在init关键字后添加问号的方式(init?)来定义一个可失败构造器,但你也可以通过在init后面添加惊叹号的方式来定义一个可失败构造器((init!)),该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。

你可以在init?中代理到init!,反之亦然。你也可以用init?重写init!,反之亦然。你还可以用init代理到init!,不过,一旦init!构造失败,则会触发一个断言。

106.必要构造器

在类的构造器前添加required修饰符表明所有该类的子类都必须实现该构造器:

class SomeClass {

required init() {

//构造器的实现代码

}

}

在子类重写父类的必要构造器时,必须在子类的构造器前也添加required修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加override修饰符:

class SomeSubclass: SomeClass {

required init() {

//构造器的实现代码

}

}

注意

如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。

107.通过闭包或函数设置属性的默认值

如果某个存储型属性的默认值需要一些定制或设置,你可以使用闭包或全局函数为其提供定制的默认值。每当某个属性所在类型的新实例被创建时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属性。

这种类型的闭包或函数通常会创建一个跟属性类型相同的临时变量,然后修改它的值以满足预期的初始状态,最后返回这个临时变量,作为属性的默认值。

下面介绍了如何用闭包为属性提供默认值:

class SomeClass {

let someProperty: SomeType = {

//在这个闭包中给someProperty创建一个默认值

// someValue必须和SomeType类型相同

return someValue

}()

}

注意闭包结尾的大括号后面接了一对空的小括号。这用来告诉Swift立即执行此闭包。如果你忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性

注意

如果你使用闭包来初始化属性,请记住在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能在闭包里访问其它属性,即使这些属性有默认值。同样,你也不能使用隐式的self属性,或者调用任何实例方法。

下面例子中定义了一个结构体Checkerboard,它构建了西洋跳棋游戏的棋盘:

西洋跳棋游戏在一副黑白格交替的10x10的棋盘中进行。为了呈现这副游戏棋盘,Checkerboard结构体定义了一个属性boardColors,它是一个包含100个Bool值的数组。在数组中,值为true的元素表示一个黑格,值为false的元素表示一个白格。数组中第一个元素代表棋盘上左上角的格子,最后一个元素代表棋盘上右下角的格子。

boardColor数组是通过一个闭包来初始化并设置颜色值的:

struct Checkerboard {

let boardColors: [Bool] = {

var temporaryBoard = [Bool]()

var isBlack = false

for i in 1...10 {

for j in 1...10 {

temporaryBoard.append(isBlack)

isBlack = !isBlack

}

isBlack = !isBlack

}

return temporaryBoard

}()

func squareIsBlackAtRow(row: Int, column: Int) -> Bool {

return boardColors[(row * 10) + column]

}

}

每当一个新的Checkerboard实例被创建时,赋值闭包会被执行,boardColors的默认值会被计算出来并返回。上面例子中描述的闭包将计算出棋盘中每个格子对应的颜色,并将这些值保存到一个临时数组temporaryBoard中,最后在构建完成时将此数组作为闭包返回值返回。这个返回的数组会保存到boardColors中,并可以通过工具函数squareIsBlackAtRow来查询:

let board = Checkerboard()

print(board.squareIsBlackAtRow(0, column: 1))

//打印"true"

print(board.squareIsBlackAtRow(9, column: 9))

//打印"false"

108.析构过程原理

Swift会自动释放不再需要的实例以释放资源。如自动引用计数章节中所讲述,Swift通过自动引用计数(ARC)处理实例的内存管理。通常当你的实例被释放时不需要手动地去清理。但是,当使用自己的资源时,你可能需要进行一些额外的清理。例如,如果创建了一个自定义的类来打开一个文件,并写入一些数据,你可能需要在类实例被释放之前手动去关闭该文件。

在类的定义中,每个类最多只能有一个析构器,而且析构器不带任何参数,如下所示:

deinit {

//执行析构过程

}

析构器是在实例释放发生前被自动调用。你不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。

因为直到实例的析构器被调用后,实例才会被释放,所以析构器可以访问实例的所有属性,并且可以根据那些属性可以修改它的行为(比如查找一个需要被关闭的文件)。

109.析构器实践

这是一个析构器实践的例子。这个例子描述了一个简单的游戏,这里定义了两种新类型,分别是Bank和Player。Bank类管理一种虚拟硬币,确保流通的硬币数量永远不可能超过10,000。在游戏中有且只能有一个Bank存在,因此Bank用类来实现,并使用静态属性和静态方法来存储和管理其当前状态。

class Bank {

static var coinsInBank = 10_000

static func vendCoins(var numberOfCoinsToVend: Int) -> Int {

numberOfCoinsToVend = min(numberOfCoinsToVend, coinsInBank)

coinsInBank -= numberOfCoinsToVend

return numberOfCoinsToVend

}

static func receiveCoins(coins: Int) {

coinsInBank += coins

}

}

Bank使用coinsInBank属性来跟踪它当前拥有的硬币数量。Bank还提供了两个方法,vendCoins(_:)和receiveCoins(_:),分别用来处理硬币的分发和收集。

vendCoins(_:)方法在Bank对象分发硬币之前检查是否有足够的硬币。如果硬币不足,Bank对象会返回一个比请求时小的数字(如果Bank对象中没有硬币了就返回0)。vendCoins方法声明numberOfCoinsToVend为一个变量参数,这样就可以在方法体内部修改分发的硬币数量,而不需要定义一个新的变量。vendCoins方法返回一个整型值,表示提供的硬币的实际数量。

receiveCoins(_:)方法只是将Bank对象接收到的硬币数目加回硬币存储中。

Player类描述了游戏中的一个玩家。每一个玩家在任意时间都有一定数量的硬币存储在他们的钱包中。这通过玩家的coinsInPurse属性来表示:

class Player {

var coinsInPurse: Int

init(coins: Int) {

coinsInPurse = Bank.vendCoins(coins)

}

func winCoins(coins: Int) {

coinsInPurse += Bank.vendCoins(coins)

}

deinit {

Bank.receiveCoins(coinsInPurse)

}

}

每个Player实例在初始化的过程中,都从Bank对象获取指定数量的硬币。如果没有足够的硬币可用,Player实例可能会收到比指定数量少的硬币.

Player类定义了一个winCoins(_:)方法,该方法从Bank对象获取一定数量的硬币,并把它们添加到玩家的钱包。Player类还实现了一个析构器,这个析构器在Player实例释放前被调用。在这里,析构器的作用只是将玩家的所有硬币都返还给Bank对象:

var playerOne: Player? = Player(coins: 100)

print("A new player has joined the game with \(playerOne!.coinsInPurse) coins")

//打印"A new player has joined the game with 100 coins"

print("There are now \(Bank.coinsInBank) coins left in the bank")

//打印"There are now 9900 coins left in the bank"

创建一个Player实例的时候,会向Bank对象请求100个硬币,如果有足够的硬币可用的话。这个Player实例存储在一个名为playerOne的可选类型的变量中。这里使用了一个可选类型的变量,因为玩家可以随时离开游戏,设置为可选使你可以追踪玩家当前是否在游戏中。

因为playerOne是可选的,所以访问其coinsInPurse属性来打印钱包中的硬币数量时,使用感叹号(!)来解包:

playerOne!.winCoins(2_000)

print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")

//输出"PlayerOne won 2000 coins & now has 2100 coins"

print("The bank now only has \(Bank.coinsInBank) coins left")

//输出"The bank now only has 7900 coins left"

这里,玩家已经赢得了2,000枚硬币,所以玩家的钱包中现在有2,100枚硬币,而Bank对象只剩余7,900枚硬币。

playerOne = nil

print("PlayerOne has left the game")

//打印"PlayerOne has left the game"

print("The bank now has \(Bank.coinsInBank) coins")

//打印"The bank now has 10000 coins"

玩家现在已经离开了游戏。这通过将可选类型的playerOne变量设置为nil来表示,意味着“没有Player实例”。当这一切发生时,playerOne变量对Player实例的引用被破坏了。没有其它属性或者变量引用Player实例,因此该实例会被释放,以便回收内存。在这之前,该实例的析构器被自动调用,玩家的硬币被返还给银行。

110.自动引用计数实践

下面的例子展示了自动引用计数的工作机制。例子以一个简单的Person类开始,并定义了一个叫name的常量属性:

class Person {

let name: String

init(name: String) {

self.name = name

print("\(name) is being initialized")

}

deinit {

print("\(name) is being deinitialized")

}

}

Person类有一个构造函数,此构造函数为实例的name属性赋值,并打印一条消息以表明初始化过程生效。Person类也拥有一个析构函数,这个析构函数会在实例被销毁时打印一条消息。

接下来的代码片段定义了三个类型为Person?的变量,用来按照代码片段中的顺序,为新的Person实例建立多个引用。由于这些变量是被定义为可选类型(Person?,而不是Person),它们的值会被自动初始化为nil,目前还不会引用到Person类的实例。

var reference1: Person?

var reference2: Person?

var reference3: Person?

现在你可以创建Person类的新实例,并且将它赋值给三个变量中的一个:

reference1 = Person(name: "John Appleseed")

// prints "John Appleseed is being initialized”

应当注意到当你调用Person类的构造函数的时候,“John Appleseed is being initialized”会被打印出来。由此可以确定构造函数被执行。

由于Person类的新实例被赋值给了reference1变量,所以reference1到Person类的新实例之间建立了一个强引用。正是因为这一个强引用,ARC会保证Person实例被保持在内存中不被销毁。

如果你将同一个Person实例也赋值给其他两个变量,该实例又会多出两个强引用:

reference2 = reference1

reference3 = reference1

现在这一个Person实例已经有三个强引用了。

如果你通过给其中两个变量赋值nil的方式断开两个强引用(包括最先的那个强引用),只留下一个强引用,Person实例不会被销毁:

reference1 = nil

reference2 = nil

在你清楚地表明不再使用这个Person实例时,即第三个也就是最后一个强引用被断开时,ARC会销毁它:

reference3 = nil

//打印“John Appleseed is being deinitialized”

111.类实例之间的循环强引用

在上面的例子中,ARC会跟踪你所新创建的Person实例的引用数量,并且会在Person实例不再被需要时销毁它。

然而,我们可能会写出一个类实例的强引用数永远不能变成0的代码。如果两个类实例互相持有对方的强引用,因而每个实例都让对方一直存在,就是这种情况。这就是所谓的循环强引用。

你可以通过定义类之间的关系为弱引用或无主引用,以替代强引用,从而解决循环强引用的问题。具体的过程在解决类实例之间的循环强引用中有描述。不管怎样,在你学习怎样解决循环强引用之前,很有必要了解一下它是怎样产生的。

下面展示了一个不经意产生循环强引用的例子。例子定义了两个类:Person和Apartment,用来建模公寓和它其中的居民:

class Person {

let name: String

init(name: String) { self.name = name }

var apartment: Apartment?

deinit { print("\(name) is being deinitialized") }

}

class Apartment {

let unit: String

init(unit: String) { self.unit = unit }

var tenant: Person?

deinit { print("Apartment \(unit) is being deinitialized") }

}

每一个Person实例有一个类型为String,名字为name的属性,并有一个可选的初始化为nil的apartment属性。apartment属性是可选的,因为一个人并不总是拥有公寓。

类似的,每个Apartment实例有一个叫unit,类型为String的属性,并有一个可选的初始化为nil的tenant属性。tenant属性是可选的,因为一栋公寓并不总是有居民。

这两个类都定义了析构函数,用以在类实例被析构的时候输出信息。这让你能够知晓Person和Apartment的实例是否像预期的那样被销毁。

接下来的代码片段定义了两个可选类型的变量john和unit4A,并分别被设定为下面的Apartment和Person的实例。这两个变量都被初始化为nil,这正是可选的优点

var john: Person?

var unit4A: Apartment?

现在你可以创建特定的Person和Apartment实例并将赋值给john和unit4A变量:

john = Person(name: "John Appleseed")

unit4A = Apartment(unit: "4A")

在两个实例被创建和赋值后,下图表现了强引用的关系。变量john现在有一个指向Person实例的强引用,而变量unit4A有一个指向Apartment实例的强引用:

现在你能够将这两个实例关联在一起,这样人就能有公寓住了,而公寓也有了房客。注意感叹号是用来展开和访问可选变量johnunit4A中的实例,这样实例的属性才能被赋值

john!.apartment = unit4A

unit4A!.tenant = john

在将两个实例联系在一起之后,强引用的关系如图所示:

不幸的是,这两个实例关联后会产生一个循环强引用。Person实例现在有了一个指向Apartment实例的强引用,而Apartment实例也有了一个指向Person实例的强引用。因此,当你断开john和unit4A变量所持有的强引用时,引用计数并不会降为0,实例也不会被ARC销毁:

john = nil

unit4A = nil

注意,当你把这两个变量设为nil时,没有任何一个析构函数被调用。循环强引用会一直阻止Person和Apartment类实例的销毁,这就在你的应用程序中造成了内存泄漏

在你将john和unit4A赋值为nil后,强引用关系如下图:

Person和Apartment实例之间的强引用关系保留了下来并且不会被断开。

解决实例之间的循环强引用

Swift提供了两种办法用来解决你在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)和无主引用(unowned reference

弱引用和无主引用允许循环引用中的一个实例引用另外一个实例而不保持强引用。这样实例能够互相引用而不产生循环强引用。

对于生命周期中会变为nil的实例使用弱引用。相反地,对于初始化赋值后再也不会被赋值为nil的实例,使用无主引用。

112.弱引用

弱引用不会对其引用的实例保持强引用,因而不会阻止ARC销毁被引用的实例。这个特性阻止了引用变为循环强引用。声明属性或者变量时,在前面加上weak关键字表明这是一个弱引用。

在实例的生命周期中,如果某些时候引用没有值,那么弱引用可以避免循环强引用。如果引用总是有值,则可以使用无主引用,在无主引用中有描述。在上面Apartment的例子中,一个公寓的生命周期中,有时是没有居民的,因此适合使用弱引用来解决循环强引用

注意

弱引用必须被声明为变量,表明其值能在运行时被修改。弱引用不能被声明为常量。

因为弱引用可以没有值,你必须将每一个弱引用声明为可选类型。在Swift中,推荐使用可选类型描述可能没有值的类型。

因为弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC会在引用的实例被销毁后自动将其赋值为nil。你可以像其他可选值一样,检查弱引用的值是否存在,你将永远不会访问已销毁的实例的引用。

下面的例子跟上面Person和Apartment的例子一致,但是有一个重要的区别。这一次,Apartment的tenant属性被声明为弱引用:

class Person {

let name: String

init(name: String) { self.name = name }

var apartment: Apartment?

deinit { print("\(name) is being deinitialized") }

}

class Apartment {

let unit: String

init(unit: String) { self.unit = unit }

weak var tenant: Person?

deinit { print("Apartment \(unit) is being deinitialized") }

}

然后跟之前一样,建立两个变量(john和unit4A)之间的强引用,并关联两个实例:

var john: Person?

var unit4A: Apartment?

john = Person(name: "John Appleseed")

unit4A = Apartment(unit: "4A")

john!.apartment = unit4A

unit4A!.tenant = john

现在,两个关联在一起的实例的引用关系如下图所示:

Person实例依然保持对Apartment实例的强引用,但是Apartment实例只持有对Person实例的弱引用。这意味着当你断开john变量所保持的强引用时,再也没有指向Person实例的强引用了:

由于再也没有指向Person实例的强引用,该实例会被销毁:

john = nil

//打印“John Appleseed is being deinitialized”

唯一剩下的指向Apartment实例的强引用来自于变量unit4A。如果你断开这个强引用,再也没有指向Apartment实例的强引用了:

由于再也没有指向Apartment实例的强引用,该实例也会被销毁:

unit4A = nil

//打印“Apartment 4A is being deinitialized”

上面的两段代码展示了变量john和unit4A在被赋值为nil后,Person实例和Apartment实例的析构函数都打印出“销毁”的信息。这证明了引用循环被打破了。

注意

在使用垃圾收集的系统里,弱指针有时用来实现简单的缓冲机制,因为没有强引用的对象只会在内存压力触发垃圾收集时才被销毁。但是在ARC中,一旦值的最后一个强引用被移除,就会被立即销毁,这导致弱引用并不适合上面的用途。

113.无主引用

和弱引用类似,无主引用不会牢牢保持住引用的实例。和弱引用不同的是,无主引用是永远有值的。因此,无主引用总是被定义为非可选类型(non-optional type。你可以在声明属性或者变量时,在前面加上关键字unowned表示这是一个无主引用。

由于无主引用是非可选类型,你不需要在使用它的时候将它展开。无主引用总是可以被直接访问。不过ARC无法在实例被销毁后将无主引用设为nil,因为非可选类型的变量不允许被赋值为nil。

注意

如果你试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。使用无主引用,你必须确保引用始终指向一个未销毁的实例。

还需要注意的是如果你试图访问实例已经被销毁的无主引用,Swift确保程序会直接崩溃,而不会发生无法预期的行为。所以你应当避免这样的事情发生。

下面的例子定义了两个类,Customer和CreditCard,模拟了银行客户和客户的信用卡。这两个类中,每一个都将另外一个类的实例作为自身的属性。这种关系可能会造成循环强引用。

Customer和CreditCard之间的关系与前面弱引用例子中Apartment和Person的关系略微不同。在这个数据模型中,一个客户可能有或者没有信用卡,但是一张信用卡总是关联着一个客户。为了表示这种关系,Customer类有一个可选类型的card属性,但是CreditCard类有一个非可选类型的customer属性。

此外,只能通过将一个number值和customer实例传递给CreditCard构造函数的方式来创建CreditCard实例。这样可以确保当创建CreditCard实例时总是有一个customer实例与之关联。

由于信用卡总是关联着一个客户,因此将customer属性定义为无主引用,用以避免循环强引用:

class Customer {

let name: String

var card: CreditCard?

init(name: String) {

self.name = name

}

deinit { print("\(name) is being deinitialized") }

}

class CreditCard {

let number: UInt64

unowned let customer: Customer

init(number: UInt64, customer: Customer) {

self.number = number

self.customer = customer

}

deinit { print("Card #\(number) is being deinitialized") }

}

注意

CreditCard类的number属性被定义为UInt64类型而不是Int类型,以确保number属性的存储量在32位和64位系统上都能足够容纳16位的卡号。

下面的代码片段定义了一个叫john的可选类型Customer变量,用来保存某个特定客户的引用。由于是可选类型,所以变量被初始化为nil:

var john: Customer?

现在你可以创建Customer类的实例,用它初始化CreditCard实例,并将新创建的CreditCard实例赋值为客户的card属性:

john = Customer(name: "John Appleseed")

john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

在你关联两个实例后,它们的引用关系如下图所示:

Customer实例持有对CreditCard实例的强引用,而CreditCard实例持有对Customer实例的无主引用。

由于customer的无主引用,当你断开john变量持有的强引用时,再也没有指向Customer实例的强引用了:

由于再也没有指向Customer实例的强引用,该实例被销毁了。其后,再也没有指向CreditCard实例的强引用,该实例也随之被销毁了:

john = nil

//打印“John Appleseed is being deinitialized”

//打印”Card #1234567890123456 is being deinitialized”

最后的代码展示了在john变量被设为nil后Customer实例和CreditCard实例的构造函数都打印出了“销毁”的信息。

114.无主引用以及隐式解析可选属性

上面弱引用和无主引用的例子涵盖了两种常用的需要打破循环强引用的场景。

Person和Apartment的例子展示了两个属性的值都允许为nil,并会潜在的产生循环强引用。这种场景最适合用弱引用来解决。

Customer和CreditCard的例子展示了一个属性的值允许为nil,而另一个属性的值不允许为nil,这也可能会产生循环强引用。这种场景最适合通过无主引用来解决。

然而,存在着第三种场景,在这种场景中,两个属性都必须有值,并且初始化完成后永远不会为nil。在这种场景中,需要一个类使用无主属性,而另外一个类使用隐式解析可选属性。

这使两个属性在初始化完成后能被直接访问(不需要可选展开),同时避免了循环引用。这一节将为你展示如何建立这种关系。

下面的例子定义了两个类,Country和City,每个类将另外一个类的实例保存为属性。在这个模型中,每个国家必须有首都,每个城市必须属于一个国家。为了实现这种关系,Country类拥有一个capitalCity属性,而City类有一个country属性:

class Country {

let name: String

var capitalCity: City!

init(name: String, capitalName: String) {

self.name = name

self.capitalCity = City(name: capitalName, country: self)

}

}

class City {

let name: String

unowned let country: Country

init(name: String, country: Country) {

self.name = name

self.country = country

}

}

为了建立两个类的依赖关系,City的构造函数接受一个Country实例作为参数,并且将实例保存到country属性。

Country的构造函数调用了City的构造函数。然而,只有Country的实例完全初始化后,Country的构造函数才能把self传给City的构造函数。(在两段式构造过程中有具体描述)

为了满足这种需求,通过在类型结尾处加上感叹号(City!)的方式,将CountrycapitalCity属性声明为隐式解析可选类型的属性。这意味着像其他可选类型一样,capitalCity属性的默认值为nil,但是不需要展开它的值就能访问它。(在隐式解析可选类型中有描述)

由于capitalCity默认值为nil,一旦Country的实例在构造函数中给name属性赋值后,整个初始化过程就完成了。这意味着一旦name属性被赋值后,Country的构造函数就能引用并传递隐式的self。Country的构造函数在赋值capitalCity时,就能将self作为参数传递给City的构造函数。

以上的意义在于你可以通过一条语句同时创建CountryCity的实例,而不产生循环强引用,并且capitalCity的属性能被直接访问,而不需要通过感叹号来展开它的可选值:

var country = Country(name: "Canada", capitalName: "Ottawa")

print("\(country.name)'s capital city is called \(country.capitalCity.name)")

//打印“Canada's capital city is called Ottawa”

在上面的例子中,使用隐式解析可选值意味着满足了类的构造函数的两个构造阶段的要求。capitalCity属性在初始化完成后,能像非可选值一样使用和存取,同时还避免了循环强引用。

115.闭包引起的循环强引用

前面我们看到了循环强引用是在两个类实例属性互相保持对方的强引用时产生的,还知道了如何用弱引用和无主引用来打破这些循环强引用。

循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时。这个闭包体中可能访问了实例的某个属性,例如self.someProperty,或者闭包中调用了实例的某个方法,例如self.someMethod()。这两种情况都导致了闭包“捕获”self,从而产生了循环强引用。

循环强引用的产生,是因为闭包和类相似,都是引用类型。当你把一个闭包赋值给某个属性时,你是将这个闭包的引用赋值给了属性。实质上,这跟之前的问题是一样的——两个强引用让彼此一直有效。但是,和两个类实例不同,这次一个是类实例,另一个是闭包。

Swift提供了一种优雅的方法来解决这个问题,称之为闭包捕获列表(closure capture list)。同样的,在学习如何用闭包捕获列表打破循环强引用之前,先来了解一下这里的循环强引用是如何产生的,这对我们很有帮助。

下面的例子为你展示了当一个闭包引用了self后是如何产生一个循环强引用的。例子中定义了一个叫HTMLElement的类,用一种简单的模型表示HTML文档中的一个单独的元素:

class HTMLElement {

let name: String

let text: String?

lazy var asHTML: Void -> String = {

if let text = self.text {

return "<\(self.name)>\(text)"

} else {

return "<\(self.name) />"

}

}

init(name: String, text: String? = nil) {

self.name = name

self.text = text

}

deinit {

print("\(name) is being deinitialized")

}

}

HTMLElement类定义了一个name属性来表示这个元素的名称,例如代表段落的“p”,或者代表换行的“br”。HTMLElement还定义了一个可选属性text,用来设置HTML元素呈现的文本。

除了上面的两个属性,HTMLElement还定义了一个lazy属性asHTML。这个属性引用了一个将name和text组合成HTML字符串片段的闭包。该属性是Void -> String类型,或者可以理解为“一个没有参数,返回String的函数”。

默认情况下,闭包赋值给了asHTML属性,这个闭包返回一个代表HTML标签的字符串。如果text值存在,该标签就包含可选值text;如果text不存在,该标签就不包含文本。对于段落元素,根据text是“some text”还是nil,闭包会返回"

some text

"或者""。

可以像实例方法那样去命名、使用asHTML属性。然而,由于asHTML是闭包而不是实例方法,如果你想改变特定HTML元素的处理方式的话,可以用自定义的闭包来取代默认值。

例如,可以将一个闭包赋值给asHTML属性,这个闭包能在text属性是nil时使用默认文本,这是为了避免返回一个空的HTML标签:

let heading = HTMLElement(name: "h1")

let defaultText = "some default text"

heading.asHTML = {

return "<\(heading.name)>\(heading.text ?? defaultText)"

}

print(heading.asHTML())

//打印“

some default text

注意

asHTML声明为lazy属性,因为只有当元素确实需要被处理为HTML输出的字符串时,才需要使用asHTML。也就是说,在默认的闭包中可以使用self,因为只有当初始化完成以及self确实存在后,才能访问lazy属性。

HTMLElement类只提供了一个构造函数,通过name和text(如果有的话)参数来初始化一个新元素。该类也定义了一个析构函数,当HTMLElement实例被销毁时,打印一条消息。

下面的代码展示了如何用HTMLElement类创建实例并打印消息:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")

print(paragraph!.asHTML())

//打印“

hello, world

注意

上面的paragraph变量定义为可选类型的HTMLElement,因此我们可以赋值nil给它来演示循环强引用。

不幸的是,上面写的HTMLElement类产生了类实例和作为asHTML默认值的闭包之间的循环强引用。循环强引用如下图所示:

实例的asHTML属性持有闭包的强引用。但是,闭包在其闭包体内使用了self(引用了self.name和self.text),因此闭包捕获了self,这意味着闭包又反过来持有了HTMLElement实例的强引用。这样两个对象就产生了循环强引用。(更多关于闭包捕获值的信息,请参考值捕获)。

注意

虽然闭包多次使用了self,它只捕获HTMLElement实例的一个强引用。

如果设置paragraph变量为nil,打破它持有的HTMLElement实例的强引用,HTMLElement实例和它的闭包都不会被销毁,也是因为循环强引用:

paragraph = nil

注意,HTMLElement的析构函数中的消息并没有被打印,证明了HTMLElement实例并没有被销毁。

解决闭包引起的循环强引用

在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。

注意

Swift有如下要求:只要在闭包内使用self的成员,就要用self.someProperty或者self.someMethod()(而不只是someProperty或someMethod())。这提醒你可能会一不小心就捕获了self。

定义捕获列表

捕获列表中的每一项都由一对元素组成,一个元素是weak或unowned关键字,另一个元素是类实例的引用(例如self)或初始化过的变量(如delegate = self.delegate!)。这些项在方括号中用逗号分开。

如果闭包有参数列表和返回类型,把捕获列表放在它们前面:

lazy var someClosure: (Int, String) -> String = {

[unowned self, weak delegate = self.delegate!] (index: Int, stringToProcess: String) -> String in

//这里是闭包的函数体

}

如果闭包没有指明参数列表或者返回类型,即它们会通过上下文推断,那么可以把捕获列表和关键字in放在闭包最开始的地方:

lazy var someClosure: Void -> String = {

[unowned self, weak delegate = self.delegate!] in

//这里是闭包的函数体

}

弱引用和无主引用

在闭包和捕获的实例总是互相引用并且总是同时销毁时,将闭包内的捕获定义为无主引用。

相反的,在被捕获的引用可能会变为nil时,将闭包内的捕获定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包体内检查它们是否存在。

注意

如果被捕获的引用绝对不会变为nil,应该用无主引用,而不是弱引用。

前面的HTMLElement例子中,无主引用是正确的解决循环强引用的方法。这样编写HTMLElement类来避免循环强引用:

class HTMLElement {

let name: String

let text: String?

lazy var asHTML: Void -> String = {

[unowned self] in

if let text = self.text {

return "<\(self.name)>\(text)"

} else {

return "<\(self.name) />"

}

}

init(name: String, text: String? = nil) {

self.name = name

self.text = text

}

deinit {

print("\(name) is being deinitialized")

}

}

上面的HTMLElement实现和之前的实现一致,除了在asHTML闭包中多了一个捕获列表。这里,捕获列表是[unowned self],表示“将self捕获为无主引用而不是强引用”。

和之前一样,我们可以创建并打印HTMLElement实例:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")

print(paragraph!.asHTML())

//打印“

hello, world

使用捕获列表后引用关系如下图所示:

这一次,闭包以无主引用的形式捕获self,并不会持有HTMLElement实例的强引用。如果将paragraph赋值为nil,HTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息:

paragraph = nil

//打印“p is being deinitialized”

你可以查看捕获列表章节,获取更多关于捕获列表的信息。

116.使用可选链式调用代替强制展开

通过在想调用的属性、方法、或下标的可选值(optional value)后面放一个问号(?),可以定义一个可选链。这一点很像在可选值后面放一个叹号(!)来强制展开它的值。它们的主要区别在于当可选值为空时可选链式调用只会调用失败,然而强制展开将会触发运行时错误。

为了反映可选链式调用可以在空值(nil)上调用的事实,不论这个调用的属性、方法及下标返回的值是不是可选值,它的返回结果都是一个可选值。你可以利用这个返回值来判断你的可选链式调用是否调用成功,如果调用有返回值则说明调用成功,返回nil则说明调用失败。

特别地,可选链式调用的返回结果与原本的返回结果具有相同的类型,但是被包装成了一个可选值。例如,使用可选链式调用访问属性,当可选链式调用成功时,如果属性原本的返回结果是Int类型,则会变为Int?类型。

下面几段代码将解释可选链式调用和强制展开的不同。

首先定义两个类Person和Residence:

class Person {

var residence: Residence?

}

class Residence {

var numberOfRooms = 1

}

Residence有一个Int类型的属性numberOfRooms,其默认值为1。Person具有一个可选的residence属性,其类型为Residence?。

如果创建一个新的Person实例,因为它的residence属性是可选的,john属性将初始化为nil:

let john = Person()

如果使用叹号(!)强制展开获得这个john的residence属性中的numberOfRooms值,会触发运行时错误,因为这时residence没有可以展开的值:

let roomCount = john.residence!.numberOfRooms

//这会引发运行时错误

john.residence为非nil值的时候,上面的调用会成功,并且把roomCount设置为Int类型的房间数量。正如上面提到的,当residence为nil的时候上面这段代码会触发运行时错误。

可选链式调用提供了另一种访问numberOfRooms的方式,使用问号(?)来替代原来的叹号(!):

if let roomCount = john.residence?.numberOfRooms {

print("John's residence has \(roomCount) room(s).")

} else {

print("Unable to retrieve the number of rooms.")

}

//打印“Unable to retrieve the number of rooms.”

在residence后面添加问号之后,Swift就会在residence不为nil的情况下访问numberOfRooms。

因为访问numberOfRooms有可能失败,可选链式调用会返回Int?类型,或称为“可选的Int”。如上例所示,当residence为nil的时候,可选的Int将会为nil,表明无法访问numberOfRooms。访问成功时,可选的Int值会通过可选绑定展开,并赋值给非可选类型的roomCount常量。

要注意的是,即使numberOfRooms是非可选的Int时,这一点也成立。只要使用可选链式调用就意味着numberOfRooms会返回一个Int?而不是Int

可以将一个Residence的实例赋给john.residence,这样它就不再是nil了:

john.residence = Residence()

john.residence现在包含一个实际的Residence实例,而不再是nil。如果你试图使用先前的可选链式调用访问numberOfRooms,它现在将返回值为1的Int?类型的值:

if let roomCount = john.residence?.numberOfRooms {

print("John's residence has \(roomCount) room(s).")

} else {

print("Unable to retrieve the number of rooms.")

}

//打印“John's residence has 1 room(s).”

117.为可选链式调用定义模型类

通过使用可选链式调用可以调用多层属性、方法和下标。这样可以在复杂的模型中向下访问各种子属性,并且判断能否访问子属性的属性、方法或下标。

下面这段代码定义了四个模型类,这些例子包括多层可选链式调用。为了方便说明,在Person和Residence的基础上增加了Room类和Address类,以及相关的属性、方法以及下标。

Person类的定义基本保持不变:

class Person {

var residence: Residence?

}

Residence类比之前复杂些,增加了一个名为rooms的变量属性,该属性被初始化为[Room]类型的空数组:

class Residence {

var rooms = [Room]()

var numberOfRooms: Int {

return rooms.count

}

subscript(i: Int) -> Room {

get {

return rooms[i]

}

set {

rooms[i] = newValue

}

}

func printNumberOfRooms() {

print("The number of rooms is \(numberOfRooms)")

}

var address: Address?

}

现在Residence有了一个存储Room实例的数组,numberOfRooms属性被实现为计算型属性,而不是存储型属性。numberOfRooms属性简单地返回rooms数组的count属性的值。

Residence还提供了访问rooms数组的快捷方式,即提供可读写的下标来访问rooms数组中指定位置的元素

此外,Residence还提供了printNumberOfRooms()方法,这个方法的作用是打印numberOfRooms的值。

最后,Residence还定义了一个可选属性address,其类型为Address?。Address类的定义在下面会说明。

Room类是一个简单类,其实例被存储在rooms数组中。该类只包含一个属性name,以及一个用于将该属性设置为适当的房间名的初始化函数:

class Room {

let name: String

init(name: String) { self.name = name }

}

最后一个类是Address,这个类有三个String?类型的可选属性。buildingName以及buildingNumber属性分别表示某个大厦的名称和号码,第三个属性street表示大厦所在街道的名称:

class Address {

var buildingName: String?

var buildingNumber: String?

var street: String?

func buildingIdentifier() -> String? {

if buildingName != nil {

return buildingName

} else if buildingNumber != nil && street != nil {

return "\(buildingNumber) \(street)"

} else {

return nil

}

}

}

Address类提供了buildingIdentifier()方法,返回值为String?。如果buildingName有值则返回buildingName。或者,如果buildingNumber和street均有值则返回buildingNumber。否则,返回nil。

通过可选链式调用访问属性

正如使用可选链式调用代替强制展开中所述,可以通过可选链式调用在一个可选值上访问它的属性,并判断访问是否成功。

下面的代码创建了一个Person实例,然后像之前一样,尝试访问numberOfRooms属性:

let john = Person()

if let roomCount = john.residence?.numberOfRooms {

print("John's residence has \(roomCount) room(s).")

} else {

print("Unable to retrieve the number of rooms.")

}

//打印“Unable to retrieve the number of rooms.”

因为john.residence为nil,所以这个可选链式调用依旧会像先前一样失败。

还可以通过可选链式调用来设置属性值:

let someAddress = Address()

someAddress.buildingNumber = "29"

someAddress.street = "Acacia Road"

john.residence?.address = someAddress

在这个例子中,通过john.residence来设定address属性也会失败,因为john.residence当前为nil。

上面代码中的赋值过程是可选链式调用的一部分,这意味着可选链式调用失败时,等号右侧的代码不会被执行。对于上面的代码来说,很难验证这一点,因为像这样赋值一个常量没有任何副作用。下面的代码完成了同样的事情,但是它使用一个函数来创建Address实例,然后将该实例返回用于赋值。该函数会在返回前打印“Function was called”,这使你能验证等号右侧的代码是否被执行。

func createAddress() -> Address {

print("Function was called.")

let someAddress = Address()

someAddress.buildingNumber = "29"

someAddress.street = "Acacia Road"

return someAddress

}

john.residence?.address = createAddress()

没有任何打印消息,可以看出createAddress()函数并未被执行。

通过可选链式调用调用方法

可以通过可选链式调用来调用方法,并判断是否调用成功,即使这个方法没有返回值。

Residence类中的printNumberOfRooms()方法打印当前的numberOfRooms值,如下所示:

func printNumberOfRooms() {

print("The number of rooms is \(numberOfRooms)")

}

这个方法没有返回值。然而,没有返回值的方法具有隐式的返回类型Void,如无返回值函数中所述。这意味着没有返回值的方法也会返回(),或者说空的元组。

如果在可选值上通过可选链式调用来调用这个方法,该方法的返回类型会是Void?,而不是Void,因为通过可选链式调用得到的返回值都是可选的。这样我们就可以使用if语句来判断能否成功调用printNumberOfRooms()方法,即使方法本身没有定义返回值。通过判断返回值是否为nil可以判断调用是否成功:

if john.residence?.printNumberOfRooms() != nil {

print("It was possible to print the number of rooms.")

} else {

print("It was not possible to print the number of rooms.")

}

//打印“It was not possible to print the number of rooms.”

同样的,可以据此判断通过可选链式调用为属性赋值是否成功。在上面的通过可选链式调用访问属性的例子中,我们尝试给john.residence中的address属性赋值,即使residence为nil。通过可选链式调用给属性赋值会返回Void?,通过判断返回值是否为nil就可以知道赋值是否成功:

if (john.residence?.address = someAddress) != nil {

print("It was possible to set the address.")

} else {

print("It was not possible to set the address.")

}

//打印“It was not possible to set the address.”

通过可选链式调用访问下标

通过可选链式调用,我们可以在一个可选值上访问下标,并且判断下标调用是否成功。

注意

通过可选链式调用访问可选值的下标时,应该将问号放在下标方括号的前面而不是后面。可选链式调用的问号一般直接跟在可选表达式的后面。

下面这个例子用下标访问john.residence属性存储的Residence实例的rooms数组中的第一个房间的名称,因为john.residence为nil,所以下标调用失败了:

if let firstRoomName = john.residence?[0].name {

print("The first room name is \(firstRoomName).")

} else {

print("Unable to retrieve the first room name.")

}

//打印“Unable to retrieve the first room name.”

在这个例子中,问号直接放在john.residence的后面,并且在方括号的前面,因为john.residence是可选值。

类似的,可以通过下标,用可选链式调用来赋值:

john.residence?[0] = Room(name: "Bathroom")

这次赋值同样会失败,因为residence目前是nil。

如果你创建一个Residence实例,并为其rooms数组添加一些Room实例,然后将Residence实例赋值给john.residence,那就可以通过可选链和下标来访问数组中的元素:

let johnsHouse = Residence()

johnsHouse.rooms.append(Room(name: "Living Room"))

johnsHouse.rooms.append(Room(name: "Kitchen"))

john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {

print("The first room name is \(firstRoomName).")

} else {

print("Unable to retrieve the first room name.")

}

//打印“The first room name is Living Room.”

访问可选类型的下标

如果下标返回可选类型值,比如Swift中Dictionary类型的键的下标,可以在下标的结尾括号后面放一个问号来在其可选返回值上进行可选链式调用:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]

testScores["Dave"]?[0] = 91

testScores["Bev"]?[0]++

testScores["Brian"]?[0] = 72

// "Dave"数组现在是[91, 82, 84]"Bev"数组现在是[80, 94, 81]

上面的例子中定义了一个testScores数组,包含了两个键值对,把String类型的键映射到一个Int值的数组。这个例子用可选链式调用把"Dave"数组中第一个元素设为91,把"Bev"数组的第一个元素+1,然后尝试把"Brian"数组中的第一个元素设为72。前两个调用成功,因为testScores字典中包含"Dave"和"Bev"这两个键。但是testScores字典中没有"Brian"这个键,所以第三个调用失败。

连接多层可选链式调用

可以通过连接多个可选链式调用在更深的模型层级中访问属性、方法以及下标。然而,多层可选链式调用不会增加返回值的可选层级。

也就是说:

如果你访问的值不是可选的,可选链式调用将会返回可选值。

如果你访问的值就是可选的,可选链式调用不会让可选返回值变得“更可选”。

因此:

通过可选链式调用访问一个Int值,将会返回Int?,无论使用了多少层可选链式调用。

类似的,通过可选链式调用访问Int?值,依旧会返回Int?值,并不会返回Int??。

下面的例子尝试访问john中的residence属性中的address属性中的street属性。这里使用了两层可选链式调用,residence以及address都是可选值:

if let johnsStreet = john.residence?.address?.street {

print("John's street name is \(johnsStreet).")

} else {

print("Unable to retrieve the address.")

}

//打印“Unable to retrieve the address.”

john.residence现在包含一个有效的Residence实例。然而,john.residence.address的值当前为nil。因此,调用john.residence?.address?.street会失败。

需要注意的是,上面的例子中,street的属性为String?。john.residence?.address?.street的返回值也依然是String?,即使已经使用了两层可选链式调用。

如果为john.residence.address赋值一个Address实例,并且为address中的street属性设置一个有效值,我们就能过通过可选链式调用来访问street属性:

let johnsAddress = Address()

johnsAddress.buildingName = "The Larches"

johnsAddress.street = "Laurel Street"

john.residence?.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {

print("John's street name is \(johnsStreet).")

} else {

print("Unable to retrieve the address.")

}

//打印“John's street name is Laurel Street.”

在上面的例子中,因为john.residence包含一个有效的Residence实例,所以对john.residence的address属性赋值将会成功。

在方法的可选返回值上进行可选链式调用

上面的例子展示了如何在一个可选值上通过可选链式调用来获取它的属性值。我们还可以在一个可选值上通过可选链式调用来调用方法,并且可以根据需要继续在方法的可选返回值上进行可选链式调用。

在下面的例子中,通过可选链式调用来调用Address的buildingIdentifier()方法。这个方法返回String?类型的值。如上所述,通过可选链式调用来调用该方法,最终的返回值依旧会是String?类型:

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {

print("John's building identifier is \(buildingIdentifier).")

}

//打印“John's building identifier is The Larches.”

如果要在该方法的返回值上进行可选链式调用,在方法的圆括号后面加上问号即可:

if let beginsWithThe =

john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {

if beginsWithThe {

print("John's building identifier begins with \"The\".")

} else {

print("John's building identifier does not begin with \"The\".")

}

}

//打印“John's building identifier begins with "The".”

注意

在上面的例子中,在方法的圆括号后面加上问号是因为你要在buildingIdentifier()方法的可选返回值上进行可选链式调用,而不是方法本身。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容