最近在学设计模式中,发现 Swift 中的 struct,class 以及 enum 在通常的使用中可以作到互相替换,所以探究其背后的逻辑就十分有必要。而这一问题又引出了 Swift 中的值类型和引用类型的区别。在网上搜寻一番,虽然也找到不少很棒的资料,不过有的有些过期,或是比较分散,所以总结一篇,以便本身加深印象,也方便与你们交流。html
因为 Swift 中的 struct 为值类型,class 为引用类型,所以文中以这两种类型为表明来具体阐述。swift
内存(RAM)中有两个区域,栈区(stack)和堆区(heap)。在 Swift 中,值类型,存放在栈区;引用类型,存放在堆区。设计模式
class RectClass { var height = 0.0 var width = 0.0 } struct RectStruct { var height = 0.0 var width = 0.0 } var rectCls = RectClass() var rectStrct = RectStruct()
值类型,即每一个实例保持一份数据拷贝。多线程
在 Swift 中,典型的有 struct,enum,以及 tuple 都是值类型。而平时使用的 Int
, Double
,Float
,String
,Array
,Dictionary
,Set
其实都是用结构体实现的,也是值类型。闭包
Swift 中,值类型的赋值为深拷贝(Deep Copy),值语义(Value Semantics)即新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响,反之同理。app
struct CoordinateStruct { var x: Double var y: Double } var coordA = CoordinateStruct(x: 0, y: 0) var coordB = coordA coordA.x = 100.0 print("coordA.x -> \(coordA.x)") print("coordB.x -> \(coordB.x)") // coordA.x -> 100.0 // coordB.x -> 0.0
若是声明一个值类型的常量,那么就意味着该常量是不可变的(不管内部数据为 var
/let
)。ide
let coordC = CoordinateStruct(x: 0, y: 0) // WRONG: coordC.x = 100.0
在 Swift 3.0 中,可使用
withUnsafePointer(to:_:)
函数来打印值类型变量的内存地址,这样就能看出两个变量的内存地址并不相同。函数
withUnsafePointer(to: &coordA) { print("\($0)") } withUnsafePointer(to: &coordB) { print("\($0)") } // 0x000000011df6ec10 // 0x000000011df6ec20
在 Swift 中,双等号(==
& !=
)能够用来比较变量存储的内容是否一致,若是要让咱们的 struct
类型支持该符号,则必须遵照 Equatable
协议。学习
extension CoordinateStruct: Equatable { static func ==(left: CoordinateStruct, right: CoordinateStruct) -> Bool { return (left.x == right.x && left.y == right.y) } } if coordA != coordB { print("coordA != coordB") } // coordA != coordB
引用类型,即全部实例共享一份数据拷贝。测试
在 Swift 中,class 和闭包是引用类型。引用类型的赋值是浅拷贝(Shallow Copy),引用语义(Reference Semantics)即新对象和源对象的变量名不一样,但其引用(指向的内存空间)是同样的,所以当使用新对象操做其内部数据时,源对象的内部数据也会受到影响。
class Dog { var height = 0.0 var weight = 0.0 } var dogA = Dog() var dogB = dogA dogA.height = 50.0 print("dogA.height -> \(dogA.height)") print("dogB.height -> \(dogB.height)") // dogA.height -> 50.0 // dogB.height -> 50.0
若是声明一个引用类型的常量,那么就意味着该常量的引用不能改变(即不能被同类型变量赋值),但指向的内存中所存储的变量是能够改变的。
let dogC = Dog() dogC.height = 50 // WRONG: dogC = dogA
在 Swift 3.0 中,可使用如下方法来打印引用类型变量指向的内存地址。从中便可发现,两个变量指向的是同一块内存空间。
print(Unmanaged.passUnretained(dogA).toOpaque()) print(Unmanaged.passUnretained(dogB).toOpaque()) // 0x0000600000031380 // 0x0000600000031380
在 Swift 中,三等号(===
& !==
)能够用来比较引用类型的引用(即指向的内存地址)是否一致。也能够在遵照 Equatable
协议后,使用双等号(==
& !=
)用来比较变量的内容是否一致。
if (dogA === dogB) { print("dogA === dogB") } // dogA === dogB if dogC !== dogA { print("dogC !== dogA") } // dogC !== dogA extension Animal: Equatable { static func ==(left: Animal, right: Animal) -> Bool { return (left.height == right.height && left.weight == right.weight) } } if dogC == dogA { print("dogC == dogA") } // dogC == dogA
定义一个 ResolutionStruct
结构体,以及一个 ResolutionClass
类。这里为了方便打印对象属性,ResolutionClass
类听从了 CustomStringConvertible
协议。
struct ResolutionStruct { var height = 0.0 var width = 0.0 } class ResolutionClass: CustomStringConvertible { var height = 0.0 var width = 0.0 var description: String { return "ResolutionClass(height: \(height), width: \(width))" } }
在 Swift 中,函数的参数默认为常量,即在函数体内只能访问参数,而不能修改参数值。具体来讲:
func test(sct: ResolutionStruct) { // WRONG: sct.height = 1080 var sct = sct sct.height = 1080 } func test(clss: ResolutionClass) { // WRONG: clss = ResolutionClass() clss.height = 1080 var clss = clss clss = ResolutionClass() clss.height = 1440 }
可是若是要改变参数值或引用,那么就能够在函数体内部直接声明同名变量,并把原有变量赋值于新变量,那么这个新的变量就能够更改其值或引用。那么在函数参数的做用域和生命周期是什么呢?咱们来测试一下,定义两个函数,目的为交换内部的 height
和 width
。
值类型
func swap(resSct: ResolutionStruct) -> ResolutionStruct { var resSct = resSct withUnsafePointer(to: &resSct) { print("During calling: \($0)") } let temp = resSct.height resSct.height = resSct.width resSct.width = temp return resSct } var iPhone4ResoStruct = ResolutionStruct(height: 960, width: 640) print(iPhone4ResoStruct) withUnsafePointer(to: &iPhone4ResoStruct) { print("Before calling: \($0)") } print(swap(resSct: iPhone4ResoStruct)) print(iPhone4ResoStruct) withUnsafePointer(to: &iPhone4ResoStruct) { print("After calling: \($0)") } // ResolutionStruct(height: 960.0, width: 640.0) // Before calling: 0x00000001138d6f50 // During calling: 0x00007fff5a512148 // ResolutionStruct(height: 640.0, width: 960.0) // ResolutionStruct(height: 960.0, width: 640.0) // After calling: 0x00000001138d6f50
小结:在调用函数先后,外界变量值并没有由于函数内对参数的修改而发生变化,并且函数体内参数的内存地址与外界不一样。所以:当值类型的变量做为参数被传入函数时,至关于建立了新的常量并初始化为传入的变量值,该参数的做用域及生命周期仅存在于函数体内。
func swap(resCls: ResolutionClass) { print("During calling: \(Unmanaged.passUnretained(resCls).toOpaque())") let temp = resCls.height resCls.height = resCls.width resCls.width = temp } let iPhone5ResoClss = ResolutionClass() iPhone5ResoClss.height = 1136 iPhone5ResoClss.width = 640 print(iPhone5ResoClss) print("Before calling: \(Unmanaged.passUnretained(iPhone5ResoClss).toOpaque())") swap(resCls: iPhone5ResoClss) print(iPhone5ResoClss) print("After calling: \(Unmanaged.passUnretained(iPhone5ResoClss).toOpaque())") // ResolutionClass(height: 1136.0, width: 640.0) // Before calling: 0x00006000000220e0 // During calling: 0x00006000000220e0 // ResolutionClass(height: 640.0, width: 1136.0) // After calling: 0x00006000000220e0
小结:在调用函数先后,外界变量值随函数内对参数的修改而发生变化,并且函数体内参数的内存地址与外界一致。所以:当引用类型的变量做为参数被传入函数时,至关于建立了新的常量并初始化为传入的变量引用,当函数体内操做参数指向的数据,函数体外也受到了影响。
inout
是 Swift 中的关键字,能够放置于参数类型前,冒号以后。使用 inout
以后,函数体内部能够直接更改参数值,并且改变会保留。
func swap(resSct: inout ResolutionStruct) { withUnsafePointer(to: &resSct) { print("During calling: \($0)") } let temp = resSct.height resSct.height = resSct.width resSct.width = temp } var iPhone6ResoStruct = ResolutionStruct(height: 1334, width: 750) print(iPhone6ResoStruct) withUnsafePointer(to: &iPhone6ResoStruct) { print("Before calling: \($0)") } swap(resSct: &iPhone6ResoStruct) print(iPhone6ResoStruct) withUnsafePointer(to: &iPhone6ResoStruct) { print("After calling: \($0)") } // ResolutionStruct(height: 1334.0, width: 750.0) // Before calling: 0x000000011ce62f50 // During calling: 0x000000011ce62f50 // ResolutionStruct(height: 750.0, width: 1334.0) // After calling: 0x000000011ce62f50
小结:值类型变量做为参数传入函数,外界和函数参数的内存地址一致,函数内对参数的更改获得了保留。
引用类型也可使用 inout
参数,但意义不大。
func swap(clss: inout ResolutionClass) { print("During calling: \(Unmanaged.passUnretained(clss).toOpaque())") let temp = clss.height clss.height = clss.width clss.width = temp } var iPhone7PlusResClss = ResolutionClass() iPhone7PlusResClss.height = 1080 iPhone7PlusResClss.width = 1920 print(iPhone7PlusResClss) print("Before calling: \(Unmanaged.passUnretained(iPhone7PlusResClss).toOpaque())") swap(clss: &iPhone7PlusResClss) print(iPhone7PlusResClss) print("After calling: \(Unmanaged.passUnretained(iPhone7PlusResClss).toOpaque())") // ResolutionClass(height: 1080.0, width: 1920.0) // Before calling: 0x000060000003e580 // During calling: 0x000060000003e580 // ResolutionClass(height: 1920.0, width: 1080.0) // After calling: 0x000060000003e580
须要注意的是:
inout
关键字的函数,在调用时须要在该参数前加上 &
符号inout
参数在传入时必须为变量,不能为常量或字面量(literal)inout
参数不能有默认值,不能为可变参数inout
参数不等同于函数返回值,是一种使参数的做用域超出函数体的方式inout
参数不能同时传入同一个变量,由于拷入拷出的顺序不定,那么最终值也不能肯定struct Point { var x = 0.0 var y = 0.0 } struct Rectangle { var width = 0.0 var height = 0.0 var origin = Point() var center: Point { get { print("center GETTER call") return Point(x: origin.x + width / 2, y: origin.y + height / 2) } set { print("center SETTER call") origin.x = newValue.x - width / 2 origin.y = newValue.y - height / 2 } } func reset(center: inout Point) { center.x = 0.0 center.y = 0.0 } } var rect = Rectangle(width: 100, height: 100, origin: Point(x: -100, y: -100)) print(rect.center) rect.reset(center: &rect.center) print(rect.center) // center GETTER call // Point(x: -50.0, y: -50.0) // center GETTER call // center SETTER call // center GETTER call // Point(x: 0.0, y: 0.0)
inout
参数的传递过程:
官方称这个行为为:copy-in copy-out
或 call by value result
。咱们可使用 KVO 或计算属性来跟踪这一过程,这里以计算属性为例。排除在调用函数以前与以后的 center GETTER call
,从中能够发现:参数值先被获取到(setter 被调用),接着被设值(setter 被调用)。
根据 inout
参数的传递过程,能够得知:inout
参数的本质与引用类型的传参并非同一回事。inout
参数打破了其生命周期,是一个可变浅拷贝。在 Swift 3.0 中,也完全摒除了在逃逸闭包(Escape Closure)中被捕获。苹果官方也有以下的说明:
As an optimization, when the argument is a value stored at a physical address in memory, the same memory location is used both inside and outside the function body. The optimized behavior is known as call by reference; it satisfies all of the requirements of the copy-in copy-out model while removing the overhead of copying. Write your code using the model given by copy-in copy-out, without depending on the call-by-reference optimization, so that it behaves correctly with or without the optimization.
做为一种优化,当参数是一个存储于内存中实际地址的值时,函数体内外共用相同的一块内存地址。该优化行为被称做经过引用调用;其知足 copy-in copy-out 模型的全部必需条件,同时消除了拷贝时的开销。不依赖于经过引用调用的优化,使用 copy-in copy-out 提供的模型来写代码,以便在进不进行优化时(都能)正确运行。
在实际使用中,其实值类型和引用类型并非孤立的,有时值类型里会存在引用类型的变量,反之亦然。这里简要介绍这四种嵌套类型。
值类型嵌套值类型时,赋值时建立了新的变量,二者是独立的,嵌套的值类型变量也会建立新的变量,这二者也是独立的。
struct Circle { var radius: Double } var circleA = Circle(radius: 5.0) var circleB = circleA circleA.radius = 10 print(circleA) print(circleB) withUnsafePointer(to: &circleA) { print("circleA: \($0)") } withUnsafePointer(to: &circleB) { print("circleB: \($0)") } withUnsafePointer(to: &circleA.radius) { print("circleA.radius: \($0)") } withUnsafePointer(to: &circleB.radius) { print("circleB.radius: \($0)") } // Circle(radius: 10.0) // Circle(radius: 5.0) // circleA: 0x000000011dc6dc90 // circleB: 0x000000011dc6dc98 // circleA.radius: 0x000000011dc6dc90 // circleB.radius: 0x000000011dc6dc98
值类型嵌套引用类型时,赋值时建立了新的变量,二者是独立的,但嵌套的引用类型指向的是同一块内存空间,当改变值类型内部嵌套的引用类型变量值时(除了从新初始化),其余对象的该属性也会随之改变。
class PointClass: CustomStringConvertible { var x: Double var y: Double var description: String { return "(\(x), \(y))" } init(x: Double, y: Double) { self.x = x self.y = y } } struct Circle { var center: PointClass } var circleA = Circle(center: PointClass(x: 0.0, y: 0.0)) var circleB = circleA circleA.center.x = 10.0 print(circleA) print(circleB) withUnsafePointer(to: &circleA) { print("circleA: \($0)") } withUnsafePointer(to: &circleB) { print("circleB: \($0)") } print("circleA.center: \(Unmanaged.passUnretained(circleA.center).toOpaque())") print("circleB.center: \(Unmanaged.passUnretained(circleB.center).toOpaque())") // Circle(center: (10.0, 0.0)) // Circle(center: (10.0, 0.0)) // circleA: 0x0000000118251fa0 // circleB: 0x0000000118251fa8 // circleA.center: 0x000060000003e100 // circleB.center: 0x000060000003e100
引用类型嵌套值类型时,赋值时建立了新的变量,可是新变量和源变量指向同一块内存,所以改变源变量的内部值,会影响到其余变量的值。
class Circle: CustomStringConvertible { var radius: Double var description: String { return "Radius:\(radius)" } init(radius: Double) { self.radius = radius } } var circleA = Circle(radius: 0.0) var circleB = circleA circleA.radius = 5.0 print(circleA) print(circleB) print("circleA: \(Unmanaged.passUnretained(circleA).toOpaque())") print("circleB: \(Unmanaged.passUnretained(circleB).toOpaque())") withUnsafePointer(to: &circleA.radius) { print("circleA.radius: \($0)") } withUnsafePointer(to: &circleB.radius) { print("circleB.radius: \($0)") } // Radius:5.0 // Radius:5.0 // circleA: 0x000060000003bc80 // circleB: 0x000060000003bc80 // circleA.radius: 0x000060000003bc90 // circleB.radius: 0x000060000003bc90
引用类型嵌套引用类型时,赋值时建立了新的变量,可是新变量和源变量指向同一块内存,内部引用类型变量也指向同一块内存地址,改变引用类型嵌套的引用类型的值,也会影响到其余变量的值。
class PointClass: CustomStringConvertible { var x: Double var y: Double init(x: Double, y: Double) { self.x = x self.y = y } var description: String { return "(\(x), \(y))" } } class Circle: CustomStringConvertible { var center: PointClass var description: String { return "Center:\(center)" } init(center: PointClass) { self.center = center } } var circleA = Circle(center: PointClass(x: 0.0, y: 0.0)) let circleB = circleA circleA.center.x = 5.0 print(circleA) print(circleB) print("circleA: \(Unmanaged.passUnretained(circleA).toOpaque())") print("circleB: \(Unmanaged.passUnretained(circleB).toOpaque())") print("circleA.center: \(Unmanaged.passUnretained(circleA.center).toOpaque())") print("circleB.center: \(Unmanaged.passUnretained(circleB.center).toOpaque())") // Center:(5.0, 0.0) // Center:(5.0, 0.0) // circleA: 0x0000608000025fa0 // circleB: 0x0000608000025fa0 // circleA.center: 0x0000608000025820 // circleB.center: 0x0000608000025820
这篇文章是我在着手写 Swift 中的 struct & class & enum 一文时抽离出来的一篇。主要仍是围绕了值类型中的 struct 和引用类型中的 class,在本文 stack & heap 一节中,只是简单描述,由于一直对此部份内容感到迷惑,也查阅不少资料,但愿最近能够总结出来一篇小文,与你们分享。
When|值类型 Value Type|引用类型 Reference Type
-----|-----|-----|-----
1|==
有意义时|===
有意义时
2|独立|共享,可变
3|在多线程使用的数据|-
在本文的叙述中,可能有许多说法与您平时所用的术语略有差池,例如变量指向的内存空间,其实也等价于变量指向的内存地址。在行文过程当中,查阅了不少国外的资料,也尽力将语言规范,以避免产生歧义,若是有任何错误或建议,您均可以在评论中直接提出,我会研究学习,虚心接受,并做出相应整改。
WWDC 2015 Building Better Apps with Value Types in Swift
Value and Reference Types
In-Out Parameters
In-Out Parameters
Reference vs Value Types in Swift: Part 1/2