关于swift:为什么选择Struct Over Class?

Why Choose Struct Over Class?

在一个公平的竞争环境,雨燕来自a Java背景,你为什么要选择A结构而不是类?似乎他们是同样的事情,一个结构提供较少的功能。为什么选择它?


根据非常流行的WWDC 2015 Talk Protocol-Oriented Programming in Swift(视频、脚本),Swift提供了许多功能,使结构在许多情况下比类更好。

如果结构相对较小且可复制,则更可取,因为复制比具有对同一实例的多个引用(与类相同)要安全得多。当将变量传递给许多类和/或多线程环境时,这一点尤其重要。如果您总是可以将变量的副本发送到其他地方,那么就不必担心其他地方会改变您下面变量的值。

对于结构,无需担心内存泄漏或多个线程竞相访问/修改变量的单个实例。(对于更具技术头脑的,例外情况是在闭包内捕获结构时,因为除非您明确地将其标记为要复制,否则实际上它正在捕获对实例的引用)。

类也可能变得膨胀,因为类只能从单个超类继承。这鼓励我们创造巨大的超类,包含许多不同的能力,只是松散相关。使用协议,特别是在可以为协议提供实现的协议扩展中,可以消除对类实现这种行为的需要。

本次讨论列出了这些首选类的场景:

  • Copying or comparing instances doesn't make sense (e.g., Window)
  • Instance lifetime is tied to external effects (e.g., TemporaryFile)
  • Instances are just"sinks"--write-only conduits to external state (e.g.CGContext)

它意味着结构应该是默认的,类应该是回退。

另一方面,Swift编程语言文档有些矛盾:

Structure instances are always passed by value, and class
instances are always passed by reference. This means that they are
suited to different kinds of tasks. As you consider the data
constructs and functionality that you need for a project, decide
whether each data construct should be defined as a class or as a
structure.

As a general guideline, consider creating a structure when one or more
of these conditions apply:

  • The structure’s primary purpose is to encapsulate a few relatively simple data values.
  • It is reasonable to expect that the encapsulated values will be copied rather than referenced when you assign or pass around an
    instance of that structure.
  • Any properties stored by the structure are themselves value types, which would also be expected to be copied rather than referenced.
  • The structure does not need to inherit properties or behavior from another existing type.

Examples of good candidates for structures include:

  • The size of a geometric shape, perhaps encapsulating a width property and a height property, both of type Double.
  • A way to refer to ranges within a series, perhaps encapsulating a start property and a length property, both of type Int.
  • A point in a 3D coordinate system, perhaps encapsulating x, y and z properties, each of type Double.

In all other cases, define a class, and create instances of that class
to be managed and passed by reference. In practice, this means that
most custom data constructs should be classes, not structures.

在这里,它声称我们应该默认只在特定情况下使用类和结构。最后,您需要了解值类型与引用类型的实际含义,然后就何时使用结构或类做出明智的决定。此外,请记住,这些概念一直在不断发展,在给出面向协议的编程谈话之前,编写了快速编程语言文档。


由于结构实例是在堆栈上分配的,而类实例是在堆上分配的,因此结构有时速度会大大加快。

但是,您应该总是自己测量它,并根据您独特的用例来决定。

考虑下面的示例,它演示了使用structclass包装Int数据类型的两种策略。我用10个重复值来更好地反映现实世界,那里有多个字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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)
}

性能是用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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、iOS 11.2.6上运行版本build,swift编译器设置为-O -whole-module-optimization

  • class版用了2.06秒
  • struct版用了4.17e-08秒(快了50000000倍)

(我不再平均多次运行,因为方差很小,低于5%)。

注意:如果没有整个模块优化,差异就不那么显著了。如果有人能指出国旗的实际作用,我会很高兴的。

更新(2016年5月7日):

从Swift 2.2.1、Xcode 7.3开始,运行版本构建在iPhone 6S、iOS 9.3.1上,平均运行5次以上,Swift编译器设置为-O -whole-module-optimization

  • class版2.159942142
  • struct版采用5.83e-08s(快37000000倍)

注意:正如有人提到的,在现实场景中,一个结构中可能会有一个以上的字段,我已经为10个字段而不是1个字段的结构/类添加了测试。令人惊讶的是,结果变化不大。

原始结果(2014年6月1日):

(在结构/类上运行,带有1个字段,而不是10个字段)

从Swift 1.2、Xcode 6.3.2开始,运行版本构建在iPhone 5S、iOS 8.3上,平均运行5次以上。

  • class版9.788332333秒
  • struct版用了0.010532942秒(快了900倍)

旧结果(未知时间)

(在结构/类上运行,带有1个字段,而不是10个字段)

在我的MacBook Pro上构建的版本:

  • class版用了1.10082秒。
  • struct版用了0.02324秒(快50倍)


结构和类之间的相似性。

