在Swift中結(jié)構(gòu)體和類最大的區(qū)別就是結(jié)構(gòu)體是值類型,類是引用類型盗尸。今天我們探究一下值類型和引用類型
一、類型表
值類型表
- 結(jié)構(gòu)體
- 枚舉
- 元組(tuple)
- 基本類型(Int泼各,Double鞍时,Bool等)
- 集合(Array, String, Dictionary, Set)
引用類型表
- 類
- 閉包
二扣蜻、存放區(qū)域
在 Swift 中, 檬嘀;全陨。但是有些值類型,如字符串或數(shù)組雨涛,會間接地將項(xiàng)保存在堆中替久。所以它們是由引用類型支持的值類型蚯根。
三僚纷、 值類型 和 引用類型
Swift 中,值類型的賦值為深拷貝(Deep Copy)痊臭,值語義(Value Semantics)即新對象和源對象是獨(dú)立的,當(dāng)改變新對象的屬性鸦致,源對象不會受到影響看疗,反之同理。
3.1值類型
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
如果聲明一個值類型的常量,那么就意味著該常量是不可變的(無論內(nèi)部數(shù)據(jù)為 var/let)。
let coordC = CoordinateStruct(x: 0, y: 0)
// WRONG: coordC.x = 100.0
在 Swift中,可以使用 withUnsafePointer(to:_:) 函數(shù)來打印值類型變量的內(nèi)存地址,這樣就能看出兩個變量的內(nèi)存地址并不相同宪睹。
withUnsafePointer(to: &coordA) { print("\($0)") }
withUnsafePointer(to: &coordB) { print("\($0)") }
// 0x00007ffee5eec040
// 0x00007ffee5eec030
在 Swift 中,雙等號( & )可以用來比較變量存儲的內(nèi)容是否一致菠齿,如果要讓我們的 類型支持該符號泞当,則必須遵守 協(xié)議。
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
3.2 引用類型
引用類型的賦值是淺拷貝(Shallow Copy),引用語義(Reference Semantics)即新對象和源對象的變量名不同襟士,但其引用(指向的內(nèi)存空間)是一樣的盗飒,因此當(dāng)使用新對象操作其內(nèi)部數(shù)據(jù)時,源對象的內(nèi)部數(shù)據(jù)也會受到影響陋桂。
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
如果聲明一個引用類型的常量逆趣,那么就意味著該常量的引用不能改變(即不能被同類型變量賦值),但指向的內(nèi)存中所存儲的變量是可以改變的嗜历。
let dogC = Dog()
dogC.height = 50
// WRONG: dogC = dogA
在 Swift 中宣渗,可以使用以下方法來打印引用類型變量指向的內(nèi)存地址。從中即可發(fā)現(xiàn)梨州,兩個變量指向的是同一塊內(nèi)存空間痕囱。
print(Unmanaged.passUnretained(dogA).toOpaque())
print(Unmanaged.passUnretained(dogB).toOpaque())
// 0x0000600000d6cac0
//0x0000600000d6cac0
在 Swift 中,三等號( & )可以用來比較引用類型的引用(即指向的內(nèi)存地址)是否一致暴匠。也可以在遵守 協(xié)議后鞍恢,使用雙等號( &)用來比較變量的內(nèi)容是否一致。
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
參數(shù) 與 inout
預(yù)備
定義一個 結(jié)構(gòu)體每窖,以及一個 類帮掉。這里為了方便打印對象屬性,類遵從了 協(xié)議窒典。
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))"
}
}
函數(shù)傳參
在 Swift 中蟆炊,函數(shù)的參數(shù)默認(rèn)為常量,即在函數(shù)體內(nèi)只能訪問參數(shù)瀑志,而不能修改參數(shù)值涩搓。具體來說:
1、值類型作為參數(shù)傳入時劈猪,函數(shù)體內(nèi)部不能修改其值
2缩膝、引用類型作為參數(shù)傳入時,函數(shù)體內(nèi)部不能修改其指向的內(nèi)存地址岸霹,但是可以修改其內(nèi)部的變量值
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
}
但是如果要改變參數(shù)值或引用疾层,那么就可以在函數(shù)體內(nèi)部直接聲明同名變量,并把原有變量賦值于新變量贡避,那么這個新的變量就可以更改其值或引用痛黎。那么在函數(shù)參數(shù)的作用域和生命周期是什么呢?我們來測試一下刮吧,定義兩個函數(shù)莉兰,目的為交換內(nèi)部的 和 洋闽。
值類型
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
小結(jié):在調(diào)用函數(shù)前后,外界變量值并沒有因?yàn)楹瘮?shù)內(nèi)對參數(shù)的修改而發(fā)生變化,而且函數(shù)體內(nèi)參數(shù)的內(nèi)存地址與外界不同许帐。因此:當(dāng)值類型的變量作為參數(shù)被傳入函數(shù)時,相當(dāng)于創(chuàng)建了新的常量并初始化為傳入的變量值,該參數(shù)的作用域及生命周期僅存在于函數(shù)體內(nèi)。
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
小結(jié):在調(diào)用函數(shù)前后器赞,外界變量值隨函數(shù)內(nèi)對參數(shù)的修改而發(fā)生變化,而且函數(shù)體內(nèi)參數(shù)的內(nèi)存地址與外界一致墓拜。因此:當(dāng)引用類型的變量作為參數(shù)被傳入函數(shù)時港柜,相當(dāng)于創(chuàng)建了新的常量并初始化為傳入的變量引用,當(dāng)函數(shù)體內(nèi)操作參數(shù)指向的數(shù)據(jù)咳榜,函數(shù)體外也受到了影響夏醉。
inout
是 Swift 中的關(guān)鍵字,可以放置于參數(shù)類型前涌韩,冒號之后畔柔。使用 之后,函數(shù)體內(nèi)部可以直接更改參數(shù)值臣樱,而且改變會保留释树。
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
小結(jié):值類型變量作為參數(shù)傳入函數(shù),外界和函數(shù)參數(shù)的內(nèi)存地址一致擎淤,函數(shù)內(nèi)對參數(shù)的更改得到了保留。
引用類型也可以使用 參數(shù)秸仙,但意義不大嘴拢。
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
需要注意的是:
使用 關(guān)鍵字的函數(shù),在調(diào)用時需要在該參數(shù)前加上 & 符號
參數(shù)在傳入時必須為變量寂纪,不能為常量或字面量(literal)
參數(shù)不能有默認(rèn)值席吴,不能為可變參數(shù)
參數(shù)不等同于函數(shù)返回值,是一種使參數(shù)的作用域超出函數(shù)體的方式
多個 參數(shù)不能同時傳入同一個變量捞蛋,因?yàn)榭饺肟匠龅捻樞虿欢ㄐ⒚埃敲醋罱K值也不能確定
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)
參數(shù)的傳遞過程:
當(dāng)函數(shù)被調(diào)用時,參數(shù)值被拷貝
在函數(shù)體內(nèi)拟杉,被拷貝的參數(shù)修改
函數(shù)返回時庄涡,被拷貝的參數(shù)值被賦值給原有的變量
官方稱這個行為為: 或 。我們可以使用 KVO 或計(jì)算屬性來跟蹤這一過程搬设,這里以計(jì)算屬性為例穴店。排除在調(diào)用函數(shù)之前與之后的 ,從中可以發(fā)現(xiàn):參數(shù)值先被獲取到(setter 被調(diào)用)拿穴,接著被設(shè)值(setter 被調(diào)用)泣洞。
根據(jù) 參數(shù)的傳遞過程,可以得知: 參數(shù)的本質(zhì)與引用類型的傳參并不是同一回事默色。參數(shù)打破了其生命周期球凰,是一個可變淺拷貝。在 Swift 中,也徹底摒除了在逃逸閉包(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.
作為一種優(yōu)化缘厢,當(dāng)參數(shù)是一個存儲于內(nèi)存中實(shí)際地址的值時,函數(shù)體內(nèi)外共用相同的一塊內(nèi)存地址义钉。該優(yōu)化行為被稱作通過引用調(diào)用昧绣;其滿足 copy-in copy-out 模型的所有必需條件,同時消除了拷貝時的開銷捶闸。不依賴于通過引用調(diào)用的優(yōu)化夜畴,使用 copy-in copy-out 提供的模型來寫代碼,以便在進(jìn)不進(jìn)行優(yōu)化時(都能)正確運(yùn)行删壮。
嵌套類型
在實(shí)際使用中贪绘,其實(shí)值類型和引用類型并不是孤立的,有時值類型里會存在引用類型的變量央碟,反之亦然税灌。這里簡要介紹這四種嵌套類型。
頂級修飾 | 次級修飾 | 賦值類型 | 存儲類型 |
---|---|---|---|
值類型 | 值類型 | 深拷貝 | 棧 |
值類型 | 引用類型 | 淺拷貝 | 堆 |
引用類型 | 值類型 | 淺拷貝 | 堆 |
引用類型 | 引用類型 | 淺拷貝 | 堆 |
值類型嵌套值類型
值類型嵌套值類型時亿虽,賦值時創(chuàng)建了新的變量菱涤,兩者是獨(dú)立的,嵌套的值類型變量也會創(chuàng)建新的變量洛勉,這兩者也是獨(dú)立的粘秆。
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
值類型嵌套引用類型
值類型嵌套引用類型時,賦值時創(chuàng)建了新的變量收毫,兩者是獨(dú)立的攻走,但嵌套的引用類型指向的是同一塊內(nèi)存空間,當(dāng)改變值類型內(nèi)部嵌套的引用類型變量值時(除了重新初始化)此再,其他對象的該屬性也會隨之改變昔搂。
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
引用類型嵌套值類型
引用類型嵌套值類型時,賦值時創(chuàng)建了新的變量输拇,但是新變量和源變量指向同一塊內(nèi)存摘符,因此改變源變量的內(nèi)部值,會影響到其他變量的值策吠。
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
引用類型嵌套引用類型
引用類型嵌套引用類型時议慰,賦值時創(chuàng)建了新的變量,但是新變量和源變量指向同一塊內(nèi)存奴曙,內(nèi)部引用類型變量也指向同一塊內(nèi)存地址别凹,改變引用類型嵌套的引用類型的值,也會影響到其他變量的值洽糟。
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