Swift 中的值类型与引用类型使用指北

Swift 中的值类型与引用类型使用指北

在本文中,咱们将探索值类型与引用类型语义的不一样之处,在 Swift 中使用值类型的一些鲜明特征和关键的好处。而后咱们会关注在设计程序时,什么时候使用值类型或者引用类型。html

Swift 中的值类型和引用类型

Swift 是一种多范式的编程语言。它有类,这是构成面向对象编程的基石。类在 Swift 中能够定义属性和方法,指定构造器,符合协议,支持集成和多态。Swift 也是一种面向协议的编程语言,经过功能丰富的协议和结构体,能够在没有继承的状况下实现抽象和多态。在 Swift 中,函数是第一类型,它能够赋给变量,做为参数和返回值在多个函数之间传递。所以 Swift 也适用于函数式编程。前端

对于多数面向对象语言的开发者来讲,Swift 中最大的不一样就是结构体的丰富功能。除了继承之外,你在一个类里能够作什么,在结构体中一样能够作到。这就引起了问题 —— 什么时候并如何使用结构体和类。更通俗的说,问题是在 Swift 中什么时候并如何使用值类型和引用类型。android

为了完整须要提醒一下,Swift 中的值类型并不只仅只有结构体。枚举和元组也是值类型。一样地,引用类型并不仅有类,函数也是引用类型。不过函数、枚举和元组在使用时更加特定化。Swift 在值类型和引用类型的争论中心都集中在结构体和类上。这是本文中的主要重点,因此在本文中术语值类型和引用类型能够和术语结构体和类相互转换。ios

如今让咱们从一些基本原理开始,即值和引用语义的区别。git

值与引用

使用值语义,变量和分配给变量的数据在逻辑上是统一的。因为变量存在于栈上,值类型在 Swift 中被称为栈分配。确切地说,全部的值类型实例并不一直在栈上。一些可能只存在于 CPU 寄存器中,另外一些可能实际在堆上分配。从逻辑上讲,值类型的实例能够被认为是包含在被赋值的变量之中。在变量和值之间存在一对一的关系。变量所包有的值不能独立于变量进行操做。github

另外一方面,在使用引用语义时,变量和数据是不一样的。引用类型的实例在堆中分配,变量只包含一个对存储数据的内存位置的引用。一个实例引用多个变量是能够的也是很常见的。任何这些引用均可以用来操做实例。编程

这会对将值或引用类型实例分配给新变量或传递给函数时发生一些影响。因为值类型实例只能拥有一个全部者,实例被复制,并将副本分配给新变量或传入某函数。每一个副本均可以修改而互不影响。对于引用类型,只有引用被复制,而且新变量或函数得到对同一实例的新引用。若是使用任何引用修改引用类型实例,则会影响全部其余引用持有者,由于它们持有的都是对同一实例的引用。swift

咱们来看看代码。后端

struct CatStruct {
    var name: String
}

let a = CatStruct(name: "Whiskers")
var b = a
b.name = "Fluffy"

print(a.name)   // Whiskers
print(b.name)   // Fluffy
复制代码

咱们定义了一个结构体表示一只猫,有一个 name 属性。咱们建立一个 CatStruct 实例,把它赋给一个变量,而后把这个变量赋给一个新的变量,并用新变量改变 name 属性。因为结构体是值语义,赋值给新变量的行为会致使实例被复制,而后咱们获得了两个不一样名字的 CatStruct安全

如今,咱们用类作一样的事:

class CatClass {
    init(name: String) {
        self.name = name
    }

    var name: String
}

let x = CatClass(name: "Whiskers")
let y = x
y.name = "Fluffy"

print(x.name)   // Fluffy
print(y.name)   // Fluffy
复制代码

在这种状况下,用新变量改变 name 属性也会修改第一个变量的 name 属性。这是由于类是引用语义,赋值给新变量的行为不会建立一个新的实例,两个变量持有对同一个实例的引用,这致使隐式数据共享,这可能会对你如何并什么时候使用引用类型产生影响。

可变性的不一样概念

为了理解可变性在值类型和引用类型之间的差别,咱们必需要分清楚变量可变性实例可变性

