为什么select类结构?

来自Java背景的Swift玩弄,为什么你要select一个结构,而不是一个类? 看起来像是同样的事情,一个Struct提供更less的function。 为什么select呢?

根据在Swift( video , 成绩单 )中很受欢迎的WWDC 2015谈话协议编程(Swift),Swift提供了许多function,使得结构在许多情况下比类更好。

如果它们相对较小并且可复制,则结构比较可取,因为复制比多次引用相同的实例更安全。 将variables传递给多个类和/或multithreading环境时,这一点尤为重要。 如果您始终可以将variables的副本发送到其他地方,则不必担心在其他位置更改您的variables的值。

有了Structs,就不用担心内存泄漏或多个线程竞相访问/修改variables的单个实例。 (对于技术上更加注重技术的人来说,这是一个例外,当在一个闭包中捕获一个结构体时,它实际上捕获了一个对该实例的引用,除非你明确地标记它被复制)。

类也可能变得臃肿,因为一个类只能从一个超类inheritance。 这鼓励我们创造巨大的超类,包含许多不同的能力,只是松散的相关。 使用协议,特别是使用协议扩展(可以向协议提供实现),可以使您无需类来实现这种行为。

这次谈话列出了这些情况下,首选类:

  • 复制或比较实例没有意义(例如,窗口)
  • 实例生命周期与外部效应(例如,TemporaryFile)
  • 实例只是“汇”(sinks) – 只写通向外部状态的通道(egCGContext)

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

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

结构实例总是按值传递,而类实例总是按引用传递。 这意味着它们适合于不同types的任务。 当您考虑项目所需的数据结构和function时,请决定每个数据结构是定义为类还是结构。

作为一般指导原则,考虑在适用以下一个或多个条件时创build一个结构:

  • 该结构的主要目的是封装一些相对简单的数据值。
  • 当您分配或传递该结构的实例时,期望封装的值将被复制而不是被引用是合理的。
  • 结构存储的任何属性都是它们自己的值types,也可能被复制而不是引用。
  • 该结构不需要inheritance其他现有types的属性或行为。

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

  • 几何形状的大小,也许封装一个宽度属性和一个高度属性,这两个types都是Double。
  • 一种引用一系列范围内的范围的方法,可能封装一个types为Int的start属性和一个length属性。
  • 3D坐标系中的一个点,可能封装了x,y和z属性,每个types都是Doubletypes。

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

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

由于struct实例是在堆栈中分配的,并且类实例是在堆上分配的,结构有时可能会大大加快,但这取决于您要存储多less个值以及它们的大小/结构。

但是,您应该始终自己衡量,并根据您的独特使用情况来决定。

考虑下面的例子,它演示了2种包装Int数据types的策略(例如作为math库的一部分)

 class IntClass { var value: Int init(_ val: Int) { self.value = val } } struct IntStruct { var value: Int init(_ val: Int) { self.value = val } } func + (x: IntClass, y: IntClass) -> IntClass { return IntClass(x.value + y.value) } func + (x: IntStruct, y: IntStruct) -> IntStruct { return IntStruct(x.value + y.value) } 

并使用测量性能

 // Test 1: IntClass measure("class (1 field)") { var x = IntClass(0) for _ in 1...10000000 { x = x + IntClass(1) } } // Test 2: IntStruct measure("struct (1 field)") { var y = IntStruct(0) for _ in 1...10000000 { y = y + IntStruct(1) } } 

measure被定义为

 func measure(name: String, @noescape block: () -> ()) { let t0 = CACurrentMediaTime() block() let dt = CACurrentMediaTime() - t0 print("\(name) -> \(dt)") } 

