当前位置: 首页 > 工具软件 > Flying-Swift > 使用案例 >

Swift 中的值类型与引用类型

聂建茗
2023-12-01

一、Stack & Heap

内存(RAM)中有两个区域,栈区(stack)和堆区(heap)。

在 Swift 中

  • 值类型,存放在栈区;
  • 引用类型,存放在堆区。

二、Swift中的值类型

值类型(Value Type)即每个实例保持一份数据拷贝。
  • 典型的: struct,enum,tuple
  • 常用的: Int, Double,Float,String,Array,Dictionary,Set ,其实他们都是用结构体实现的,也是值类型。

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

struct StructA {
    var x: Double
    var y: Double
}

var sA = StructA(x: 0, y: 0)
var sB = sA

sA.x = 100.0
print("sA.x -> \(sA.x)")
print("sB.x -> \(sB.x)")

// sA.x -> 100.0
// sB.x -> 0.0

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

let sC = StructA(x: 0, y: 0)
// WRONG: sC.x = 100.0  不可变

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

withUnsafePointer(to: &sA) { print("\($0)") }
withUnsafePointer(to: &sB) { print("\($0)") }

// 0x000000011df6ec10
// 0x000000011df6ec20

在 Swift 中,双等号(== & !=)可以用来比较变量存储的内容是否一致,如果要让我们的 struct 类型支持该符号,则必须遵守 Equatable 协议。

extension StructA: Equatable {
    static func == (left: StructA, right: StructA) -> Bool {
        return (left.x == right.x && left.y == right.y)
    }
}

if sA != sB {
    print("sA != sB")
}

// sA != sB

三、Swift 中引用类型(Reference Type)

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

引用类型: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

四、Swift 中函数传参

函数的参数默认为常量,即在函数体内只能访问参数,而不能修改参数值。具体来说:

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

定义一个 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))"
    }
}

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。

1.值类型
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

小结:在调用函数前后,外界变量值并没有因为函数内对参数的修改而发生变化,而且函数体内参数的内存地址与外界不同。因此:当值类型的变量作为参数被传入函数时,相当于创建了新的常量并初始化为传入的变量值,该参数的作用域及生命周期仅存在于函数体内。

2.引用类型
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

小结:在调用函数前后,外界变量值随函数内对参数的修改而发生变化,而且函数体内参数的内存地址与外界一致。因此:当引用类型的变量作为参数被传入函数时,相当于创建了新的常量并初始化为传入的变量引用,当函数体内操作参数指向的数据,函数体外也受到了影响。

 类似资料: