什么时候用 struct?什么时候用 class?

翻译:muhlenXi
校对:YousanflicsnumbbbbbCee
定稿:CMBhtml

在 Swift 的世界中,有一个热议好久的主题,什么时候使用 class 和什么时候使用 struct ,今天,我想发表一下我本身的观点。编程

值类型 VS 引用类型

事实上,这个问题的答案很简单:当你须要值语义的时候用 struct,当你须要引用语义的时候就用 class。swift

好了,下周同一时间请再次访问个人博客……数组

等等网络

怎么了?app

这没有回答上述中的问题socket

你什么意思?答案就在那儿。函数

是的,可是……性能

可是什么?ui

那什么是值语义,什么是引用语义呢?

昂,你提醒了我。我确实应该讲解一下。

还有它们和 struct、class 的关系

好吧。

这些问题的核心就是数据和数据的存储位置。咱们用局部变量、参数、属性和全局变量来存储数据。存储数据有两种最基本的方式。

对于值语义,数据是直接保存在变量中。对于引用语义,数据保存在其余地方,变量存储的是该数据的引用地址。当咱们访问数据时,这种差别不必定很明显。可是拷贝数据时就彻底不同了。对于值语义,你获得的是该数据的拷贝。对于引用语义,你获得的是该数据的引用地址拷贝。

这有些抽象,咱们经过一个示例来了解一下。先暂时跳过 Swift 的示例,一块儿来看一个 Objective-C 的示例:

@interface SomeClass : NSObject 
    @property int number;
    @end
    @implementation SomeClass
    @end
    
    struct SomeStruct {
        int number;
    };
    
    SomeClass *reference = [[SomeClass alloc] init];
    reference.number = 42;
    SomeClass *reference2 = reference;
    reference.number = 43;
    NSLog(@"The number in reference2 is %d", reference2.number);
    
    struct SomeStruct value = {};
    value.number = 42;
    struct SomeStruct value2 = value;
    value.number = 43;
    NSLog(@"The number in value2 is %d", value2.number);
复制代码

打印的结果以下:

The number in reference2 is 43
    The number in value2 is 42
复制代码

为何打印结果会不同?

代码 SomeClass *reference = [[SomeClass alloc] init] 在内存中建立了 SomeClass 类的一个新实例,而后将该实例的引用放到 reference 变量中。代码 reference2 = reference 将 reference 变量的值(实例的引用)赋值给新的 reference2 变量。而后 reference.number = 43 将两个变量指向的对象(同一个对象)的 number 属性修改成 43。 这就致使打印的 reference2 的值也是 43。

代码 struct SomeStruct value = {} 建立 SomeStruct 结构体的一个新实例并赋值给变量 value。代码 value2 = value 拷贝 value 的值到 变量 value2 中。每一个变量包含各自的数据块。而代码 value.number = 43 仅仅修改 value 变量的值。因此,value2 变量的值仍然是 42。

用 Swift 实现这个例子:

class SomeClass {
        var number: Int = 0
    }
    
    struct SomeStruct {
        var number: Int = 0
    }
    
    var reference = SomeClass()
    reference.number = 42
    var reference2 = reference
    reference.number = 43
    print("The number in reference2 is \(reference2.number)")
    
    var value = SomeStruct()
    value.number = 42
    var value2 = value
    value.number = 43
    print("The number in value2 is \(value2.number)")
复制代码

和以前同样,打印以下:

The number in reference2 is 43
    The number in value2 is 42
复制代码

使用值类型的经验

值类型不是新出的类型。可是对于不少人来讲,他们感受上很新。这是怎么回事?

大部分 Objective-C 代码不会用到 struct。咱们一般操做的是 CGRect 、 CGPoint ,不多本身定义结构体。一方面,结构体不实用,没法作函数式的引用赋值。在 Objective-C 中,正确保存对象的引用到 struct 中是很困难的,尤为是使用 ARC 的时候。

大部分语言没有相似 struct 结构体的东西。像 Python 和 JavaScript 这样“一切皆对象”的语言都只有引用类型。若是你是从这样的语言转到 Swift,值类型这个概念可能对你来讲更加陌生。

不过等一下!有一个地方几乎全部的语言都会使用值类型:数值(number)!只要你写过一段时间代码,不管是什么语言,确定能理解下面这段代码的行为:

