Swift 中的值类型与引用类型

由于种种原因,简书等第三方平台博客不再保证能够同步更新,欢迎移步 GitHub:https://github.com/kingcos/Perspective/。谢谢!

Value Type & Reference Type

  • Info:
  • macOS 10.12.2 Beta
  • Xcode 8.2 Beta
  • Swift 3.0

2016-11-28 Update: 调整了行文顺序,并增加了一部分图文。

前言

最近在学设计模式中,发现 Swift 中的 struct,class 以及 enum 在一般的使用中能够做到互相替换,因此探究其背后的逻辑就十分有必要。而这一问题又引出了 Swift 中的值类型和引用类型的区别。在网上搜寻一番,虽然也找到很多很棒的资料,不过有的有些过时,或是比较分散,因此总结一篇,以便自己加深印象,也方便与大家交流。

由于 Swift 中的 struct 为值类型,class 为引用类型,因此文中以这两种类型为代表来具体阐述。

stack & heap

内存(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()
stack & heap in RAM

值类型 & 引用类型

值类型(Value Type)

值类型,即每个实例保持一份数据拷贝。

在 Swift 中,典型的有 struct,enum,以及 tuple 都是值类型。而平时使用的 IntDoubleFloatStringArrayDictionarySet 其实都是用结构体实现的,也是值类型。

Swift 中,值类型的赋值为深拷贝(Deep Copy),值语义(Value Semantics)即新对象和源对象是独立的,当改变新对象的属性,源对象不会受到影响,反之同理。

值类型
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)。

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

引用类型(Reference Type)

引用类型,即所有实例共享一份数据拷贝。

在 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

参数 与 inout

预备

定义一个 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 中,函数的参数默认为常量,即在函数体内只能访问参数,而不能修改参数值。具体来说:

  1. 值类型作为参数传入时,函数体内部不能修改其值
  2. 引用类型作为参数传入时,函数体内部不能修改其指向的内存地址,但是可以修改其内部的变量值
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
}

但是如果要改变参数值或引用,那么就可以在函数体内部直接声明同名变量,并把原有变量赋值于新变量,那么这个新的变量就可以更改其值或引用。那么在函数参数的作用域和生命周期是什么呢?我们来测试一下,定义两个函数,目的为交换内部的 heightwidth

值类型

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

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

需要注意的是:

  1. 使用 inout 关键字的函数,在调用时需要在该参数前加上 & 符号
  2. inout 参数在传入时必须为变量,不能为常量或字面量(literal)
  3. inout 参数不能有默认值,不能为可变参数
  4. inout 参数不等同于函数返回值,是一种使参数的作用域超出函数体的方式
  5. 多个 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 参数的传递过程:

  1. 当函数被调用时,参数值被拷贝
  2. 在函数体内,被拷贝的参数修改
  3. 函数返回时,被拷贝的参数值被赋值给原有的变量

官方称这个行为为:copy-in copy-outcall 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

延伸阅读

Reference vs Value Types in Swift: Part 2/2
Swift 3 必看:foundation 中数据引用类型改为值类型
Mutability and Foundation Value Types

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,827评论 1 317
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,511评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,318评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,108评论 0 189
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,112评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,387评论 1 185
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,905评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,657评论 0 177
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,144评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,744评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,100评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,565评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,041评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,769评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,301评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,173评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,313评论 2 242

推荐阅读更多精彩内容