本篇文章翻译自:Reference vs Value Types in Swift: Part 1/2
原作: Eric Cerney on November 9, 2015
如果你已经看了WWDC 2015的sessions, swift正在着重反思代码架构。从OC转变到Swift,开发者注意到最大的改变是对于值类型的倚重要甚于值类型。
这个分上下两部分的系列文章会解释说明两者之间的不同,并向你展示什么时候使用什么类型比较合适。在第一部分,你会学到2种类型的主要概念;在第二部分,你会解决一个现实问题,期间你会学到更多的高级概念并发现两者微妙、但很重要的点。
不管你是否有OC背景,或者精通Swift,你的确需要了解Swift类型的前世今生。
这篇tutorial工作在Swift 1.2 和 Swift 2.0
开始
首先,在Xcode中创建一个新的playground,选择File\New\Playground...然后给playgound命名ValueSemanticsPart1。这篇tutorial与平台无关,我们只关注Swift语言层面,所以你可以选择任何平台。点击Next,选择合适保存位置,打开它。
引用类型 VS 值类型
那么两个类型的核心区别在哪里呢?快速粗略的解释是值类型保留一份独一无二的数据备份,而引用类型共有数据。
Swift中引用类型的代表是class,这跟OC很相似,在OC中所有的对象都继承于NSObject,并以引用类型来存储。
在Swift中有很多值类型,例如: 结构体, 枚举,和元组。你可能没有注意到OC也使用值类型像NSInteger,或者C结构体CGPoint。
为了更好的理解两者的不同,我们最好从认识OC的引用类型开始。
引用类型
在OC中,以及大部分其他的面向对象语言中,你可以持有对象的引用。在Swift中,你可以使用class,它实现了引用语义。
class Dog {
var wasFed = false
}
上面的类代表一个宠物狗,和有没有喂过食物。创建一个Dog的实例:
let dog = Dog()
仅仅指向存储dog实例的内存地址。为了添加另一个持有相同dog实例的对象,添加如下代码:
let puppy = dog
因为dog是内存地址的一个引用,puppy指向相同的地址。设置宠物的wasFed属性为true。
puppy.wasFed = true
puppy和dog都指向相同的内存地址。
但是实际情况,你并不想一个对象的改变反应到另一个对象上。我们来检查一下属性:
dog.wasFed //true
puppy.wasFed //true
因为引用相同的对象,所以改变一个实例会影响到另一个。在OC中这种场景很常见。
值类型
值类型跟引用类型完全不同。你可以用Swift的基础类型来探索这一规律。添加整形变量和相应的操作到playgound中:
var a = 42
var b = a
++b
a // 42
b //43
你会想到a和b是相等的吗? 当然事实很清楚,a等于42,b等于43。如果你把他们声明成引用类型,那么a和b都会等于43,因为引用类型指向相同的内存地址。
上述规律同样适用于其他值类型。在playground中添加如下代码,实现 Cat struct:
struct Cat {
var wasFed = false
}
var cat = Cat()
var kitty = cat
kitty.wasFed = true
cat.wasFed //false
kitty.wasFed //true
这展示引用类型和值类型的一个微妙但很重要的不同点:设置kitty的wasFed属性不会影响cat实例。kitty变量接受一份cat的值拷贝,并不是引用。
看起来,你的cat今晚儿要挨饿了。(😄,wasFed还是false)。
尽管给变量赋值引用会快很多,copy大多数情况下也一样经济实惠。copy操作时间复杂度是O(n),它们基于数据的大小使用固定数量的引用计数操作。
值类型的这种性能损耗好像成为了总是使用引用类型的一个理由,但是在系列文章的第二部分,会向你展示一些Swift优化这些copy操作的聪明方法。
可变性
var 和 let 在引用类型和值类型中的作用是不同的。注意到你把dog和puppy用let定义。但是你还能改变wasFed属性,这怎么可能?
对于引用类型来说,let意味着引用必须是不可变的。换句话说,你不能改变实例的不可变引用,但是你可以改变实例本身。
对于值类型,let意味着实例必须保持不可变。实例的任何属性都不能改变,不管属性是用let或var声明。
使用值类型能够很容易控制不可变性。如果要实现相同的引用类型的可变性/不可变性,你需要实现可变性/不可变性类,例如:NSString和NSMutableString。
Swift更喜欢哪种类型呢?
这可能让你感到很惊讶:Swift标准库几乎全部使用值类型。大致搜索了一下Swift 1.2和Swift 2.0,enum, struct, 和类的使用情况:
Swift 1.2:
- struct : 81
- enum : 8
- class : 3
Swift 2.0:
- struct : 87
- enum : 8
- class : 4
这些类型包括String, Array,和Dictionary,他们都是用Struct实现的。
什么时候该使用哪一种类型呢?
现在你知道了两种类型的不同,但是我们该使用哪一个呢?
有一种情况你是没得选的:很多Cocoa APIs需要NSObject子类,那么你只能使用class。但除此之外,你可以借鉴苹果的Swift blog来决定是使用struct/enmu值类型还是使用class引用类型。
什么时候使用值类型
大体说来:在以下情况可以考虑使用值类型:
使用==比较实例有意义
你会说:"当然,我想要每一个对象都能够比较"。但是你要考虑数据是否应该被比较。考虑下面Point的实现:
struct Point {
var x: Float
var y: Float
}
两个有相同的x和y成员的变量应该被认为是相等的吗?
let point1 = Point(x: 2, y: 3)
let point2 = Point(x: 2, y: 3)
这很清楚拥有相同的内部值的两个Point实例应该被认为是相等的。这两个实例的内存地址是无所谓的,你只关心值本身,不是吗?
因此,你需要遵守Equatable协议,对于所有的值类型这是一个很好的实践。这个协议只定义了一个函数,为了能够比较两个实例你必须在全局实现该方法。这就意味着==操作符必须有以下特征:
- 反射性: x == x 结果为true
- 对称性: 如果 x == y 那么 y == x
- 传递性: 如果 x == y 并且 y == z 那么 x == z
这里有一个Point的==操作符的实现
extension Point: Equatable {
}
func ==(lhs: Point, rhs: Point) -> Bool {
return lhs.x == rhs.x && lhs.y == rhs.y
}
拷贝应该有独立的状态
我们进一步探索Point示例,考虑下面拥有相等center的2个Shape实例。
struct Shape {
var center: Point
}
let initialPoint = Point(x: 0, y: 0)
let circle = Shape(center: initialPoint)
var square = Shape(center: initialPoint)
如果你改变一个Shape实例中的一个的center将会发生什么?
square.center.x = 5 //{x: 5, y: 0 }
circle.center //{x: 0, y: 0}
每一个Shape实例都需要一份point的拷贝,所以你可以保持他们每个独立状态。你可以想象所有的Shape实例共享同一份center的混乱状态。
数据在多线程中使用
这个可能会有点复杂。多线程能访问这条数据吗?如果可以,那么多线程中值不相等会有影响吗?
想要让你的数据适用于多线程并在多线程中相等,你需要使用引用类型并且实现锁 --- 艰巨的任务🐶!
如果线程可以独有数据,那么使用值类型也变得没啥意义,因为数据的每一个拥有者会直接引用数据,而不是持有引用。(这句有点晦涩。笔者认为,引用类型可以有多个owner, 而值类型有且只会有一个owner,所以在多线程中,修改值类型,它会马上生效,不会受到其他线程的干扰,不会有像引用类型的数据同步的问题。)
什么时候使用引用类型
尽管在很多情况下使用值类型都大有裨益,但是引用类型在下面的情形中作用还是很明显的:
用 === 操作符比较实例有意义
=== 检查两个对象是否完全相等,包括存储数据的内存地址。
举个现实的例子,考虑下面的情景:你的同事拿20$跟你换20$, 那么你肯定没所谓呀,因为你关心的是数值。
然而,如果有一个人偷了《大宪章》,并且用羊皮纸做一份相同的文件,放回去。那么你还认为没有关系吗,因为文件的固有身份已经是不同了。
你可以用相同的思路来思考决定是否使用引用类型;通常情况下,你很少关注固有的身份---也就是,数据的内存地址。你更多关注的是值。
你想要一个共享,可变的状态
有时,你想要一条数据以单例的形式存储,可以被多个使用者访问和修改。
一个共享,可变状态的普遍示例是银行账户。你可以像下面一样实现基本的账户和人。
class Account {
var balance = 0.0
}
class Person {
let account: Account
init(_ account: Account) {
self.account = account
}
}
如果联合账户的持有者往账户注资,那么绑定该账户的所有信用卡的总资产会有所反应。
let account = Account()
let person1 = Person(account)
let person2 = Person(account)
person2.account.balance += 100.0
person1.account.balance //100.0
person2.account.balance //100.0
因为账户是一个类,每一个人持有账户的引用,所有的事情保持同步。
还在犹豫不决?
如果你不确定哪种机制适用你的情景,那么默认就值类型。之后,你可以毫不费力得转换到class。
Swift几乎全部使用值类型,这真的难以置信,在OC那儿情况完全相反。
作为新的Swift规范下的代码架构者,你需要前期做一些计划---数据怎么使用。使用值类型或者引用类型,你几乎可以解决所有的问题,但是使用不当会产生一大堆bug和让人困惑的代码。
在所有的情形下,当新要求来了,尝试去改变你的架构是最事。挑战你自己去遵守Swift的规范;结果你会产出比之前更加优雅的代码。
延伸阅读
在这里你可以下载完整的playground代码。
现在你已经了解值类型和引用类型的区别,和何时使用他们。在系列文章第二部分, 你会面对一个现实问题和学习值类型的高级技术。