第二章——介绍(基本术语)

319 阅读10分钟

程序员总是喜欢动不动就提出一个新的术语,为了避免混淆,这里定义了一些我们书中用到的术语。如果可能的话,我们会沿用官方文档的一些术语,有时也会采用被swift社区广泛接受的术语。其中大部分术语会在后面的章节中具体讨论,所以即使对他们不熟悉的话也不用担心。如果你已经熟悉了这些术语,最好也大概浏览一下以确保你没有误解他们的真实意思。

在Swift中,我们严格区分值(value),变量(variable),引用(reference)和常量(constant)。

是永远不可变的,比如1true[1,2,3]这些都是值。刚刚给出的这些是字面量,但值还可以在运行时被创建。当你计算5的平方时得到的结果,也是一个值

当我们把一个值通过var x = [1,2]语句赋给某个符号时,我们其实创建了一个叫做x变量,这个变量的值是[1,2]。如果改变了x,比如执行x.append(3),我们改变的不是原来的,而是让x具有一个新的值:[1,2,3]。至少在逻辑上来说是这样的,虽然在实际编程的时候可能只是在某一块已存在的内存末尾新增了一条数据。我们称之为值的变异(mutating)

我们可以声明一个常量[1],只需要用let替换var既可。一旦一个常量被赋值了,它就不能被再次赋值。

创建变量后可以不用立刻赋值。我们可以先这么写:let x: Int然后稍后再给它赋值:x = 1。Swift很看重安全性,它会在变量被使用之前检查所有可能对它赋值的地方,所以变量在被使用时,是不可能处于"尚未赋值"状态的。当然,如果变量被定义为let,那就只能被赋值一次。

结构体和枚举都是值类型。当你把一个结构体的值赋给另一个结构体时,两个结构体有相同的值。你可以理解为新的变量由原变量复制得到,但更准确的说法是:新的结构体的值被改变成和原结构体的值相等。

引用是一种特殊的,一种“指向”变量的值。由于两个引用可能指的是同一个对象,这使得同一个对象同一时刻有可能在程序的不同部分被修改。

类是引用类型。 你需要让一个变量持有某个类的实例的引用并通过这引用来访问它,而不是直接持有这个类的实例(有时候我们会把类的实例叫做对象,不过这个术语有可能会带来歧义)。

每个引用类型都有唯一标识[2],所以你可以通过===运算符来判断两个引用是否指向的的是同一个对象,如果该类型的==运算符被实现了,你还可以判断两个对象是否相等。不同(标识)的两个对象依然是可以相等的,因为相等的定义由我们决定,比如我们可以让相同年龄的人是相等的。值类型没有唯一标识,所以你不能判断某一个值为2的变量是否就是另一个值为2的变量,你只能检查两个变量的是否都为2。===运算符其实是在询问两个变量是否持有相同的引用,作为它们各自的值。

类应用并不是Swift中唯一的引用类型,比如还有通过UnsafeMutablePointer函数获取的指针。但类是最容易使用的引用类型,一部分原因在于它们和引用有关的一部分被通过语法糖隐藏了,所以你不用像在其它语言中使用指针一样去解引用(dereferencing)。我们会在后面的章节中讨论更多关于引用的细节。

持有一个引用(作为值)的变量可以被定义为let,也就是一个常引用。这表示这个变量所持有的引用不能再被改变,去指向别的东西了。关键点在于,这并不意味着它所指向的对象本身不能改变。这就类似于你娶了某个女孩子为妻,你的妻子就不能再换成别的人,但你妻子本人还是可以发生一些变化的。所以我们所说的常量,只表示不能改变它指向的东西,而不表示它指向的东西不能改变。如果这几句话比较绕,不用担心,我们会在后面的章节详细讨论。因此,不幸的是,当我们第一眼看到一个被声明为let的变量时,你无法区分它是否是完全不可变的,因为你需要首先判断它到底持有的是一个值还是一个引用。

这又是一个比较复杂的话题,我们知道结构体是值类型,但结构体本身可以由很多种其他类型的元素组成,比如其中就可能有引用类型。这意味着把一个值类型的变量赋值给另一个变量时,是浅拷贝,也就是只拷贝引用,而非引用所指向的对象。

