为什么选择结构体而不是类?

562

在使用Swift时玩耍,来自Java背景,为什么要选择结构体而不是类?似乎它们是相同的东西,只是结构体提供的功能较少。那为什么要选择它呢?


13
在代码中传递结构体时,它们总是被复制,而不使用引用计数。 - holex
4
我会说,结构体更适合用于保存数据而不是逻辑。用Java术语来说,可以将结构体想象为“值对象”。 - Vincent Guerci
6
我很惊讶在这整个对话中没有直接提到写时复制,也称为延迟复制。由于这种设计,对于结构体复制性能的任何担忧基本上都是无关紧要的。 - David James
5
选择结构体还是类并不是一个主观问题,有具体的原因来选择其中之一。 - David James
1
我强烈建议查看为什么数组不是线程安全的。这与数组和结构体都是值类型有关。所有答案都提到,使用结构体/数组/值类型永远不会出现线程安全问题,但实际上还存在一种特殊情况。 - mfaani
显示剩余3条评论
18个回答

624
根据非常受欢迎的WWDC 2015演讲《Swift中的协议导向编程》(videotranscript),Swift提供了许多功能,使得结构体在许多情况下比类更好。
如果结构体相对较小且可复制,那么复制比使用类引用同一实例安全得多。当将变量传递给多个类和/或多线程环境时,这尤其重要。如果您始终可以将变量的副本发送到其他位置,就不必担心其他位置在您之下更改变量的值。
使用结构体,我们无需过多担心内存泄漏或多个线程竞争访问/修改单个变量实例。(对于更加技术性的人来说,唯一的例外是在闭包中捕获结构体,因为那时实际上是捕获了对实例的引用,除非您明确标记要进行复制)。

类也可能变得臃肿,因为一个类只能继承自一个超类。这鼓励我们创建包含许多不太相关的能力的庞大超类。使用协议,特别是通过协议扩展提供实现的方式,可以消除类需要实现此类行为的需求。

