使用 Swift,来自 Java 背景,为什么要选择 Struct 而不是 Class?似乎它们是相同的东西,结构提供的功能较少。那为什么选择它?
根据非常流行的 WWDC 2015 演讲 Protocol Oriented Programming in Swift (video, transcript),Swift 提供了许多特性,使结构在许多情况下优于类。
如果结构相对较小且可复制,则结构更可取,因为复制比使用类对同一实例进行多个引用更安全。当将变量传递给许多类和/或在多线程环境中时,这一点尤其重要。如果您始终可以将变量的副本发送到其他地方,那么您永远不必担心其他地方会更改您下面的变量的值。
使用 Structs,不必担心内存泄漏或多个线程竞相访问/修改变量的单个实例。 (对于更有技术头脑的人,例外情况是在闭包内捕获结构时,因为它实际上是在捕获对实例的引用,除非您明确将其标记为要复制)。
类也可能变得臃肿,因为一个类只能从一个超类继承。这鼓励我们创建巨大的超类,其中包含许多只是松散相关的不同能力。使用协议,尤其是可以提供协议实现的协议扩展,可以让您消除对实现此类行为的类的需求。
该演讲列出了首选类的这些场景:
复制或比较实例没有意义(例如,Window) 实例生命周期与外部效果相关联(例如,TemporaryFile) 实例只是“接收器”--只写管道到外部状态(例如CGContext)
这意味着结构应该是默认的,类应该是后备的。
另一方面,The Swift Programming Language 文档有些矛盾:
结构实例总是按值传递,类实例总是按引用传递。这意味着它们适用于不同类型的任务。当您考虑项目所需的数据结构和功能时,请决定是否应将每个数据结构定义为类或结构。作为一般准则,当满足以下一个或多个条件时,请考虑创建结构: 结构的主要目的是封装一些相对简单的数据值。当您分配或传递该结构的实例时,期望封装的值将被复制而不是引用是合理的。结构存储的任何属性本身都是值类型,也应该被复制而不是引用。该结构不需要从另一个现有类型继承属性或行为。结构的良好候选示例包括: 几何形状的大小,可能封装了宽度属性和高度属性,两者都是 Double 类型。一种引用系列中范围的方法,可能封装了 Int 类型的 start 属性和 length 属性。 3D 坐标系中的一个点,可能封装了 x、y 和 z 属性,每个属性都是 Double 类型。在所有其他情况下,定义一个类,并创建该类的实例以通过引用进行管理和传递。实际上,这意味着大多数自定义数据结构应该是类,而不是结构。
这里声称我们应该默认使用类并仅在特定情况下使用结构。最终,您需要了解值类型与引用类型在现实世界中的含义,然后您可以就何时使用结构或类做出明智的决定。另外,请记住,这些概念一直在发展,并且 Swift 编程语言文档是在面向协议编程演讲之前编写的。
这个答案最初是关于结构和类之间的性能差异。不幸的是,围绕我用于测量的方法存在太多争议。我把它留在下面,但请不要读太多。我认为这么多年过去了,在 Swift 社区中已经很清楚 struct(连同 enum)由于其简单性和安全性而始终是首选。
如果性能对您的应用很重要,请自行衡量。我仍然认为大多数时候 struct 性能是优越的,但最好的答案就像评论中有人说的那样:这取决于。
=== 旧答案 ===
由于结构实例是在堆栈上分配的,而类实例是在堆上分配的,因此结构有时会快得多。
但是,您应该始终自己衡量并根据您的独特用例做出决定。
考虑以下示例,该示例演示了使用 struct
和 class
包装 Int
数据类型的 2 种策略。我使用 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
:
类版本耗时 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 build,平均运行 5 次以上,Swift Compiler 设置为 -O -whole-module-optimization
:
类版本耗时 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 build,平均运行超过 5 次
类版本耗时 9.788332333s
struct 版本耗时 0.010532942s(快 900 倍)
旧结果(来自未知时间)
(在结构/类上运行 1 个字段,而不是 10 个)
在我的 MacBook Pro 上发布版本:
课程版本耗时 1.10082 秒
struct 版本耗时 0.02324 秒(快 50 倍)
结构和类之间的相似之处。
我用简单的例子为此创建了要点。 https://github.com/objc-swift/swift-classes-vs-structures
和差异
1. 继承。
结构不能迅速继承。如果你想
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 代替,这是一个有效的表达式。因为在一个类中,不可变常量是对类本身的引用,而不是它的实例变量(除非那些变量定义为常量)
假设我们知道 Struct 是一个值类型,而 Class 是一个引用类型。
如果您不知道什么是值类型和引用类型,请参阅 What's the difference between passing by reference vs. passing by value?
基于 mikeash's post:
... 让我们先看一些极端、明显的例子。整数显然是可复制的。它们应该是值类型。网络套接字不能被明智地复制。它们应该是引用类型。 x, y 对中的点是可复制的。它们应该是值类型。代表磁盘的控制器不能被合理地复制。那应该是引用类型。有些类型可以被复制,但它可能不是你想要一直发生的事情。这表明它们应该是引用类型。例如,在概念上可以复制屏幕上的按钮。副本不会与原件完全相同。单击副本不会激活原件。副本不会在屏幕上占据相同的位置。如果您传递按钮或将其放入新变量中,您可能希望引用原始按钮,并且您只想在明确请求时制作副本。这意味着您的按钮类型应该是引用类型。视图和窗口控制器是一个类似的例子。可以想象,它们可能是可复制的,但这几乎不是您想要做的。它们应该是引用类型。模型类型呢?你可能有一个 User 类型代表你系统上的一个用户,或者一个 Crime 类型代表一个用户采取的行动。这些是非常可复制的,因此它们可能应该是值类型。但是,您可能希望在程序的某个位置对用户犯罪的更新对程序的其他部分可见。这表明您的用户应该由某种作为引用类型的用户控制器管理。例如 struct User {} class UserController { var users: [User] func add(user: User) { ... } func remove(userNamed: String) { ... } func ... } 集合是一个有趣的例子。这些包括诸如数组和字典之类的东西,以及字符串。它们是可复制的吗?明显地。复制您想要的东西是否容易且经常发生?这不太清楚。大多数语言对此说“不”,并使其集合引用类型。在 Objective-C、Java、Python 和 JavaScript 以及我能想到的几乎所有其他语言中都是如此。 (一个主要的例外是具有 STL 集合类型的 C++,但 C++ 是语言世界的疯狂疯子,它所做的一切都很奇怪。)斯威夫特说“是的”,这意味着像 Array 和 Dictionary 和 String 这样的类型是结构而不是类。它们在分配时被复制,并在作为参数传递时被复制。只要副本便宜,这是一个完全明智的选择,Swift 非常努力地做到这一点。 ...
我个人不会这样命名我的课程。我通常命名我的 UserManager 而不是 UserController 但想法是一样的
此外,当您必须覆盖函数的每个实例时不要使用类,即它们没有任何共享功能。
因此,与其拥有一个类的多个子类。使用多个符合协议的结构。
结构的另一个合理案例是当您想要对旧模型和新模型进行增量/差异时。使用引用类型,您不能开箱即用。对于值类型,突变不共享。
以下是需要考虑的其他一些原因:
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) // [一、二、三] 显然,不变性与可变性是一个巨大的话题,但很多聪明人认为不变性——在这种情况下是结构——更可取。可变对象与不可变对象
internal
范围之外可用,则需要自己实际编写初始化程序。
mutating
,以便明确哪些函数会更改其状态。但它们作为值类型的性质才是重要的。如果您使用 let
声明一个结构,则不能在其上调用任何变异函数。 通过值类型更好地编程 的 WWDC 15 视频是一个很好的资源。
一些优点:
由于不可共享而自动线程安全
由于没有 isa 和 refcount 使用更少的内存(实际上通常是堆栈分配的)
方法总是静态分派的,因此可以内联(尽管@final 可以对类执行此操作)
出于与线程安全相同的原因,更容易推理(无需像 NSArray、NSString 等那样典型地“防御性复制”)
Structs
是 value type
,Classes
是 reference type
值类型比引用类型快
值类型实例在多线程环境中是安全的,因为多个线程可以改变实例而不必担心竞争条件或死锁
与引用类型不同,值类型没有引用;因此没有内存泄漏。
在以下情况下使用 value
类型:
您希望副本具有独立状态,数据将在多个线程的代码中使用
在以下情况下使用 reference
类型:
您想要创建共享的、可变的状态。
更多信息也可以在 Apple 文档中找到
https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html
附加信息
Swift 值类型保存在堆栈中。在一个进程中,每个线程都有自己的堆栈空间,因此没有其他线程能够直接访问您的值类型。因此没有竞争条件、锁、死锁或任何相关的线程同步复杂性。
值类型不需要动态内存分配或引用计数,这两者都是昂贵的操作。同时,值类型的方法是静态分派的。这些在性能方面创造了有利于价值类型的巨大优势。
提醒一下,这里是 Swift 的列表
值类型:
结构
枚举
元组
基元(Int、Double、Bool 等)
集合(数组、字符串、字典、集合)
参考类型:
班级
任何来自 NSObject 的东西
功能
关闭
结构比类快得多。此外,如果您需要继承,那么您必须使用 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)
现在如果我们打印航班 A 的 id 和描述,我们得到
id = 200
description = "second flight of Virgin Airlines"
在这里,我们可以看到 FlightA 的 id 和 description 发生了变化,因为传递给 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
从值类型与引用类型的角度回答这个问题,从 this Apple blog post 看起来很简单:
在以下情况下使用值类型[例如结构、枚举]:比较实例数据与 == 有意义您希望副本具有独立状态数据将在跨多个线程的代码中使用在以下情况下使用引用类型[例如类]:比较实例身份with === 有意义你想创建共享的、可变的状态
正如那篇文章中提到的,没有可写属性的类与结构的行为相同,但(我将添加)一个警告:结构最适合线程安全模型——现代应用程序架构中越来越迫切的要求。
结构与类
[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
(堆上的分配)
对于类,您可以获得继承并通过引用传递,结构没有继承并通过值传递。
有很多关于 Swift 的 WWDC 会议,其中一个对这个特定问题进行了详细的回答。确保您观看这些内容,因为它可以让您比语言指南或 iBook 更快地上手。
我不会说结构提供的功能更少。
当然,self 是不可变的,除了在一个 mutating 函数中,但仅此而已。
只要您坚持每个类都应该是抽象的或最终的,继承就可以正常工作。
将抽象类实现为协议,将最终类实现为结构。
结构的好处是您可以使字段可变而无需创建共享可变状态,因为写入时复制会处理该问题:)
这就是为什么以下示例中的属性/字段都是可变的,我不会在 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 中,引入了一种新的编程模式,称为面向协议的编程。
创作模式:
在 swift 中,Struct 是一种自动克隆的值类型。因此,我们免费获得了实现原型模式所需的行为。
而 classes 是引用类型,在分配期间不会自动克隆。要实现原型模式,类必须采用 NSCopying
协议。
浅拷贝只复制指向这些对象的引用,而深拷贝复制对象的引用。
为每个引用类型实现深拷贝已成为一项乏味的任务。如果类包含更多引用类型,我们必须为每个引用属性实现原型模式。然后我们必须通过实现 NSCopying
协议来实际复制整个对象图。
class Contact{
var firstName:String
var lastName:String
var workAddress:Address // Reference type
}
class Address{
var street:String
...
}
通过使用结构和枚举,我们使我们的代码更简单,因为我们不必实现复制逻辑。
许多 Cocoa API 需要 NSObject 子类,这迫使您使用类。但除此之外,您可以使用 Apple Swift 博客中的以下案例来决定是使用结构/枚举值类型还是类引用类型。
https://developer.apple.com/swift/blog/?id=10
在这些答案中没有引起注意的一点是,包含类与结构的变量可以是 let
,同时仍允许更改对象的属性,而您不能对结构执行此操作。
如果您不希望变量指向另一个对象,但仍需要修改该对象,即在您希望一个接一个地更新许多实例变量的情况下,这很有用。如果它是一个结构,则必须允许使用 var
将变量完全重置为另一个对象才能做到这一点,因为 Swift 中的常量值类型正确地允许零突变,而引用类型(类)不表现这边走。
由于结构是值类型,您可以非常轻松地创建存储到堆栈中的内存。结构可以很容易地访问,并且在工作范围之后,它很容易通过堆栈顶部的弹出从堆栈内存中释放。另一方面,类是存储在堆中的引用类型,一个类对象中所做的更改将影响另一个对象,因为它们是紧密耦合的引用类型。结构的所有成员都是公共的,而类的所有成员都是私有的.
struct 的缺点是不能被继承。
结构和类是用户定义的数据类型
默认情况下,结构是公共的,而类是私有的
类实现封装的原则
类的对象是在堆内存上创建的
类用于可重用性,而结构用于将数据分组到同一结构中
结构体数据成员不能直接初始化,但可以由结构体外部赋值
类数据成员可以直接由无参构造函数初始化,由有参构造函数赋值
不定期副业成功案例分享
In practice, this means that most custom data constructs should be classes, not structures.
你能向我解释一下,读完之后你会如何理解大多数数据集应该是结构而不是类?当某些东西应该是结构时,他们给出了一组特定的规则,并且几乎说“类更好的所有其他场景”。