#闭包(Closures)
本页包含内容:
- 闭包表达式(Closure Expressions)
- 尾随闭包(Trailing Closures)
- 值捕获(Capturing Values)
- 闭包是引用类型(Closures Are Reference Types)
闭包是自包含的函数代码块,可以在代码中被传递和使用。 Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 lambdas 函数比较相似。
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。 这就是所谓的闭合并包裹着这些常量和变量,俗称闭包。Swift 会为您管理在捕获过程中涉及到的所有内存操作。
注意: 如果您不熟悉捕获`(capturing)`这个概念也不用担心,您可以在 值捕获 章节对其进行详细了解。
在函数 章节中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采取如下三种形式之一:
- 全局函数是一个有名字但不会捕获任何值的闭包
- 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
- 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包
Swift 的闭包表达式拥有简洁的风格,并鼓励在常见场景中进行语法优化,主要优化如下:
- 利用上下文推断参数和返回值类型
- 隐式返回单表达式闭包,即单表达式闭包可以省略return关键字
- 参数名称缩写
- 尾随(Trailing)闭包语法
## 闭包表达式(Closure Expressions)
**嵌套函数** 是一个在较复杂函数中方便进行命名和定义自包含代码模块的方式。当然,有时候撰写小巧的没有完整定义和命名的类函数结构也是很有用处的,尤其是在您处理一些函数并需要将另外一些函数作为该函数的参数时。
闭包表达式是一种利用简洁语法构建内联闭包的方式。 闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。 下面闭包表达式的例子通过使用几次迭代展示了sorted函数定义和语法优化的方式。 每一次迭代都用更简洁的方式描述了相同的功能。
###sorted 函数(The Sorted Function)
Swift 标准库提供了`sorted`函数,会根据您提供的基于输出类型排序的闭包函数将已知类型数组中的值进行排序。 一旦排序完成,函数会返回一个与原数组大小相同的新数组,该数组中包含已经正确排序的同类型元素。
下面的闭包表达式示例使用`sorted`函数对一个`String`类型的数组进行字母逆序排序,以下是初始数组值:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
`sorted`函数需要传入两个参数:
- 已知类型的数组
- 闭包函数,该闭包函数需要传入与数组类型相同的两个值,并返回一个布尔类型值来告诉sorted函数当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回true,反之返回false。
该例子对一个`String`类型的数组进行排序,因此排序闭包函数类型需为`(String, String) -> Bool`。
提供排序闭包函数的一种方式是撰写一个符合其类型要求的普通函数,并将其作为`sort`函数的第二个参数传入:
func backwards(s1: String, s2: String) -> Bool {
return s1 > s2
}
var reversed = sorted(names, backwards)
// reversed 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
如果第一个字符串 `(s1)` 大于第二个字符串 `(s2)`,`backwards`函数返回`true`,表示在新的数组中`s1`应该出现在`s2`前。 对于字符串中的字符来说,“大于” 表示 “按照字母顺序较晚出现”。 这意味着字母`"B"`大于字母`"A"`,字符串`"Tom"`大于字符串`"Tim"`。 其将进行字母逆序排序,`"Barry"`将会排在`"Alex"`之前。
然而,这是一个相当冗长的方式,本质上只是写了一个单表达式函数 `(a > b)`。 在下面的例子中,利用闭合表达式语法可以更好的构造一个内联排序闭包。
###闭包表达式语法(Closure Expression Syntax)
闭包表达式语法有如下一般形式:
{ (parameters) -> returnType in
statements
}
闭包表达式语法可以使用常量、变量和inout类型作为参数,不提供默认值。 也可以在参数列表的最后使用可变参数。 元组也可以作为参数和返回值。
下面的例子展示了之前`backwards`函数对应的闭包表达式版本的代码:
reversed = sorted(names, { (s1: String, s2: String) -> Bool in
return s1 > s2
})
需要注意的是内联闭包参数和返回值类型声明与backwards函数类型声明相同。 在这两种方式中,都写成了`(s1: String, s2: String) -> Bool`。 然而在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。
闭包的函数体部分由关键字`in`引入。 该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
因为这个闭包的函数体部分如此短以至于可以将其改写成一行代码:
reversed = sorted(names, { (s1: String, s2: String) -> Bool in return s1 > s2 } )
这说明`sorted`函数的整体调用保持不变,一对圆括号仍然包裹住了函数中整个参数集合。而其中一个参数现在变成了内联闭包(相比于`backwards`版本的代码)。
###根据上下文推断类型(Inferring Type From Context)
因为排序闭包函数是作为`sorted`函数的参数进行传入的,Swift可以推断其参数和返回值的类型。 `sorted`期望第二个参数是类型为`(String, String) -> Bool`的函数,因此实际上`String,String`和`Bool`类型并不需要作为闭包表达式定义中的一部分。 因为所有的类型都可以被正确推断,返回箭头 `(->)` 和围绕在参数周围的括号也可以被省略:
reversed = sorted(names, { s1, s2 in return s1 > s2 } )
实际上任何情况下,通过内联闭包表达式构造的闭包作为参数传递给函数时,都可以推断出闭包的参数和返回值类型,这意味着您几乎不需要利用完整格式构造任何内联闭包。
###单表达式闭包隐式返回(Implicit Return From Single-Expression Clossures)
单行表达式闭包可以通过隐藏return关键字来隐式返回单行表达式的结果,如上版本的例子可以改写为:
reversed = sorted(names, { s1, s2 in s1 > s2 } )
在这个例子中,`sorted`函数的第二个参数函数类型明确了闭包必须返回一个`Bool`类型值。 因为闭包函数体只包含了一个单一表达式 `(s1 > s2)`,该表达式返回`Bool`类型值,因此这里没有歧义,`return`关键字可以省略。
###参数名称缩写(Shorthand Argument Names)
Swift 自动为内联函数提供了参数名称缩写功能,您可以直接通过`$0,$1,$2`来顺序调用闭包的参数。
如果您在闭包表达式中使用参数名称缩写,您可以在闭包参数列表中省略对其的定义,并且对应参数名称缩写的类型会通过函数类型进行推断。 `in`关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
reversed = sorted(names, { $0 > $1 } )
在这个例子中,$0和$1表示闭包中第一个和第二个String类型的参数。
###运算符函数(Operator Functions)
实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。 Swift 的`String`类型定义了关于大于号 (>) 的字符串实现,其作为一个函数接受两个String类型的参数并返回`Bool`类型的值。 而这正好与`sorted`函数的第二个参数需要的函数类型相符合。 因此,您可以简单地传递一个大于号,Swift可以自动推断出您想使用大于号的字符串函数实现:
reversed = sorted(names, >)
更多关于运算符表达式的内容请查看 `运算符函数`。
##尾随闭包(Trailing Closures)
如果您需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。 尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
func someFunctionThatTakesAClosure(closure: () -> ()) {
// 函数体部分
}
// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure({
// 闭包主体部分
})
// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}
- 注意: 如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉。
在上例中作为`sorted`函数参数的字符串排序闭包可以改写为:
reversed = sorted(names) { $0 > $1 }
当闭包非常长以至于不能在一行中进行书写时,尾随闭包变得非常有用。 举例来说,Swift 的`Array`类型有一个`map`方法,其获取一个闭包表达式作为其唯一参数。 数组中的每一个元素调用一次该闭包函数,并返回该元素所映射的值(也可以是不同类型的值)。 具体的映射方式和返回值类型由闭包来指定。
当提供给数组闭包函数后,`map`方法将返回一个新的数组,数组中包含了与原数组一一对应的映射后的值。
下例介绍了如何在`map`方法中使用尾随闭包将Int类型数组`[16,58,510]`转换为包含对应`String`类型的数组`["OneSix", "FiveEight", "FiveOneZero"]`:
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`方法来创建对应的字符串版本数组。 需要注意的是调用`numbers.map`不需要在`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"]
`map`在数组中为每一个元素调用了闭包表达式。 您不需要指定闭包的输入参数`number`的类型,因为可以通过要映射的数组类型进行推断。
闭包`number`参数被声明为一个变量参数(变量的具体描述请参看常量参数和变量参数),因此可以在闭包函数体内对其进行修改。闭包表达式制定了返回类型为`String`,以表明存储映射值的新数组类型为`String`。
闭包表达式在每次被调用的时候创建了一个字符串并返回。 其使用求余运算符 `(number % 10)` 计算最后一位数字并利用digitNames字典获取所映射的字符串。
- 注意: 字典digitNames下标后跟着一个叹号 (!),因为字典下标返回一个可选值 (optional value),表明即使该 key 不存在也不会查找失败。 在上例中,它保证了number % 10可以总是作为一个digitNames字典的有效下标 key。 因此叹号可以用于强制解析 (force-unwrap) 存储在可选下标项中的String类型值。
从`digitNames`字典中获取的字符串被添加到输出的前部,逆序建立了一个字符串版本的数字。 (在表达式`number % 10`中,如果number为16,则返回6,58返回8,510返回0)。
number变量之后除以10。 因为其是整数,在计算过程中未除尽部分被忽略。 因此 16变成了1,58变成了5,510变成了51。
整个过程重复进行,直到`number /= 10`为0,这时闭包会将字符串输出,而map函数则会将字符串添加到所映射的数组中。
上例中尾随闭包语法在函数后整洁封装了具体的闭包功能,而不再需要将整个闭包包裹在map函数的括号内。
##捕获值(Capturing Values)
闭包可以在其定义的上下文中捕获常量或变量。 即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数。 嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
下例为一个叫做`makeIncrementor`的函数,其包含了一个叫做`incrementor`嵌套函数。 嵌套函数`incrementor`从上下文中捕获了两个值,`runningTotal`和`amount`。 之后`makeIncrementor`将`incrementor`作为闭包返回。 每次调用`incrementor`时,其会以`amount`作为增量增加`runningTotal`的值。
func makeIncrementor(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
return incrementor
}
`makeIncrementor`返回类型为`() -> Int`。 这意味着其返回的是一个函数,而不是一个简单类型值。 该函数在每次调用时不接受参数只返回一个Int类型的值。 关于函数返回其他函数的内容,请查看函数类型作为返回类型。
`makeIncrementor`函数定义了一个整型变量`runningTotal`(初始为0) 用来存储当前跑步总数。 该值通过`incrementor`返回。
`makeIncrementor`有一个Int类型的参数,其外部命名为`forIncrement`, 内部命名为`amount`,表示每次`incrementor`被调用时`runningTotal`将要增加的量。
`incrementor`函数用来执行实际的增加操作。 该函数简单地使`runningTotal`增加`amount`,并将其返回。
如果我们单独看这个函数,会发现看上去不同寻常:
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
`incrementor`函数并没有获取任何参数,但是在函数体内访问了`runningTotal`和`amount`变量。这是因为其通过捕获在包含它的函数体内已经存在的`runningTotal`和`amount`变量而实现。
由于没有修改`amount`变量,`incrementor`实际上捕获并存储了该变量的一个副本,而该副本随着`incrementor`一同被存储。
然而,因为每次调用该函数的时候都会修改`runningTotal`的值,`incrementor`捕获了当前`runningTotal`变量的引用,而不是仅仅复制该变量的初始值。捕获一个引用保证了当`makeIncrementor`结束时候并不会消失,也保证了当下一次执行`incrementor`函数时,`runningTotal`可以继续增加。
- 注意: Swift 会决定捕获引用还是拷贝值。 您不需要标注`amount`或者`runningTotal`来声明在嵌入的`incrementor`函数中的使用方式。 Swift 同时也处理`runingTotal`变量的内存管理操作,如果不再被`incrementor`函数使用,则会被清除。
下面代码为一个使用makeIncrementor的例子:
let incrementByTen = makeIncrementor(forIncrement: 10)
该例子定义了一个叫做`incrementByTen`的常量,该常量指向一个每次调用会加10的`incrementor`函数。 调用这个函数多次可以得到以下结果:
incrementByTen()
// 返回的值为10
incrementByTen()
// 返回的值为20
incrementByTen()
// 返回的值为30
如果您创建了另一个`incrementor`,其会有一个属于自己的独立的ru`nningTotal`变量的引用。 下面的例子中,incrementBySevne捕获了一个新的runningTotal变量,该变量和incrementByTen中捕获的变量没有任何联系:
let incrementBySeven = makeIncrementor(forIncrement: 7)
incrementBySeven()
// 返回的值为7
incrementByTen()
// 返回的值为40
- 注意: 如果您将闭包赋值给一个类实例的属性,并且该闭包通过指向该实例或其成员来捕获了该实例,您将创建一个在闭包和实例间的强引用环。 Swift 使用捕获列表来打破这种强引用环。更多信息,请参考 闭包引起的循环强引用。
##闭包是引用类型(Closures Are Reference Types)
上面的例子中,`incrementBySeven`和`incrementByTen`是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量值。 这是因为函数和闭包都是引用类型。
无论您将函数/闭包赋值给一个常量还是变量,您实际上都是将常量/变量的值设置为对应函数/闭包的引用。 上面的例子中,`incrementByTen`指向闭包的引用是一个常量,而并非闭包内容本身。
这也意味着如果您将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值为50
# 类和结构体本页包含内容:- 类和结构体对比- 结构体和枚举是值类型- 类是引用类型- 类和结构体的选择- 集合(collection)类型的赋值与复制行为类和结构体是人们构建代码所用的一种通用且灵活的构造体。为了在类和结构体中实现各种功能,我们必须要严格按照常量、变量以及函数所规定的语法规则来定义属性和添加方法。与其他编程语言所不同的是,Swift 并不要求你为自定义类和结构去创建独立的接口和实现文件。你所要做的是在一个单一文件中定义一个类或者结构体,系统将会自动生成面向其它代码的外部接口。注意: 通常一个类的实例被称为对象。然而在Swift 中,类和结构体的关系要比在其他语言中更加的密切,本章中所讨论的大部分功能都可以用在类和结构体上。因此,我们会主要使用实例而不是对象。###类和结构体对比Swift 中类和结构体有很多共同点。共同处在于:- 定义属性用于存储值- 定义方法用于提供功能- 定义附属脚本用于访问值- 定义构造器用于生成初始化值- 通过扩展以增加默认实现的功能- 符合协议以对某类提供标准功能####与结构体相比,类还有如下的附加功能:- 继承允许一个类继承另一个类的特征- 类型转换允许在运行时检查和解释一个类实例的类型- 解构器允许一个类实例释放任何其所被分配的资源- 引用计数允许对一个类的多次引用注意: 结构体总是通过被复制的方式在代码中传递,因此请不要使用引用计数。定义类和结构体有着类似的定义方式。我们通过关键字`class`和`struct`来分别表示类和结构体,并在一对大括号中定义它们的具体内容: class SomeClass { // class definition goes here } struct SomeStructure { // structure definition goes here }注意: 在你每次定义一个新类或者结构体的时候,实际上你是有效地定义了一个新的 Swift 类型。因此请使用 `UpperCamelCase` 这种方式来命名(如 `SomeClass` 和`SomeStructure`等),以便符合标准Swift 类型的大写命名风格(如`String,Int`和`Bool`)。相反的,请使用`lowerCamelCase`这种方式为属性和方法命名(如`framerate`和`incrementCount`),以便和类区分。
以下是定义结构体和定义类的示例:
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
在上面的示例中我们定义了一个名为`Resolution`的结构体,用来描述一个显示器的像素分辨率。这个结构体包含了两个名为`width`和`height`的存储属性。存储属性是捆绑和存储在类或结构体中的常量或变量。当这两个属性被初始化为整数0的时候,它们会被推断为Int类型。
在上面的示例中我们还定义了一个名为`VideoMode`的类,用来描述一个视频显示器的特定模式。这个类包含了四个储存属性变量。第一个是分辨率,它被初始化为一个新的`Resolution`结构体的实例,具有`Resolution`的属性类型。新`VideoMode`实例同时还会初始化其它三个属性,它们分别是,初始值为`false`(意为`“non-interlaced video”`)的`interlaced`,回放帧率初始值为`0.0`的`frameRate`和值为可选`String`的`name`。`name`属性会被自动赋予一个默认值`nil`,意为“没有`name`值”,因为它是一个可选类型。
###类和结构体实例
`Resolution`结构体和`VideoMode`类的定义仅描述了什么是`Resolution`和`VideoMode`。它们并没有描述一个特定的分辨率(`resolution`)或者视频模式(`video mode`)。为了描述一个特定的分辨率或者视频模式,我们需要生成一个它们的实例。
生成结构体和类实例的语法非常相似:
let someResolution = Resolution()
let someVideoMode = VideoMode()
结构体和类都使用构造器语法来生成新的实例。构造器语法的最简单形式是在结构体或者类的类型名称后跟随一个空括弧,如`Resolution()`或`VideoMode()`。通过这种方式所创建的类或者结构体实例,其属性均会被初始化为默认值。构造过程章节会对类和结构体的初始化进行更详细的讨论。
###属性访问
通过使用点语法(`dot syntax`),你可以访问实例中所含有的属性。其语法规则是,实例名后面紧跟属性名,两者通过点号`(.)`连接:
println("The width of someResolution is \(someResolution.width)")
// 输出 "The width of someResolution is 0"
在上面的例子中,`someResolution.width`引用`someResolution`的`width`属性,返回`width`的初始值0。
你也可以访问子属性,如`VideoMode`中`Resolution`属性的`width`属性:
println("The width of someVideoMode is \(someVideoMode.resolution.width)")
// 输出 "The width of someVideoMode is 0"
你也可以使用点语法为属性变量赋值:
someVideoMode.resolution.width = 1280
println("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// 输出 "The width of someVideoMode is now 1280"
注意: 与Objective-C语言不同的是,Swift 允许直接设置结构体属性的子属性。上面的最后一个例子,就是直接设置了`someVideoMode`中`resolution`属性的`width`这个子属性,以上操作并不需要重新设置`resolution`属性。
结构体类型的成员逐一构造器(`Memberwise Initializers for structure Types`)
所有结构体都有一个自动生成的成员逐一构造器,用于初始化新结构体实例中成员的属性。新实例中各个属性的初始值可以通过属性的名称传递到成员逐一构造器之中:
let vga = Resolution(width:640, height: 480)
与结构体不同,类实例没有默认的成员逐一构造器。构造过程章节会对构造器进行更详细的讨论。
###结构体和枚举是值类型
值类型被赋予给一个变量,常数或者本身被传递给一个函数的时候,实际上操作的是其的拷贝。
在之前的章节中,我们已经大量使用了值类型。实际上,在 `Swift` 中,所有的基本类型:整数(`Integer`)、浮点数(`floating-point`)、布尔值(`Booleans`)、字符串(`string`)、数组(`array`)和字典(`dictionaries`),都是值类型,并且都是以结构体的形式在后台所实现。
在 `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:
println("cinema is now \(cinema.width) pixels wide")
// 输出 "cinema is now 2048 pixels wide"
然而,初始的hd实例中width属性还是1920:
println("hd is still \(hd.width ) pixels wide")
// 输出 "hd is still 1920 pixels wide"
在将`hd`赋予给`cinema`的时候,实际上是将hd中所存储的值(`values`)进行拷贝,然后将拷贝的数据存储到新的`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 {
println("The remembered direction is still .West")
}
// 输出 "The remembered direction is still .West"
上例中`rememberedDirection`被赋予了`currentDirection`的值(`value`),实际上它被赋予的是值(`value`)的一个拷贝。赋值过程结束后再修改`currentDirection`的值并不影响`rememberedDirection`所储存的原始值(`value`)的拷贝。
###类是引用类型
与值类型不同,引用类型在被赋予到一个变量、常量或者被传递到一个函数时,操作的是引用,其并不是拷贝。因此,引用的是已存在的实例本身而不是其拷贝。
请看下面这个示例,其使用了之前定义的VideoMode类:
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
以上示例中,声明了一个名为`tenEighty`的常量,其引用了一个`VideoMode`类的新实例。在之前的示例中,这个视频模式(`video mode`)被赋予了`HD`分辨率(`1920*1080`)的一个拷贝(`hd`)。同时设置为交错(`interlaced`),命名为`“1080i”`。最后,其帧率是25.0帧每秒。
然后,`tenEighty` 被赋予名为`alsoTenEighty`的新常量,同时对`alsoTenEighty`的帧率进行修改:
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0
因为类是引用类型,所以`tenEight`和`alsoTenEight`实际上引用的是相同的`VideoMode`实例。换句话说,它们是同一个实例的两种叫法。
下面,通过查看`tenEighty`的`frameRate`属性,我们会发现它正确的显示了基本`VideoMode`实例的新帧率,其值为30.0:
println("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// 输出 "The frameRate property of theEighty is now 30.0"
需要注意的是`tenEighty`和`alsoTenEighty`被声明为常量((`constants`)而不是变量。然而你依然可以改变`tenEighty.frameRate`和`alsoTenEighty.frameRate`,因为这两个常量本身不会改变。它们并不存储这个`VideoMode`实例,在后台仅仅是对`VideoMode`实例的引用。所以,改变的是被引用的基础`VideoMode`的`frameRate`参数,而不改变常量的值。
###恒等运算符
因为类是引用类型,有可能有多个常量和变量在后台同时引用某一个类实例。(对于结构体和枚举来说,这并不成立。因为它们作为值类型,在被赋予到常量、变量或者传递到函数时,其值总是会被拷贝。)
如果能够判定两个常量或者变量是否引用同一个类实例将会很有帮助。为了达到这个目的,Swift 内建了两个恒等运算符:
等价于 ( === )
不等价于 ( !== )
以下是运用这两个运算符检测两个常量或者变量是否引用同一个实例:
if tenEighty === alsoTenEighty {
println("tenEighty and alsoTenEighty refer to the same Resolution instance.")
}
//输出 "tenEighty and alsoTenEighty refer to the same Resolution instance."
请注意“等价于"(用三个等号表示,`===`) 与“等于"(用两个等号表示,`==`)的不同:
- “等价于”表示两个类类型(class type)的常量或者变量引用同一个类实例。
- “等于”表示两个实例的值“相等”或“相同”,判定时要遵照类设计者定义定义的评判标准,因此相比于“相等”,这是一种更加合适的叫法。
当你在定义你的自定义类和结构体的时候,你有义务来决定判定两个实例“相等”的标准。在章节运算符函数(Operator Functions)中将会详细介绍实现自定义“等于”和“不等于”运算符的流程。
###指针
如果你有 C,C++ 或者 Objective-C 语言的经验,那么你也许会知道这些语言使用指针来引用内存中的地址。一个 Swift 常量或者变量引用一个引用类型的实例与 C 语言中的指针类似,不同的是并不直接指向内存中的某个地址,而且也不要求你使用星号(*)来表明你在创建一个引用。Swift 中这些引用与其它的常量或变量的定义方式相同。
###类和结构体的选择
在你的代码中,你可以使用类和结构体来定义你的自定义数据类型。
然而,结构体实例总是通过值传递,类实例总是通过引用传递。这意味两者适用不同的任务。当你在考虑一个工程项目的数据构造和功能的时候,你需要决定每个数据构造是定义成类还是结构体。
按照通用的准则,当符合一条或多条以下条件时,请考虑构建结构体:
- 结构体的主要目的是用来封装少量相关简单数据值。
- 有理由预计一个结构体实例在赋值或传递时,封装的数据将会被拷贝而不是被引用。
- 任何在结构体中储存的值类型属性,也将会被拷贝,而不是被引用。
- 结构体不需要去继承另一个已存在类型的属性或者行为。
合适的结构体候选者包括:
- 几何形状的大小,封装一个width属性和height属性,两者均为Double类型。
- 一定范围内的路径,封装一个start属性和length属性,两者均为Int类型。
- 三维坐标系内一点,封装x,y和z属性,三者均为Double类型。
在所有其它案例中,定义一个类,生成一个它的实例,并通过引用来管理和传递。实际中,这意味着绝大部分的自定义数据构造都应该是类,而非结构体。
###集合(Collection)类型的赋值和拷贝行为
Swift 中字符串(`String`),数组(`Array`)和字典(`Dictionary`)类型均以结构体的形式实现。这意味着`String,Array,Dictionary`类型数据被赋值给新的常量(或变量),或者被传入函数(或方法)中时,它们的值会发生拷贝行为(值传递方式)。
`Objective-C`中字符串(`NSString`),数组(`NSArray`)和字典(`NSDictionary`)类型均以类的形式实现,这与`Swfit`中以值传递方式是不同的。`NSString,NSArray,NSDictionary`在发生赋值或者传入函数(或方法)时,不会发生值拷贝,而是传递已存在实例的引用。
注意: 以上是对于数组,字典,字符串和其它值的拷贝的描述。 在你的代码中,拷贝好像是确实是在有拷贝行为的地方产生过。然而,在 `Swift` 的后台中,只有确有必要,实际(`actual`)拷贝才会被执行。`Swift` 管理所有的值拷贝以确保性能最优化的性能,所以你也没有必要去避免赋值以保证最优性能。(实际赋值由系统管理优化)
网友评论