Swift 开发中我们会大量使用变量(可变变量)和常量(不可变变量),很多时候我们习惯将变量声明为 var,以便于在程序中更新变量的值。而实际上,理解并合理使用不可变变量,限制变量的可变状态能够给我们带来非常多的好处。
本章关键词
请带着以下关键词阅读本文:
值类型和引用类型
概述
在 Swift 中,我们分别使用 let 和 var 来声明不可变变量和可变变量:
let a: Int = 5
var b: Int = 10
a = 10 // error: cannot assign to value: 'a' is a 'let' constant
b = 5
不可变变量的值在初始化过后不能再被修改,编译器会保证 let 常量的不可变性。除了变量的不可变类型,我们最常接触的就是以结构体和类为代表的值类型(value types)和引用类型(reference types)。
A small distinction in behavior drives the architectural possibilities at play here: structs are value types and classes are reference types.
Instances of value types are copied whenever they’re assigned or used as a function argument. Numbers, strings, arrays, dictionaries, enums, tuples, and structs are value types.
Instances of reference types (chiefly: classes, functions) can have multiple owners. When assigning a reference to a new variable or passing it to a function, those locations all point to the same instance. This is the behavior you’re used to with objects.
对于值类型,无论是赋值还是作为函数参数,都会将原值进行复制,如此,对新值的修改并不影响原值。不止是数字,字符串、数组、字典、布尔值、枚举、元组和结构体都是值类型。
与值类型相对应的就是引用类型,引用类型的实例是可以拥有多个所有者的,即多个变量都指向同一个实例。
var c = UIView()
var d = c // 引用类型
d.alpha = 0.5
print("c.alpha: \(c.alpha); d.alpha: \(d.alpha)")
// c.alpha: 0.5; d.alpha: 0.5
var e = c.frame // 值类型
e.origin.x = 100
print("c.frame: \(c.frame); e: \(e)")
// c.frame: (0.0, 0.0, 0.0, 0.0); e: (100.0, 0.0, 0.0, 0.0)
在这个例子中,c 本身是引用类型(类),所以 c 和 d 都是 UIView() 的所有者,指向同一个实例,因此修改 d 的属性同时就修改了 c;而 c.frame 却是一个值类型(结构体),所以 e 是 c.frame 的拷贝,修改 e 并不影响原值。
这个结果看起来对我们已经习以为常了,然而“这一行为上的细微区别造就了架构上的无限可能”,选择值类型还是引用类型会给系统架构带来很大的差异。下面摘录一段郭中强(@Onetaway)《A Warm Welcome to Structs and Value Types》译文中的一段:
我们在代码中引用对象和我们在现实生活中引用对象是一样的。编程书籍经常使用一个现实世界的隐喻来教授人们面向对象编程:你可以创建一个 Dog 类,然后将它实例化来定义 fido (译注:狗的名字)。如果你将 fido 在系统的不同部分之间传递,它们谈论的仍然是同一个 fido。这是有意义的,因为如果你的确有一只叫 Fido 的狗,无论何时你谈到它时,你将会使用它的名字进行信息传输 —— 而不是传输狗本身。你可能依赖于其他人知道 Fido 是谁。当你使用对象的时候,你是在系统内传递着实例的名字。
值就像数据一样。如果你向别人发出了一张费用开销表,你发出的不是一个代表那个信息的标签 —— 你是在传递信息本身。消息接收者可以在不和任何人交流的情况下,计算总和,或者把费用写下来供日后查阅。如果消息接收者打印了费用表并且修改了它们,这也没有修改你自己的那张表。
一个值可以是一个数字,也许代表一个价格,或是一个类似字符串的描述。它可以是枚举中的一个选项:这次的花费是因为一顿晚餐,还是旅行,还是材料?在指定的位置中还能包括一些其他的值,比如一个代表经度和纬度的 CLLocationCoordinate2D 结构体。或者它可以是一些其他值的列表等等。
Fido 可能在自己的地盘里来回跑叫。它也许会有特殊的行为使它区别于其他的狗。他可能会同其他的狗建立关系。你不能把 Fido 换成其他的狗 —— 你的孩子们会发现的!但是一张费用开销表是独立的。那些字符串和数字不会做任何事情。它们不会背着你私下改变,不管你用多少种不同的方式在第一列写入了一个 6,它永远只会是一个 6。
这就是值类型的伟大之处。
如何选择
了解了值类型和引用类型的区别之后,就需要问一句:如何选择?这才是我们开发过程更加关注的。苹果在 Swift Blog 《Value and Reference Types》中给出了以下建议:
Use a value type when:
- Comparing instance data with == makes sense
- You want copies to have independent state
- The data will be used in code across multiple threads
Use a reference type (e.g. use a class) when:
- Comparing instance identity with === makes sense
- You want to create shared, mutable state
什么时候该用值类型
- 要用 == 运算符来比较实例的数据时
如果我们希望 == 运算符比较的是实例数据的“值”,则需要使实例为值类型:
var frame_1 = CGRect(x: 0, y: 0, width: 10, height: 10)
var frame_2 = CGRect(x: 0, y: 0, width: 10, height: 10)
print(frame_1 == frame_2) // true
frame_2.origin.x = 10
print(frame_1 == frame_2) // false
- 希望实例的拷贝能保持独立的状态时
var arr_1: Array = [1]
var arr_2 = arr_1
print("arr_1: \(arr_1), arr_2: \(arr_2)")
// arr_1: [1], arr_2: [1]
arr_2.append(Int(4))
print("arr_1: \(arr_1), arr_2: \(arr_2)")
// arr_1: [1], arr_2: [1, 2]
在这个例子中,将 arr_1 赋值给 arr_2,由于 Array 是值类型,因此我们对 arr_2 的操作并未破坏 arr_1,也就是说,它们各自保持独立的状态。
- 数据会被多个线程访问时
使用值类型,我们总是能够得到一个独立的拷贝,就可以放心使用,而不担心被其他代码修改,这尤其体现在多线程环境下,一旦因为另外一个线程暗地里修改了实例的值,则可能会造成严重错误,并且这在调试过程中难以排除。
什么时候该用引用类型
- 要用 == 运算符来比较实例时
先来看一个例子:
let view_1 = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
let view_2 = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 10))
print(view_1 == view_2) // false,UIView 为引用类型
print(view_1.frame == view_2.frame) // true,CGRect 为值类型
如果我们希望通过 == 来判断实例本身是否相等,则应使用引用类型(本例中为 UIView)。
- 希望创建一个共享的、可变状态时
如果我们希望实例本身是可变的,并且能够共享给多个所有者(一处修改、全部生效),那么则应该使用引用类型。
不可变性
我们花了很大篇幅来讨论值类型和引用类型,归根结底其实是对可变与不可变的讨论。合理利用不可变性带来的好处主要有以下三点。
安全性
使用值类型时,常常只是在使用实例的副本,这样做带来的价值就是对原值的保护,每当我们使用值类型数据时都能得到一个正确的、新的原始副本,这样我们就可以放心使用而不用担心原始数据被篡改,这是非常安全的。
松耦合
耦合度用来描述代码各个独立部分之间的依赖程度,依赖的产生通常就是由于“共享”了某些实例变量,从而导致不同模块的运行存在较强相关性,假如代码中存在大量这样的“共享”实例,则可能导致整个系统“错综复杂”,难以维护和扩展。而合理的利用不可变性,减少独立模块之间的依赖关系,将可变性控制在模块内部,则能够有效降低耦合。
优化设计
Swift 中提供了 var 和 let 来区分可变和不可变数据,上述讨论指导我们应该倾向于使用 let,以此来降低程序的复杂度,不用总是关注变量的值是什么。基于这个原则也有助于我们设计、编写引用透明的函数,即只要输入值相同,输出的值也一定相同。这对于代码的可读性、可维护性和扩展性有非常重要的贡献。
同时,编写引用透明的函数更有利于单独进行随机测试(例如前一章所讲的 QuickCheck)。