Swift引用类型 VS 值类型 (1/2)

本篇文章翻译自:Reference vs Value Types in Swift: Part 1/2
原作: Eric Cerney on November 9, 2015


如果你已经看了WWDC 2015的sessions, swift正在着重反思代码架构。从OC转变到Swift,开发者注意到最大的改变是对于值类型的倚重要甚于值类型。

这个分上下两部分的系列文章会解释说明两者之间的不同,并向你展示什么时候使用什么类型比较合适。在第一部分,你会学到2种类型的主要概念;在第二部分,你会解决一个现实问题,期间你会学到更多的高级概念并发现两者微妙、但很重要的点。

不管你是否有OC背景,或者精通Swift,你的确需要了解Swift类型的前世今生。

这篇tutorial工作在Swift 1.2 和 Swift 2.0

开始

首先,在Xcode中创建一个新的playground,选择File\New\Playground...然后给playgound命名ValueSemanticsPart1。这篇tutorial与平台无关,我们只关注Swift语言层面,所以你可以选择任何平台。点击Next,选择合适保存位置,打开它。

引用类型 VS 值类型

那么两个类型的核心区别在哪里呢?快速粗略的解释是值类型保留一份独一无二的数据备份,而引用类型共有数据。
Swift中引用类型的代表是class,这跟OC很相似,在OC中所有的对象都继承于NSObject,并以引用类型来存储。
在Swift中有很多值类型,例如: 结构体, 枚举,和元组。你可能没有注意到OC也使用值类型像NSInteger,或者C结构体CGPoint。
为了更好的理解两者的不同,我们最好从认识OC的引用类型开始。

引用类型

在OC中,以及大部分其他的面向对象语言中,你可以持有对象的引用。在Swift中,你可以使用class,它实现了引用语义。

class Dog {
    var wasFed = false
}

上面的类代表一个宠物狗,和有没有喂过食物。创建一个Dog的实例:

let dog = Dog()

仅仅指向存储dog实例的内存地址。为了添加另一个持有相同dog实例的对象,添加如下代码:

let puppy = dog

因为dog是内存地址的一个引用,puppy指向相同的地址。设置宠物的wasFed属性为true。

puppy.wasFed = true

puppy和dog都指向相同的内存地址。


但是实际情况,你并不想一个对象的改变反应到另一个对象上。我们来检查一下属性:

dog.wasFed //true
puppy.wasFed //true

因为引用相同的对象,所以改变一个实例会影响到另一个。在OC中这种场景很常见。

值类型

值类型跟引用类型完全不同。你可以用Swift的基础类型来探索这一规律。添加整形变量和相应的操作到playgound中:

var a = 42
var b = a
++b

a // 42
b //43

你会想到a和b是相等的吗? 当然事实很清楚,a等于42,b等于43。如果你把他们声明成引用类型,那么a和b都会等于43,因为引用类型指向相同的内存地址。

上述规律同样适用于其他值类型。在playground中添加如下代码,实现 Cat struct:

struct Cat {
     var wasFed = false
}

var cat = Cat()
var kitty = cat 
kitty.wasFed = true

cat.wasFed    //false
kitty.wasFed    //true

这展示引用类型和值类型的一个微妙但很重要的不同点:设置kitty的wasFed属性不会影响cat实例。kitty变量接受一份cat的值拷贝,并不是引用。



看起来,你的cat今晚儿要挨饿了。(😄,wasFed还是false)。

尽管给变量赋值引用会快很多,copy大多数情况下也一样经济实惠。copy操作时间复杂度是O(n),它们基于数据的大小使用固定数量的引用计数操作。

值类型的这种性能损耗好像成为了总是使用引用类型的一个理由,但是在系列文章的第二部分,会向你展示一些Swift优化这些copy操作的聪明方法。

可变性

var 和 let 在引用类型和值类型中的作用是不同的。注意到你把dog和puppy用let定义。但是你还能改变wasFed属性,这怎么可能?
对于引用类型来说,let意味着引用必须是不可变的。换句话说,你不能改变实例的不可变引用,但是你可以改变实例本身。
对于值类型,let意味着实例必须保持不可变。实例的任何属性都不能改变,不管属性是用let或var声明。

使用值类型能够很容易控制不可变性。如果要实现相同的引用类型的可变性/不可变性,你需要实现可变性/不可变性类,例如:NSString和NSMutableString。

Swift更喜欢哪种类型呢?

这可能让你感到很惊讶:Swift标准库几乎全部使用值类型。大致搜索了一下Swift 1.2和Swift 2.0,enum, struct, 和类的使用情况:

Swift 1.2:

  • struct : 81
  • enum : 8
  • class : 3

Swift 2.0:

  • struct : 87
  • enum : 8
  • class : 4

这些类型包括String, Array,和Dictionary,他们都是用Struct实现的。

什么时候该使用哪一种类型呢?

现在你知道了两种类型的不同,但是我们该使用哪一个呢?
有一种情况你是没得选的:很多Cocoa APIs需要NSObject子类,那么你只能使用class。但除此之外,你可以借鉴苹果的Swift blog来决定是使用struct/enmu值类型还是使用class引用类型。

什么时候使用值类型

大体说来:在以下情况可以考虑使用值类型:

