Swift 值语义(一)
约 1896 字大约 6 分钟
swiftios
2024-08-02
在 Swift 中,一个被不断讨论话题:什么时候使用结构体什么时候使用类?
答案其实很简单,当你需要值语义的时候使用结构体,当你需要引用语义的时候使用类。
那么掌握值语义和引用语义就非常必要了。
值语义和引用语义
Swift 中的值语义官方文档:Value Semantics in Swift
什么是值语义
For a type with value semantics, variable initialization, assignment, and argument-passing (hereafter, "the big three operations") each create an independently modifiable copy of the source value that is interchangeable with the source.
值语义(Value Semantics):所谓值语义是指目标对象由源对象拷贝生成,且生成后与源对象完全无关,彼此独立存在,改变互不影响。就像 Int 类型变量相互拷贝一样,拷贝之后就与源对象完全脱离关系。
什么是引用语义
引用语义(Reference Semantics):通常是指一个目标对象由源对象拷贝生成,但生成后与源对象之间依然共享底层资源,对任何一个的改变都将随之改变另一个。
有值语义的例子
一个简单的struct, 只有一个存储属性:
struct Foo {
var value: Int = 0
}假设 Foo 有以下调用:
var a = Foo()
var b = a
a.value = 2在这种情况下,b.value是否也会改变?
值语义:改变其中一个不会影响另一个
因为Foo是一个值类型,所以改变a.value不会改变b.value。

在这种情况下,a和b是独立于变化的(对a或b的变化不会影响另一个),所以我们可以说Foo有值语义。
没有值语意的例子
接下来,让我们看看一个没有值语义的例子。 让我们把前面代码中的struct部分改为class。 除此之外,其它是完全相同。
class Foo {
var value: Int = 0
}
var a = Foo()
var b = a
a.value = 2在这种情况下,Foo是一个类,因此是一个引用类型。因此,改变a.value也将改变b.value。

在这个例子中,Foo不具有值语义,因为它的变化不具有独立性。
语义与类型
从前面的例子和名称可以推断出,值语义与值类型密切相关,引用语义与引用类型密切相关。 但是它们并不是一回事,小心将它们混淆了。
- 值语义 ≠ 值类型。
- 引用语义 ≠ 引用类型。
例如,有一些类型是值类型但没有值语义,还有一些类型是引用类型但具有值语义。正确区分值语义/引用语义和值类型/引用类型非常重要。
是值类型但没有值语义的例子
为了区分语义和类型,让 我们看一个值类型但没有值语义的示例。
我们引入了一个叫做Bar的类:
class Bar {
var value: Int = 0
}给Foo添加这个Bar类型的属性。
struct Foo {
var value: Int = 0
var bar: Bar = Bar() // 👈 添加一个Bar类型的属性
}我们可以用它来做与前面的例子类似的事情。只是除了改变a.value,也改变a.bar.value。
var a = Foo()
var b = a
a.value = 2
a.bar.value = 3 // 👈在这种情况下,Foo是一个值类型,所以变量a和b包含Foo的独立实例。然而,Bar是一个引用类型,所以a和b的bar属性将包含同一个Bar实例的地址,并将通过该地址引用同一个实例。
在这种情况下,改变a.value不会影响b.value,因为a和b包含不同的Foo实例。然而,a.bar和b.bar指的是同一个Bar实例,所以对a.bar.value的任何改变也会改变b.bar.value。

因此,虽然Foo是一个值类型,但变化不具备独立性,即它没有值语义。注意,a.value和b.value可以独立改变,所以这个Foo也没有引用语义。
像这样既没有值语义也没有引用语义的Foo类型是很难处理。这块在下篇文章将进行详细阐述,不在此进行累赘。
是引用类型但有值语义的例子
具有引用类型属性并不一定意味着丢失了值语义。接下来,让我们看一个具有引用类型属性的值类型具有值语义的示例。
将前面代码中的Bar类改为不可变类。
final class Bar {
let value: Int = 0
}为了使Bar不可变,将Bar的value属性改为let,并将其改为final class。之所以声明为 final class,是因为如果创建一个Bar的可变子类,Bar类的不可变性将被破坏。
让我们对Foo和Bar做同样的事情。然后,由于Bar现在是一个不可变的类,任何试图改变a.bar.value的行为当然会导致编译错误。
var a = Foo()
var b = a
a.value = 2
a.bar.value = 3 // ⛔在这种情况下,a.bar和b.bar确实指的是同一个实例,但是Bar是不可变的,所以它的状态不能通过该实例改变。因此,拥有一个Bar类型的属性并不会破坏Foo实例的独立性。其结果是,Foo具有值语义,尽管它有引用类型的属性。
这种情况很常见,不会导致值语义的破坏,例如,如果该属性持有以下类别的实例。
NSNumber,NSNullUIImageKeyPath
不变性和语义
如何看待不可变类本身的语义?
考虑以下不可变的类 Foo:
final class Foo {
let value: Int = 0
}对于这个Foo,执行同样的事情:
var a = Foo()
var b = a
a.value = 2 // ⛔不可变类的实例初始化后不能被改变,不会发生改变一个就改变另一个的情况,这种情况也有值语义。
当具有不变性时,你无法区分值语义和引用语义。因此,我们可以说,像上面这样的不可变的Foo类同时具有值语义和引用语义。另外,如果它是不可变,即使它是值类型,也可以说它具有引用语义。例如,下面的Foo是一个值类型,但同时具有值语义和引用语义。
struct Foo {
let value: Int = 0
}用可变引用类型作为属性但有值语义的例子
在前面的例子中,如果你有一个可变引用类型的属性,你就没有值语义。
struct Foo {
var value: Int = 0
var bar: Bar = Bar()
}
class Bar {
var value: Int = 0
}“如果你有一个可变引用类型的属性,你就没有值语义”,用这种模式来判断语义是很危险的。 例如,标准库Array持有内部的可变引用类型,但使用一种叫做Copy-on-Write的机制来实现值语义。
要根据定义而不是模式来决定是否有值语义。
总结
在 Swift 中,当一个类型具有值语义时,该类型的值与变化无关。
值类型和值语义、引用类型和引用语义不是一回事,值类型不一定有值语义,而引用类型也可以有值语义。区分类型和语义很重要。在考虑一个模式是否具有值语义时,必须考虑到各种例外情况。根据定义决定是否具有值语义。

