Swift中的引用类型和值类型 Reference and Value Types in Swift

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看110 回复回复3 收藏收藏1 分享淘帖 转播转播 分享分享 微信
查看: 110|回复: 3
收起左侧

Swift中的引用类型和值类型 Reference and Value Types in Swift

[复制链接]
cocoaswift 发表于 2016-9-1 16:14:26 | 显示全部楼层 |阅读模式
快来登录
获取最新的苹果动态资讯
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码

引用类型和值类型是编程中的一个重要却很基础的知识点,网络上也有很多关于两者的讨论,文章深入分析了两者的不同以及在Swift中该如何选择使用的问题,推荐编程新兵及对引用类型和值类型有疑问的Swift使用者阅读。

Swift中的引用类型和值类型 0

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 0

(文章略长,精读大概需要半小时到1小时)

内容提纲:

  • 引用类型
  • 值类型
  • 拷贝语意:浅拷贝、深拷贝
  • 引用类型的问题:隐式的数据共享
  • 值类型的实例:没有隐式的数据共享
  • 引用类型/值类型的不可变性
  • 引用类型/值类型该如何选择
  • 结论




我们将在这篇文章探讨Swift中引用类型和值类型的区别,介绍两者的概念和各自的优缺点,以及该如何使用。




1、引用类型

引用类型初始化后,无论是分配给变量还是常量,或是通过参数传递给函数,都将是同一个实例对象。


Object是一个典型的引用类型,一旦初始化完成,我们不管是将它作为一个值进行分配还是传递,我们都是分配或传递了一个原始对象的引用(实际上它就是内存中的一块区域),引用类型的分配我们一般叫它浅拷贝。

Swift中,用关键字class来定义objects:

[Swift] 纯文本查看 复制代码
class PersonClass {
    var name: String
    init(name: String) {
        self.name = name
    }
}
var person = PersonClass(name: "John Doe")





2、值类型

值类型每次分配给变量/常量或者作为参数传递到函数时,都会重新创建(复制)一个新的实例。


所有的基本类型都是典型的值类型,常用的基本类型也是值类型的有:Int, Double, String, Array, Dictionary, Set。值类型每次初始化以后,当我们将它分配或者传递时,实际上是分配或传递了它的一个拷贝。

Swift中最常用的值类型是structs,enums和tuples,值类型的分配叫做深拷贝。




3、拷贝语意

我会用图片来展示一个实际的例子以描述它们在语法上的区别。假设我们有一个树的数据结构:

[Swift] 纯文本查看 复制代码
class Node<T: Comparable> {
    let value: T
    var left: Node?
    var right: Node?
    convenience init(value: T) { […] }
    init(value: T, left: Node?, right: Node?){ […] }

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


我们创建一个二叉树的实例如下:

[Swift] 纯文本查看 复制代码
let binaryTree = Node(value: 8)
tree.add(2)
tree.add(13)


Swift中的引用类型和值类型 1

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 1

一个二叉树实例

现在,我们来看一下复制语意在行为上的区别。



3.1、浅拷贝(引用类型)

复制一个引用类型时,Swift编译器将会复制实例的一个引用。但不包含实例的属性。因此,当对一个引用类型进行多次复制时,每一份复制都将共享同一份数据。

Swift中的引用类型和值类型 2

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 2

二叉树浅拷贝



3.2、深拷贝(值类型)

当我们复制一个值类型时,Swift编译器将复制一个全新的实例,包括它的所有属性。整个过程会复制它所有的值类型属性。因此,当对一个值类型进行多次复制时,每次复制都会产生一个单独的、没有数据共享的新实例。

Swift中的引用类型和值类型 3

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 3

二叉树深拷贝




4、引用类型的问题:隐式的数据共享

为了说明引用类型的这种典型问题,我们先定义一个类用来表示2D平面上的一个点。

[Swift] 纯文本查看 复制代码
class PointClass {
    var x: Int = 0
    var y: Int = 0
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}


现在,当我们实例化一个PointClass对象并将它分配给另一个变量时发生了什么?

[Swift] 纯文本查看 复制代码
var pointA = PointClass(x: 1, y: 3)
var pointB = pointA


因为 PointClass 是一个引用类型,最后一句的变量声明实际上是把分配给pointA的引用也分配给了pointB。我们可以用下面的图来形象的描述上述情形:

Swift中的引用类型和值类型 4

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 4

Reference type instances

在这种情形下,pointA和pointB 共享 同一个实例。因此,任何对pointA的改变也将反应到pointB上,反之亦然。这在很多情况下没什么问题,但它也会导致一些不太显而易见的bug。

让我们来看一个很普遍的隐式数据共享问题。假设实例化了一个 view controller 1,并分配了一个 Person 对象(Person是一个model)的实例给它。这时,用户进行操作,我们push了另一个 view controller 2 到上一个 view controller 1 的上面,并把分配了同一个Person的引用实例给 view controller 2。我们可以想象下图这样的特殊情景:

Swift中的引用类型和值类型 5

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 5

Assigning a reference type to more than one view controller

当两个 view controller 同时持有Person实例的用一个引用,如果我们在 view controlelr 2 中修改了Person的任何属性,将导致之前 view controller 1 中所持有Person的属性也同时被修改。因此,在view controller 2 中对数据模型的修改都将传递到 view controller 1 中。

回到我们最初那个例子,要避免数据隐式共享的问题,一个方法是创建一个实例真正的拷贝,以代替将变量pointA直接进行新变量的分配赋值,如下手动创建拷贝并分配给pointB:

[Swift] 纯文本查看 复制代码
var pointB = pointA.copy()


现在,pointB 有了它自己独立的引用,它和 pointA 之间不再共享数据。这个技巧可正常工作,但还是有一些缺点:

