Swift 中的值类型和引用类型

Swift 中的类型分为两类:值类型_和_引用类型。它们的行为方式不同,理解这种差异是理解 Swift 的重要组成部分。

如果你是编程新手或者从其他语言转到 Swift,这些概念对你来说可能是新的。

在查看代码之前,这里有两个相同场景的变体,说明了值类型和引用类型在行为上的基本差异。

想象你正在处理一个文档,可能是一份报告或电子表格,你想让朋友看一下。有两种常见的方式可以与朋友分享这个文档:

  1. 你可以通过电子邮件给你的朋友发送文档的副本。

  2. 如果文档在 Google Docs 或 iCloud Pages 中,你可以通过电子邮件给你的朋友发送文档的链接。

在这两种情况下,你的朋友都能够阅读和修改你的文档,但有显著的差异。

当你发送副本给你的朋友时,你的朋友拥有一个完全独立的文档副本。他们可以随意编辑文档,但这不会影响你的副本。

当你发送链接给你的朋友时,你并不是发送实际的文档。你是发送一个指向云端文档的 URL。由于你们都有指向同一个文档的链接,你们任何一方做出的更改都会被双方看到。

分享文档副本或分享共享文档链接的行为差异,非常类似于值类型和引用类型的行为差异。

值类型

在 Swift 中,结构体、枚举和元组都是值类型。它们的行为类似于给你的朋友发送文档副本。

将值赋给常量或变量,或将值传递给函数或方法时,总是会创建该值的副本。

在下面的代码中,声明了一个类型为 Documentstruct,带有一个 text 属性。

创建了一个 Document 实例并赋值给 myDoc。

myDoc 被赋值给变量 friendDoc 时,原始实例被复制到一个新实例。

由于它是一个独立的实例,更改 friendDoctext 不会影响 myDoctext

struct Document {
  var text: String
}

var myDoc = Document(text: "Great new article")
var friendDoc = myDoc

friendDoc.text = "Blah blah blah"

print(friendDoc.text) // 打印 "Blah blah blah"
print(myDoc.text) // 打印 "Great new article"

当你发送文档副本给你的朋友时,你完全控制你的副本何时更改。你永远不用担心你的朋友会对你的文档副本做出意外更改。

类似地,使用值类型时,你永远不用担心程序的其他部分会更改该值。

引用类型

在 Swift 中,类、actor 和闭包都是引用类型。它们的行为类似于发送朋友共享文档的链接。

将引用类型赋给常量或变量,或将其传递给函数或方法时,总是赋值或传递对共享实例的引用。

下面的代码与上面的示例相同,只有一个小但重要的变化。现在不是声明 struct,而是将 Document 类型声明为 class

这是一个小的代码更改,但行为有显著变化。

像之前一样创建了一个 Document 实例并赋值给 myDoc

但现在,当 myDoc 被赋值给变量 friendDoc 时,赋值的是对该实例的引用。

由于它是对同一实例的引用,更改 friendDoctext 会更新该共享实例,包括 myDoc 的值。

class Document {
  var text: String
}

var myDoc = Document(text: "Great new article")
var friendDoc = myDoc

friendDoc.text = "Blah blah blah"

print(friendDoc.text) // 打印 "Blah blah blah"
print(myDoc.text) // 打印 "Blah blah blah"

当你发送共享文档的链接给你的朋友时,你的朋友可以在你不知情的情况下更改文档。你可能依赖于你的文档保持不变。

类似地,使用引用类型时,程序中拥有引用的任何部分都可以进行更改。有时意外的更改会导致错误。

局部推理

在上面的小代码示例中,你可以逐行阅读代码,看到同一个引用如何被赋给两个不同的变量,以及使用一个变量更改属性如何更新两个变量引用的实例。

能够在单个位置查看代码并弄清楚发生了什么,这称为_局部推理_。

现在想象一个更大的程序,其中程序的不同部分都有对同一事物的引用。你的代码可能在某个地方设置了一个值并依赖于该值,但在其他地方,程序的不相关部分可能会在你不知情的情况下更改该值。

从多个地方可以更改的数据的正式名称是_共享可变状态_。共享是因为它可以从代码的许多地方访问,可变因为它可以更改(即变异),状态作为数据的同义词,如”事物的当前状态”。

在这种情况下,你无法完全理解代码的一部分而不了解可能在代码的许多不同位置发生的情况。你失去了局部推理的能力,这使得你的代码更难理解和调试。

使用值类型的一个优点是你可以确定程序的其他地方不能影响该值。你可以推理眼前的代码,而不需要知道在其他地方发生了什么。

这使得你的代码更容易理解,并防止因意外或意外的共享可变状态更改而导致的错误。

选择值类型或引用类型

回到分享文档的例子,你和你的朋友能够看到和编辑同一个文档可能非常有用。

类似地,在程序中,有时引用类型提供的共享可变状态可能非常有用。引用类型本身并不坏,但如上所述,它们确实增加了额外的复杂性和出错的可能性。

一般来说,优先使用结构体而不是类。如果你不需要引用类型的行为,就没有必要承担额外的复杂性和陷阱。

文章在结构体和类之间选择更详细地描述了权衡。

组合值类型

代码中的一个常见设计模式是_组合_,即将较小的元素组合在一起创建较大的元素。

在 Swift 中,你可以轻松地将值类型组合在一起创建更复杂的值类型。

所以你可以定义一个包含一些基本类型的结构体,如 String、Int、Bool,可能还有一个枚举值。由于结构体中的所有内容都是值类型,该结构体的行为就像一个值类型。

你可能有一个更复杂的结构体,它包含第一个结构体的实例和一些其他值。同样,由于它是由值类型组成的,这个结构体是一个值类型。

集合是值类型

但在 Swift 中,值类型的组合不止于结构体和枚举。

虽然在许多语言中,集合如数组和字典是引用类型,但在 Swift 中,标准集合 ArrayDictionaryString 都是值类型。

这意味着结构体可以包含结构体数组,可能还有键值对字典,枚举的集合。只要所有内容都由值类型组成,即使是复杂类型的实例也被视为一个值。

结论

理解什么是值类型和引用类型以及它们行为方式的差异,是学习 Swift 和能够推理你的代码的重要组成部分。在值类型和引用类型之间的选择通常归结为声明类型为 struct 还是 class 的选择。你可以在《Swift 编程语言》的结构体和类章节中了解更多关于结构体和类的信息。