演讲概述了以下情况下优先选择类:

  • 复制或比较实例没有意义(例如Window
  • 实例生命周期与外部影响相关(例如TemporaryFile
  • 实例只是“接口”——只写的对外状态通道(例如CGContext

这意味着结构体应该是默认选择,而类应作为备选。

另一方面,The Swift Programming Language文档有些自相矛盾:

结构实例总是按值传递,而类实例总是按引用传递。这意味着它们适用于不同类型的任务。在考虑项目所需的数据结构和功能时,请决定每个数据结构应该定义为类还是结构。
作为一般准则,在以下情况下考虑创建一个结构:
- 结构的主要目的是封装一些相对简单的数据值。 - 当你分配或传递该结构的实例时,可以合理地预期封装的值将被复制而不是引用。 - 由结构存储的任何属性本身都是值类型,也可以预期将其复制而不是引用。 - 结构不需要从另一个现有类型继承属性或行为。
适合使用结构的示例包括:
- 几何形状的大小,可能封装了一个宽度属性和一个高度属性,都是Double类型。 - 引用系列中的范围的方式,可能封装了一个起始属性和一个长度属性,都是Int类型。 - 三维坐标系中的一个点,可能封装了x、y和z属性,每个属性都是Double类型。
在所有其他情况下,请定义一个类,并创建该类的实例以进行管理和按引用传递。实际上,这意味着大多数自定义数据结构应该是类,而不是结构。
这里声称我们应该默认使用类,并仅在特定情况下使用结构体。最终,你需要理解值类型和引用类型的真实世界影响,然后才能做出明智的决策,何时使用结构体或类。此外,请记住这些概念始终在不断发展,Swift编程语言文档是在进行面向协议编程讲座之前编写的。

15
@ElgsQianChen 这篇文章的主要观点是应该默认选择结构体,只有在必要时才使用类。结构体更加安全和没有 bug,特别是在多线程环境下。当然,你可以始终使用类来代替结构体,但结构体更为可取。 - drewag
21
@ drewag,这似乎与其所说的完全相反。它是在说一个类应该是你使用的默认值,而不是一个结构体。"In practice, this means that most custom data constructs should be classes, not structures." 你能解释一下吗?他们给出了一组特定的规则,说明何时应该使用结构体,并且基本上说“在所有其他情况下,类更好。” - Matt
48
最后一行应该写成:“我的个人建议与文档相反:”,然后就是一个非常好的回答了! - Dan Rosenstark
5
《Swift 2.2》书籍仍然建议在大多数情况下使用类(classes)。 - David James
8
结构体(Struct)相对于类(Class),确实能降低复杂度。但是,当结构体成为默认选择时,其对内存使用的影响是什么?当事物被无处不在地复制而非引用时,它应该会增加应用程序的内存使用量。难道不是吗? - MadNik
显示剩余17条评论

180

原本这篇回答是关于struct和class在性能方面的区别。不幸的是,我使用的测量方法引起了太多争议。我将它保留在下面,但请不要过多解读。我认为,在这么多年之后,Swift社区已经清楚地认识到,由于其简单性和安全性,struct(以及enum)始终是首选。

如果性能对你的应用程序很重要,请自行进行测量。我仍然认为大多数情况下,struct性能优于class,但最好的答案就像评论中有人所说的那样:这取决于情况。

=== 旧回答 ===

由于struct实例分配在堆栈上,而class实例分配在堆上,因此struct有时可能会快得多。

但是,您应该始终自行测量并根据自己独特的用例进行决策。

考虑以下示例,其中演示了使用struct和class包装Int数据类型的2种策略。我使用了10个重复值来更好地反映现实情况,其中有多个字段。

class Int10Class {
    let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int
    init(_ val: Int) {
        self.value1 = val
        self.value2 = val
        self.value3 = val
        self.value4 = val
        self.value5 = val
        self.value6 = val
        self.value7 = val
        self.value8 = val
        self.value9 = val
        self.value10 = val
    }
}

struct Int10Struct {
    let value1, value2, value3, value4, value5, value6, value7, value8, value9, value10: Int
    init(_ val: Int) {
        self.value1 = val
        self.value2 = val
        self.value3 = val
        self.value4 = val
        self.value5 = val
        self.value6 = val
        self.value7 = val
        self.value8 = val
        self.value9 = val
        self.value10 = val
    }
}

func + (x: Int10Class, y: Int10Class) -> Int10Class {
    return IntClass(x.value + y.value)
}

func + (x: Int10Struct, y: Int10Struct) -> Int10Struct {
    return IntStruct(x.value + y.value)
}

性能是使用测量的

// Measure Int10Class
measure("class (10 fields)") {
    var x = Int10Class(0)
    for _ in 1...10000000 {
        x = x + Int10Class(1)
    }
}

// Measure Int10Struct
measure("struct (10 fields)") {
    var y = Int10Struct(0)
    for _ in 1...10000000 {
        y = y + Int10Struct(1)
    }
}

func measure(name: String, @noescape block: () -> ()) {
    let t0 = CACurrentMediaTime()
    
    block()
    
    let dt = CACurrentMediaTime() - t0
    print("\(name) -> \(dt)")
}
代码可在https://github.com/knguyen2708/StructVsClassPerformance找到。
更新(2018年3月27日): 截至Swift 4.0,Xcode 9.2,在iPhone 6S上运行Release版本的iOS 11.2.6上,Swift编译器设置为-O -whole-module-optimization: - class版本花费了2.06秒 - struct版本花费了4.17e-08秒(快了5000万倍)
(我不再平均多次运行,因为差异非常小,小于5%)
注意:如果没有整个模块优化,差异会小得多。如果有人能指出该标志实际上是做什么的,我将不胜感激。
更新(2016年5月7日): 截至Swift 2.2.1,Xcode 7.3,在iPhone 6s上运行Release版本的iOS 9.3.1上,平均5次运行,Swift编译器设置为-O -whole-module-optimization: - class版本需要2.159942142秒 - struct版本需5.83E-08秒(快3700万倍)
注意:由于某人提到在实际情况下,结构体中可能会有多个字段,因此我已添加了测试,以测试具有10个字段而不是1个字段的结构体/类。令人惊讶的是,结果并没有太大差异。
原始结果(2014年6月1日): (在结构体/类上运行,而不是10个) 截至Swift 1.2,Xcode 6.3.2,在iPhone 5S上运行Release版本的iOS 8.3,平均5次运行: - class版本需要9.788332333秒 - struct版本需0.010532942秒(快了900倍)
旧结果(时间未知): (在结构体/类上运行,而不是10个) 使用我的MacBook Pro发行版: - class版本需要1.10082秒 - struct版本需要0.02324秒(快50倍)

28
没错,但是复制一堆结构体会比复制一个对象的引用慢。换句话说,复制一个指针要比复制任意大小的内存块快。 - Tylerc230
14
这个测试并不是一个好的例子,因为结构体上只有一个变量。请注意,如果您添加了多个值和一两个对象,结构体版本将与类版本相当。您添加的变量越多,结构体版本就会变得越慢。 - joshrl
6
@joshrl,我理解你的观点,但是一个例子是否“好”取决于具体情况。这段代码是从我的应用程序中提取出来的,因此它是一个有效的用例,并且使用结构体极大地提高了我的应用程序的性能。只是这可能不是一个常见的用例(对于大多数应用程序而言,人们并不关心数据传递的速度有多快,因为瓶颈通常发生在其他地方,例如网络连接。当你拥有拥有GHz处理器和GB内存的设备时,优化并不是那么关键)。 - Khanh Nguyen
29
据我所了解,Swift 中的复制操作是在“写入”时进行优化的。这意味着除非新的副本将被修改,否则不会进行物理内存复制。 - Matjan
8
2016年的测试结果看起来有缺陷。iPhone 6S中的A9处理器主频为1.8 GHz,意味着一个时钟周期需要约5.6E-10秒。你的基准测试用了5.83E-08秒,换句话说:大约100个时钟周期。这意味着你的CPU每个周期进行了100,000次加法操作。我的猜测是编译器可能已经删除了整个代码块,因为它认为结果永远不会被使用,或者它认为你没有使用任何中间结果,仅仅用预先在编译时计算得出的静态结果替换了最终结果。无论哪种情况,你都可能只是测量了测量开销本身。 - Marten
显示剩余11条评论

65

结构体和类之间的相似之处。

我创建了一个简单示例的代码片段。 https://github.com/objc-swift/swift-classes-vs-structures

以及它们之间的区别

1. 继承。

在 Swift 中,结构体无法继承。如果你想要继承,那么你需要使用类。

class Vehicle{
}

class Car : Vehicle{
}

参加一堂课程。

2. 传值与传引用(Pass By)

Swift中,结构体采用传值方式,而类实例采用传引用方式。

语境差异

结构体常量和变量

示例(用于WWDC 2014)

struct Point{
 
   var x = 0.0;
   var y = 0.0;

} 

定义了一个名为 Point 的结构体。

var point = Point(x:0.0,y:2.0)

现在如果我尝试改变x,这是一个有效的表达式。

point.x = 5

但是如果我将一个点定义为常量。

let point = Point(x:0.0,y:2.0)
point.x = 5 //This will give compile time error.

在这种情况下,整个点是不可变常量。

如果我使用了一个Point类,那么这就是一个有效的表达式。因为在类中,不可变常量是对类本身的引用,而不是它的实例变量(除非这些变量被定义为常量)。


你可以在Swift中继承结构体。https://gist.github.com/AliSoftware/9e4946c8b6038572d678 - thatguy
13
以上要点是关于我们如何实现结构体的继承方式。你会看到类似 A: B 的语法,这表示结构体 A 实现了协议 B。苹果的文档明确指出结构体不支持纯继承,事实也确实如此。 - MadNik
2
你的最后一段话写得很好。我一直知道你可以改变常量……但有时候我看到你不能改变,所以感到困惑。这种区别使它更加明显。 - mfaani

38
假设我们已知 Struct 是一个值类型,而 Class 是一个引用类型

如果您不知道什么是值类型和引用类型,请参见按引用传递与按值传递的区别是什么?

基于 mikeash的帖子

... Let's look at some extreme, obvious examples first. Integers are obviously copyable. They should be value types. Network sockets can't be sensibly copied. They should be reference types. Points, as in x, y pairs, are copyable. They should be value types. A controller that represents a disk can't be sensibly copied. That should be a reference type.

Some types can be copied but it may not be something you want to happen all the time. This suggests that they should be reference types. For example, a button on the screen can conceptually be copied. The copy will not be quite identical to the original. A click on the copy will not activate the original. The copy will not occupy the same location on the screen. If you pass the button around or put it into a new variable you'll probably want to refer to the original button, and you'd only want to make a copy when it's explicitly requested. That means that your button type should be a reference type.

View and window controllers are a similar example. They might be copyable, conceivably, but it's almost never what you'd want to do. They should be reference types.

What about model types? You might have a User type representing a user on your system, or a Crime type representing an action taken by a User. These are pretty copyable, so they should probably be value types. However, you probably want updates to a User's Crime made in one place in your program to be visible to other parts of the program. This suggests that your Users should be managed by some sort of user controller which would be a reference type. e.g

struct User {}
class UserController {
    var users: [User]

    func add(user: User) { ... }
    func remove(userNamed: String) { ... }
    func ...
}

Collections are an interesting case. These include things like arrays and dictionaries, as well as strings. Are they copyable? Obviously. Is copying something you want to happen easily and often? That's less clear.

Most languages say "no" to this and make their collections reference types. This is true in Objective-C and Java and Python and JavaScript and almost every other language I can think of. (One major exception is C++ with STL collection types, but C++ is the raving lunatic of the language world which does everything strangely.)

Swift said "yes," which means that types like Array and Dictionary and String are structs rather than classes. They get copied on assignment, and on passing them as parameters. This is an entirely sensible choice as long as the copy is cheap, which Swift tries very hard to accomplish. ...

我个人不会使用这样的类名。我通常使用UserManager而不是UserController,但是想法是相同的。
此外,当您必须覆盖每个函数实例时,请勿使用类,即它们没有任何共享功能。
因此,不要使用类的几个子类。使用符合协议的几个结构体。

另一个使用结构体的合理情况是当您想要对旧模型和新模型进行差异/比较时。使用引用类型无法直接完成此操作。使用值类型,变异不会被共享。


3
我很高兴能为您提供所需的解释。写得不错 :) - androCoder-BD
非常有用的控制器示例。 - Ask P
1
@AskP 我给 Mike 本人发了电子邮件,得到了那个额外的代码片段 :) - mfaani
多年的简单字符串编程经验。谢谢。 - tanmoy

30

以下是考虑的其他原因:

  1. 结构体会自动初始化,您完全不需要在代码中维护它。

struct MorphProperty {
   var type : MorphPropertyValueType
   var key : String
   var value : AnyObject

   enum MorphPropertyValueType {
       case String, Int, Double
   }
 }

 var m = MorphProperty(type: .Int, key: "what", value: "blah")

为了将其放入类中,您需要添加初始化程序并 维护 初始化程序...

  1. 基本的集合类型(如Array)是结构体。您在自己的代码中使用它们越多,就越习惯按值传递而不是引用传递。例如:

func removeLast(var array:[String]) {
   array.removeLast()
   println(array) // [one, two]
}

var someArray = ["one", "two", "three"]
removeLast(someArray)
println(someArray) // [one, two, three]
  • 显然,不可变性和可变性是一个很大的话题,但许多聪明的人认为在这种情况下不可变性——即结构体——更可取。可变 vs 不可变对象


  • 5
    确实,你会得到自动初始化器。当所有属性都是可选类型时,你也会得到一个空的初始化器。但是,如果你在一个框架中有一个结构体,并且希望在internal之外使用它,你需要自己编写初始化器。 - Abizern
    2
    @Abizern 确认了 - https://dev59.com/g18d5IYBdhLWcg3w-mTX#26224873 - 真的很烦人。 - Dan Rosenstark
    我记得读过有关它的好理由,所以这是预期行为。但具体原因我记不清了。 - Abizern
    2
    @Abizern,Swift 中的每个东西都有很好的理由,但是每当某个东西在一个地方是正确的而在另一个地方不正确时,开发人员就必须了解更多的内容。我想这就是我应该说“在这样一种具有挑战性的语言中工作是令人兴奋的”的地方。 - Dan Rosenstark
    4
    我可以补充一点,结构体的有用之处并不仅限于其不变性(尽管这是一件很好的事情)。你可以改变结构体,但你必须将方法标记为 mutating ,以便明确表示哪些函数会更改它们的状态。但是,它们作为 值类型 的本质非常重要。如果你使用 let 声明一个结构体,你将无法对其调用任何可变函数。WWDC 15 上关于通过值类型进行更好编程的视频是一个绝佳资源。 - Abizern
    1
    感谢@Abizern,读了你的评论后我终于明白了。对于对象来说,let和var并没有太大的区别,但对于结构体来说却是巨大的。感谢你指出这一点。 - Dan Rosenstark

    22

    一些优势:

    • 由于不可共享,自动线程安全
    • 由于没有isa和refcount(实际上通常是堆栈分配),所以使用更少的内存
    • 方法始终静态分派,因此可以内联(尽管@final可以为类执行此操作)
    • 由于与线程安全相同的原因,更容易理解(无需像NSArray、NSString等一样进行“防御性复制”)

    不确定是否超出了这个答案的范围,但您能否解释一下“方法始终是静态分派”的观点(或提供链接)? - Dan Rosenstark
    3
    可以的。我也可以附带一个警告。动态派发的目的是在不预先知道要使用哪个实现时选择一种实现。在Swift中,这可能是由于继承(可能会在子类中被覆盖),或者由于函数是泛型的(你不知道泛型参数将是什么)。结构体不能从中继承,并且整个模块优化和泛型特化大多数消除了未知的泛型,因此方法可以直接调用而不必查找要调用的内容。未经专门化的通用程序对结构体仍然进行动态派发。 - Catfish_Man
    1
    谢谢,解释得非常好。那么我们期望更快的运行速度,还是从IDE角度减少歧义,或者两者都有? - Dan Rosenstark
    1
    大多数情况下是前者。 - Catfish_Man
    请注意,如果您通过协议引用结构体,则方法不会静态分派。 - Cristik

    18

    结构体值类型,而引用类型

    • 值类型比引用类型更快
    • 值类型的实例在多线程环境中是安全的,因为多个线程可以对实例进行修改,而不必担心竞争条件或死锁
    • 与引用类型不同,值类型没有引用,因此不存在内存泄漏问题。

    当:

    • 您希望副本具有独立状态,并且数据将在多个线程的代码中使用时,请使用类型

    当:

    • 您想创建共享可变状态时,请使用引用类型。

    进一步的信息也可以在苹果文档中找到

    https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html


    附加信息

    Swift值类型存储在堆栈中。在进程中,每个线程都有自己的堆栈空间,因此其他线程将无法直接访问您的值类型。因此,没有竞争条件、锁定、死锁或任何相关的线程同步复杂性。

    值类型不需要动态内存分配或引用计数,这两者都是昂贵的操作。同时,值类型上的方法是静态分派的。这些优势在性能方面给值类型创造了巨大的优势。

    以下是 Swift 的值类型列表:

    值类型:

    • 结构体
    • 枚举
    • 元组
    • 基本类型(Int、Double、Bool 等)
    • 集合类型(数组、字符串、字典、集合)

    引用类型:

    • 任何来自 NSObject 的东西
    • 函数
    • 闭包

    在多线程环境下这个函数是不安全的。如果对象实例同时被多个线程访问,此函数可能仍然会返回true。因此,您只能从具有适当线程同步的可变方法中调用此函数。这将确保isKnownUniquelyReferenced(_:)仅在实际只有一个访问器或存在竞争条件(已定义为未定义行为)时才返回true。 - john07

    14

    结构体比类更快,如果需要继承,则必须使用类。最重要的一点是,类是引用类型,而结构体是值类型。例如:

    class Flight {
        var id:Int?
        var description:String?
        var destination:String?
        var airlines:String?
        init(){
            id = 100
            description = "first ever flight of Virgin Airlines"
            destination = "london"
            airlines = "Virgin Airlines"
        } 
    }
    
    struct Flight2 {
        var id:Int
        var description:String
        var destination:String
        var airlines:String  
    }
    

    现在让我们创建两个实例。

    var flightA = Flight()
    
    var flightB = Flight2.init(id: 100, description:"first ever flight of Virgin Airlines", destination:"london" , airlines:"Virgin Airlines" )
    

    现在让我们将这些实例传递给两个函数,这些函数会修改id、description、destination等内容。

    func modifyFlight(flight:Flight) -> Void {
        flight.id = 200
        flight.description = "second flight of Virgin Airlines"
        flight.destination = "new york"
        flight.airlines = "Virgin Airlines"
    }
    

    另外,

    func modifyFlight2(flight2: Flight2) -> Void {
        var passedFlight = flight2
        passedFlight.id = 200
        passedFlight.description = "second flight from virgin airlines" 
    }
    

    所以,

    modifyFlight(flight: flightA)
    modifyFlight2(flight2: flightB)
    

    现在,如果我们打印flightA的id和描述,我们会得到:

    id = 200
    description = "second flight of Virgin Airlines"
    

    在这里,我们可以看到FlightA的id和description已经被更改,因为传递给修改方法的参数实际上是指向flightA对象(引用类型)的内存地址。

    现在,如果我们打印FLightB实例的id和description,我们会得到:

    id = 100
    description = "first ever flight of Virgin Airlines"
    

    在这里我们可以看到,FlightB实例没有被改变,因为在modifyFlight2方法中传递的是Flight2的实际实例而不是引用(值类型)。


    2
    你从未创建过FlightB的实例。 - David Seek
    1
    那你为什么要谈论FlightB呢?“在这里我们可以看到,FlightB实例没有被改变。” - David Seek
    @ManojKarki,非常好的回答。只是想指出您声明了flightA两次,而我认为您想要声明FlightA,然后是FlightB。 - ScottyBlades

    7
    从值类型和引用类型的角度回答这个问题,根据苹果公司的这篇博客文章,似乎非常简单:
    使用值类型(例如结构体、枚举)当: - 用==比较实例数据是有意义的 - 您希望副本具有独立状态 - 数据将在跨多个线程的代码中使用
    使用引用类型(例如类)当: - 使用===比较实例标识时有意义 - 您想创建共享的可变状态
    正如该文章所述,一个没有可写属性的类将与结构体完全相同,但要注意:结构体最适合于线程安全模型——这是现代应用程序架构中越来越迫切的需求。

    5

    结构体 vs 类

    [堆 vs 栈]
    [值类型 vs 引用类型]

    结构体更加可取。但是结构体并不能默认解决所有问题。通常你会听到值类型被分配在栈上,但这并不总是正确的。只有局部变量才会被分配在栈上。

    //simple blocks
    struct ValueType {}
    class ReferenceType {}
    
    struct StructWithRef {
        let ref1 = ReferenceType()
    }
    
    class ClassWithRef {
        let ref1 = ReferenceType()
    }
    
    func foo() {
        
        //simple  blocks
        let valueType1 = ValueType()
        let refType1 = ReferenceType()
        
        //RetainCount
        //StructWithRef
        let structWithRef1 = StructWithRef()
        let structWithRef1Copy = structWithRef1
        
        print("original:", CFGetRetainCount(structWithRef1 as CFTypeRef)) //1
        print("ref1:", CFGetRetainCount(structWithRef1.ref1)) //2 (originally 3)
        
        //ClassWithRef
        let classWithRef1 = ClassWithRef()
        let classWithRef1Copy = classWithRef1
        
        print("original:", CFGetRetainCount(classWithRef1)) //2 (originally 3)
        print("ref1:", CFGetRetainCount(classWithRef1.ref1)) //1 (originally 2)
         
    }
    

    *你不应该使用/依赖于retainCount,因为它并不提供有用的信息。
    要检查堆栈或堆
    在编译时,Swift中间语言(SIL)可以优化你的代码。
    swiftc -emit-silgen -<optimization> <file_name>.swift
    //e.g.
    swiftc -emit-silgen -Onone file.swift
    
    //emit-silgen -> emit-sil(is used in any case)
    //-emit-silgen           Emit raw SIL file(s)
    //-emit-sil              Emit canonical SIL file(s)
    //optimization: O, Osize, Onone. It is the same as Swift Compiler - Code Generation -> Optimization Level
    

    在那里你可以找到alloc_stack(堆栈分配)和alloc_box(堆分配)。

    [优化级别(SWIFT_OPTIMIZATION_LEVEL)]


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接