  • 不得不:
    • – 继承NSObject并实现NSCopying
    • – 实现一个新的Copyable协议
  • 每一次赋值都需要显示的调用 copy() 带来的额外开销
  • 很容易就忘记调用 copy()





5、值类型实例:没有隐式共享

当分配一个值类型时,编译器会自动创建(并返回)一个实例的拷贝。来看看发生了什么,我们用 struct (值类型) 的 PointStruct 来代替 class (引用类型) 的 Point 。

[Swift] 纯文本查看 复制代码
struct PointStruct {
    var x: Int = 0
    var y: Int = 0
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}


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

[Swift] 纯文本查看 复制代码
var pointA = PointStruct(x: 1, y: 3)
var pointB = pointA


因为 PointStruct 是值类型, 最后一行的声明会先创建一个PointA的拷贝,并将拷贝分配给了PointB。这就使得两个实例分配是安全并独立的。上述情形图示如下:

Swift中的引用类型和值类型 6

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 6

Values type instances

我们看到 pointB 有它自己独立的引用,并且不会被 pointA 共享。这表明了使用值类型时,我们可以很容易的确保所有值类型的实例都是相互独立并且不会产生数据共享的。

从性能方面看,使用值类型不会产生巨大的开销:

低成本的拷贝:

  • 基础数据类型的复制花费恒定时间
  • 值类型(struct, enum, tuple)的复制花费恒定时间


可扩展的数据结构使用即写即拷:

  • 拷贝包含一个固定数量的引用计数操作
  • 这项技术常用于许多标准库的类型:String, Array, Set, Dictionary, …

除了以上说的,值类型的另一个性能方面的优点是栈分配,它相对于堆分配(引用类型)有着更高的效率。这将使得访问更快但无法支持继承。

有一点需要注意的是,只有当 structs, enums, tuples 它们的所有属性也都是值类型时,它们才是真正的值类型。如果它们包含引用类型的属性,则依然会有前面所提到的隐式数据共享的问题。

看下面的 struct :

[Swift] 纯文本查看 复制代码
struct PersonView {
    let person: PersonStruct
    let view: UIView
}


我们预期的目标是创建一个容器,用来跟踪一个 person, 并用一个视图来显示这个person的所有的相关信息。我们确信上面的代码是合理的;我们用 let 定义了属性,对吗?但很不幸,事实并非如此。因为 view 是一个引用(UIView是引用类型),我们依然可以修改它的属性!但这会造成一些更微妙的bugs。为了展示这个问题,我们创建一个 PersonView 的实例并进行一份拷贝:

[Swift] 纯文本查看 复制代码
let personViewA =
    PersonView(person: PersonStruct(name: "John Doe"),
               view: UIView())
let personViewB = personViewA


由于 view 是引用类型,这将导致两个 PersonView 的实例共享了同一个 view 属性!所以当我们修改 view 的属性时,实际上最终修改的是 共享的 view 。通过下图可以更容易理解,再一次,我们又回到了隐式共享数据的问题讨论:

Swift中的引用类型和值类型 7

Swift中的引用类型和值类型 Reference and Value Types in Swift - 敏捷大拇指 - Swift中的引用类型和值类型 7

Value type containing a reference type




6、引用类型,值类型和不可变性

不可变性: 一个实例的属性在创建之后不可更改。


不可变性是函数式编程中很重要的一个严格约束。使用值类型可确保我们所创建的实例的状态是确定的,定义后即不可改变。不可变的对象有着一些有趣的优缺点。

优点:

  • 不可变对象不会共享数据,因此,也不会在实例间共享状态。这就避免了不可预见的状态改变所带来的副作用。
  • 一个直接结果是对于前面的不可变对象Point来说是线程安全的。这意味着我们将不必担心竞争条件和线程同步。
  • 因为不可变对象保存其自身的状态,这将更容易的组织代码。


缺点:

  • 不可变性对机器模型来说不总是高效映射的。一个典型的例子是执行立即改变的算法(例如快速排序)。在保持原来性能的前提下,通过值类型来实现这些是不容易的。


Swift中,我们可以使用两个不同的单词来定义变量:

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


上面两个关键字有着不同的行为,这取决于他们是用于引用还是值类型。



可变实例声明:var

引用类型 引用可以改变(mutable):你可以改变实例本身和它的引用。

值类型 实例可以改变(mutable): 你可以改变实例的属性。

不可变实例声明:let 引用类型 引用将保持不变(immutable): 你不能改变实例的引用,但可以改变实例它本身。

值类型 实例保持不变(不可变的):不能修改实例的属性,无论这个属性是用let还是var定义的。




7、引用类型和值类型该如何选择?

一个很常见的问题:“我该在什么时候用引用类型,什么时候用值类型?” 你能在网上看到很多这样的讨论。以下是我喜欢的一些例子:



作为一个基本的规则,我们每次创建引用类型都要继承自 NSObject 。这是一个使用 Cocoa SDK 常见的场景。对于引用类型和值类型的使用,苹果还提供了一些通用规则。我总结如下:

以下情况使用引用类型:

  • NSObject的子类必须是class类型
  • 使用 === (全等于)来比较实例
  • 需要创建可共享和改变的状态


以下情况使用值类型

  • 使用 == 来比较实例数据
  • 需要拷贝独立的状态
  • 代码会运行在跨多线程上 (避免显示同步)


有趣的是,Swift 的标准库大量依赖于值类型:

  • 基本数据类型(Int,Double,String…)是值类型
  • 标准集合类型(Array,Dictionary,Set…)也是值类型


通过查阅Swift标准库文档,可收集到有关上面讨论的准确数据:

  • Classes = 4
  • Structs = 103
  • Enums = 9


抛开上面的讨论,选择引用还是值类型真的应该取决于你要实现什么。一般说来,如果没有特殊的限制迫使你要选择引用类型,或者不确定哪个选择更合适时,可以先实现一个值类型的数据结构。如果以后需要,你可以相对简单的将它转化为一个引用类型。




8、总结

回复本帖,你可以下载本文中的代码(一个playground文件)。

Swift中的引用类型和值类型 Reference and Value Types in Swift SwiftPlaygrounds-ma.zip (7.28 KB, 下载次数: 0, 售价: 10 金钱)

都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

嗯,不错!期待更多好内容,支持一把:
支持敏捷大拇指,用支付宝支付10.24元 支持敏捷大拇指,用微信支付10.24元

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

firefighter 发表于 2016-9-1 16:27:37 | 显示全部楼层
晚点花半小时来看,Mark!
代码买卖 发表于 2016-9-1 17:00:55 | 显示全部楼层
后面那些英文的例子不错~
攻城狮 发表于 2016-9-2 13:09:31 | 显示全部楼层
深浅拷贝就是针对值和引用这两种类型的么?