咱们上面已经知道,值类型实例和被赋值的变量在逻辑上是一致的。所以,若是变量是不可变的,那不管该实例是否有可变属性或者可变方法,变量都会忽略让实例不可变。只有当值类型的实例赋给一个可变变量时,实例的可变性才能够起做用。

对于引用类型,实例和被赋值的变量是不一样的,所以他们的可变性也是不一样的。当咱们声明一个不可变的变量引用一个实例,咱们能肯定的是,这个变量的引用永远不会改变。即它总会指向同一个实例。实例的可变属性仍是能够经过这个或者其余的引用改变。若是要让类实例不可变,必须保证它的全部存储属性都是不可变的。

在刚才的代码中,咱们看到,能够声明 a 将第一个 CatStruct 实例做为 let 常量,由于它不会被修改。而 b 必须被声明为一个 var,由于咱们修改了它的 name 属性和值。对于 CatClassxy 都被声明为 let 常量,然而咱们能修改 name 属性。

定义为值类型的特征

为了能更好的理解何时以及如何使用值类型,咱们须要看一下定义为值类型的一些特征:

  1. **基于属性的相等:**任何两个同类型值,其属性相等,均可以认为他们是相等的。考虑一个货币类型,它表示货币具备货币和金额属性。若是咱们建立一个 5 美圆的实例,它与任何其余 5 美圆实例都相等。
  2. **淡化的标识及生明周期:**值类型没有固定的身份。它仅由其属性而定义。对于数字 2 或者 “Swift” 这种简单的值就是这种状况。对于复杂的值来讲也是如此。值也没有须要保存状态变化的生命周期。它能够随时被建立、销毁或重建。表明 5 美圆的货币实例,等于表明 5 美圆的任何其余实例,不管这两个实例是什么时候或如何建立。
  3. **可替代性:**没有明确的标识和生命周期给了值类型可替代性,这意味着,若是两个实例相等,即它们经过了基于属性的相等测试,那么任何实例均可以被自由地替代。回到咱们的货币类型例子,一旦咱们建立了一个表明 5 美圆的实例,程序能够根据状况自由的建立或放弃这个实例的副本。不管什么时候咱们须要递交一个 5 美圆的实例,这个 5 美圆的实例是不是先前建立的那个已经可有可无,咱们要关心的是值的属性。

使用值类型的优势

1. 效率

引用类型在堆上分配,这比在栈上分配要昂贵的多。为了确保在引用类型不须要时内存被释放,须要保持一个对每一个引用类型的全部活动的引用计数,并在没有引用时销毁实例。值类型没有这种开销,因此在建立和复制上很高效。值类型的复制是廉价的,由于值类型的实例在不变(constant)的时间被复制。

Swift 实现了内置的可扩展的数据结构,好比 StringArrayDictionary 等等。然而,这些并不能在栈上分配,由于他们的大小在编译时是不知道的。为了能有效地使用堆分配而且保有值语义,Swift 使用一种名为写时复制的优化技术。这意味着每一个复制的实例都是逻辑意义上的副本,只有当复制的实例发生变化时才会在堆上建立实际的副本,在此以前,全部的逻辑副本都会指向相同的底层实例。由于更少的副本被建立,而且在建立的时候,涉及了固定数量的引用计数操做,因此提供了更好的性能。若是须要,这种性能优化还能够对自定义值类型使用。

2. 可预测的代码

使用引用类型时,持有对实例的引用的代码的任何部分都不能肯定该实例包含的内容,由于可使用任何其余引用来修改该实例包含的内容。因为值类型实例在复制时没有隐式数据共享,因此咱们不须要考虑代码的某部分的行为会影响其余部分行为所形成的意外后果。并且,当咱们看到一个变量声明为 let 常量并持有一个值类型的实例时,咱们能够确定,不管如何定义值类型,该值都不能被修改。这为代码的行为提供了强有力的守护以及细粒度的控制,让代码变的易于推理和预测。

有人可能会争辩说,能够编写代码,使得每次将引用类型实例交给新全部者时,都会建立一个副本。 可是这会致使不少防护性复制,这样效率会很是低,由于复制一个引用类型会带来很大的开销。若是正在复制的引用类型实例具备也是引用类型实例的属性,而且咱们但愿避免任何隐式数据共享,则每次都必须建立深度拷贝,这会使让性能更糟。咱们也能够尝试经过使全部引用类型不可变来解决共享状态和可变性的问题。可是这仍然会涉及到不少低效率的复制,并且没法改变引用类型的状态会失去引用类型的用意。

