为什么选择结构而不是类?

玩Swift,来自Java背景,为什么要选择Struct而不是Class?看起来它们是一样的东西,只不过Struct提供的功能更少。那为什么选择它呢?

182386 次浏览

对于类,您获得继承并通过引用传递,而结构则没有继承并通过值传递。

有很多关于Swift的WWDC会议,其中一个会议详细回答了这个问题。确保你看了这些,因为它会让你更快地跟上语言指南或iBook。

根据2015年非常流行的WWDC演讲,Swift中面向协议的编程(视频成绩单), Swift提供了许多特性,使结构在许多情况下比类更好。

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

使用Structs,不太需要担心内存泄漏或多个线程竞相访问/修改变量的单个实例。(对于更有技术头脑的人来说,例外情况是在闭包内捕获结构时,因为它实际上是在捕获实例的引用,除非您显式地将其标记为要复制)。

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

演讲列出了优先使用类的这些场景:

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

这意味着结构应该是默认的,而类应该是备用的。

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

结构实例总是按值和类传递 实例总是通过引用传递。这意味着他们是 适合不同类型的任务。当你考虑数据时 决定项目所需的结构和功能 每个数据构造应该定义为类还是类 结构。< / p > 作为一般准则,当一个或多个结构

  • 该结构的主要目的是封装一些相对简单的数据值。
  • 类的赋值或传递时,希望封装的值被复制而不是被引用是合理的 该结构的实例
  • 结构存储的任何属性本身都是值类型,也希望复制而不是引用这些值类型。
  • 该结构不需要从另一个现有类型继承属性或行为。

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

  • 几何形状的大小,可能封装了Double类型的width属性和height属性。
  • 在一个系列中引用范围的一种方法,可能封装了一个start属性和一个length属性,都是Int类型。
  • 3D坐标系中的一个点,可能封装了x、y和z属性,每个属性都是Double类型。
在所有其他情况下,定义一个类,并创建该类的实例 通过引用来管理和传递。在实践中,这意味着 大多数自定义数据结构应该是类,而不是结构

在这里,它声称我们应该只在特定的情况下默认使用类和使用结构。最后,您需要了解值类型与引用类型的实际含义,然后才能就何时使用结构或类做出明智的决定。此外,请记住,这些概念一直在发展,Swift编程语言文档是在面向协议编程演讲之前编写的。

一些好处:

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

这个答案最初是关于结构和类之间性能的差异。不幸的是,关于我使用的测量方法有太多的争议。我把它留在下面,但请不要过多地去理解它。我认为经过这么多年,在Swift社区中,struct(以及enum)由于其简单和安全而一直是首选。

如果性能对你的应用很重要,那就自己衡量。我仍然认为大多数时候结构性能更优越,但最好的答案就像有人在评论中说的那样:这要看情况。

=== # eyz0 ===

由于结构实例是在堆栈上分配的,而类实例是在堆上分配的,因此结构有时会快得多。

但是,您应该始终自己衡量它,并根据您独特的用例进行决定。

考虑下面的例子,它演示了使用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找到

# EYZ0:

从Swift 4.0, Xcode 9.2,运行版本构建在iPhone 6S, iOS 11.2.6, Swift编译器设置为-O -whole-module-optimization:

  • class版本耗时2.06秒
  • struct版本用了4.17e-08秒(快了50,000,000倍)

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

请注意:如果没有整个模块的优化,差异就没有那么大了。如果有人能指出这面旗子到底是干什么的,我会很高兴。


# EYZ0:

Swift 2.2.1, Xcode 7.3,在iPhone 6S上运行Release build, iOS 9.3.1,平均超过5次运行,Swift编译器设置为-O -whole-module-optimization:

  • class版本花了2.159942142秒
  • struct版本耗时5.83 -08秒(速度快37,000,000倍)

请注意:正如有人提到的,在现实场景中,一个结构中可能有多个字段,我已经为结构/类添加了10个字段而不是1个字段的测试。令人惊讶的是,结果变化不大。


原来的结果(2014年6月1日):

(在struct/class上运行,只有1个字段,而不是10个)

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

  • class版本花了9.788332333秒
  • struct版本花了0.010532942秒(快了900倍)

旧的结果(未知时间)

(在struct/class上运行,只有1个字段,而不是10个)

在我的MacBook Pro上发布:

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

以下是一些值得考虑的其他原因:

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

To get this in a class, you would have to add the initializer, and maintain the intializer...

  1. Basic collection types like Array are structs. The more you use them in your own code, the more you will get used to passing by value as opposed to reference. For instance:

    func removeLast(var array:[String]) {
    array.removeLast()
    println(array) // [one, two]
    }
    
    
    var someArray = ["one", "two", "three"]
    removeLast(someArray)
    println(someArray) // [one, two, three]
    
  2. Apparently immutability vs. mutability is a huge topic, but a lot of smart folks think immutability -- structs in this case -- is preferable. Mutable vs immutable objects

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

我用简单的例子创建了主旨。 # EYZ0 < / p >

和差异

1. 继承。

结构不能在swift中继承。如果你愿意

class Vehicle{
}


class Car : Vehicle{
}

参加一个培训班。

2. 经过

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

语境的差异

结构常量和变量

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

结构比类快得多。同样,如果你需要继承,那么你必须使用Class。最重要的一点是类是引用类型,而结构是值类型。例如,

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)

现在如果我们打印航班a的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的实际实例是传递而不是引用(值类型)。

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