使用==比较实例有意义

你会说:"当然,我想要每一个对象都能够比较"。但是你要考虑数据是否应该被比较。考虑下面Point的实现:

struct Point {
     var x: Float
     var y: Float
}

两个有相同的x和y成员的变量应该被认为是相等的吗?

let point1 = Point(x: 2, y: 3)
let point2 = Point(x: 2, y: 3)

这很清楚拥有相同的内部值的两个Point实例应该被认为是相等的。这两个实例的内存地址是无所谓的,你只关心值本身,不是吗?

因此,你需要遵守Equatable协议,对于所有的值类型这是一个很好的实践。这个协议只定义了一个函数,为了能够比较两个实例你必须在全局实现该方法。这就意味着==操作符必须有以下特征:

  • 反射性: x == x 结果为true
  • 对称性: 如果 x == y 那么 y == x
  • 传递性: 如果 x == y 并且 y == z 那么 x == z

这里有一个Point的==操作符的实现

extension Point: Equatable {
}

func ==(lhs: Point, rhs: Point) -> Bool {
    return lhs.x == rhs.x && lhs.y == rhs.y
}

拷贝应该有独立的状态

我们进一步探索Point示例,考虑下面拥有相等center的2个Shape实例。

struct Shape {
    var center: Point
}

let initialPoint = Point(x: 0, y: 0)
let circle = Shape(center: initialPoint)
var square = Shape(center: initialPoint)

如果你改变一个Shape实例中的一个的center将会发生什么?

square.center.x = 5 //{x: 5, y: 0 }
circle.center  //{x: 0, y: 0}

每一个Shape实例都需要一份point的拷贝,所以你可以保持他们每个独立状态。你可以想象所有的Shape实例共享同一份center的混乱状态。

数据在多线程中使用

这个可能会有点复杂。多线程能访问这条数据吗?如果可以,那么多线程中值不相等会有影响吗?
想要让你的数据适用于多线程并在多线程中相等,你需要使用引用类型并且实现 --- 艰巨的任务🐶!
如果线程可以独有数据,那么使用值类型也变得没啥意义,因为数据的每一个拥有者会直接引用数据,而不是持有引用。(这句有点晦涩。笔者认为,引用类型可以有多个owner, 而值类型有且只会有一个owner,所以在多线程中,修改值类型,它会马上生效,不会受到其他线程的干扰,不会有像引用类型的数据同步的问题。)

什么时候使用引用类型

尽管在很多情况下使用值类型都大有裨益,但是引用类型在下面的情形中作用还是很明显的:

用 === 操作符比较实例有意义
=== 检查两个对象是否完全相等,包括存储数据的内存地址。

举个现实的例子,考虑下面的情景:你的同事拿20$跟你换20$, 那么你肯定没所谓呀,因为你关心的是数值。
然而,如果有一个人偷了《大宪章》,并且用羊皮纸做一份相同的文件,放回去。那么你还认为没有关系吗,因为文件的固有身份已经是不同了。

你可以用相同的思路来思考决定是否使用引用类型;通常情况下,你很少关注固有的身份---也就是,数据的内存地址。你更多关注的是值。

你想要一个共享,可变的状态

有时,你想要一条数据以单例的形式存储,可以被多个使用者访问和修改。
一个共享,可变状态的普遍示例是银行账户。你可以像下面一样实现基本的账户和人。

class Account {
    var balance = 0.0
}

class Person {
    let account: Account
    
    init(_ account: Account) {
        self.account = account
    }
}

如果联合账户的持有者往账户注资,那么绑定该账户的所有信用卡的总资产会有所反应。

let account = Account()

let person1 = Person(account)
let person2 = Person(account)

person2.account.balance += 100.0

person1.account.balance //100.0
person2.account.balance //100.0

因为账户是一个类,每一个人持有账户的引用,所有的事情保持同步。

还在犹豫不决?

如果你不确定哪种机制适用你的情景,那么默认就值类型。之后,你可以毫不费力得转换到class。


Swift几乎全部使用值类型,这真的难以置信,在OC那儿情况完全相反。
作为新的Swift规范下的代码架构者,你需要前期做一些计划---数据怎么使用。使用值类型或者引用类型,你几乎可以解决所有的问题,但是使用不当会产生一大堆bug和让人困惑的代码。

在所有的情形下,当新要求来了,尝试去改变你的架构是最事。挑战你自己去遵守Swift的规范;结果你会产出比之前更加优雅的代码。

延伸阅读

在这里你可以下载完整的playground代码。
现在你已经了解值类型和引用类型的区别,和何时使用他们。在系列文章第二部分, 你会面对一个现实问题和学习值类型的高级技术。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • Swift 介绍 简介 Swift 语言由苹果公司在 2014 年推出,用来撰写 OS X 和 iOS 应用程序 ...
    大L君阅读 3,168评论 3 25
  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young阅读 3,771评论 1 10
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 我轻轻叫醒一朵棉花糖 尝到了云的滋味
    青灯古佛空无眠阅读 168评论 0 0
  • 首先先抱歉,昨天晚上跟妈妈一起理东西就没有写,自己也把这个事情忘了,一直在想离开妈妈后,妈妈怎么生活,过的会不会不...
    也牛牛肉阅读 124评论 0 1