Swift中的引用和值类型

在这篇文章中,我们将研究引用类型和值类型之间的差异。 我们将介绍这两个概念,看看它们的优缺点,并研究如何在Swift中利用它们。

参考类型

引用类型 :一种类型,一旦初始化,将其分配给变量或常量,或传递给函数时,将返回对相同现有实例的引用。

引用类型的典型示例是对象。 一旦实例化,当我们分配它或将其作为值传递时,实际上是在分配或传递对原始实例的引用(即,它在内存中的位置)。 引用类型分配据说具有浅表复制语义。

在Swift中,使用class关键字定义对象:

 类PersonClass { 
变量名称:字符串
  init(name:String){ 
self.name =名称
}
}
  var person = PersonClass(name:“ John Doe”) 

值类型

值类型 :一种类型,在分配给变量或常量或传递给函数时会创建新实例(副本)的类型。

值类型的典型示例是原始类型。 常见的基本类型(也是值类型)是: IntDoubleStringArrayDictionarySet 。 实例化后,当我们分配它或将其作为值传递时,实际上是在获取原始实例的副本。

Swift中最常见的值类型是struct枚举元组可以是值类型。 值类型赋值据说具有深层复制语义。

复制语义

我将通过一个实际的例子来说明复制语义之间的区别。 假设我们正在使用通用树数据结构:

 类Node  { 
出租价值:T
var左:节点?
var对:节点?
 便利init(值:T){[…]} 
  init(值:T,左:节点?,右:节点?){[…]} 

func add(value:T){[…]}
}

我们可以很容易地创建一个二叉树的实例,如下所示:

 让binaryTree = Node(值:8) 
tree.add(2)
tree.add(13)

现在,让我们看一下复制语义的不同行为。

浅拷贝(引用类型)

复制引用类型时,Swift编译器将复制实例的引用。 但不是它的属性。 因此,在创建引用类型对象的多个副本时,每个副本将共享由实例属性表示的相同数据。

深拷贝(值类型)

复制值类型时,Swift编译器会为原始实例创建一个全新的副本。 这意味着将所有原始实例属性复制到一个新的实例属性中。 对于每个本身就是值类型的属性,都将复制此过程。 因此,当创建一个值类型对象的多个副本时,每个副本将是一个没有共享数据的新的单独实例。

参考类型实例的问题:隐式数据共享

为了显示引用类型的典型问题,让我们定义一个来表示2D空间中的点。

 类PointClass { 
var x:Int = 0
var y:Int = 0
  init(x:Int,y:Int){ 
self.x = x
self.y = y
}
}

现在,如果我们实例化一个PointClass对象并将其分配给另一个对象会发生什么?

  var pointA = PointClass(x:1,y:3) 
var pointB = pointA

因为PointClass是引用类型,所以最后一条语句实际上是将对pointA的引用分配给pointB 。 我们可以用图形表示以下情况:

在这种情况下, pointBpointA 共享同一实例。 因此,对pointA的任何更改都将反映在pointB上 ,反之亦然。 在许多情况下,这可能很好,但也是常见的细微错误来源。

让我们看一个解决隐式共享数据问题的非常简单的方法。 假设我们实例化了一个视图控制器,并为其分配了对Person对象(我们的对象模型 )实例的引用。 然后,响应用户交互,我们推送另一个视图控制器(在第一个视图控制器的顶部),并为其分配相同的引用实例。 我们可以将这种特殊情况可视化如下:

由于两个视图控制器都被分配了相同的引用实例,因此,如果我们在SecondViewController中修改其任何属性,我们最终将修改最初分配给FirstViewController的原始(共享) Person实例。 因此,在SecondViewController内部对对象模型所做的任何修改都将传播到FirstViewController

回到我们的原始示例,避免隐式数据共享问题的一种方法是显式创建实例的副本。 我们可以手动创建一个副本并分配它,而不仅仅是分配pointA

  var pointB = pointA.copy() 

现在, pointB将有其自己的单独引用,并且pointApointB之间将不再有共享数据。 该技术可以正常工作,但有一些缺点:

  • 必须:
  • —从NSObject继承并实现NSCopying
  • —实现新的可复制协议以在Swift中复制对象
  • 显式调用每个分配的copy()会带来一些开销
  • 很容易忘记为每个分配调用copy()

值类型实例:无隐式共享

分配值类型时,编译器将自动创建(并返回)实例的副本。 让我们看看如果不是将2D点定义为 (引用类型),而是将其构造结构 (值类型),会发生什么情况。

  struct PointStruct { 
var x:Int = 0
var y:Int = 0
  init(x:Int,y:Int){ 
self.x = x
self.y = y
}
}

现在,我们可以创建PointStruct的实例并将其分配给另一个实例。

  var pointA = PointStruct(x:1,y:3) 
var pointB = pointA

因为PointStruct是值类型,所以最后一条语句正在创建要分配给pointB的pointA的副本 。 这使分配安全,因为两个实例是不同的。 我们可以用图形表示这种情况,如下所示:

我们可以看到pointB有其自己的独立引用,并且pointApointB之间将没有共享数据。 这表明通过使用值类型,我们可以轻松地确保所有实例都是不同的并且不共享任何数据。

从性能的角度来看,使用值类型不会增加大量开销:

份量便宜
•复制基本类型( IntDouble ,…)需要花费固定时间
•复制值类型的structenumtuple需要固定时间