3. 线程安全

值类型实例能够在多线程环境中使用,而不用担忧一个线程正在改变另外一个线程实例的状态。因为没有竞态条件和死锁,因此没有必要实现同步机制。使用值类型编写多线程的代码变得更简单、更安全、更高效。

4. 无内存泄漏

Swift 使用自动引用计数,并在没有引用的状况下,释放引用类型实例。这解决了正常事件过程当中的内存泄漏问题。不过,经过强循环引用仍会内存泄漏,即当两个类实例彼此强引用互相阻止彼此的释放。当一个类与一个闭包(在 Swift 中也是引用类型)彼此强引用也会发生相同的状况。因为值类型没有引用,因此内存泄漏的问题也就不存在。

5. 易于测试

由于引用类型的生命周期会保有状态,因此在对引用类型进行单元测试时,常用模拟框架来观察各类方法被调用时对测试对象的状态和行为的影响。并且因为引用类型实例的行为会随状态的变化而改变,一般须要设置代码来保证测试对象处于正确的状态。对值类型而言,要关心的所有是值类型的属性。因此咱们须要作的,就是建立一个新的值,这个值的属性和指望的值属性相同。

用值类型和引用类型设计程序

值类型和引用类型不该该被看做是相互竞争的。他们不一样的语义和行为,让他们适用于不一样的情景。咱们的目的是理解并运用值和引用语义,让他们以最能知足应用目标的方式结合起来。

1. 使用引用类型模拟具备标识的实体

几乎全部现实世界领域都有在生命周期里保持着标识和状态的实体。这些实体应该使用类来建模。

考虑有一个使用员工类型来表明员工的薪酬应用。简单地,假设只存储员工的姓和名。可能有两个或者更多的员工实例的姓名相同,可是这并不能让他们相等,由于在现实世界中,这些实例表明着不一样的员工。

若是把一个员工类实例赋给一个新的变量或者把它传到一个函数里,新的引用会指向相同的实例。这是咱们能够肯定的。例如,若是咱们在应用的某个模块中使用一个引用来记录员工的工时,那么当应用另外一个模块计算每个月工资时,它使用的都是具备正确工时的同一个实例。一样,若是在某个位置更新员工的地址,那么咱们对员工的全部引用都会更新为正确的地址,由于他们是对同一实例的引用。

若是尝试使用结构体来模拟员工的话会致使错误而且先后矛盾,由于每次把员工实例赋给一个变量或者传给一个函数时,它会被复制。程序中不一样的部分会以它们各自的实例结束,而且其中某部分状态改变并不会在其余部分体现出来。

2. 用值类型来封装状态和暴露行为

虽然有标识和生命周期的实体须要用类来建模,可是须要用值类型来封装它们的状态,表示相关的业务而且暴露行为。

继续以员工类型为例。假设要保留每一个员工的我的数据,工资绩效信息。咱们能够建立我的信息工资绩效值类型,将状态、业务规则和行为这些元素联系在一块儿。这可让类不那么臃肿,由于它只负责维护标识,而它包含的值类型实例会处理该状态的各类元素和相关行为。

这也很是符合单一原则。例如,相比于员工类型不得不实现一些方法来暴露各类层面的行为,客户代码只对员工的绩效感兴趣,因此交给绩效实例来处理。由于处理的是值类型,咱们无需担忧隐式数据共享与客户端背后变化,而对员工实例的状态产生影响。

这种方式也更加适用于多线程。表示引用类型实例状态的各类元素的值类型实例副本,能够自由地切换到不一样线程上的进程,而不须要同步。这能够提升性能,并提升应用交互的响应。

3. 上下文的重要性

要注意的是,有时值类型和引用类型的选择是由上下文驱动的。应用开发不是绝对意义上的对现实世界的建模练习,而是建模问题的具体方面,以知足给定的用例。所以,要判断在应用程序的上下文中使用值语义仍是引用语义,具体取决于实体在相关领域问题中扮演的角色。