var x = 42
    var x2 = x
    x++
    print("x=\(x) x2=\(x2)")
    // prints: x=43 x2=42
复制代码

这对咱们来讲是很是明显和天然的,咱们甚至没有意识到它的行为不同凡响。可是它确确实实是值类型。从你编程的第一天开始就一直在使用值类型,即便你没有意识到这一点。

因为许多语言的核心是“一切皆对象”,number 实际上是用引用类型来实现的。然而,它们是不可变引用类型,不可变引用类型和值类型的差别是很难察觉的。它们的行为和值类型同样,即便它们不是以这种方式实现。

这是理解值类型和引用类型的重要部分。就语言语义方面,区别是很重要的。当修改数据时,若是你的数据是不可变的,那么值类型/引用类型之间的区别就消失了,或者至少变成纯粹的性能问题而不是语义问题。

Objective-C 中也有相似的东西,就是标记指针(tagged pointers)。标记指针把对象直接存储在指针值中,所以它其实是值类型,拷贝指针至关于拷贝对象。Objective-C 的库只会把不可变类型存储到标记指针中,因此使用的时候感觉不到区别。有些 NSNumber 是引用类型,有些是值类型,可是使用上没有区别。

作出选择

既然咱们已经知道值类型是如何工做的,那么你本身的数据类型该用什么呢?

这二者之间的根本区别在于,当你使用 = 时会发生什么。值类型会获得该对象的副本,引用类型仅仅获得该对象的引用。

所以,决定使用哪个的基本问题是:是否须要拷贝?是否须要常常拷贝?

首先来看一些毫无争议的例子。Integer 显然是可拷贝的,它应该是值类型。网络套接字(Network sockets)明显是不可拷贝的,它应该是引用类型。再好比使用 (x, y) 实数对表示的坐标(Points)是可拷贝的,它应该是值类型。表明磁盘的控制器是明显不可拷贝的,它应该是引用类型。

有些类型理论上能够拷贝,可是这种拷贝可能不是你想要的。这种状况下,它们应该是引用类型。举个例子,屏幕上的按钮在代码层面能够拷贝,可是拷贝的按钮和原始按钮并不同。点击拷贝的按钮并不会触发原始按钮,拷贝的按钮在屏幕上的位置也和原始按钮不同。若是你须要把按钮当成参数传递,或者将它赋值给一个新变量,那你须要的是原始按钮的引用,只有明确声明的时候才进行拷贝。所以,按钮应该是引用类型。

视图和窗口控制器也相似。它们能够支持拷贝,但通常来讲这不是你指望的行为,它们应该是引用类型。

接着谈谈模型(model)类型。假设你有一个 User 类型,用来表示系统中的用户,而后用 Crime 类型来表示 User 的操做。这两个类型看起来均可以拷贝,能够设置成值类型。可是,若是你的程序须要更新 User 的 Crime 而且能把改动同步到其余代码,那最好用一个用户控制器(User Controller)来管理 User,显然这个用户控制器应该是引用类型。

集合是个有趣的例子。集合包括数组、字典、字符串等类型。它们是可拷贝的吗?显然是。是否须要常常拷贝?这就很差说了

大部分语言的回答是“No”,它们的集合是引用类型。好比 Objective-C、Java、Python、JavaScript 以及一些我能想到的语言。(一个例外是 C++ 的 STL 集合,可是 C++ 是语言中的疯子,它作的每件事都很奇怪。)

Swift 是可拷贝的。这意味着 Array、Dictionary 和 String 是结构体而不是类。能够将他们的拷贝做为参数来使用。若是拷贝付出的代价很小,这么作就彻底合理。Swift 为了实现这个功能花了很大功夫。。

嵌套类型

嵌套值类型和引用类型有四种方式。哪怕只用到了其中一种,你的生活都会变得更加有趣。

  1. 包含其余引用类型的引用类型,这没什么特别的。若是持有内部或外部值的引用,就能够修改这个值。改动会同步到全部持有者。
  2. 包含其余值类型的值类型,这样作的结果是一个更庞大的值类型。当内部值是外部值的一部分时,若是你将外部值存储到某个新地方,整个值类型都会被拷贝,包括内部值。若是你将内部值储存到新地方,那就只拷贝内部值。
  3. 包含值类型的引用类型,被引用的值会变大。外部值的引用能够操做整个对象,包括内部值。修改内部值时,外部值引用的持有者都会同步改动。若是你将内部值储存到新地方,它会被拷贝。
  4. 包含引用类型的值类型,这就有点复杂了。你可能会遇到意料以外的行为。这有利有弊,取决于你的使用方式。若是你将一个引用类型放到值类型中,而后拷贝这个值类型到一个新地方,拷贝中的内部对象的引用值是相同的,它们都指向相同的地方。下面是一个示例:
