为什么选择Struct Over Class?

blu*_*l2k 465 struct class design-principles swift

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

dre*_*wag 531

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

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

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

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

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

  • 复制或比较实例没有意义(例如,Window)
  • 实例生命周期与外部效应相关(例如,TemporaryFile)
  • 实例只是"下沉" - 外部状态的只写管道(例如CGContext)

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

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

结构实例始终按值传递,类实例始终通过引用传递.这意味着它们适用于不同类型的任务.在考虑项目所需的数据结构和功能时,请确定是将每个数据结构定义为类还是结构.

作为一般准则,考虑在满足以下一个或多个条件时创建结构:

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

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

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

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

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

  • 最后一行应该说,"我的个人建议与文档相反:"......然后这是一个很好的答案! (39认同)
  • @drewag这似乎与它所说的完全相反.它是说一个类应该是你使用的默认值,而不是一个结构`在实践中,这意味着大多数自定义数据结构应该是类,而不是结构.你能解释一下,在阅读之后,你得到了什么大多数数据集应该是结构而不是类?当某些东西应该是一个结构时,他们给出了一套特定的规则,并且几乎说"所有其他情景都是一个类更好". (15认同)
  • @ElgsQianChen这篇文章的重点是默认选择struct,只在必要时才使用class.结构更安全,没有bug,特别是在多线程环境中.是的,您总是可以使用类来代替结构,但结构更可取. (10认同)
  • Struct over Class绝对会降低复杂性.但是当结构成为默认选择时,对内存使用的含义是什么.当事情被复制到处而不是引用它应该增加应用程序的内存使用量.不应该吗? (6认同)
  • 在大多数情况下,Swift 2.2书仍然说使用类. (5认同)
  • "除非你明确表示要复制" - 我怎么能这样做?我做了很多搜索,文档中没有任何内容,当你在这里说"标记"时,你的意思是什么?只是一个copy()调用? (3认同)
  • @Matt,这是真的.我选择这样看的方式是我们有特定的位置可以使用Struct,而Class可以在任何地方使用,包括结构.这对我来说意味着类是结构不会削减它的后备. (2认同)
  • Swift 语言参考**不**矛盾,但也许可以写得更清楚。结构更像是哑数据对象。如果您想以 OOP 方式构建任何东西,您将不会拥有一堆数据以及对其进行操作的大量全局函数,而是具有数据和例程的类实例。从这个意义上说,除非您正在构建类似 JSON 包装器的东西,否则您定义的大多数数据类型都将是类。如果您的项目不是这种情况,我不确定它是否是面向对象的。 (2认同)
  • Swift 中的@BridgeTheGap 结构绝不是愚蠢的对象。除了继承之外,它们可以执行类可以执行的所有操作。您当然不需要继承来执行 OOP,尤其是当您可以使用协议时。最重要的区别是它们在内存中的表示方式(作为纯数据或引用)。 (2认同)

Kha*_*yen 158

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

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

请考虑以下示例,该示例演示了Int使用struct和包装数据类型的两种策略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)
}
Run Code Online (Sandbox Code Playgroud)

使用表现来衡量绩效

// 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)")
}
Run Code Online (Sandbox Code Playgroud)

代码可以在https://github.com/knguyen2708/StructVsClassPerformance找到

更新(2018年3月27日):

截至Swift 4.0,Xcode 9.2,在iPhone 6S上运行Release版本,iOS 11.2.6,Swift Compiler设置为-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倍)

注意:正如有人提到的那样,在实际场景中,结构中可能会有超过1个字段,我已经为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倍)

  • 没错,但似乎复制一堆结构会比复制对单个对象的引用要慢.换句话说,复制单个指针比复制任意大的内存块更快. (27认同)
  • 据我所知,swift中的复制优化为在WRITE时间发生.这意味着除非新副本即将被更改,否则不会进行物理内存复制. (25认同)
  • -1这个测试不是一个很好的例子,因为结构上只有一个var.请注意,如果添加多个值和一个或两个对象,则struct版本可与类版本进行比较.您添加的vars越多,struct版本越慢. (14认同)
  • @joshrl得到了你的观点,但一个例子是"好"与否取决于具体情况.此代码是从我自己的应用程序中提取的,因此它是一个有效的用例,并且使用结构确实大大提高了我的应用程序的性能.它可能不是一个常见的用例(嗯,常见的用例是,对于大多数应用程序,没有人关心数据传递速度有多快,因为瓶颈发生在其他地方,例如网络连接,无论如何,优化不是那样的当您拥有带有GB或RAM的GHz设备时,这是至关重要的). (6认同)
  • 这个答案显示了一个非常简单的例子,对于许多情况来说是不现实的,因此是不正确的.一个更好的答案是"它取决于". (5认同)
  • 2016年的结果看起来有缺陷.iPhone 6S中的A9以1.8 GHz运行,这意味着时钟周期大约需要5.6E-10秒.您的基准测试采用5.83E-08s,换句话说:大约100个时钟周期.这意味着您的CPU每个周期执行100,000次添加操作.我的猜测是编译器删除了整个块,因为它认为结果从未使用过,或者它认为你没有使用任何中间结果,只是用编译时计算的静态结果替换了最终结果.无论哪种方式,您可能只是测量了测量开销本身. (5认同)
  • @Matjan的评论是Swift结构性能的关键.本教程中的更多信息:https://www.raywenderlich.com/112029/reference-value-types-in-swift-part-2 (3认同)
  • 在大多数情况下,您不仅仅是复制引用,而且对象的引用计数器也应该递增/递减,这会影响性能. (2认同)
  • 5.83E-08s 数字看起来好得令人难以置信。我认为编译器可能足够聪明,只是在 struct 测试用例中折叠所有操作。这被称为“恒定折叠”。尝试使用相同的优化标志生成目标文件,并使用 Hopper Disassembler 检查伪代码,看看优化后的真实代码是什么。 (2认同)

Mad*_*Nik 60

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

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

和差异

1.继承.

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

class Vehicle{
}

class Car : Vehicle{
}
Run Code Online (Sandbox Code Playgroud)

去上课.

通过

Swift结构按值传递,类实例按引用传递.

情境差异

结构常量和变量

示例(在WWDC 2014上使用)

struct Point{

   var x = 0.0;
   var y = 0.0;

} 
Run Code Online (Sandbox Code Playgroud)

定义一个名为Point的结构.

var point = Point(x:0.0,y:2.0)
Run Code Online (Sandbox Code Playgroud)

现在,如果我尝试更改x.它是一个有效的表达.

point.x = 5
Run Code Online (Sandbox Code Playgroud)

但是如果我将一个点定义为常数.

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

在这种情况下,整点是不可变的常数.

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

  • 上面的要点是关于如何实现struct的继承风格.你会看到类似的语法.答:B.这是一种称为A实现协议的结构,称为B. Apple文档清楚地提到struct不支持纯继承,但它不支持. (12认同)
  • 你最后一段的男人很棒.我一直都知道你可以改变常数...但是我不时地看到你不能改变的地方,所以我感到很困惑.这种区别使其可见 (2认同)

Dan*_*ark 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")
    
    Run Code Online (Sandbox Code Playgroud)

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

  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]
    
    Run Code Online (Sandbox Code Playgroud)
  2. 显然,不变性与可变性是一个很大的话题,但很多聪明的人认为不可变性 - 在这种情况下的结构 - 是更可取的.可变对象与不可变对象

  • 你可以获得自动初始化程序.当所有属性都是可选项时,您还会获得一个空的初始化程序.但是,如果你在Framework中有一个结构,你需要自己编写初始化器,如果你希望它在`internal`范围之外可用. (4认同)
  • 我是否还可以补充一点,结构的不变性并不能使它们变得有用(尽管这是一件非常好的事情).你可以改变结构,但是你必须将这些方法标记为"mutating",这样你就明确了哪些函数改变了它们的状态.但它们作为_value类型_的本质是重要的.如果使用`let`声明一个结构,则不能在其上调用任何变异函数.WWDC 15视频**通过值类型进行更好的编程**是一个很好的资源. (4认同)
  • @Abizern确认 - http://stackoverflow.com/a/26224873/8047 - 和那个很讨厌的男人. (2认同)
  • @Abizern在Swift中有很多理由,但每次在某个地方而不是在另一个地方都是真的,开发者必须知道更多东西.我想这就是我应该说的地方,"用这种具有挑战性的语言工作真是令人兴奋!" (2认同)

Hon*_*ney 23

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

如果你不知道什么是值类型和引用类型,然后看看有什么引用传递与路过值之间的差异?

基于mikeash的帖子:

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

某些类型可以被复制,但它可能不是您想要一直发生的事情.这表明它们应该是引用类型.例如,屏幕上的按钮可以在概念上被复制.副本与原件不完全相同.单击副本不会激活原件.副本不会占据屏幕上的相同位置.如果您传递按钮或将其放入新变量中,您可能需要引用原始按钮,并且您只想在明确请求时制作副本.这意味着您的按钮类型应该是引用类型.

视图和窗口控制器是一个类似的例子.可以想象它们可以复制,但它几乎不是你想要做的.它们应该是引用类型.

模型类型怎么样?您可能具有表示系统上用户的User类型,或表示用户采取的操作的Crime类型.这些都是可复制的,所以它们应该是值类型.不过,你可能要更新到用户的犯罪在一个地方在你的程序做成的程序的其他部分可见. 这表明您的用户应该由某种用户控制器管理,该用户控制器将是一种引用类型.例如

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

    func add(user: User) { ... }
    func remove(userNamed: String) { ... }
    func ...
}
Run Code Online (Sandbox Code Playgroud)

收藏是一个有趣的案例.这些包括数组和字典,以及字符串.它们是可复制的吗?明显.是否经常复制你想要发生的事情?那不太清楚.

大多数语言对此说"不"并使其集合引用类型.这在Objective-C和Java,Python和JavaScript以及我能想到的几乎所有其他语言中都是如此.(一个主要的例外是带有STL集合类型的C++,但C++是语言世界的狂热疯子,它可以完成任何奇怪的工作.)

Swift说"是的",这意味着像Array,Dictionary和String这样的类型是结构而不是类.它们在赋值时被复制,并在作为参数传递时被复制.这是一个完全合理的选择,只要副本很便宜,Swift很难完成....

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

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

  • 正是我正在寻找的那种解释。写得不错:) (2认同)

Cat*_*Man 18

一些优点:

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

  • 当然.我还可以附上一个警告.动态调度的目的是在您事先不知道要使用哪一个时选择一个实现.在Swift中,这可能是由于继承(可能在子类中被覆盖),或者由于函数是通用的(您不知道泛型参数是什么).结构不能继承而且整个模块优化+泛型专业化大多消除了未知泛型,因此可以直接调用方法而不必查找要调用的内容.非特化泛型仍然为结构进行动态调度 (2认同)

小智 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  
}
Run Code Online (Sandbox Code Playgroud)

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

var flightA = Flight()

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

现在让我们将这些实例传递给两个修改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"
}
Run Code Online (Sandbox Code Playgroud)

也,

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

所以,

modifyFlight(flight: flightA)
modifyFlight2(flight2: flightB)
Run Code Online (Sandbox Code Playgroud)

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

id = 200
description = "second flight of Virgin Airlines"
Run Code Online (Sandbox Code Playgroud)

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

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

id = 100
description = "first ever flight of Virgin Airlines"
Run Code Online (Sandbox Code Playgroud)

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

  • 您从未创建FLightB的实例 (2认同)

cas*_*las 8

Structsvalue typeClassesreference type

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

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

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

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

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

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

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


附加信息

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

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

提醒一下,这里是Swift列表

值类型:

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

参考类型:

  • 任何来自NSObject的东西
  • 功能
  • 关闭


Dav*_*mes 5

从值类型与引用类型的角度回答问题,在此Apple博客文章中,它看起来非常简单:

在以下情况下,请使用值类型[例如struct,enum]:

  • 将实例数据与==进行比较很有意义
  • 您希望副本具有独立状态
  • 数据将在多个线程的代码中使用

在以下情况下,使用引用类型[例如类]:

  • 将实例身份与===进行比较比较有意义
  • 您要创建共享的可变状态

如该文章所述,没有可写属性的类的行为与结构相同,但有一个警告:(我要补充一点):结构最适合线程安全模型 -现代应用程序体系结构中日益迫切的需求。


yoA*_*ex5 5

结构与类

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

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)
     
}
Run Code Online (Sandbox Code Playgroud)

*您应该使用/依赖retainCount,因为它没有说有用的信息

检查堆栈或堆

在编译期间Swift Intermediate Language(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
Run Code Online (Sandbox Code Playgroud)

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

[优化级别(SWIFT_OPTIMIZATION_LEVEL)]


归档时间:

查看次数:

143265 次

最近记录:

5 年,11 月 前