想想前面介绍的 CatStructCatClass 类型。咱们更愿意使用哪种模型来模拟宠物猫呢?因为实例将表明一只真正的猫,因此应该使用一个类。例如,当咱们把猫交给兽医来打疫苗时,咱们不但愿兽医给一只猫的副本打疫苗,若是使用一个结构体,就会发生这样的事情。可是,若是咱们正在设计一个处理宠物猫的饮食习惯的应用,那么就应该使用结构体来处理通常意义上的猫,而不是寻找一只特定标识的猫。对于这样的应用,咱们的 CatStruct 不会拥有 name 属性,但可能有消耗食物类型,天天的服务数量等的属性。

不久前,咱们使用货币类型做为一个值为模型的概念的绝佳例子。在银行,金融或其余应用的状况下,咱们只关心货币的属性,即货币的多少和种类。可是,若是咱们正在创建一个实物货币的印刷,分配和最终处理的应用,咱们就须要将每一个纸币视为具备惟一标识和生命周期的实体。

相同地,对于为轮胎制造商开发的应用程序来讲,每一个轮胎均可能是一个具备惟一标识和生命周期的实体,用于销售点以追踪退货,保修索赔等。可是,对制造汽车的公司而言,他们也许不想看轮胎的属性来跟踪哪辆车使用哪一个轮胎,尽管他们能够看到他们制造的汽车具备独特的标识和生命周期。

4. 基于属性相等的测试

值类型没有固定的标识来区分它是不是那个类型实例。惟一比较它们的方式就是比较它们的属性。事实上,基于属性相等性的概念在值类型中是很是基本的,因此决定一个特定的类型是值类型仍是引用类型,它能够做为一个指引。若是一个类型的两个实例不能仅使用基于属性的相等来比较的话,那咱们就要处理一些元素的标识,这一般意味着他们是引用类型,或者它们能够用值和引用语义区分。

实际上,这意味着要比较任何两个实例是否相等都要使用 == 运算符。所以,全部的值类型都必须符合 Equatable 协议。

5. 结合值类型和引用类型

如上面提到过的,把引用类型的属性封装为值类型的实例,以达到封装状态,表示业务规则而且暴露行为的目的是很是可取的。这些值类型能够高效传递,而不用担忧意外后果,如线程安全性等。可是,值类型应该保存引用类型的实例吗?这一般应该避免,由于在值类型上使用引用类型属性会引入堆分配,引用计数和隐式数据共享,影响值类型的性能和其余优势。事实上,它会致使值类型失去其基于属性的平等,淡化标识和可替代性的特色。所以,重要的是要遵照规则,不能以损害二者完整性的方式来结合值与引用语义。

有不少方式描述了值类型和引用类型是如何在实际应用中工做的。如 Andy Matuschak这篇文章中所说的:把对象看做是可预测的纯净的值层之上的一个轻薄的必要的层。在 Andy 的文章的参考文献部分是 Gary Bernhardt此次演讲,一种使用他称之为的函数性核心和命令式外壳来构建系统的方法。函数核心由纯粹的值,特定领域逻辑和业务规则组成。很容易得出,这套系统有利于并发而且易于测试,由于它经过命令式外壳与外部依赖隔离,所以保留了状态并链接到用户界面,持久化机制,网络等等。

Swift 标准库与 Cocoa 框架

Swift 的标准库主要由值类型组成。全部的内建基本类型和集合都是用结构体实现的。构成 Cocoa 框架的部分主要由类构成。有些地方须要类的缘由是,类对于 MVC,用户界面元素,网络链接,文件处理等等是很恰当的方式。

可是 Cocoa 在 Foundation 框架里也有不少类是值类型的,不过做为引用类型而存在,由于他们是用 Objective-C 来编写的。这就是 Swift 标准覆盖的地方,为愈来愈多的 Objective-C 引用类型提供了值类型的桥接。更多桥接类型和 Swift 与 Cocoa 框架之间交互的细节,能够看看苹果开发者网站上的这一页

结论

Swift 提供了强大而高效的值类型,让咱们的代码更加高效,可预测并且线程安全。这就须要理解值和引用语义之间的差别,才能以最能知足应用程序目标的方式来结合值类型和引用类型。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索