class Inner {
            var value = 42
        }
    
        struct Outer {
            var value = 42
            var inner = Inner()
        }
    
        var outer = Outer()
        var outer2 = outer
        outer.value = 43
        outer.inner.value = 43
        print("outer2.value=\(outer2.value) outer2.inner.value=\(outer2.inner.value)")
复制代码

打印以下:

outer2.value=42 outer2.inner.value=43
复制代码

outer2outer 的拷贝,它仅仅拷贝了 inner 的引用,所以两个结构体的 inner 共享一个存储空间。所以更新 outer.inner.value 的值会影响 outer2.inner.value 的值。神奇!

若是使用得当,上面的这种行为使编程变得很方便,它容许你建立一个支持写时复制的 struct,容许你不须要拷贝大量的数据就能够实现值语义。这就是 Swift 的集合工做机制,你也能够建立本身的集合。若是想了解更多,能够阅读 一块儿来构建 Swift Array

这种行为也至关危险。举个例子,你有一个可拷贝的 Person 类,因此它能够是 struct 类型,为了怀旧,你决定用 NSString 类型来保存姓名:

struct Person {
         var name: NSString
    }
复制代码

而后生成一对夫妇的实例,分别给每一个实例的姓名赋值:

let name = NSMutableString()
    name.appendString("Bob")
    name.appendString(" ")
    name.appendString("Josephsonson")
    let bob = Person(name: name)
    
    name.appendString(", Jr.")
    let bobjr = Person(name: name)
复制代码

打印他们的姓名:

print(bob.name)
    print(bobjr.name)
复制代码

结果以下:

Bob Josephsonson, Jr.
    Bob Josephsonson, Jr.
复制代码

喔!

发生了什么?与 Swift 中的 String 类型不一样,NSString 是一个引用类型,是不可变的,可是它有一个可变的子类 NSMutableString。构建 bob 时,生成了一个被 name 中字符串所持有的引用。随后改变 这个字符串时,改动被同步到了 bob 中。虽然 bob 是用 let 声明值类型,可是此处的赋值操做显然改变了 bob。事实上,这没有覆写 bob,只不过是改变了 bob 持有的引用的数据。由于 name 是 bob 的一部分数据,从语义上看,就好像覆写了 bob。

这种行为在 Objective-C 中一直存在。每一个有经验的 Objective-C 开发者都能避免这种行为。由于一个 NSString 实际上多是一个 NSMutableString。为了防止这种行为,能够声明一个 copy 的属性或者在初始化的时候显式的调用 copy 方法。在许多 Cocoa 的集合中能够发现这种作法。

Swift 的解决方法很简单:用值类型而不是引用类型。在这种状况下,声明 name 为 String 类型便可。这样就不用担忧无心中出现存储共享的问题。

有些状况下,解决方法可能没有这么简单。举个例子,你可能会建立一个 包含引用类型变量 view 的 struct,而且它不能改变为值类型。这也许表示你的类型不该该是 struct,由于你不管如何也不能实现值语义。

结论

移动值语义类型的数据时,新数据是原数据的拷贝。然而,引用语义类型的数据获得的是原数据的引用拷贝。这意味着你能够在任何地方经过引用覆写原数据。而值语义只能经过改变原数据来改变原数据的值。选择类型时,要考虑该类型是否适合拷贝和倾向于拷贝的固有类型。最后,注意值类型中嵌套的引用类型,若是你不留心将会发生一些糟糕的事情。

今天的内容到此结束,此次是真的结束了,下次再见。大家的建议对 Friday Q&A 是最好的鼓励,因此若是你关于这个主题有什么好的想法,请发邮件到这里

你喜欢这篇文章么?个人书里还有更多有意思的内容!第二卷 和 第三卷正在出售中!包括 ePub,PDF,纸质版,iBooks 和 Kindle,点击查看更多信息

相关文章
相关标签/搜索