为什么选择Struct Over Class?

时间:2014-06-15 18:33:03

标签: swift class struct design-principles

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

17 个答案:

答案 0 :(得分:512)

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

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

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

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

该演讲列出了首选课程的场景:

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

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

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

  

结构实例总是按值和类传递   实例总是通过引用传递。这意味着他们是   适合不同类型的任务。在考虑数据时   您决定项目所需的构造和功能   是否应将每个数据结构定义为类或类   结构

     

作为一般准则,请考虑在一个或多个时创建结构   这些条件适用:

     
      
  • 该结构的主要目的是封装一些相对简单的数据值。
  •   
  • 可以合理地预期在分配或传递时,将复制封装的值而不是引用   该结构的一个例子。
  •   
  • 结构存储的任何属性本身都是值类型,也可以复制而不是引用它们。
  •   
  • 该结构不需要从另一个现有类型继承属性或行为。
  •   
     

结构的良好候选者的例子包括:

     
      
  • 几何形状的大小,可能封装了width属性和height属性,两者都是Double。
  •   
  • 一种引用系列中范围的方法,可能封装了一个类型为Int的起始属性和长度属性。
  •   
  • 3D坐标系中的一个点,可能包含x,y和z属性,每个属性为Double。
  •   
     

在所有其他情况下,定义一个类,并创建该类的实例   通过引用进行管理和传递。在实践中,这意味着   大多数自定义数据结构应该是类,而不是结构。

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

答案 1 :(得分:154)

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

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

请考虑以下示例,该示例演示了使用Intstruct包装class数据类型的两种策略。我使用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秒(快50,000,000倍)

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

注意:如果没有整个模块优化,差异会大得多。如果有人能够指出旗帜实际上做了什么,我会很高兴。


更新(2016年5月7日)

从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倍)

注意:有人提到在实际场景中,结构中可能会有多个字段,我已经为10个字段而不是1个字符串添加了结构/类的测试。 ,结果差异不大。


原始结果(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倍)

旧结果(来自未知时间)

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

在我的MacBook Pro上发布版本:

  • class版本耗时1.10082秒
  • struct版本花了0.02324秒(快了50倍)

答案 2 :(得分:60)

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

我用简单的例子为此创造了要点。 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类,那么这是一个有效的表达式。因为在一个类中,不可变常量是对类本身的引用而不是它的实例变量(除非那些变量定义为常量)

答案 3 :(得分:28)

以下是其他一些需要考虑的理由:

  1. 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")
    
  2. 要在课程中获得此功能,您必须添加初始值设定项,维护初个化程序...

    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]
      
    2. 显然,不变性和可变性是一个很大的话题,但很多聪明的人认为不可变性 - 在这种情况下结构 - 是更可取的。 Mutable vs immutable objects

答案 4 :(得分:20)

假设我们知道 Struct 值类型引用类型

如果您不知道值类型和引用类型是什么,请参阅What's the difference between passing by reference vs. passing by value?

基于mikeash's post

  

......让我们首先看一些极端明显的例子。整数是   显然可复制。它们应该是价值类型。网络套接字无法实现   明智地复制。它们应该是引用类型。点,如x,y   对,可复制。它们应该是价值类型。一个控制器   表示磁盘无法合理复制。那应该是一个参考   类型。

     

某些类型可以复制,但可能不是您想要的   一直发生。这表明它们应该被引用   类型。例如,屏幕上的按钮可以在概念上被复制。   副本与原件不完全相同。点击一下   复制不会激活原件。副本不会占用相同的   屏幕上的位置。如果您通过按钮或将其放入   新变量你可能想要引用原始按钮,和   您只需要在明确请求时制作副本。那   表示您的按钮类型应该是引用类型。

     

视图和窗口控制器是一个类似的例子。他们可能是   可以想象,这是可复制的,但它几乎不是你想要做的。   它们应该是参考类型。

     