当然,self是不可变的,除了在突变函数中,但仅此而已。

继承可以很好地工作,只要您坚持每个类都应该是抽象的或最终的。

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

struct的好处是你可以在不创建共享可变状态的情况下使你的字段可变,因为写时复制会照顾到这一点:)

这就是为什么下面例子中的属性/字段都是可变的,我不会在Java或c#或swift# EYZ0中这样做。

示例继承结构,在底部名为" 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)")
}

假设我们知道结构体值类型引用类型

如果你不知道值类型和引用类型是什么,那么请参阅按引用传递和按值传递之间的区别是什么?< / >

基于mikeash的文章:

< p >…让我们先看一些极端的、明显的例子。整数是 显然可以复制。它们应该是值类型。网络套接字不能 明智地模仿别人。它们应该是引用类型。点,在x y中 对,是可复制的。它们应该是值类型。一个控制器 表示无法合理复制的磁盘。这应该是一个参考 类型。< / p > 有些类型可以复制,但它可能不是你想要的 这种事经常发生。这表明他们应该是参考 类型。例如,在概念上可以复制屏幕上的按钮。 复制件与原件不完全相同。点击 副本不会激活原件。副本不会占用相同的空间 屏幕上的位置。如果你传递按钮或把它放入 新变量,您可能想要引用原始按钮,和 只有在明确要求时才需要复制。那 意思是你的按钮类型应该是一个引用类型 视图和窗口控制器也是类似的例子。他们可能是 可以复制,可以想象,但这几乎不是你想做的。 它们应该是引用类型

模型类型呢?您可能有一个表示用户的User类型 ,或者表示由对象执行的操作的Crime类型 用户。这些都是可复制的,所以它们应该是有价值的 类型。然而,你可能想要更新用户犯罪 程序中对程序的其他部分可见的一个位置。 这表明你的用户应该由某种类型的用户管理 控制器,它将是一个引用类型。如< / p >

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


func add(user: User) { ... }
func remove(userNamed: String) { ... }
func ...
}
集合是一个有趣的例子。这包括数组 还有字典和字符串。它们是可复制的吗?很明显。是 复制你想要的东西容易且经常发生?这是少 清楚。< / p > 大多数语言对此说“不”,并使它们的集合引用 类型。在Objective-C, Java, Python和JavaScript中都是如此 以及几乎所有我能想到的语言。(一个主要例外 是c++与STL的集合类型,但c++是疯狂的 语言世界的一切都很奇怪)

Swift说“是”,这意味着像数组和字典和 字符串是结构体而不是类。他们在作业中被复制, 把它们作为参数传递。这是一个完全明智的选择 只要拷贝是便宜的,斯威夫特一直在努力做到这一点 完成。 …< / p >

我个人不会这样命名我的类。我通常命名我的UserManager而不是UserController,但想法是一样的

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

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


使用结构体的另一种合理情况是,当你想对新旧模型进行delta/diff运算时。对于引用类型,你不能开箱即用。对于值类型,突变是不共享的。

从值类型和引用类型的角度回答这个问题,从这篇苹果博客文章开始看起来很简单:

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

  • 用==比较实例数据是有意义的
  • 你希望副本有独立的状态
  • 这些数据将在代码中跨多个线程使用

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

  • 比较实例标识和===是有意义的
  • 您希望创建共享的、可变的状态

正如在那篇文章中提到的,没有可写属性的类将与结构体的行为相同,但有一点需要注意:结构体最适合线程安全的模型——这是现代应用程序架构中日益迫切的需求。

许多Cocoa api需要NSObject子类,这迫使你使用class。但除此之外,你可以使用以下苹果Swift博客中的案例来决定是使用struct / enum值类型还是类引用类型。

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

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

创建型模式:

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

是引用类型,在赋值过程中不会自动克隆。要实现原型模式,类必须采用NSCopying协议。


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


为每个引用类型实现深拷贝已经成为一项乏味的任务。如果类包含进一步的引用类型,我们必须为每个引用属性实现原型模式。然后我们必须通过实现NSCopying协议来复制整个对象图。

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


class Address{
var street:String
...
}

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

在这些回答中没有注意到的一点是,持有类和结构的变量可以是let,同时仍然允许对对象的属性进行更改,而对于结构则不能这样做。

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

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

struct的缺点是不能被继承。

  • 结构和类是用户蔑视的数据类型

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

  • 类实现了封装的原则

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

  • Class用于可重用性,structure用于分组 同一结构中的数据

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

  • 赋值
  • 类数据成员可以通过less形参直接初始化 构造函数并由参数化构造函数赋值

Structsvalue typeClassesreference type

  • 值类型比引用类型快
  • 值类型实例在多线程环境中是安全的 多线程可以改变实例,而不必担心 关于竞态条件或死锁
  • 与引用类型不同,值类型没有引用;因此,

使用value类型:

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

使用reference类型:

  • 您希望创建共享的、可变的状态。

更多的信息也可以在苹果文档中找到

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


额外的信息

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

值类型不需要动态内存分配或引用计数,这两者都是昂贵的操作。同时,值类型上的方法是静态分派的。就性能而言,这为值类型创造了巨大的优势。

作为提醒,这里有一个Swift列表

值类型:

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

引用类型:

  • 任何来自NSObject的东西
  • 函数
  • 关闭

结构vs类

< p > # EYZ0
# EYZ0 < / p >

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中间语言)可以优化你的代码

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