在 Swift 中,如何获取 `Any` 变量的真实大小?

In Swift, how to get the true size of an `Any` variable?

我希望能够获取 Swift 中 Any 变量的基础数据类型的大小。我希望 运行 MemoryLayout.size(ofValue: anyObject) 可以实现这一点,但无论 Any 对象的基础数据类型如何,该表达式总是 returns 32。我假设 32 是内部 Any construct/type 的大小,它包含有关它存储的对象的元数据。

如何获取基础数据类型的大小?

let regularInt: Int = 1
let anyInt: Any = Int(2) as Any

MemoryLayout<Int>.size                 // 4
MemoryLayout<type(of: anyInt)>.size    // Can't do that

MemoryLayout.size(ofValue: regularInt) // 4
MemoryLayout.size(ofValue: anyInt)     // 32

// How do I get size "4" out of `anyInt`?

我要做的是将 Any 转换为所有支持的类型。当你知道它是什么类型时,为什么要把 Int 转换为 Any !无论如何?

var value: Any = Int(2) as Any

switch value {

case value is Int:
// ... other cases here
} 

在这种情况下,我将从一些有关 Any 限制的技术细节开始。

那么,什么是Any?这是一个空协议,每个类型都隐含地遵守它。

编译器如何表示协议类型的变量?它是通过将实际值包装在一个存在的容器中。所以基本上当你引用这种变量时,你实际上是在与容器对话(好吧,实际上不是你,但编译器是:)。

一个存在的容器有一个可以像这样表示的 C 结构的布局:

struct OpaqueExistentialContainer {
  void *fixedSizeBuffer[3];
  Metadata *type;
  WitnessTable *witnessTables[NUM_WITNESS_TABLES];
};

容器元素在this document中有很好的解释,我也会在这里总结一下:

  • fixedSizeBuffer 要么保存整个值,如果它少于 24 个字节,要么保存指向堆分配区域的指针,其中包含值
  • type 是指向类型元数据的指针
  • witnessTables 是使此布局占据各种大小的原因,因为协议见证表的数量可以从零到几乎任意数量的协议不等。

所以,考虑到以上几点:

  • Any不需要见证表,因此占用32字节
  • 单个协议变量占用40字节
  • 一个组合协议变量占用32+N*8,其中N为参与组合的“独立”协议的个数

注意如果没有涉及到class协议的话上面是正确的,如果涉及到class协议,那么existential container layout会稍微简化一点,这个也比较好描述在上面的 linked document 中。


现在,回到问题的问题,是编译器创建的存在容器阻止您访问实际类型。编译器不提供此结构,并且透明地将任何对协议要求的调用转换为通过存储在容器中的见证表进行调度。

但是,请问你,你为什么要循环Any?我假设您不想以通用方式处理所有可能的和未来的类型。标记协议可能在此处有所帮助:

protocol MemoryLayouted { }

extension MemoryLayouted {
    var memoryLayoutSize: Int { MemoryLayout.size(ofValue: self) }
}

那么您剩下要做的就是为您想要支持的类型添加一致性:

extension Int: MemoryLayouted { }
extension String: MemoryLayouted { }
extension MyAwesomeType: MemoryLayouted { }

考虑到上述情况,您可以将初始代码重写为如下内容:

let regularInt: Int = 1
let anyInt: MemoryLayouted = 2

print(regularInt.memoryLayoutSize) // 8
print(anyInt.memoryLayoutSize)     // 8

您将获得一致的行为和类型安全,这种类型安全可能会转化为更稳定的应用程序。


P.S。一种允许您使用 Any 的 hacky 方法可能会通过直接内存访问解压现有容器来实现。 Swift ABI 在这一点上是稳定的,所以现有的容器布局保证在未来不会改变,但是除非绝对必要,否则不建议走那条路。

也许遇到这个问题并且有ABI布局代码经验的人可以提供代码。