模型类型怎么样?您可能拥有表示用户的用户类型   在您的系统上,或表示由某人采取的行动的犯罪类型   用户。这些都是可复制的,所以它们应该是有价值的   类型。但是,您可能希望更新用户犯罪   程序的其他部分可以看到程序中的一个位置。   这表明您的用户应该由某种用户管理   控制器,它将是一个参考类型。 e.g

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

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

收藏是一个有趣的案例。这些包括数组之类的东西   和字典,以及字符串。它们是可复制的吗?明显。是   复制你想要轻松经常发生的事情?那更少   清楚。

     

大多数语言都说" no"对此,并使他们的集合参考   类型。在Objective-C和Java以及Python和JavaScript中都是如此   几乎我能想到的所有其他语言。 (一个主要的例外   是C ++与STL集合类型,但C ++是疯狂的疯子   语言世界奇怪地做了一切。)

     斯威夫特说"是的,"这意味着像Array和Dictionary这样的类型   字符串是结构而不是类。他们被复制作业,   并将它们作为参数传递。这是一个完全明智的选择   只要副本很便宜,Swift就会非常努力   完成。   ...

此外,当您必须覆盖某个功能的每个实例,即他们没有任何共享功能时,请不要使用类。

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

答案 5 :(得分:18)

一些优点:

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

答案 6 :(得分:12)

结构比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的实际实例是传递而不是引用(值类型)。

答案 7 :(得分:4)

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

  

使用值类型[例如struct,enum] when:

     
      
  • 比较实例数据与==有意义
  •   
  • 您希望副本具有独立状态
  •   
  • 数据将用于跨多个线程的代码
  •   
     

使用参考类型[例如课时]:

     
      
  • 比较实例标识与===有意义
  •   
  • 您想创建共享的可变状态
  •   

正如那篇文章中所提到的,没有可写属性的类将与结构具有相同的行为,(我将添加)一个警告:结构最适合线程安全模型 - 越来越多现代应用程序架构中迫在眉睫的要求。

答案 8 :(得分:3)

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

在Swift上有很棒的WWDC会议,这个具体的问题在其中一个中得到了详细的回答。请确保您观看这些内容,因为它可以让您比语言指南或iBook更快地加快速度。

答案 9 :(得分:2)

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

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

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

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

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

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

示例继承结构,在名为&#34的函数的底部有一些简单易用的用法;示例":

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

答案 10 :(得分:2)

  

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

创作模式:

在swift中,Struct是一个值类型,它们会被自动克隆。因此,我们获得了免费实现原型模式所需的行为。

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

浅拷贝仅重复指向那些对象的引用,而深拷贝重复对象的引用。

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

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

class Address{
   var street:String
   ...
} 

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

答案 11 :(得分:2)

Structsvalue type,而Classesreference type

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

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

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

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

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

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

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


其他信息

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

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

提醒一下,这里是Swift列表

值类型:

  • 结构
  • 枚举
  • 元组
  • 原语(整数,双精度,布尔等)
  • 集合(数组,字符串,字典,集合)

引用类型:

  • 班级
  • 一切都来自NSObject
  • 功能
  • 关闭

答案 12 :(得分:1)

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

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

答案 13 :(得分:1)

结构与类

[Stack vs Heap]
[Value vs Reference type]

Struct首选。但默认情况下 Struct 并不能解决所有问题。通常您可以听到 value type 在堆栈上分配,但并非总是如此。只有局部变量被分配到栈上

//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,因为它没有说明有用的信息

在编译 SIL(Swift Intermediate Language) 时可以优化你的代码

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(堆上的分配)

答案 14 :(得分:0)

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

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

答案 15 :(得分:0)

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

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

答案 16 :(得分:-7)

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

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

  • Class实现封装的原理

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

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

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

  • 类数据成员可以通过参数less直接初始化 构造函数,并由参数化构造函数分配