Swift 中的值类型和引用类型

May 30, 2022 • 预计阅读时间 2 分钟

Swift 中的 struct, enum, tuple 是值类型,class 是引用类型。

值类型在传递的时候是直接拷贝一份数据副本,而引用类型不拷贝数据,只是增加引用计数。

为了避免内存浪费,Swift 对值类型增加了一个写时复制(Copy-On-Write)的特性,只有在赋值后做了修改才会发生拷贝数据副本的行为,否则就和引用类型一样共享一份数据。

当然写时复制这个特性并不是免费的,也不是所有值类型都拥有这个特性。

struct Child {
    let name: String
    private(set) var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    mutating func growup() {
        age = age + 1
    }
}

定义一个 Array 存储数据:

var my_children = [
    Child(name: "aaa", age: 11),
    Child(name: "bbb", age: 12),
    Child(name: "ccc", age: 13),
    Child(name: "ddd", age: 14),
    Child(name: "eee", age: 15)
]

对数据进行修改:

var child = my_children[3]
child.growup()

let child2 = my_children[3]

因为是值类型,所以 var child 是把数据拷贝了一份,所做的修改都不会影响数组中的数据,所以再用 let child2 取出数据会发现互不影响。

要修改数组里的内容,有两种方法:

  1. 把修改后的数据替换数组元素

    var child = my_children[3]
    child.growup()
    my_children[3] = child
    
  2. 使用 inout 直接拿到数组元素进行修改

    func growup(child: inout Child) {
        child.growup()
    }
    
    growup(child: &my_children[3])
    

inout 只能用于函数的参数类型中,所以需要定义一个方法。

实现写时复制

如果 struct 里都是值类型,那么发生拷贝的时候,都会生成新的副本。

如果 struct 里有引用类型,比如某些字段类型是 class,那么在发生拷贝的时候,这些引用类型不会生成新的副本。

如果需要生成全新的拷贝副本,就需要自己实现写时复制

final class Ref<T> {
  var val: T
  init(_ v: T) {val = v}
}

struct Box<T> {
    var ref: Ref<T>
    init(_ x: T) { ref = Ref(x) }

    var value: T {
        get { return ref.val }
        set {
          if !isKnownUniquelyReferenced(&ref) {
            ref = Ref(newValue)
            return
          }
          ref.val = newValue
        }
    }
}

以上代码来自官方的例子: Advice: Use copy-on-write semantics for large values

实现写时复制关键是使用 isKnownUniquelyReferenced 判断对象的是否有多个持有者,如果有多个持有者就在发生修改的时候生成一个新的拷贝,否则就在原对象上直接做修改。

参考资料

Writing High-Performance Swift Code

Swift
版权声明:如果转发请带上本文链接和注明来源。

lvv.me

iOS/macOS Developer

自定义 Sh Bash Zsh Shell 配置

C++ 实现一个 AutoLayout 的 DSL