Swift 中的值类型和引用类型

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

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

在查看一些代码之前,这里有两种相同场景的变体,它们说明了值类型和引用类型行为方式之间的基本差异。

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

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

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

在这两种情况下,您的朋友都能够阅读并更改您的文档,但存在 significant 差异。

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

当您向朋友发送链接时,您发送的不是实际的文档。您发送的是指向云端文档的 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) // prints "Blah blah blah"
print(myDoc.text) // prints "Great new article"

当您向朋友发送文档的副本时,您可以完全控制您的副本何时更改。您永远不必担心您的朋友对您的文档副本进行一些意外的更改。

类似地,对于值类型,您永远不必担心程序的其他部分会更改该值。

引用类型

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

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

下面的代码与上面的示例相同,但有一个细微但重要的更改。 Document 类型现在声明为 class,而不是声明 struct

这是一个很小的代码更改,但行为却发生了 significant 变化。

像之前一样,创建了一个 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) // prints "Blah blah blah"
print(myDoc.text) // prints "Blah blah blah"

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

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

局部推理

在上面的小代码示例中,您可以逐行阅读代码,了解如何将相同的引用分配给两个不同的变量,以及如何使用一个变量更改属性会更新两个变量引用的实例。

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

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

从多个位置更改数据的正式名称是共享可变状态。共享是因为它可以从代码中的多个位置访问,可变是因为它可以更改,也称为 mutate,状态是数据的同义词,如“事物的当前状态”。

在这种情况下,如果不了解代码中许多不同位置可能发生的情况,您就无法完全理解代码的某个部分中正在发生的事情。您失去了进行局部推理的能力,这使得您的代码更难理解和调试。

使用值类型的一个优点是,您可以确定程序中的其他任何位置都不会影响该值。您可以推理您面前的代码,而无需知道其他地方正在发生什么。

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

选择值类型还是引用类型

回到共享文档的示例,您和您的朋友都能够查看和编辑同一文档可能非常有用。

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

一般来说,首选使用结构体而不是类。如果您不需要引用类型的行为,则无需承担额外的复杂性和陷阱。

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

组合值类型

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

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

因此,您可以定义一个结构体,其中包含一些基本类型,例如 String、Int、Bool,甚至可能是枚举值。由于结构体中的所有内容都是值类型,因此该结构体的行为类似于值类型。

您可能有一种类型,它是一个更复杂的结构体,其中包含第一个结构体的实例和一些其他值。同样,由于它由值类型组成,因此该结构体也是一个值类型。

集合是值类型

但在 Swift 中,组合值类型并不仅限于结构体和枚举。

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

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

结论

理解值类型和引用类型是什么,以及它们行为方式的差异,是学习 Swift 并能够推理代码的重要组成部分。两者之间的选择通常归结为将类型声明为 struct 还是 class 之间的选择。您可以在The Swift Programming Language结构体和类 章节中了解有关结构体和类的更多信息。