swift编程开发iOS,object-c和swift开发

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

2016-08-14  本文已影响104人  matrix_lab

本篇文章翻译自: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:

Swift 2.0:

这些类型包括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协议,对于所有的值类型这是一个很好的实践。这个协议只定义了一个函数,为了能够比较两个实例你必须在全局实现该方法。这就意味着==操作符必须有以下特征:

这里有一个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代码。
现在你已经了解值类型和引用类型的区别,和何时使用他们。在系列文章第二部分, 你会面对一个现实问题和学习值类型的高级技术。

上一篇下一篇

猜你喜欢

热点阅读