我一直以为引用类型变量存储在堆中,而值类型变量存储在栈中。
在 Swift 中,这只是部分正确。一般来说,Swift 不保证对象和值存储的位置,除非:
技术上,这意味着如果编译器知道一个对象在同一堆栈帧内创建和销毁且没有逃逸引用,则对象类型可以存储在栈中,但在实际情况中,您基本上可以假设所有对象都是在堆上分配的。
对于值类型,情况要复杂一些:
&
对结构体进行引用),否则结构体可以完全位于 寄存器 中:对小结构体进行操作可能会将其成员放置在 CPU 寄存器中,因此它甚至不会存在于内存中。(对于像 Int
和 Double
这样的小型、可能短暂存在的值类型,它们保证适合寄存器)那么当 int、double、string 等定义在类中(即引用类型)时,它们在哪里保存呢?
这是一个很好的问题,涉及到值类型的核心。一个思考值类型存储的方式是 内联 到需要它的任何地方。想象一下一个
struct Point {
var x: Double
var y: Double
}
结构体是在内存中布局的。暂时不考虑Point
本身也是一个结构体这个事实,x
和y
相对于Point
存储在哪里?那么,内联在Point
所在的位置:
┌───────────┐
│ Point │
├─────┬─────┤
│ x │ y │
└─────┴─────┘
当您需要存储一个Point
时,编译器会确保您有足够的空间同时存储x
和y
,通常是一个紧跟着另一个。如果Point
存储在堆栈上,则x
和y
将依次存储在堆栈上;如果Point
存储在堆上,则x
和y
作为Point
的一部分存在于堆上。无论Swift在何处放置Point
,它始终确保您有足够的空间,并且当您将值分配给x
和y
时,它们将被写入该空间。这并不特别重要。
那么当Point
是另一个对象的一部分时呢?例如:
class Location {
var name: String
var point: Point
}
那么Point
无论在哪里存储,它也会以内联的方式布局,并且它的值也会以内联方式布局:
┌──────────────────────┐
│ Location │
├──────────┬───────────┤
│ │ Point │
│ name ├─────┬─────┤
│ │ x │ y │
└──────────┴─────┴─────┘
在这种情况下,当您创建一个Location
对象时,编译器确保有足够的空间来存储一个String
和两个Double
,并将它们依次排列。这些位置再次不重要,但在这种情况下,它们都在堆上(因为Location
是引用类型,其中包含值)。
至于另一种情况,对象存储有两个组件:
假设我们将Point
从结构体更改为类。在此之前,Location
直接存储了Point
的内容,现在,它只存储对它们在内存中实际存储的一个引用:
┌──────────────────────┐ ┌───────────┐
│ Location │ ┌───▶│ Point │
├──────────┬───────────┤ │ ├─────┬─────┤
│ name │ point ──┼─┘ │ x │ y │
└──────────┴───────────┘ └─────┴─────┘
之前,Swift 在创建“位置”(Location
)时存储了一个 String
和两个 Double
;现在,它存储了一个 String
和一个指向 Point
的 指针。不像 C 或者 C++ 等语言一样,你不需要知道 Location.point
现在是一个指针,也不会改变你访问对象的方式;但底层,Location
的大小和“形状”已经改变。
所有其他引用类型,包括闭包,在存储时也是如此。保存闭包的变量主要是指向闭包元数据的指针,以及执行闭包代码的方法(尽管这方面的具体细节超出了本答案的范围):
┌───────────────────────────────┐ ┌───────────┐
│ MyStruct │ │ closure │
├─────────┬─────────┬───────────┤ ┌──▶│ storage │
│ prop1 │ prop2 │ closure ─┼─┘ │ + code │
└─────────┴─────────┴───────────┘ └───────────┘