翻译:muhlenXi
校对:Yousanflics,numbbbbb,Cee
定稿:CMB
在 Swift 的世界中,有一个热议很久的主题,何时使用 class 和何时使用 struct ,今天,我想发表一下我自己的观点。
值类型 VS 引用类型
事实上,这个问题的答案很简单:当你需要值语义的时候用 struct,当你需要引用语义的时候就用 class。
好了,下周同一时间请再次访问我的博客……
等等
怎么了?
这没有回答上述中的问题
你什么意思?答案就在那儿。
是的,但是……
但是什么?
那什么是值语义,什么是引用语义呢?
昂,你提醒了我。我确实应该讲解一下。
还有它们和 struct、class 的关系
好吧。
这些问题的核心就是数据和数据的存储位置。我们用局部变量、参数、属性和全局变量来存储数据。存储数据有两种最基本的方式。
对于值语义,数据是直接保存在变量中。对于引用语义,数据保存在其他地方,变量存储的是该数据的引用地址。当我们访问数据时,这种差异不一定很明显。但是拷贝数据时就完全不一样了。对于值语义,你得到的是该数据的拷贝。对于引用语义,你得到的是该数据的引用地址拷贝。
这有些抽象,我们通过一个示例来了解一下。先暂时跳过 Swift 的示例,一起来看一个 Objective-C 的示例:
@interface SomeClass : NSObject
@property int number;
@end
@implementation SomeClass
@end
struct SomeStruct {
int number;
};
SomeClass *reference = [[SomeClass alloc] init];
reference.number = 42;
SomeClass *reference2 = reference;
reference.number = 43;
NSLog(@"The number in reference2 is %d", reference2.number);
struct SomeStruct value = {};
value.number = 42;
struct SomeStruct value2 = value;
value.number = 43;
NSLog(@"The number in value2 is %d", value2.number);
打印的结果如下:
The number in reference2 is 43
The number in value2 is 42
为什么打印结果会不一样?
代码 SomeClass *reference = [[SomeClass alloc] init] 在内存中创建了 SomeClass 类的一个新实例,然后将该实例的引用放到 reference 变量中。代码 reference2 = reference 将 reference 变量的值(实例的引用)赋值给新的 reference2 变量。然后 reference.number = 43 将两个变量指向的对象(同一个对象)的 number 属性修改为 43。 这就导致打印的 reference2 的值也是 43。
代码 struct SomeStruct value = {} 创建 SomeStruct 结构体的一个新实例并赋值给变量 value。代码 value2 = value 拷贝 value 的值到 变量 value2 中。每个变量包含各自的数据块。而代码 value.number = 43 仅仅修改 value 变量的值。所以,value2 变量的值仍然是 42。
用 Swift 实现这个例子:
class SomeClass {
var number: Int = 0
}
struct SomeStruct {
var number: Int = 0
}
var reference = SomeClass()
reference.number = 42
var reference2 = reference
reference.number = 43
print("The number in reference2 is \(reference2.number)")
var value = SomeStruct()
value.number = 42
var value2 = value
value.number = 43
print("The number in value2 is \(value2.number)")
和之前一样,打印如下:
The number in reference2 is 43
The number in value2 is 42
使用值类型的经验
值类型不是新出的类型。但是对于很多人来说,他们感觉上很新。这是怎么回事?
大部分 Objective-C 代码不会用到 struct。我们通常操作的是 CGRect 、 CGPoint ,很少自己定义结构体。一方面,结构体不实用,无法做函数式的引用赋值。在 Objective-C 中,正确保存对象的引用到 struct 中是很困难的,尤其是使用 ARC 的时候。
大部分语言没有类似 struct 结构体的东西。像 Python 和 JavaScript 这样“一切皆对象”的语言都只有引用类型。如果你是从这样的语言转到 Swift,值类型这个概念可能对你来说更加陌生。
不过等一下!有一个地方几乎所有的语言都会使用值类型:数值(number)!只要你写过一段时间代码,无论是什么语言,肯定能理解下面这段代码的行为:
var x = 42
var x2 = x
x++
print("x=\(x) x2=\(x2)")
// prints: x=43 x2=42
这对我们来说是非常明显和自然的,我们甚至没有意识到它的行为与众不同。但是它确确实实是值类型。从你编程的第一天开始就一直在使用值类型,即使你没有意识到这一点。
由于许多语言的核心是“一切皆对象”,number 其实是用引用类型来实现的。然而,它们是不可变引用类型,不可变引用类型和值类型的差异是很难察觉的。它们的行为和值类型一样,即使它们不是以这种方式实现。
这是理解值类型和引用类型的重要部分。就语言语义方面,区别是很重要的。当修改数据时,如果你的数据是不可变的,那么值类型/引用类型之间的区别就消失了,或者至少变成纯粹的性能问题而不是语义问题。
Objective-C 中也有类似的东西,就是标记指针(tagged pointers)。标记指针把对象直接存储在指针值中,因此它实际上是值类型,拷贝指针相当于拷贝对象。Objective-C 的库只会把不可变类型存储到标记指针中,所以使用的时候感受不到区别。有些 NSNumber 是引用类型,有些是值类型,但是使用上没有区别。
做出选择
既然我们已经知道值类型是如何工作的,那么你自己的数据类型该用什么呢?
这两者之间的根本区别在于,当你使用 = 时会发生什么。值类型会得到该对象的副本,引用类型仅仅得到该对象的引用。
因此,决定使用哪一个的基本问题是:是否需要拷贝?是否需要经常拷贝?
首先来看一些毫无争议的例子。Integer 显然是可拷贝的,它应该是值类型。网络套接字(Network sockets)明显是不可拷贝的,它应该是引用类型。再比如使用 (x, y) 实数对表示的坐标(Points)是可拷贝的,它应该是值类型。代表磁盘的控制器是明显不可拷贝的,它应该是引用类型。
有些类型理论上可以拷贝,但是这种拷贝可能不是你想要的。这种情况下,它们应该是引用类型。举个例子,屏幕上的按钮在代码层面可以拷贝,但是拷贝的按钮和原始按钮并不一样。点击拷贝的按钮并不会触发原始按钮,拷贝的按钮在屏幕上的位置也和原始按钮不一样。如果你需要把按钮当成参数传递,或者将它赋值给一个新变量,那你需要的是原始按钮的引用,只有明确声明的时候才进行拷贝。因此,按钮应该是引用类型。
视图和窗口控制器也类似。它们可以支持拷贝,但一般来说这不是你期望的行为,它们应该是引用类型。
接着谈谈模型(model)类型。假设你有一个 User 类型,用来表示系统中的用户,然后用 Crime 类型来表示 User 的操作。这两个类型看起来都可以拷贝,可以设置成值类型。但是,如果你的程序需要更新 User 的 Crime 并且能把改动同步到其他代码,那最好用一个用户控制器(User Controller)来管理 User,显然这个用户控制器应该是引用类型。
集合是个有趣的例子。集合包括数组、字典、字符串等类型。它们是可拷贝的吗?显然是。是否需要经常拷贝?这就不好说了
大部分语言的回答是“No”,它们的集合是引用类型。比如 Objective-C、Java、Python、JavaScript 以及一些我能想到的语言。(一个例外是 C++ 的 STL 集合,但是 C++ 是语言中的疯子,它做的每件事都很奇怪。)
Swift 是可拷贝的。这意味着 Array、Dictionary 和 String 是结构体而不是类。可以将他们的拷贝作为参数来使用。如果拷贝付出的代价很小,这么做就完全合理。Swift 为了实现这个功能花了很大功夫。。
嵌套类型
嵌套值类型和引用类型有四种方式。哪怕只用到了其中一种,你的生活都会变得更加有趣。
- 包含其他引用类型的引用类型,这没什么特别的。如果持有内部或外部值的引用,就可以修改这个值。改动会同步到所有持有者。
- 包含其他值类型的值类型,这样做的结果是一个更庞大的值类型。当内部值是外部值的一部分时,如果你将外部值存储到某个新地方,整个值类型都会被拷贝,包括内部值。如果你将内部值储存到新地方,那就只拷贝内部值。
- 包含值类型的引用类型,被引用的值会变大。外部值的引用可以操作整个对象,包括内部值。修改内部值时,外部值引用的持有者都会同步改动。如果你将内部值储存到新地方,它会被拷贝。
- 包含引用类型的值类型,这就有点复杂了。你可能会遇到意料之外的行为。这有利有弊,取决于你的使用方式。如果你将一个引用类型放到值类型中,然后拷贝这个值类型到一个新地方,拷贝中的内部对象的引用值是相同的,它们都指向相同的地方。下面是一个示例:
class Inner {
var value = 42
}
struct Outer {
var value = 42
var inner = Inner()
}
var outer = Outer()
var outer2 = outer
outer.value = 43
outer.inner.value = 43
print("outer2.value=\(outer2.value) outer2.inner.value=\(outer2.inner.value)")
打印如下:
outer2.value=42 outer2.inner.value=43
outer2 是 outer 的拷贝,它仅仅拷贝了 inner 的引用,因此两个结构体的 inner 共享一个存储空间。因此更新 outer.inner.value 的值会影响 outer2.inner.value 的值。神奇!
如果使用得当,上面的这种行为使编程变得很方便,它允许你创建一个支持写时复制的 struct,允许你不需要拷贝大量的数据就可以实现值语义。这就是 Swift 的集合工作机制,你也可以创建自己的集合。如果想了解更多,可以阅读 一起来构建 Swift Array。
这种行为也相当危险。举个例子,你有一个可拷贝的 Person 类,所以它可以是 struct 类型,为了怀旧,你决定用 NSString 类型来保存姓名:
struct Person {
var name: NSString
}
然后生成一对夫妇的实例,分别给每个实例的姓名赋值:
let name = NSMutableString()
name.appendString("Bob")
name.appendString(" ")
name.appendString("Josephsonson")
let bob = Person(name: name)
name.appendString(", Jr.")
let bobjr = Person(name: name)
打印他们的姓名:
print(bob.name)
print(bobjr.name)
结果如下:
Bob Josephsonson, Jr.
Bob Josephsonson, Jr.
喔!
发生了什么?与 Swift 中的 String 类型不同,NSString 是一个引用类型,是不可变的,但是它有一个可变的子类 NSMutableString。构建 bob 时,生成了一个被 name 中字符串所持有的引用。随后改变 这个字符串时,改动被同步到了 bob 中。虽然 bob 是用 let 声明值类型,但是此处的赋值操作显然改变了 bob。事实上,这没有覆写 bob,只不过是改变了 bob 持有的引用的数据。因为 name 是 bob 的一部分数据,从语义上看,就好像覆写了 bob。
这种行为在 Objective-C 中一直存在。每个有经验的 Objective-C 开发者都能避免这种行为。因为一个 NSString 实际上可能是一个 NSMutableString。为了防止这种行为,可以声明一个 copy 的属性或者在初始化的时候显式的调用 copy 方法。在许多 Cocoa 的集合中可以发现这种做法。
Swift 的解决方法很简单:用值类型而不是引用类型。在这种情况下,声明 name 为 String 类型即可。这样就不用担心无意中出现存储共享的问题。
有些情况下,解决方法可能没有这么简单。举个例子,你可能会创建一个 包含引用类型变量 view 的 struct,并且它不能改变为值类型。这也许表示你的类型不应该是 struct,因为你无论如何也不能实现值语义。
结论
移动值语义类型的数据时,新数据是原数据的拷贝。然而,引用语义类型的数据得到的是原数据的引用拷贝。这意味着你可以在任何地方通过引用覆写原数据。而值语义只能通过改变原数据来改变原数据的值。选择类型时,要考虑该类型是否适合拷贝和倾向于拷贝的固有类型。最后,注意值类型中嵌套的引用类型,如果你不留心将会发生一些糟糕的事情。
今天的内容到此结束,这次是真的结束了,下次再见。你们的建议对 Friday Q&A 是最好的鼓励,所以如果你关于这个主题有什么好的想法,请发邮件到这里。
你喜欢这篇文章么?我的书里还有更多有意思的内容!第二卷 和 第三卷正在出售中!包括 ePub,PDF,纸质版,iBooks 和 Kindle,点击查看更多信息。