Swift5 值类型与引用类型探究

作者: 天方夜歌 | 来源:发表于2020-05-19 15:19 被阅读0次

    在Swift中结构体和类最大的区别就是结构体是值类型,类是引用类型。今天我们探究一下值类型和引用类型

    一、类型表

    值类型表

    • 结构体
    • 枚举
    • 元组(tuple)
    • 基本类型(Int,Double,Bool等)
    • 集合(Array, String, Dictionary, Set)

    引用类型表

    • 闭包

    二、存放区域

    在 Swift 中, \color{red}{值类型,存放在栈区}\color{red}{引用类型,存放在堆区}。但是有些值类型,如字符串或数组,会间接地将项保存在堆中。所以它们是由引用类型支持的值类型。

    三、 值类型 和 引用类型

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

    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
    

    如果声明一个值类型的常量,那么就意味着该常量是不可变的(无论内部数据为 var/let)。

    let coordC = CoordinateStruct(x: 0, y: 0)
    // WRONG: coordC.x = 100.0
    

    在 Swift中,可以使用 withUnsafePointer(to:_:) 函数来打印值类型变量的内存地址,这样就能看出两个变量的内存地址并不相同。

    withUnsafePointer(to: &coordA) { print("\($0)") }
    withUnsafePointer(to: &coordB) { print("\($0)") }
    
    // 0x00007ffee5eec040
    // 0x00007ffee5eec030
    

    在 Swift 中,双等号( \color{red}{==} & \color{red}{!=})可以用来比较变量存储的内容是否一致,如果要让我们的 \color{red}{struct} 类型支持该符号,则必须遵守\color{red}{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
    

    3.2 引用类型

    引用类型的赋值是浅拷贝(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 中,可以使用以下方法来打印引用类型变量指向的内存地址。从中即可发现,两个变量指向的是同一块内存空间。

    print(Unmanaged.passUnretained(dogA).toOpaque())
    print(Unmanaged.passUnretained(dogB).toOpaque())
    
    // 0x0000600000d6cac0
    //0x0000600000d6cac0
    

    在 Swift 中,三等号(\color{red}{===} & \color{red}{!==})可以用来比较引用类型的引用(即指向的内存地址)是否一致。也可以在遵守\color{red}{ Equatable} 协议后,使用双等号(\color{red}{==} &\color{red}{ !=})用来比较变量的内容是否一致。

    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

    预备

    定义一个 \color{red}{ResolutionStruct}结构体,以及一个 \color{red}{ResolutionClass} 类。这里为了方便打印对象属性,\color{red}{ResolutionClass }类遵从了 \color{red}{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
    }
    

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

    \color{red}{inout}是 Swift 中的关键字,可以放置于参数类型前,冒号之后。使用 \color{red}{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
    

    小结:值类型变量作为参数传入函数,外界和函数参数的内存地址一致,函数内对参数的更改得到了保留。

    引用类型也可以使用 \color{red}{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
    

    需要注意的是:

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

    \color{red}{inout} 参数的传递过程:

    当函数被调用时,参数值被拷贝
    在函数体内,被拷贝的参数修改
    函数返回时,被拷贝的参数值被赋值给原有的变量
    官方称这个行为为:\color{red}{copy-in copy-out}\color{red}{call by value result}。我们可以使用 KVO 或计算属性来跟踪这一过程,这里以计算属性为例。排除在调用函数之前与之后的 \color{red}{center GETTER call},从中可以发现:参数值先被获取到(setter 被调用),接着被设值(setter 被调用)。

    根据\color{red}{inout} 参数的传递过程,可以得知:\color{red}{inout} 参数的本质与引用类型的传参并不是同一回事。\color{red}{inout}参数打破了其生命周期,是一个可变浅拷贝。在 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.
    作为一种优化,当参数是一个存储于内存中实际地址的值时,函数体内外共用相同的一块内存地址。该优化行为被称作通过引用调用;其满足 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
    

    相关文章

      网友评论

        本文标题:Swift5 值类型与引用类型探究

        本文链接:https://www.haomeiwen.com/subject/ijkbohtx.html