定义

值类型

值类型是指变量直接存储自身字段或描述信息的类型。值类型具有值语义,当将一个值类型变量赋值给另一个变量时,语义上会得到一份独立的值;底层是否立即复制完整数据,取决于编译器优化和写时拷贝等实现。

引用类型

引用类型是指变量存储的是指向数据在内存中位置的引用(指针)。当将一个引用类型变量赋值给另一个变量时,两个变量指向同一块内存区域。

Objective-C中的类型分类

在Objective-C中:

  • 值类型:基础数据类型(int、float、double、BOOL、char等)、结构体(struct)、枚举(enum)
  • 引用类型:除基础数据类型之外的大部分类型,包括NSObject及其子类(NSString、NSArray、NSDictionary、自定义类等)

Swift中的类型分类

Swift对类型的分类更加清晰:

  • 值类型:基础数据类型(Int、Float、Double、Bool等)、字符串(String)、结构体(Struct)、枚举(Enum)、数组(Array)、字典(Dictionary)、集合(Set)等
  • 引用类型:类(Class)、闭包(Closure)、Actor等

拷贝机制差异

值类型 - 值语义拷贝

  • 每个值类型变量都有自己的字段存储
  • 对一个变量的操作不会影响另一个变量
  • 赋值语义上会得到独立副本;如果字段是引用类型,复制的是引用本身,底层也可能通过写时拷贝延迟真正的数据复制

引用类型 - 浅拷贝

  • 引用类型在内存中有一个指向该位置的引用
  • 引用类型的变量可以指向相同类型的数据
  • 对一个变量进行的操作会影响另一变量所指向的数据

内存分配机制

需要注意:值类型/引用类型描述的是语义模型,不等价于栈/堆分配规则。Swift编译器会根据生命周期、逃逸分析、优化级别、协议类型包装、集合存储等因素决定具体放在哪里。

理解Swift对象和字段的内存位置,可以先记住三个规则:

  • class实例本体通常在堆上,通过引用计数管理生命周期
  • struct/enum的字段通常内联存储在这个值本身所在的位置
  • class类型的字段存储的是对象引用,也就是一个指针,真实class实例仍然在堆上

常见存储位置

  • 局部值类型变量:生命周期明确、未逃逸时,通常可以放在当前线程的栈区,甚至被优化到寄存器中
  • class实例:实例本体在堆区,局部变量中保存的是指向堆对象的引用
  • class里的struct属性:struct属性内联存储在class实例这块堆内存中
  • struct里的class属性:struct内部只保存class引用,真实class实例仍然在堆上
  • 被逃逸闭包捕获的值类型可能随闭包上下文一起存储在堆区
  • Array、Dictionary、Set、String等写时拷贝值类型通常只有一小段描述信息是值本身,真实元素或字符缓冲区可能在堆上
  • 协议类型(existential container)持有大值类型时,如果超过存在容器的内联缓冲区大小,会使用堆分配
  • indirect enum的间接关联值会通过堆上的盒子存储,常见于递归枚举
  • 全局变量、静态变量不属于栈区,也不是普通意义上的堆对象,它们通常位于全局/静态存储区

栈区内存分配和销毁通常只需移动栈顶指针,成本较低;堆区更动态,但分配、释放和引用计数维护都有额外成本。

struct中包含class属性

class Dog {
    var name: String

    init(name: String) {
        self.name = name
    }
}

struct Person {
    var age: Int
    var dog: Dog
}

var p1 = Person(age: 18, dog: Dog(name: "Lucky"))
var p2 = p1

p2.age = 20
p2.dog.name = "Max"

print(p1.age)       // 18
print(p1.dog.name)  // Max

内存关系可以近似理解为:

p1所在位置(通常是栈/寄存器,也可能被优化)
Person {
  age: 18
  dog: 指向Dog实例的引用 ───┐
}                          │
                        堆上的Dog实例
                        Dog { name: "Max" }

p2所在位置(p1赋值而来)
Person {
  age: 20
  dog: 指向同一个Dog实例的引用 ─┘
}

Person是值类型,p2 = p1会复制Person的字段,所以age变成两份独立的值;但dog字段本身只是一个引用,复制时复制的是指针,不会把Dog实例也复制一份。因此修改p2.age不影响p1.age,修改p2.dog.name会影响p1.dog.name

struct包含class属性时,struct整体仍然是值语义,但它内部的引用属性指向的对象可能被多个值共享

class中包含struct属性

struct Point {
    var x: Int
    var y: Int
}

class ViewModel {
    var point: Point

    init(point: Point) {
        self.point = point
    }
}

let vm1 = ViewModel(point: Point(x: 1, y: 2))
let vm2 = vm1

vm2.point.x = 10

print(vm1.point.x)  // 10

内存关系可以近似理解为:

vm1局部变量
引用 ─────┐
       堆上的ViewModel实例
       ViewModel {
         point: Point {
           x: 10
           y: 2
         }
       }

vm2局部变量
同一个引用 ─┘

ViewModel是引用类型,实例本体在堆上;point是struct属性,它不是单独的堆对象,而是内联存储在ViewModel实例这块堆内存中。vm2 = vm1只是复制引用,vm1vm2仍然指向同一个ViewModel实例,所以修改vm2.point.x也会反映到vm1.point.x

如果把point从对象里取出来,再修改局部变量,则不会影响原对象:

var point = vm1.point
point.x = 99

print(vm1.point.x)  // 10
print(point.x)      // 99

这里point是从ViewModel实例中复制出来的一份新的Point值,后续修改的是局部变量自己的副本。

四种组合总结

组合宿主在哪里属性如何存储赋值后的影响
struct中包含structstruct值在哪里,内部struct字段就内联在哪里字段整体复制修改新值不影响旧值
struct中包含classstruct值在哪里,class引用字段就在哪里只复制对象引用,class实例仍在堆上修改引用指向的对象会影响共享者
class中包含structclass实例在堆上struct字段内联在class实例内部多个class引用共享同一份struct字段
class中包含class两个class实例都在堆上属性中保存另一个对象的引用修改被引用对象会影响共享者

值类型的逃逸

如果一个值类型需要在创建它的函数作用域之外继续被访问,即发生了逃逸,Swift编译器可能会把相关存储放到堆上。典型场景是被逃逸闭包捕获:

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

func makePrinter() -> () -> Void {
    let point = Point(x: 1, y: 2)
    return {
        print(point.x)     // 闭包逃逸,捕获的point会随闭包上下文保存
    }
}

但“值类型作为函数返回值”不应简单理解为必然堆分配。返回普通结构体时,编译器可能通过寄存器、调用方栈空间或返回值优化完成传递;返回Array这类集合时,数组这个值本身是一个结构体,底层元素缓冲区通常位于堆上,这属于写时拷贝容器的实现细节。

性能对比

  • 栈内存的操作性能远远高于堆内存
  • 值类型如果发生真实数据拷贝,成本可能高于引用类型的浅拷贝;Swift为了优化值类型性能,引入了写时拷贝(Copy-on-Write)机制

写时拷贝(Copy-on-Write)

基本原理

写时拷贝是Swift对值类型的重要性能优化。当一个值类型被赋值给另一个变量时,不会立即创建副本,而是让多个变量共享同一份底层存储。只有当其中一个变量尝试修改数据时,才会触发真正的拷贝。

var array1 = [1, 2, 3, 4, 5]
var array2 = array1  // 此时不拷贝,共享底层存储

array2.append(6)     // 此时才触发拷贝,array1和array2变为独立

这种机制在大多数情况下既保证了值语义的安全性,又避免了不必要的拷贝开销。

实现机制

Swift标准库中的Array、Dictionary、Set等集合类型都内置了写时拷贝优化。其核心实现依赖于isKnownUniquelyReferenced函数:

// 简化的写时拷贝实现原理
struct MyArray<Element> {
    private var storage: ArrayStorage<Element>
    
    mutating func append(_ element: Element) {
        // 检查底层存储是否被唯一引用
        if !isKnownUniquelyReferenced(&storage) {
            // 如果被多个变量共享,先创建副本
            storage = storage.copy()
        }
        // 然后执行修改操作
        storage.append(element)
    }
}

当检测到底层存储被多个变量共享时,会先创建副本再进行修改;如果只有一个变量引用,则直接修改,无需拷贝。

多次赋值的共享

var array1 = [1, 2, 3, 4, 5]  // 原始数组
var array2 = array1           // 共享存储
var array3 = array1           // 同样共享存储

// 此时 array1、array2、array3 共享同一份底层数据

array2.append(6)              // array2 触发拷贝,获得独立副本
// 现在 array1 和 array3 仍共享存储,array2 有自己的副本

底层内存布局

详见Objective-C底层原理-NSObject以及Swift底层原理-结构体、类和协议