Swift中关于栈和堆的误解

8
我一直知道引用类型变量存储在堆中,而值类型变量存储在栈中。最近,我发现这张图片说整型、双精度浮点数、字符串等都是值类型,而函数和闭包是引用类型: enter image description here 现在我真的很困惑了。那么当它们在类中被定义时,即引用类型时,整型、双精度浮点数、字符串等存储在哪里?同样地,当在结构体中定义函数和闭包,即值类型时,它们存在哪里?

我认为Swift并没有承诺存储东西的位置... - Sweeper
1个回答

18

我一直以为引用类型变量存储在堆中,而值类型变量存储在栈中。

在 Swift 中,这只是部分正确。一般来说,Swift 不保证对象和值存储的位置,除非:

  1. 引用类型在内存中具有 稳定 的位置,使得所有对同一对象的引用都指向完全相同的位置;
  2. 值类型不保证在内存中具有稳定的位置,并且可以根据编译器的需要任意复制。

技术上,这意味着如果编译器知道一个对象在同一堆栈帧内创建和销毁且没有逃逸引用,则对象类型可以存储在栈中,但在实际情况中,您基本上可以假设所有对象都是在堆上分配的。

对于值类型,情况要复杂一些:

  • 除非需要值的基于位置的引用(例如,使用 & 对结构体进行引用),否则结构体可以完全位于 寄存器 中:对小结构体进行操作可能会将其成员放置在 CPU 寄存器中,因此它甚至不会存在于内存中。(对于像 IntDouble 这样的小型、可能短暂存在的值类型,它们保证适合寄存器)
  • 大型值类型 实际上会 获得堆分配:虽然这是 Swift 的一个实现细节,理论上未来可能会发生变化,但是结构体如果大于 3 个机器字(例如,在 32 位机器上大于 12 字节,在 64 位机器上大于 24 字节),基本上可以保证分配并存储在堆中。这不会与值类型的值性冲突:它仍然可以根据编译器的意愿任意复制,并且编译器非常擅长避免不必要的分配。

那么当 int、double、string 等定义在类中(即引用类型)时,它们在哪里保存呢?

这是一个很好的问题,涉及到值类型的核心。一个思考值类型存储的方式是 内联 到需要它的任何地方。想象一下一个

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

结构体是在内存中布局的。暂时不考虑Point本身也是一个结构体这个事实,xy相对于Point存储在哪里?那么,内联Point所在的位置:

┌───────────┐
│   Point   │
├─────┬─────┤
│  x  │  y  │
└─────┴─────┘

当您需要存储一个Point时,编译器会确保您有足够的空间同时存储xy,通常是一个紧跟着另一个。如果Point存储在堆栈上,则xy将依次存储在堆栈上;如果Point存储在堆上,则xy作为Point的一部分存在于上。无论Swift在何处放置Point,它始终确保您有足够的空间,并且当您将值分配给xy时,它们将被写入该空间。这并不特别重要。

那么当Point是另一个对象的一部分时呢?例如:

class Location {
    var name: String
    var point: Point
}

那么Point无论在哪里存储,它也会以内联的方式布局,并且它的值也会以内联方式布局:

┌──────────────────────┐
│       Location       │
├──────────┬───────────┤
│          │   Point   │
│   name   ├─────┬─────┤
│          │  x  │  y  │
└──────────┴─────┴─────┘

在这种情况下,当您创建一个Location对象时,编译器确保有足够的空间来存储一个String和两个Double,并将它们依次排列。这些位置再次不重要,但在这种情况下,它们都在堆上(因为Location是引用类型,其中包含值)。


至于另一种情况,对象存储有两个组件:

  1. 用于访问对象的变量
  2. 对象的实际存储

假设我们将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   │
└─────────┴─────────┴───────────┘     └───────────┘

非常感谢您提供如此精彩的答案!保存函数的变量也只是指向该函数的指针,就像您解释闭包一样,我是对的吗? - MaryLitv21
@MaryLitv21 没错!函数变量和闭包变量都是指向某些可执行代码的指针。(函数和闭包对象在底层略有不同,但你对变量的理解是正确的。) - Itai Ferber

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接