可扩展的数据结构使用写时复制
•复制涉及固定数量的引用计数操作
•许多标准库类型都使用此技术: 字符串数组集合字典 ,…

除上述内容外,值类型的另一个性能优势是它们是堆栈分配的,这比堆分配(用于引用类型)更有效。 这样可以加快访问速度,但是具有必须放弃对继承的支持的缺点。

必须指出,仅当structenumstuples的所有属性均为值类型时,它们才是真正的值类型。 如果它们的任何属性都是引用类型,我们仍然可能遇到上一段中说明的隐式数据共享问题。

让我们看一下以下结构

  struct PersonView { 
让人:PersonStruct
让视图:UIView
}

我们的预期目标是创建一个容器来跟踪人并处理显示所有相关信息的视图。 我们可能会对上面的代码是正确的有信心; 毕竟我们使用let声明了所有属性,对吗? 好吧,不幸的是,事实并非如此。 由于view属性是一个引用,因为UIView是引用类型,所以我们仍然可以更改其属性! 但是,错误的来源更加微妙。 为了说明这一点,让我们创建一个PersonView实例并进行复制:

 让personViewA = 
PersonView(person:PersonStruct(name:“ John Doe”),
视图:UIView())
 让personViewB = personViewA 

现在,由于view是引用类型,因此发生的情况是PersonView的两个实例都共享相同的属性! 这意味着,如果我们修改任何实例的view属性,我们最终实际上将修改共享视图。 下图应该更容易看到这一点,并帮助我们认识到我们再次遇到了前面讨论的隐式数据共享问题:

参考类型,值类型和不变性

不可变性 :实例的属性,其状态在创建后无法修改。

不变性是一个非常重要的属性,它与功能编程范例紧密相关。 我们看到通过使用值类型,我们能够创建实例来无限期保留其状态,这些实例根据定义是不可变的。 不可变的对象有一些有趣的利弊。

优点:

  • 不可变对象不共享数据,因此实例之间没有共享状态。 这避免了由副作用引起的意外更改问题
  • 上一点的直接结果是,不可变对象本质上是线程安全的。 这意味着我们无需担心竞争条件和线程同步
  • 由于不可变对象保留其状态,因此更容易推理代码

缺点:

  • 不变性并不总是有效地映射到机器模型。 一个典型的例子是执行就地修改的算法(例如Quicksort)。 在使用值类型同时保持原始性能的同时,实现这些值并不容易。

在Swift中,我们可以使用两个不同的关键字定义变量:

  • var :定义可变实例
  • let :定义不可变的实例

上述关键字具有不同的行为,具体取决于它们是用于引用还是用于值类型。

可变实例:var

参考类型

可以更改引用可变的 ):您可以更改实例本身,也可以更改实例引用。

值类型

实例可以更改( 可变 ):您可以更改实例的属性。

不变实例:让

参考类型

引用保持不变( 不可变 ):您不能更改实例引用,但可以更改实例本身。

值类型

实例保持不变( 不可变 ):无论使用let还是var声明属性,您都无法更改实例的属性

您应该选择哪种类型?

一个非常常见的问题是: “我该如何决定何时使用引用类型以及何时使用值类型?”您可以在Internet上找到许多有关引用类型的讨论。 我最喜欢的一些示例是:

  • 我应该使用Swift结构还是类?
  • 2015-07-17星期五问答:何时使用Swift结构和类
  • 热烈欢迎结构和值类型
  • RE:我应该使用Swift结构还是类?
  • 结构与类
  • Swift中的引用与值类型:第1/2部分

作为基本规则,每次从NSObject进行子类化时,我们都必须创建引用类型。 与Cocoa SDK交互时,这是常见的情况。 Apple提供了一些使用引用类型与值类型的通用规则。 我在下面总结了它们。

参考类型何时:

  • NSObject的子类必须是类类型
  • 实例身份===进行比较比较有意义
  • 您要创建共享的可变状态

值类型何时:

  • 实例数据==进行比较很有意义( Equatable协议)
  • 您希望副本具有独立状态
  • 数据将在多个线程的代码中使用(避免显式同步)

有趣的是,Swift标准库非常喜欢值类型:

  • 基本类型( IntDoubleString ,…)是值类型
  • 标准集合( 数组字典集合等)是值类型

通过查看Swift标准库参考,可以收集准确的数字以确认上述声明。 这是类型的拆分:

  • 班级= 4
  • 结构= 103
  • 枚举= 9

除了上面说明的内容之外,选择实际上还取决于您要实现的内容。 根据经验,如果没有强制您选择引用类型的特定约束,或者您不确定哪个选项最适合您的特定用例,则可以从使用值类型实现数据结构开始。 如果需要,您以后应该可以用较少的精力将其转换为引用类型。

结论

您可以在此处下载带有此代码的游乐场。

在这篇文章中,我们研究了引用类型和值类型之间的差异。 在研究了隐式数据共享的常见问题之后,我们看到了如何通过使用值类型而不是引用类型来避免这种情况。

我们还介绍了不变性的概念,并了解了它如何应用于Swift中的引用和值类型。 最后,我们回顾了一些用例,在这些用例中,引用类型和值类型之间的选择非常简单。 对于所有其他情况,实验是找出哪种可能是最佳选择的最佳方法。

有关

  • UITableView和UICollectionView中的平滑滚动
  • 使用iOS 10预提取API增强平滑滚动
  • Swift中的通用数据源
  • 通用协议类型擦除的替代方法