你用,或是不用,术语就在那里,不多不少。你懂,或是不懂,定义就在那里,不偏不倚。
程序员总是喜欢说行话 [6] 。为了避免困扰,接下来我们会介绍一些贯穿于本书的术语定义。我们会尽可能遵守Swift官方文档中的术语用法,使用被Swift社区所广泛接受的定义。这些定义大多都会在接下来的章节中被详细介绍,所以就算一开始你对它们一头雾水,也大可不必在意。如果你已经对这些术语非常了解,那么我们也还是建议你再浏览一下它们,并且确定你能接受我们的表述。
在Swift中,我们需要对值、变量、引用以及常量加以区分。
值(value) 是不变的,永久的,它从不会改变。比如,1、true和[1,2,3]都是值。这些是 字面量(literal) 的例子,值也可以是运行代码时生成的。当你计算5的平方时,你得到的数字也是一个值。
当我们使用var x=[1,2]来将一个值进行命名的时候,我们实际上创建了一个名为x的 变量(variable) 来持有[1,2]这个值。通过像执行x.append(3)这样的操作来改变x时,我们并没有改变原来的值。相反,我们所做的是使用[1,2,3]这个新的值来替代原来x中的内容。可能实际上它的内部实现真的只是在某段内存的后面添加上一个条目,并不是进行全体替换,但是至少从 逻辑 上来说此值是全新的。我们将这个过程称为变量的 改变(mutating) 。
我们还可以使用let而不是var来声明一个 常量变量(constant variables) ,或者简称为常量。一旦常量被赋予一个值,它就不能再次被赋一个新的值了。
我们不需要在一个变量被声明的时候就立即为它赋值。我们可以先对变量进行声明(let x: Int),然后稍后再给它赋值(x=1)。Swift是强调安全的语言,它将检查所有可能的代码路径,并确保变量在被读取之前一定是完成了赋值的。在Swift中变量不会存在未定义状态。当然,如果一个变量是用let声明的,那么它只能被赋值一次。
结构体(struct)和枚举(enum)是 值类型(value type) 。当你把一个结构体变量赋值给另一个变量,那么这两个变量将会包含同样的值。你可以将它理解为内容被复制了一遍,但是更精确地描述,则是被赋值的变量与另外的那个变量包含了同样的值。
引用(reference) 是一种特殊类型的值:它是一个“指向”另一个值的值。两个引用可能会指向同一个值,这引入了一种可能性,那就是这个值可能会被程序的两个不同的部分所改变。
类(class)是 引用类型(reference type) 。你不能在一个变量里直接持有一个类的实例(我们偶尔可能会把这个实例称作 对象(object) ,这个术语经常被滥用,会让人困惑)。对于一个类的实例,我们只能在变量里持有对它的引用,然后使用这个引用来访问它。
引用类型具有 同一性(identity) ,也就是说,你可以使用===来检查两个变量是否确实引用了同一个对象。如果相应类型的==运算符被实现了,那你也可以用==来判断两个变量是否相等。两个不同的对象按照定义也是可能相等的。
值类型不存在同一性的问题。比如你不能判定某个变量判定是否和另一个变量持有“相同”的数字2。你只能检查它们是否都包含了2这个值。===运算符实际做的是询问“这两个变量是不是持有同样的引用”。在程序语言的论文里,==有时候被称为 结构相等 ,而===则被称为 指针相等 或者 引用相等 。
在Swift中,类引用不是唯一的引用类型。Swift中依然有指针,比如使用withUnsafeMutable-Pointer和类似方法所得到的就是指针。不过类是使用起来最简单引用类型,这与它们的引用特性被部分隐藏在语法糖之后是不无关系的。你不需要像在其他一些语言中那样显式地处理指针的“解引用”。(在互用性章节中会详细提及其他种类的引用。)
一个引用变量也可以用let来声明,这样做会使引用变为常量。换句话说,这会使变量不能再被用来引用其他东西,不过很重要的是,这并不意味着这个变量 所引用 的对象本身不能被改变。所以,当用常量的方式来引用变量的时候要格外小心,其后果是——只有指向关系被常量化了,而对象本身还是可变的。(如果前面这几句话看起来有些不明白,不要担心,我们在第5章还会详细解释)。这一点造成的问题是,就算在一个声明变量的地方看到let,你也不能一下子就知道声明的东西是不是完全不可变的。想要做出正确的判断,你必须先 知道 这个变量持有的是值类型还是引用类型。
我们通过值类型是否执行 深复制 来对它们进行分类,判断它们是否具有 值语义(value semantics) 。这种复制可能是在赋值新变量时就发生的,也可能会延迟到变量内容发生变更的时候。
这里我们会遇到另一件复杂的事情。如果我们的结构体中包含有引用类型,在将结构体赋值给一个新变量时所发生的复制行为中,这些引用类型的内容是不会被自动复制一份的,只有引用本身会被复制。这种复制的行为被称作 浅复制(shallow copy) 。
举个例子,Foundation框架中的Data结构体实际上是对引用类型NSData的一个封装。不过,Data的作者采取了额外的步骤,来保证当Data结构体发生变化的时候对其中的NSData对象进行深复制。他们使用一种名为“写时复制”(copy-on-write)的技术来保证操作的高效,我们会在结构体和类章节里详细介绍这种机制。现在我们需要重点知道的是,这种写时复制的特性并不是直接具有的,它需要额外进行实现。
在Swift中,像是数组这样的集合类型也都是对引用类型的封装,它们同样使用了写时复制的方式来在提供值语义的同时保持高效。不过,如果集合类型的元素是引用类型(比如一个含有对象的数组),那么对象本身将不会被复制,只有对它的引用会被复制。也就是说,Swift的数组只有当其中的元素满足值语义时,数组本身才具有值语义。在下一章,我们会讨论Swift中的集合类型与Foundation框架中NSArray和NSDictionary这些集合类型的不同之处。
有些类是完全不可变的,也就是说,从被创建以后,它们就不提供任何方法来改变它们的内部状态。这意味着即使它们是类,它们依然具有值语义(因为它们就算被到处使用也从不会改变)。但是要注意的是,只有那些标记为final的类能够保证不被子类化,也不会被添加可变状态。
在Swift中,函数也是值。你可以将一个函数赋值给一个变量,也可以创建一个包含函数的数组,或者调用变量所持有的函数。如果一个函数接受别的函数作为参数(比如map函数接受一个转换函数,并将其应用到数组中的所有元素上),或者一个函数的返回值是函数,那么这样的函数就叫作 高阶函数(higher-order function) 。
函数不需要被声明在最高层级——你可以在一个函数内部声明另一个函数,也可以在一个do作用域或者其他作用域中声明函数。如果一个函数被定义在外层作用域中,但是被传递出这个作用域(比如把这个函数作为其他函数的返回值返回时),它将能够“捕获”局部变量。这些局部变量将存在于函数中,不会随着局部作用域的结束而消亡,函数也将持有它们的状态。这种行为的变量被称为“闭合变量”,我们把这样的函数叫作 闭包(closure) 。
函数可以通过func关键字来定义,也可以通过{}这样的简短的 闭包表达式(closure expression) 来定义。有时候我们只把通过闭包表达式创建的函数叫作“闭包”,不过不要让这种叫法蒙蔽了你的双眼。实际上使用func关键字定义的函数也是闭包。
函数是引用类型。也就是说,将一个通过闭合变量保存状态的函数赋值给另一个变量,并不会导致这些状态被复制。和对象引用类似,这些状态会被共享。换句话说,当两个闭包持有同样的局部变量时,它们是共享这个变量以及它的状态的。这可能会让你有点儿惊讶,我们将在函数一章中涉及这方面的更多内容。
定义在类或者协议中的函数就是 方法(method) ,它们有一个隐式的self参数。如果一个函数不是接受多个参数,而是只接受部分参数,然后返回一个接受其余参数的函数,那么这个函数就是一个 柯里化函数(curried function) 。我们会在第6章中讲解一个方法是如何成为柯里化函数的。有时候我们会把那些不是方法的函数叫作 自由函数(free function) ,这可以将它们与方法区分开来。
自由函数和那些在结构体上调用的方法是 静态派发(statically dispatched) 的。对于这些函数的调用,在编译的时候就已经确定了。对于静态派发的调用,编译器可能能够 内联(inline)
这些函数,也就是说,完全不去做函数调用,而是将这部分代码替换为需要执行的函数。静态派发还能够帮助编译器丢弃或者简化那些在编译时就能确定不会被实际执行的代码。
类或者协议上的方法可能是 动态派发(dynamically dispatched) 的。编译器在编译时不需要知道哪个函数将被调用。在Swift中,这种动态特性要么由vtable [7] 来完成,要么通过selector 和objc_msgSend来完成,前者的处理方式和Java或是C++中类似,而后者只针对@objc的类和协议上的方法。
子类型和方法 重写(overriding) 是实现 多态(polymorphic) 特性的手段,也就是说,根据类型的不同,同样的方法会呈现出不同的行为。另一种方式是函数 重载(overloading) ,它是指为不同的类型多次写同一个函数的行为。(注意不要把重写和重载弄混了,它们是完全不同的。)实现多态的第三种方法是通过泛型,也就是一次性地编写能够接受任意类型的函数或者方法,不过这些方法的实现会各有不同。与方法重写不同的是,泛型中的方法在编译期间就是静态已知的。我们会在泛型章节中提及关于这方面的更多内容。