谁能忘记2015年是“值类型(Value Type)之年”?Swift 社区通过许许多多的博客和视频来探讨怎么让值类型(struct 和 enum)实现更新,更简单,更安全的应用框架。Alexis Gallagher 认为这有个重要信息:社区其实是更多的在讨论值语义(value semantics),而不是值类型。值语义很难给它下定义,但一经理解其含义收获将是巨大的,如禅意(satori-like)般的启蒙,同时也能帮助你如何更好地使用 Swift。
这次演讲是 Swift 语言用户社群(Swift Language User Group,SLUG)对值语义的解释,以及为使用值语义提供更方便简单的方法,在 Swift 使用值类型,引用类型或两者混合都可以派上用场。
概述 (00:00)
我是 Alexis Gallagher,接下来我要谈谈关于值语义的话题。在2014和2015年,至少有一半以上的时间在谈论值类型,而苹果论坛对其讨论就占了三成。在所有的讨论中,尤其是那些有更超前分析的内容里,我注意到他们有提到一个尚未明确定义的词汇- “值语义”。
我还注意到“值语义”经常出现在复杂话题讨论里面。所以我不禁想,这些东西可以是相互联系的,如果我理解了值语义,也就明白所有的讨论为什么都会在这个点上变得复杂。
我认为用一种简明的方式去理解值语义可以帮助你更好的理解值类型,同时让你的架构更清晰,更能明白每一个值类型的联系。值类型很重要,是吧?因为他们在任何地方都以更好的方式在使用它们。所以这不是 Swift 的一个小问题,而是理解整个基础体系的一部分。它是让我们理解构建不同 App 的核心类型的一部分。
审阅 (4:07)
我认为了解值类型和值语义的最佳方式是游戏,而不是从抽象的定义开始。因此我开发了一个游戏,叫 Mutation Game。
Mutation Game 是一个两人游戏。我们有 Victor Valucist,他是一个好人,而 Salazar SideEffector 是一个坏人。这个游戏是 Victor Valucist 先要选择(实例)一个 Foo
类型,然后设置一个值为 Foo
类型的变量,然后 Salazar 去尝试改变这个变量的值。
var v:Foo = Foo() // Valucist chooses Foo to
var s = v // defend the variable v
let v0 = valueOf(v)
/* { SideEffector attacks v using only s } */
let v1 = valueOf(v)
assert(v0 == v1) // v unchanged? Valucist wins!
Salazar 不会直接碰到变量,他所使用的是另一个变量 s
。Salazar 还定义了一个审查函数,用于检查 v
的值,这个函数是纯净单一的,而不是拉取一个随机的变量。
下面是游戏的场景,可以看出这很像我们看到过的一些关于值类型的讨论:Valucist 定义了类型为 Foo
的变量 v
,然后在下面我们看到一个重要的语句: var s = v
。因此 s
的值现在是由 v
来决定。
我们用 valueOf
函数获取 v
的初始值,并保存到 v0
中。Salazar 可以在被注释的地方上做任何事情,但不能碰 v
变量,只能使用 s
变量。
然后我们再取得 v
值,对比下它是否改变了?如果没有改变,那么是 Victor 赢,如果改变了,则 Salazar 赢。
Mutation Game 与 Int (6:24)
我们先把这个游戏玩几遍。如果 Victor 决定选择 Int
类型,会发生什么事?
var v:Int = 100
var s = v
let v0 = valueOf(v)
s = 200 // attempted attack!
let v1 = valueOf(v)
assert(v0 == v1) // but true, Valuecist wins!
如果 Victor 选择了 Int
,那么他会获得胜利。 s
不能改变 v
的值。为什么?因为 Int
是一个值类型。
在 Swift 上下文中,当我们说某样东西是一个值类型或引用类型时,其实是一种分配的行为,例如在分配给这个变量时会发生什么。
值类型使用的是复制赋值(assign-by-copy)。所以我们从一开始声明了变量 v
,它指向一个特定的实例,而这个实例具有一个值。然后我们把 v
赋值给一个新变量 s
,而复制赋值的意思是我们创建了一个实例的副本再赋值给 s
。
因此无论 Salazar 怎么动 s
,都只会影响 s
指向的那个实例,而不会改变 v
的值。你可以看到 v
的值其实是变量 v
所指向的实例的值。这是关于复制赋值方面的说明,这也是为什么 Valucist 选择了类似 Int
这样的值类型总是可以胜利。
Mutation Game 与 NSMutableString (8:10)
但 Victor 放松了警惕,他选择了 NSMutableString
会怎么样?他会输,因为这是个引用类型。引用类型使用的是引用赋值(assign-by-reference)。
var v:NSMutableString = NSMutableString()
var s = v
let v0 = valueOf(v)
s.append("Hello, world") // attempted attack!
let v1 = valueOf(v)
assert(v0 == v1) // false! SideEffector wins
可以看到这有张同类的图,变量 v
指向一个实例,该实例是一个初始值为空
的可变字符串。当你编写 var s = v
时,你会说:“我想要一个新变量,想让这个变量指向同一个实例。”
所以现在 v
和 s
是共享一个实例。接下来可能发生的是,如果有人修改了实际的实例,例如:把该可变字符串改成“hello”,那么值就是“hello”。因此现在 v
的值是 hello,s
的值也是。
所以 Salazar 只是利用了 s
就可以破坏 v
,而这种破坏对我们意味着什么?
值类型的好处 (9:35)
那么值类型有什么好处?值类型可以防止意外的改变。
如果我有一个函数,它需要一个 Int
,并返回一个 Int
,这会是一个表现良好的函数,因为我不需要担心这个函数下执行的代码会影响到我传入的 Int
。因为在函数体中使用变量的规则,和在代码范围内分配变量的分配规则,是同一类型的。这也有助于线程安全。
结构与类 (10:38)
我们是不是考虑安全的时候就使用结构(struct),当安全对我们不那么重要的时候就可以使用类(class)了?
不完全是,因为你不能一直从值类型那里得到好处。接下来看看使用 UIImage
会发生什么。
var v:UIImage = UIImage(named:"smile.jpg")
var s = v
let v0 = valueOf(v)
// Hammer on s all you please. It's useless!
let v1 = valueOf(v)
assert(v0 == v1)
这里我定义了一个 UIImage
的笑脸。我们可能要担心 Salazar 会利用 s
去修改这个图片,例如改变它的颜色等等。但他不能这样做,因为 UIImage
已经细心的帮你定义了不可变的属性。待会你可以回头去看看 UIImage
的属性,它们都是只读的。
但不是所有的值类型都是安全的。我再创建一个 Array
,里面的元素是 NSMutableString
。Array
是值类型,NSMutableString
是引用类型。
var v:Array<NSMutableString> = [NSMutableString()]
var s = v
let v0 = valueOf(v)
s[0].append("Hello, world")
let v1 = valueOf(v)
assert(v0 == v1) // false, SideEffector wins
苹果在谈到这个问题的时候表示这是意外的共享(unintended sharing),但并不会总是这样。
有时你可能出于效率的考虑,会一起维护一份共享的存储内容,但你又不希望别人对这块内容的修改会影响你的使用。所以这时候就能用到一些特别的技巧,在这个共享的内容里面做一些调整,无论别人怎么修改都不会影响你的正常调用。这就是关于写入时复制(copy-on-write)的优化。
那么要什么类型才能赢得游戏?
当然是应该具有值语义的类型。
值语义的定义 (14:57)
有一种类型可以赢得这个游戏,那就是含有值语义的类型。像 Int
类型,它就具有值语义,你可以用它来做很多复杂的事。所以我在这的做法是:用 Mutation Game 来测试定义值语义。
有一个概念的描述,对大部分的情况都通用:值语义可以保证变量值的独立性。
独立并不是指结构性的东西,我们所讨论的是它会不会影响另一件东西。一个具有值语义类型的变量,如果想要修改它只有一种方法,那就是通过变量本身进行修改。反过来如果只能通过变量本身修改值,那么这是一个具有值语义的变量。
苹果对值语义的定义 (17:57)
苹果在定义变量的逻辑上有不一样的地方。我并不喜欢他们的定义,因为变量在逻辑上总是不同的。我认为他们想要得到不一样的结构。但事实上他们并不是说变量在结构上是不同的,而是指如何让它们的存储结构上不一样。
但这也不重要,因为你不需要在这些不同结构上取得不可变的东西。重要的是,你拿到的东西应该不受其他操作的影响。
当然他们当中讨论的一些表述是正确的,但我会用一种更友好的方式来表达:修改一个变量永远不会影响其他值语义类型的变量。如果它完全不受其他变量的引用和修改的影响,那么它就是值语义类型(这不是指它不会影响其他变量,而是其他变量不会影响它。)
值语义是关于接口,而值类型和引用类型是关于实现的。
我给你一个值语义类型的变量,你尽管使用就行了,不需要考虑如何安全的使用,也不需要把重心放在想着值类型或引用类型在内存里面到底发生了什么转换。
值语义整体的好处是可以让你忘记实例的存在。因为值类型实际上是没有实例和引用的概念的,它只有变量和值的概念。一个类型或一个变量具有值语义的话,修改它们的唯一途径就是通过它们的变量。引用实例这个概念已经过时了,你心目中首选的应该是值语义,它们才是现在最完美的东西。
结论 (24:24)
我大胆的概括了我对值语义的定义,但我认为我所描述的和它本身的意义是一致的。下面是对这次定义的一些结论:
-
不可变的引用类型具有值语义,我认为这是值语义合理的定义。没错,它们是引用类型,在某种行为和情况下它们是具有值语义的,这个是不能被打乱的。
-
类型的访问权限也是和值语义有关的。因为修改一个具有值语义变量的值,只能通过其自身变量才能修改。在某些类型变量上加了私有访问(file-private)修饰符,我的代码如果和定义这个私有变量在同一个文件,那么我是可以访问它的,但如果不是私有变量,那么我就不能从文件外部访问它。所以你可以想想看,这里的意思是,从一个访问权限里,类型可能有值语义,但另一种情况就不是了。
诀窍 (26:38)
假如你正在使用原始的值类型,例如 Int
,那么没问题,你已经做到了,因为这些类型都是具有值语义的。
假如你在使用引用类型 - 像使用类定义出来的一些东西,它使用的是引用赋值,这种情况你应该让它不可变。可以使用 let 或者 constant,这些属性本身就具有值语义。
又假如你在使用复合类型,例如结构(struct),这里可以让所有的存储属性都使用值语义类型。所以如果我有一个结构,我直接把所有属性变成值语义类型就完成了。
当然如果要做一些复制的深度共享的存储值,这就需要你用写入时复制(copy-on-write)的方式来处理这个问题。为其他人会调用的类型设定一个具有值语义的访问级别,可能是 public 或者一个 module。将任何可变的引用类型属性,限定为较低的访问级别,然后定义一个可访问的设置器(setter),再定义一个 mutating 函数用于复制可变实例,而不是直接改变这个共享实例。
如果你想知道你做的是否正确,你可以用 Mutation Game 来测试。
Q&A
这与 Erlang 和 Erlang 虚拟机有关系吗? (31:41)
我认为这里肯定有联系,而且值语义的概念比不可变变量更广泛。不可变是保证你可以拿到的值不会变,但通常这里还是会有一些异常发生。我很熟悉 Clojure,我会拿它来构建一些系统,在 Clojure 中你给出的原语(primitives)往往都是不可变的。我觉得写入时复制(copy-on-write)是保持值语义的最佳优化方案,在这个函数里面你可以维持最简单的存储,也可以实现最简单的值复制。还可以使用像 Clojure 函数式语言来实现。
是否会有一个框架或者结构来帮助我们更好的使用值语义? (34:29)
没有,老实说这有点令人费解。因为我真的觉得值语义比值类型更重要,你更应该注意如何安全地使用类型。但显然人们对它如何运行已经有很好的了解,因为苹果和其他社区的人都非常细心地构建经过写入时复制(copy-on-write)优化后的类型,同时还保持了值语义。虽然这是人们在致力的维护的事情,但值语义这个术语很少被一致的使用。
About the content
This content has been published here with the express permission of the author.