我用简单的例子为这个创建了要点。https://github.com/objc-swift/swift-classes-vs-结构

与差异1。继承。

结构不能在swift中继承。如果你想要

1
2
3
4
5
class Vehicle{
}

class Car : Vehicle{
}

去上课。

2。路过

swift结构传递值,类实例传递引用。

上下文差异

结构常量和变量

示例(用于WWDC 2014)

1
2
3
4
5
6
struct Point{

   var x = 0.0;
   var y = 0.0;

}

定义名为Point的结构。

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

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

1
point.x = 5

但如果我把一个点定义为常数。

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

在这种情况下,整个点是不变的常数。

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


以下是需要考虑的其他原因:

  • 结构得到一个自动初始值设定项,您根本不需要在代码中维护它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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")
  • 要在类中得到这个,您必须添加初始值设定项,并维护初始化器…

  • Array这样的基本集合类型是结构。在自己的代码中使用它们的次数越多,就越习惯于通过值而不是引用来传递。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    func removeLast(var array:[String]) {
       array.removeLast()
       println(array) // [one, two]
    }

    var someArray = ["one","two","three"]
    removeLast(someArray)
    println(someArray) // [one, two, three]
  • 显然,不可变与不可变是一个巨大的话题,但许多聪明人认为不可变——本例中的结构——更可取。可变对象与不可变对象


  • 假设我们知道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

    1
    2
    3
    4
    5
    6
    7
    8
    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.
    ...

    此外,当必须重写函数的每个实例(即它们没有任何共享功能)时,不要使用类。

    所以没有一个类的几个子类。使用符合协议的多个结构。


    一些优势:

      百万千克1由于不可共享而自动线程安全百万千克1百万千克1由于没有isa和refcount,使用较少的内存(实际上通常是堆栈分配的)百万千克1百万千克1方法总是静态调度的,因此可以内联(尽管@final可以为类这样做)百万千克1百万千克1更容易推理(无需"防御性复制",这与NSarray、NSstring等的典型情况相同),原因与线程安全相同。百万千克1


    结构比类快得多。另外,如果需要继承,那么必须使用类。最重要的一点是类是引用类型,而结构是值类型。例如,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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  
    }

    现在让我们创建这两者的实例。

    1
    2
    3
    var flightA = Flight()

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

    现在让我们将这些实例传递给两个函数,它们修改ID、描述、目的地等。

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

    也,

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

    所以,

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

    现在,如果我们打印Flighta的ID和描述,我们会

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

    在这里,我们可以看到flighta的ID和描述被更改,因为传递给modify方法的参数实际上指向flighta对象(引用类型)的内存地址。

    现在,如果我们打印FlightB实例的ID和描述,

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

    在这里,我们可以看到flightb实例没有更改,因为在modifyflight2方法中,flight2的实际实例是传递而不是引用(值类型)。


    从价值类型和参考类型的角度回答这个问题,从这个苹果博客文章中,它看起来非常简单:

    Use a value type [e.g. struct, enum] when:

    • Comparing instance data with == makes sense
    • You want copies to have independent state
    • The data will be used in code across multiple threads

    Use a reference type [e.g. class] when:

    • Comparing instance identity with === makes sense
    • You want to create shared, mutable state

    正如那篇文章中提到的,没有可写属性的类将与结构的行为相同,但(我将添加)有一个警告:结构最适合线程安全模型,这是现代应用程序体系结构中日益迫切的要求。


    对于获得继承并通过引用传递的类,结构没有继承并通过值传递。

    有很多关于swift的WWDC会议,其中一个详细回答了这个特定的问题。一定要看这些,因为它会使你更快地达到速度,比语言指南或iBook。


    Structsvalue typeClassesreference type

    • 值类型比引用类型快
    • 值类型实例在多线程环境中是安全的,因为多个线程可以在不必担心的情况下改变实例关于比赛条件或僵局
    • 值类型没有与引用类型不同的引用;因此没有内存泄漏。

    在下列情况下使用value类型:

    • 如果希望副本具有独立状态,数据将在跨多个线程的代码

    在下列情况下使用reference类型:

    • 您希望创建共享的可变状态。

    更多信息也可以在苹果文档中找到。

    https://docs.swift.org/swift-book/languageguide/classesandstructures.html

    附加信息

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

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

    作为提醒,这里有一份SWIFT清单

    值类型:

    • 结构
    • 枚举
    • 元组
    • 原语(int、double、bool等)
    • 集合(数组、字符串、字典、集合)

    引用类型:

    • 等级
    • 任何来自nsobject的内容
    • 功能
    • 关闭

    In Swift, a new programming pattern has been introduced known as Protocol Oriented Programming.

    创造模式:

    在swift中,struct是自动克隆的值类型。因此,我们可以免费获得实现原型模式所需的行为。

    而类是引用类型,在分配期间不会自动克隆。为了实现原型模式,类必须采用NSCopying协议。

    浅复制只复制引用,该引用指向那些对象,而深复制复制复制对象的引用。

    为每个引用类型实现深度复制已经成为一项乏味的任务。如果类包含进一步的引用类型,我们必须为每个引用属性实现原型模式。然后我们必须通过实现NSCopying协议来实际复制整个对象图。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Contact{
      var firstName:String
      var lastName:String
      var workAddress:Address // Reference type
    }

    class Address{
       var street:String
       ...
    }

    通过使用结构和枚举,我们简化了代码,因为我们不必实现复制逻辑。


    我不会说结构提供的功能更少。

    当然,自我是不变的,除了在一个变异函数中,但这就是它。

    只要您坚持一个好的旧观念,即每个类都应该是抽象的或最终的,继承就可以很好地工作。

    将抽象类作为协议实现,将最终类作为结构实现。

    结构的好处在于,您可以在不创建共享可变状态的情况下使字段可变,因为写时复制可以做到这一点:)

    这就是为什么下面的例子中的属性/字段都是可变的,这是我在Java或C++或SWIFT类中不会做的。

    示例继承结构,在名为"example"的函数的底部有一点肮脏和直接的用法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    protocol EventVisitor
    {
        func visit(event: TimeEvent)
        func visit(event: StatusEvent)
    }

    protocol Event
    {
        var ts: Int64 { get set }

        func accept(visitor: EventVisitor)
    }

    struct TimeEvent : Event
    {
        var ts: Int64
        var time: Int64

        func accept(visitor: EventVisitor)
        {
            visitor.visit(self)
        }
    }

    protocol StatusEventVisitor
    {
        func visit(event: StatusLostStatusEvent)
        func visit(event: StatusChangedStatusEvent)
    }

    protocol StatusEvent : Event
    {
        var deviceId: Int64 { get set }

        func accept(visitor: StatusEventVisitor)
    }

    struct StatusLostStatusEvent : StatusEvent
    {
        var ts: Int64
        var deviceId: Int64
        var reason: String

        func accept(visitor: EventVisitor)
        {
            visitor.visit(self)
        }

        func accept(visitor: StatusEventVisitor)
        {
            visitor.visit(self)
        }
    }

    struct StatusChangedStatusEvent : StatusEvent
    {
        var ts: Int64
        var deviceId: Int64
        var newStatus: UInt32
        var oldStatus: UInt32

        func accept(visitor: EventVisitor)
        {
            visitor.visit(self)
        }

        func accept(visitor: StatusEventVisitor)
        {
            visitor.visit(self)
        }
    }

    func readEvent(fd: Int) -> Event
    {
        return TimeEvent(ts: 123, time: 56789)
    }

    func example()
    {
        class Visitor : EventVisitor
        {
            var status: UInt32 = 3;

            func visit(event: TimeEvent)
            {
                print("A time event: \(event)")
            }

            func visit(event: StatusEvent)
            {
                print("A status event: \(event)")

                if let change = event as? StatusChangedStatusEvent
                {
                    status = change.newStatus
                }
            }
        }

        let visitor = Visitor()

        readEvent(1).accept(visitor)

        print("status: \(visitor.status)")
    }

    许多CocoaAPI都需要nsObject子类,这迫使您使用类。但除此之外,您可以使用苹果的swift博客中的以下情况来决定是使用结构/枚举值类型还是类引用类型。

    https://developer.apple.com/swift/blog/?ID=10


    在这些答案中,有一点没有引起注意,即持有类与结构的变量可以是let,但仍允许对对象属性进行更改,而不能对结构进行更改。

    如果您不希望变量指向另一个对象,但仍然需要修改对象,即在有许多实例变量希望逐个更新的情况下,这是很有用的。如果它是一个结构,则必须允许使用var将变量重置为另一个对象,这样做是因为swift中的常量值类型正确地允许零突变,而引用类型(类)不这样做。


    因为struct是值类型,您可以很容易地创建存储到堆栈中的内存。struct可以很容易地访问,并且在工作范围结束后,可以很容易地从堆栈内存中释放struct,并从堆栈顶部弹出。另一方面,类是一种引用类型,它存储在堆中,在一个类对象中所做的更改将影响到另一个对象,因为它们是紧密耦合的引用类型。结构的所有成员都是公共的,而类的所有成员都是私有的。

    结构的缺点是它不能被继承。


    • 结构和类是用户定义的数据类型

    • 默认情况下,结构是公共的,而类是私有的

    • 类实现封装的主体

    • 类的对象在堆内存上创建

    • 类用于重用,而结构用于分组相同结构中的数据

    • 结构数据成员不能直接初始化,但可以由结构外部分配

    • 类数据成员可以直接用无参数初始化由参数化构造函数分配的