更新(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 :如果没有整个模块优化,差异就会大打折扣。 如果有人能指出这个旗子究竟做了什么,我会很高兴。

注意#2 :正如有人提到,在现实世界的情况下,将有可能在一个结构中有超过1个字段,我已经添加了10个结构域/类的testing,而不是1个。令人惊讶的是,结果没有太大的变化。

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


旧成果 (2014年6月1日):

从Swift 1.2起,XCode 6.3.2在iPhone 5S,iOS 8.3上运行Release版本,平均超过5次

  • class版本花了9.788332333s
  • struct版本花了0.010532942s

快了900倍


旧的结果 (来自未知的时间)

在发行版本(在我的MacBook上),第一个testing需要1.10082秒,而第二个testing需要0.02324秒。 快50倍 ! (在debugging版本中结果大致相同)。

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

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

和差异

1.inheritance。

结构不能在swift中inheritance。 如果你想

 class Vehicle{ } class Car : Vehicle{ } 

去上课。

2.通过

Swift结构通过值传递,类实例通过引用传递。

上下文差异

结构常量和variables

示例(用于WWDC 2014)

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

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

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

现在,如果我尝试更改x。 它是一个有效的expression。

 point.x = 5 

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

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

在这种情况下,整个点是不变的恒定的。

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

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

  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这样的基本集合types是结构体。 您在自己的代码中使用的越多,您将越习惯于通过值传递而不是参考。 例如:

     func removeLast(var array:[String]) { array.removeLast() println(array) // [one, two] } var someArray = ["one", "two", "three"] removeLast(someArray) println(someArray) // [one, two, three] 
  2. 显然,不可变性与可变性是一个巨大的话题,但许多聪明人认为不可变性 – 在这种情况下的结构 – 是可取的。 可变与不可变对象

一些优点:

  • 自动线程安全,由于不可共享
  • 由于没有isa和refcount(实际上通常是堆栈分配),所以使用较less的内存。
  • 方法总是静态调度,所以可以内联(尽pipe@final可以为类做这个)
  • 更容易推理(不需要像NSArray,NSString等典型的“防御复制”),这与线程安全相同

假设我们知道Struct是一个值typesClass是一个引用types

如果你不知道,那么看看通过引用与按价值传递有什么区别?

基于mikeash的post :

先来看一些极端明显的例子。 整数显然是可复制的。 他们应该是价值types。 networking套接字不能被明智地复制。 他们应该是参考types。 点,如在x,y对,是可复制的。 他们应该是价值types。 代表磁盘的控制器不能被合理地复制。 这应该是一个参考types。

有些types可以被复制,但是它可能不是你想要一直发生的事情。 这表明他们应该是参考types。 例如,屏幕上的button可以在概念上被复制。 该副本不会与原来的完全相同。 点击副本将不会激活原件。 副本不会占用屏幕上的相同位置。 如果你传递button或者把它放到一个新的variables中,你可能会想要引用原来的button,而且只有在明确请求时才需要复制。 这意味着你的buttontypes应该是一个引用types。

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

模型types呢? 您可能有一个代表您系统上的用户的用户types,或表示用户采取的操作的犯罪types。 这些都是可复制的,所以他们应该可能是值types。 但是,您可能希望更新程序中某个地方的用户犯罪,以便该程序的其他部分可见。 这表明你的用户应该由某种用户控制器来pipe理,这将是一个引用types

集合是一个有趣的案例。 这些包括像数组和字典的东西,以及string。 他们是可复制的吗? 明显。 正在复制你想要经常发生的事情吗? 这不太清楚。

大多数语言对此都表示“否”,并使其集合引用types。 Objective-C和Java,Python和JavaScript以及我能想到的几乎所有其他语言都是如此。 (一个主要的例外是带有STL集合types的C ++,但是C ++是语言世界的疯狂的疯子,它奇怪地做了一切事情。)

斯威夫特说“是”,这意味着像数组和字典和stringtypes是结构而不是类。 他们被赋值,并作为parameter passing。 这是一个完全合理的select,只要副本是廉价的,斯威夫特很难完成。 …

另外,当你必须重写函数的每个实例,也就是说它们没有任何共享的function时,不要使用类。

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

通过类inheritance,并通过引用传递,结构体没有inheritance,并通过值传递。

在Swift上有很棒的WWDC会议,这个具体的问题在其中一个很详细的回答。 确保你看这些,因为它会让你的速度更快,然后是语言指南或iBook。

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

 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 } 

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

 var flightA = Flight() var flightA = 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" } 

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

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

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

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

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

在这里我们可以看到FlightB实例没有被改变,因为在modifyFlight2方法中,Flight2的实际实例是pass而不是reference(值types)。

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

创造型模式:

在swift中,Struct是一个自动克隆的值types 。 因此,我们得到所需的行为来免费实现原型模式。

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


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


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

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

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

我不会说结构提供更less的function。

当然,除了变异函数之外,自我是不可改变的,但就是这样。

只要你坚持每一个阶级都应该是抽象的或最终的古老思想,inheritance就可以正常工作。

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

关于结构的好处是,你可以让你的字段可变而不会创build共享的可变状态,因为在写入时需要照顾那:)

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

在名为“example”的函数底部有一些脏和简单用法的inheritance结构示例:

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

许多Cocoa API需要NSObject子类,这迫使你使用类。 但除此之外,您可以使用Apple的Swift博客中的以下案例来决定是使用结构/枚举值types还是类引用types。

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

从价值types与参考types的angular度回答这个问题,从这篇苹果博客文章看起来很简单:

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

  • 用==比较实例数据是有道理的
  • 你想要副本有独立的状态
  • 数据将在跨多个线程的代码中使用

在以下情况下使用引用types[例如class]:

  • 将实例标识与===进行比较是有意义的
  • 你想创build共享的,可变的状态

正如在那篇文章中提到的那样,一个没有可写属性的类将会像一个结构一样行事,我会添加一个警告:结构对于线程安全的模型来说是最好的 – 在现代应用程序架构中,这个要求越来越迫切。