我们指向的,都是那些具有值语义的类型。它可以分辨出含有引用的值类型并且执行深拷贝,也就是不仅仅拷贝引用本身,还拷贝引用所指向的值。对于包含了引用却没有实现值语义的值类型,没有与之对应的术语。

有些类是完全不可变的,也就是说它们在被创建后,不会提供能改变内部状态的方法。这意味着尽管它们是类,但也不会提供什么值语义(因为即使对象被几个引用共享,也没法被改变)。尽管如此,我们也要小心,只有被标记为final的类才能确保在被继承时不会添加可以被改变的内部状态。

Swift标准库中的集合类型是持有一些引用的结构体,同时有自己的值语义。在下一章中会介绍这和OC的Foundation类有什么区别。简单来说,Swift运用了一种叫做写时复制(copy-on-write)的技术,它的具体实现原理在数组与可变性一节中有具体介绍。就目前为止,我们需要知道这个技术并不是只要结构体含有引用就可以使用的,需要结构体的实现者来完成这个特性。写时复制不是唯一,而是一种很常见的模拟值语义的方法,

在Swift中,函数也是值。你可以把函数赋值给一个变量,可以用一个数组存放很多函数,可以调用某一个变量持有的函数。把其他函数当做参数(比如map方法把一个变换函数作为参数)或返回一个函数的函数被称为高阶函数

函数不必被定义为全局的,它还可以被定义在另一个函数里,或者在一个do代码块或其他代码块里。定义在外部区域之内,但被传出外部作用域之外(比如作为外部函数的返回值)的函数可以截获本地变量,这样本地变量在自身的作用域结束后就不会销毁,因为截获它的函数会一直持有它。这种拥合[3]变量的能力说明Swift的函数其实是闭包。

函数可以用func关键字声明,也可以用被称为闭包表达式的缩写语法——{ }。很多时候,这种写法用于闭包,所以给你留下了只有闭包表达式才能作为闭包使用的错误印象,但事实上用func关键字声明的函数也可以当做闭包使用。

函数通过引用被变量持有,这说明把一个因截获变量而具有状态的函数赋值给另一个函数不会复制那个变量,而是共享它。不仅如此,如果两个函数共享某一个变量,那他们会共享同一个状态。这可能确实有些令人惊讶,我们会在函数章节展开更多讨论。

被定义在类或协议内部的函数叫方法,而且有一个没有被显式写出的参数self。如果一个函数返回的不是多个参数的运算结果,而是在使用了部分参数后得到的一个新函数,这种情况被称为函数的柯里化(currying),被返回的函数成为柯里化函数(curried function)。我们会在函数章节具体研究柯里化函数。由于方法也是也是一种函数,为了区分那些不是方法的函数,我们称它为自由函数(不属于某个类或协议)。

自由函数和结构体的函数会被静态分发,也就是我们在编译期就可以确定具体哪个函数会被调用。这同时也意味着编译器可能会内联函数,也就是不通过指令跳转来调用函数,而是直接用函数的实现代码替换调用函数的代码(类似于C语言的宏)。编译器在编译时还有可能删除或简化那些它确定不会真正运行的代码。

属于类和协议的方法就有可能被动态分发了。这意味着编译器在编译期不必知道具体哪个函数会被调用,这种技术既可以通过类似于C++或Java的虚函数表(vtables),也可以通过OC中的选择子和objc_msgSend方法实现。

子类化和方法重写(overriding)是一种实现多态的方法。另一种方法是通过函数重载(overloading),也就是有多个同名但不同参数的函数。注意千万不要把重载和重写弄混淆,它们是两个完全不同的概念。第三种实现多态的方式是范型,也就是一个函数可以接受任何类型的参数,这些参数需要提供固定的方法,但方法的具体实现可以千差万别。与方法重写不同,函数重载和范型函数可以在编译期就被确定。我们会在以后的章节讨论更多细节。

#译者注: [1]:由于中英文语法问题,这里可能不太好理解。常量,在原文中是constant variable,可以理解为不会变的变量。所以本书后面可能会用变量表示真正的变量和不会变的变量。

[2]:可以类比对象的地址

[3]:原文用的是close over,我试着翻译为拥合,实在找不到确切的名词,其实闭包得名的由来恐怕就是因为这个close吧。不过了解闭包原理的人应该都能理解,闭包会截获临时变量,这个临时变量的定义其实并不在闭包内,我们就称闭包拥合了这个临时变量。具体的解释参见这篇文章