在Java背景下玩Swift,为什么要选择Struct而不是Class?似乎它们是同一回事,但Struct提供的功能较少。为什么选择它呢?
在Java背景下玩Swift,为什么要选择Struct而不是Class?似乎它们是同一回事,但Struct提供的功能较少。为什么选择它呢?
Answers:
根据非常流行的WWDC 2015演讲《 Swift中的面向协议的编程》(视频,成绩单),Swift提供了许多功能,这些功能在许多情况下都比类更好。
如果结构相对较小且可复制,则结构是可取的,因为与在类中多次引用同一实例相比,复制要安全得多。当将变量传递给许多类和/或在多线程环境中时,这一点尤其重要。如果您始终可以将变量的副本发送到其他位置,则不必担心其他位置会更改您下面的变量的值。
使用Structs,无需担心内存泄漏或多个线程争夺访问/修改变量的单个实例的麻烦。(从更专业的角度来说,例外是在闭包内部捕获结构时,因为它实际上是在捕获对实例的引用,除非您明确地将其标记为要复制)。
类也可能变得肿,因为一个类只能从单个超类继承。这鼓励我们创建巨大的超类,其中包含仅松散相关的许多不同能力。使用协议,尤其是协议扩展,可以在其中提供协议的实现,可以消除类对实现此类行为的需要。
演讲列出了首选班级的这些情况:
- 复制或比较实例没有意义(例如Window)
- 实例的生存期与外部影响(例如TemporaryFile)有关
- 实例只是“接收器”-到外部状态(例如CGContext)的只写管道
这意味着结构应该是默认值,而类应该是后备。
另一方面,Swift编程语言文档有些矛盾:
结构实例始终按值传递,而类实例始终按引用传递。这意味着它们适合于各种任务。在考虑项目所需的数据结构和功能时,请确定是将每个数据结构定义为类还是结构。
作为一般准则,请考虑在以下一个或多个条件适用时创建结构:
- 该结构的主要目的是封装一些相对简单的数据值。
- 合理的是,当您分配或传递该结构的实例时,将封装的值复制而不是引用。
- 该结构存储的任何属性本身都是值类型,也应该将其复制而不是引用。
- 该结构不需要继承其他现有类型的属性或行为。
良好的结构候选人包括:
- 尺寸均为Double的几何形状的大小,可能封装了width属性和height属性。
- 引用系列中范围的一种方法,可能封装了Int类型的start属性和length属性。
- 3D坐标系中的一个点,可能封装了x,y和z属性,每个属性均为Double类型。
在所有其他情况下,请定义一个类,然后创建该类的实例以通过引用进行管理和传递。实际上,这意味着大多数自定义数据构造应该是类,而不是结构。
在这里,我们声称我们应该默认仅在特定情况下使用类和结构。最终,您需要了解值类型与引用类型在现实世界中的含义,然后可以就何时使用结构或类做出明智的决定。另外,请记住,这些概念一直在发展,并且在进行面向协议的编程演讲之前编写了Swift编程语言文档。
In practice, this means that most custom data constructs should be classes, not structures.
。您可以向我解释一下,读完之后如何得到大多数数据集应该是结构而不是类?当某些东西应该是一个结构时,他们给出了一套特定的规则,并且几乎说“在所有其他情况下,一个班级会更好”。
由于struct实例是在堆栈上分配的,而class实例是在堆上分配的,因此结构有时有时会更快。
但是,您应该始终自己进行测量,并根据您的独特用例进行决策。
考虑以下示例,该示例演示了Int
使用struct
和包装数据类型的2种策略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,iOS 11.2.6上运行Release build,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.159942142sstruct
版本花费了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.788332333sstruct
版本花费了0.010532942s(速度提高了900倍)旧结果(来自未知时间)
(对结构/类使用1个字段,而不是10个)
在我的MacBook Pro上发布版本:
class
版本了1.10082秒struct
版本耗时0.02324秒(快50倍)我通过简单的示例为此创建了要点。 https://github.com/objc-swift/swift-classes-vs-structures
结构不能快速继承。如果你想
class Vehicle{
}
class Car : Vehicle{
}
去上课。
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类,则这是一个有效的表达式。因为在类中,不可变常量是对类本身的引用,而不是对其实例变量的引用(除非那些变量定义为常量)
还有其他一些要考虑的原因:
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")
为了在一个类中做到这一点,您必须添加初始化程序,并维护初始化程序。
基本的集合类型如Array
结构。您在自己的代码中使用它们的次数越多,您就会习惯于按值传递而不是引用。例如:
func removeLast(var array:[String]) {
array.removeLast()
println(array) // [one, two]
}
var someArray = ["one", "two", "three"]
removeLast(someArray)
println(someArray) // [one, two, three]
显然,不变性与可变性是一个巨大的话题,但是许多聪明人认为,不变性(在这种情况下为结构)是更可取的。可变对象与不可变对象
internal
范围外使用它,则需要自己实际编写该初始化程序。
mutating
更改结构,但是必须将方法标记为,这样您就可以清楚了解哪些函数会更改其状态。但是它们作为值类型的性质很重要。如果声明一个结构体,let
则不能对其调用任何变异函数。关于通过值类型更好地编程的WWDC 15视频是一个很好的资源。
假设我们知道Struct是一个值类型,而Class是一个引用类型。
如果您不知道值类型和引用类型是什么,请参阅按引用传递与按值传递有什么区别?
根据mikeash的帖子:
...让我们先来看一些极端的,显而易见的例子。整数显然是可复制的。它们应该是值类型。无法明智地复制网络套接字。它们应该是引用类型。x,y对中的点是可复制的。它们应该是值类型。不能明智地复制代表磁盘的控制器。那应该是一个引用类型。
某些类型可以被复制,但可能并非您一直希望发生。这表明它们应该是引用类型。例如,可以从概念上复制屏幕上的按钮。该副本将与原始副本不太相同。单击副本将不会激活原件。该副本将不会在屏幕上占据相同的位置。如果您将按钮传递或放入新变量中,则可能要引用原始按钮,并且只希望在明确请求时进行复制。这意味着您的按钮类型应该是引用类型。
视图和窗口控制器是类似的示例。可以想象,它们可能是可复制的,但几乎从来都不是您想要做的。它们应该是引用类型。
型号类型呢?您可能具有代表系统上用户的“用户”类型,或代表用户采取的操作的“犯罪”类型。这些都是可复制的,因此它们应该是值类型。但是,您可能希望在程序的某个位置进行的用户犯罪更新对于程序的其他部分是可见的。 这表明您的用户应该由某种用户控制器来管理,这将是一个引用类型。例如
struct User {} class UserController { var users: [User] func add(user: User) { ... } func remove(userNamed: String) { ... } func ... }
收藏是一个有趣的案例。这些包括数组和字典之类的东西,以及字符串。它们可以复制吗?明显。复制想要轻松且经常发生的事情吗?这还不清楚。
大多数语言对此表示“否”,并使其集合引用类型。在Objective-C,Java,Python和JavaScript以及我能想到的几乎所有其他语言中都是如此。(一个主要的例外是具有STL集合类型的C ++,但是C ++是语言界疯狂的疯子,它奇怪地完成了所有工作。)
Swift说“是”,这意味着诸如Array和Dictionary和String之类的类型是结构而不是类。它们在分配时以及作为参数传递时被复制。只要副本便宜,这是完全明智的选择,Swift会非常努力地做到这一点。...
我个人不这样命名我的班级。我通常将其命名为UserManager而不是UserController,但想法是相同的
另外,当您必须覆盖函数的每个实例(即它们没有任何共享功能)时,请不要使用类。
因此,而不是拥有一个类的几个子类。使用符合协议的几种结构。
对于结构的另一种合理情况是,当您想对旧模型和新模型进行增量/差异处理时。对于引用类型,您不能开箱即用。对于值类型,不共享突变。
一些优点:
结构比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,描述,目标等。
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的实际实例是传递而不是引用(值类型)。
Here we can see that the FlightB instance is not changed
Structs
是value type
和Classes
是reference type
在以下情况下使用value
类型:
在以下情况下使用reference
类型:
进一步的信息也可以在Apple文档中找到
https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html
附加信息
Swift值类型保留在堆栈中。在一个进程中,每个线程都有其自己的堆栈空间,因此,其他线程将无法直接访问您的值类型。因此,没有竞争条件,锁,死锁或任何相关的线程同步复杂性。
值类型不需要动态内存分配或引用计数,这两者都是昂贵的操作。同时,值类型的方法是静态分派的。这些在性能方面为值类型提供了巨大的优势。
提醒一下,这里是Swift列表
值类型:
参考类型:
从值类型与引用类型的角度回答问题,在此Apple博客文章中,它看起来非常简单:
在以下情况下,请使用值类型[例如struct,enum]:
- 将实例数据与==进行比较很有意义
- 您希望副本具有独立状态
- 数据将在多个线程的代码中使用
在以下情况下,请使用引用类型[例如类]:
- 将实例身份与===进行比较比较有意义
- 您要创建共享的可变状态
如该文章所述,没有可写属性的类的行为与结构相同,但有一个警告:(我要补充一点):结构最适合线程安全模型 -现代应用程序体系结构中日益迫切的需求。
对于类,您可以继承并通过引用传递,而结构不具有继承,而是通过值传递。
在Swift上有很棒的WWDC会议,其中一个具体问题得到了详细解答。确保您观看这些内容,因为它可以使您比语言指南或iBook更快地掌握速度。
我不会说结构提供较少的功能。
当然,除了可变功能之外,自我是不可变的,仅此而已。
只要您坚持每个类都应该是抽象类或最终类的好主意,继承就可以正常工作。
将抽象类实现为协议,将最终类实现为结构。
关于结构的好处是,您可以使字段可变而无需创建共享可变状态,因为写时复制可解决此问题:)
这就是以下示例中的属性/字段全部可变的原因,而我在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)")
}
在Swift中,引入了一种新的编程模式,称为面向协议的编程。
创作模式:
快速地,Struct是一个自动克隆的值类型。因此,我们获得了免费实现原型模式所需的行为。
而类是引用类型,在分配过程中不会自动将其克隆。要实现原型模式,类必须采用NSCopying
协议。
浅拷贝仅复制指向这些对象的引用,而深拷贝则复制对象的引用。
为每种引用类型实现深拷贝已成为一项繁琐的任务。如果类包含其他引用类型,则必须为每个引用属性实现原型模式。然后,我们必须通过实现协议来实际复制整个对象图。NSCopying
class Contact{
var firstName:String
var lastName:String
var workAddress:Address // Reference type
}
class Address{
var street:String
...
}
通过使用structs和enums,由于不必实现复制逻辑,我们使代码更简单。
许多Cocoa API需要NSObject子类,这迫使您使用类。但是除此之外,您可以使用Apple的Swift博客中的以下情况来决定是使用struct / enum值类型还是使用类引用类型。