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

作者: matrix_lab | 来源:发表于2016-08-14 20:05 被阅读104次

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

    相关文章

      网友评论

        本文标题:Swift引用类型 VS 值类型 (1/2)

        本文链接:https://www.haomeiwen.com/subject/vyrysttx.html