首页 文章

为什么选择Struct Over Class?

提问于
浏览
408

使用Swift,来自Java背景,为什么要选择Struct而不是Class?看起来它们是相同的东西,使用Struct提供更少的功能 . 为什么选择呢?

16 回答

  • 28

    根据Swift中非常流行的WWDC 2015谈话定向编程(videotranscript),Swift提供了许多功能,使得结构在许多情况下比类更好 .

    如果结构相对较小且可复制,则结构是优选的,因为复制比具有与类相同的实例的多个引用更安全 . 将变量传递给多个类和/或在多线程环境中时,这一点尤为重要 . 如果您始终可以将变量的副本发送到其他位置,则无需担心其他位置会更改您变量的值 .

    使用Structs,更不用担心内存泄漏或多线程竞争访问/修改变量的单个实例 . (对于更具技术意识的人来说,例外情况是在闭包中捕获一个struct时,因为它实际上捕获了对实例的引用,除非你明确地将它标记为要复制) .

    类也可能变得臃肿,因为类只能从单个超类继承 . 这鼓励我们创建巨大的超级类,其中包含许多不同的能力,这些能力只是松散相关的 . 使用协议,特别是协议扩展,您可以为协议提供实现,允许您消除类实现此类行为的需要 .

    该演讲列出了这些场景首选的场景:

    复制或比较实例没有意义(例如,Window)实例生存期与外部效果相关(例如,TemporaryFile)实例只是“接收器” - 只写入外部状态的管道(例如,CGContext)

    它意味着结构应该是默认的,类应该是一个后备 .

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

    结构实例始终按值传递,类实例始终通过引用传递 . 这意味着它们适用于不同类型的任务 . 在考虑项目所需的数据结构和功能时,请确定是将每个数据结构定义为类还是结构 . 作为一般准则,考虑在适用这些条件中的一个或多个时创建结构:结构的主要目的是封装一些相对简单的数据值 . 当您分配或传递该结构的实例时,期望复制封装的值而不是引用是合理的 . 结构存储的任何属性本身都是值类型,也可以复制而不是引用它们 . 该结构不需要从另一个现有类型继承属性或行为 . 结构的良好候选者的示例包括:几何形状的大小,可能封装宽度属性和高度属性,两者都是Double类型 . 一种引用系列中的范围的方法,可能包含类型为Int的start属性和length属性 . 3D坐标系中的一个点,可能包含x,y和z属性,每个属性为Double . 在所有其他情况下,定义一个类,并创建要通过引用进行管理和传递的类的实例 . 实际上,这意味着大多数自定义数据结构应该是类,而不是结构 .

    这里声称我们应该默认使用类并仅在特定情况下使用结构 . 最终,您需要了解值类型与引用类型的真实含义,然后您可以就何时使用结构或类做出明智的决定 . 另外,请记住,这些概念总是在不断发展,Swift编程语言文档是在面向协议编程讲话之前编写的 .

  • 2

    由于struct实例是在堆栈上分配的,并且类实例是在堆上分配的,因此结构有时可以更快 .

    However, you should always measure it yourself and decide based on your unique use case.

    请考虑以下示例,该示例演示了使用 structclass 包装 Int 数据类型的两种策略 . 我使用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找到

    UPDATE (27 Mar 2018)

    截至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秒(快50,000,000倍)

    (我不再平均多次运行,因为差异非常小,低于5%)

    Note :如果没有整个模块优化,差异会大得多 . 如果有人能够指出实际的旗帜,我会很高兴的确实 .


    UPDATE (7 May 2016)

    截至Swift 2.2.1,Xcode 7.3,在iPhone 6S,iOS 9.3.1上运行Release版本,平均超过5次运行,Swift Compiler设置为 -O -whole-module-optimization

    • class 版本占用了2.159942142s

    • struct 版本耗时5.83E-08s(快37,000,000倍)

    Note :正如有人提到的那样,在实际场景中,结构中可能会有超过1个字段,我已经为10个字段而不是1个字符串添加了结构/类的测试 . 令人惊讶的是,结果变化不大 .


    ORIGINAL RESULTS (2014年6月1日):

    (在1个字段的结构/类上,而不是10)

    截至Swift 1.2,Xcode 6.3.2,在iPhone 5S,iOS 8.3上运行Release版本,平均超过5次运行

    • class 版本占用了9.788332333s

    • struct 版本花了0.010532942s(快了900倍)


    OLD RESULTS (来自未知时间)

    (在1个字段的结构/类上,而不是10)

    在我的MacBook Pro上发布版本:

    • class 版本耗时1.10082秒

    • struct 版本耗时0.02324秒(快50倍)

  • 58

    结构和类之间的相似之处 .

    我用简单的例子为此创造了要点 . https://github.com/objc-swift/swift-classes-vs-structures

    和差异

    1.继承 .

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

    class Vehicle{
    }
    
    class Car : Vehicle{
    }
    

    去上课 .

    2.通过

    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类,那么这是一个有效的表达式 . 因为在类中,不可变常量是对类本身的引用而不是其实例变量(除非那些变量定义为常量)

  • 141

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

    • structs获得一个自动初始化程序,您根本不需要在代码中维护它 .
    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")
    

    要在课程中获得此功能,您必须添加初始化程序,并且 maintain 是初始化程序...

    • Array 等基本集合类型是结构体 . 您在自己的代码中使用它们的次数越多,您就越习惯于通过值而不是引用 . 例如:
    func removeLast(var array:[String]) {
       array.removeLast()
       println(array) // [one, two]
    }
    
    var someArray = ["one", "two", "three"]
    removeLast(someArray)
    println(someArray) // [one, two, three]
    
    • 显然,不变性与可变性是一个很大的话题,但很多聪明的人认为不可变性 - 在这种情况下的结构 - 是更可取的 . Mutable vs immutable objects
  • 1

    一些优点:

    • 由于不可共享而自动进行线程安全
      由于没有isa和refcount,

    • 使用更少的内存(实际上通常是堆栈)

    • 方法总是静态调度,因此可以内联(尽管@final可以为类执行此操作)

    • 更容易推理(不需要像NSArray,NSString等那样典型的"defensively copy")与线程安全相同的原因

  • 3

    假设我们知道 Struct 是值类型而 Class 是引用类型 .

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

    基于mikeash's post

    ...让我们先看看一些极端明显的例子 . 整数显然是可复制的 . 它们应该是 Value 类型 . 网络套接字无法合理复制 . 它们应该是引用类型 . 像x,y对一样的点是可复制的 . 它们应该是 Value 类型 . 代表磁盘的控制器无法合理复制 . 那应该是一个参考类型 . 某些类型可以被复制,但它可能不是您想要一直发生的事情 . 这表明它们应该是参考类型 . 例如,可以在概念上复制屏幕上的按钮 . 副本与原始副本不完全相同 . 单击副本不会激活原件 . 副本不会占据屏幕上的相同位置 . 如果您传递按钮或将其放入新变量中,您可能需要引用原始按钮,而您只需要在明确请求时制作副本 . 这意味着您的按钮类型应该是引用类型 . 视图和窗口控制器是一个类似的例子 . 它们可能是可复制的,可以想象,但它几乎不是你想要做的 . 它们应该是引用类型 . 模型类型怎么样?您可能具有表示系统上用户的用户类型,或表示用户采取的操作的犯罪类型 . 这些都是可复制的,所以它们应该是值类型 . 但是,您可能希望程序中某个位置的用户犯罪更新对程序的其他部分可见 . 这表明您的用户应该由某种用户控制器管理,该用户控制器将是一种引用类型 . 收藏是一个有趣的案例 . 这些包括数组和字典,以及字符串 . 它们是可复制的吗?明显 . 是否经常复制你想要发生的事情?那不太清楚 . 大多数语言都说对此“不”并使其集合引用类型 . 这在Objective-C和Java,Python和JavaScript以及我能想到的几乎所有其他语言中都是如此 . (一个主要的例外是C与STL集合类型,但C是语言世界的狂热疯子,它做了一切奇怪的事情 . )Swift说“是”,这意味着像Array和Dictionary和String这样的类型是结构而不是类 . 它们在赋值时被复制,并在作为参数传递时被复制 . 这是一个完全明智的选择,只要副本很便宜,Swift很难完成 . ...

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

    所以不要有一个类的几个子类 . 使用符合协议的几个结构 .

  • 18

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

    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和描述被更改,因为传递给modify方法的参数实际上指向了flightA对象的内存地址(引用类型) .

    现在,如果我们打印出我们得到的FLightB实例的id和描述,

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

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

  • 2

    从值类型与引用类型的角度回答问题,从this Apple blog post看起来很简单:

    使用值类型[例如struct,enum] when:将实例数据与==进行比较有意义您希望副本具有独立状态数据将在跨多个线程的代码中使用使用引用类型[例如class] when:比较实例标识与===有意义你想创建共享的,可变的状态

    正如那篇文章中提到的,没有可写属性的类将与结构相同,并且(我将添加)一个警告:结构最适合线程安全模型 - 现代应用程序体系结构中越来越迫切的要求 .

  • 0

    对于继承并通过引用传递的类,结构体没有继承并且通过值传递 .

    在Swift上有很棒的WWDC Session ,这个具体的问题在其中一个中得到了非常详细的回答 . 请确保您观看这些内容,因为它可以让您比语言指南或iBook快得多 .

  • 4

    我不会说结构提供的功能较少 .

    当然,除了变异功能之外,自我是不可改变的,但这就是它 .

    只要你坚持每个类应该是抽象的或最终的好主意,继承就可以正常工作 .

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

    关于结构的好处是你可以使你的字段变得可变,而不创建共享的可变状态,因为写入时的副本负责:)

    这就是为什么以下示例中的属性/字段都是可变的,我不会在Java或C#或swift类中执行这些操作 .

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

    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)")
    }
    
  • 0

    在Swift中,引入了一种称为面向协议编程的新编程模式 .

    Creational Pattern:

    在swift中,Struct是一个自动克隆的 value types . 因此,我们获得了免费实现原型模式所需的行为 .

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


    Shallow copy 仅复制指向那些对象的引用,而 deep copy 复制对象的引用 .


    为每个 reference type 实现 deep copy 已成为一项繁琐的工作 . 如果类包含进一步的引用类型,我们必须为每个引用属性实现原型模式 . 然后我们必须通过实现 NSCopying 协议来实际复制整个对象图 .

    class Contact{
      var firstName:String
      var lastName:String
      var workAddress:Address // Reference type
    }
    
    class Address{
       var street:String
       ...
    }
    

    通过使用 structs and enums ,我们使代码更简单,因为我们不必实现复制逻辑 .

  • 14

    许多Cocoa API需要NSObject子类,这会强制您使用类 . 但除此之外,您可以使用Apple的Swift博客中的以下案例来决定是使用struct / enum值类型还是类引用类型 .

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

  • 11

    Structsvalue typeClassesreference type

    • 值类型比引用类型快

    • 值类型实例在多线程环境中是安全的,因为多线程可以在不必担心竞争条件或死锁的情况下改变实例

    • 值类型没有与引用类型不同的引用;因此没有内存泄漏 .

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

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

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

    • 您想要创建共享的可变状态 .

    更多信息也可以在Apple文档中找到

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


    Additional Information

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

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

    作为提醒,这里是Swift列表

    Value types:

    • 结构

    • 枚举

    • 元组

    • 原语(Int,Double,Bool等)

    • 集合(数组,字符串,字典,集)

    Reference types:

    • class

    • 任何来自NSObject的东西

    • 功能

    • 关闭

  • -2

    在这些答案中没有引起注意的一点是,持有类与结构的变量可以是 let ,同时仍然允许更改对象的属性,而不能使用结构执行此操作 .

    如果您不希望变量指向另一个对象,但仍需要修改该对象,即在您希望一个接一个地更新许多实例变量的情况下,这非常有用 . 如果它是一个结构,你必须允许使用 var 将变量重置为另一个对象才能执行此操作,因为Swift中的常量值类型正确地允许零变异,而引用类型(类)不会以这种方式运行 .

  • 1

    由于struct是值类型,你可以非常容易地创建存储到stack.Struct中的内存可以很容易地访问,并且在工作范围之后,它可以通过堆栈顶部的pop来轻松地从堆栈内存中释放出来 . 另一方面,类是存储在堆中的引用类型,并且在一个类对象中所做的更改将影响到其他对象,因为它们是紧密耦合的和引用类型 . 结构的所有成员都是公共的,而类的所有成员都是私有的 .

    struct的缺点是它不能被继承 .

  • 480
    • 结构和类是用户违反的数据类型

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

    • Class实现封装的主体

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

    • 类用于可重用性,而结构用于将数据分组到同一结构中

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

    • 类数据成员可以由参数less构造函数直接初始化,并由参数化构造函数指定

相关问题