简体   繁体   中英

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

I want to be able to get the size of the underlying data type of an Any variable in Swift. I expected this to be possible by running MemoryLayout.size(ofValue: anyObject) , but that expression always returns 32 , regardless of the the underlying data type of the Any object. I assume 32 is the size of the internal Any construct/type, which holds metadata about the object it stores.

How do I get the underlying data type's size?

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`?

I'll begin with some technical details about the limitations of Any in this case.

So, what is Any ? It's an empty protocol to which every type implicitly conforms to.

And how does the compiler represent variables of protocol types? It's by wrapping the actual value in an existential container. So basically when you're referencing a variable of this kind, you're actually talking to the container (well, actually not you, but the compiler is:).

An existential container has a layout that can be represented like this C structure:

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

The container elements are greatly explained in this document , I'll also try to summarize them here:

  • fixedSizeBuffer either holds the whole value, if it takes less than 24 bytes, or holds a pointer to a heap allocated zone, containing the value
  • type is a pointer to the type metadata
  • witnessTables is what makes this layout occupy various sizes, as the number of protocol witness tables can vary from zero to virtually any number of protocols.

So, with the above in mind:

  • Any needs no witness tables, thus it occupies 32 bytes
  • a single protocol variable occupies 40 byes
  • a composed protocol variable occupies 32 + N*8, where N is the number of "independent" protocols involved in the composition

Note that the above is true if there are no class protocols involved, if a class protocol is involved, then the existential container layout is a little bit simplified, this is also better described in the linked document from above.


Now, back to the problem from the question, it's the existential container created by the compiler the one which prevents you from accessing the actual type. The compiler doesn't make this structure available, and transparently translates any calls to protocol requirements to dispatches through the witness tables stored in the container.

But, might I ask you, why are you circulating Any ? I assume you don't want to handle all possible and future types in a generic manner. A marker protocol might help here:

protocol MemoryLayouted { }

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

Then all you have left to do is to add conformance for the types you want to support:

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

With the above in mind, you can rewrite your initial code to something like this:

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

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

You get consistent behaviour and type safety, a type safety that might translate to a more stable application.


PS A hacky approach, that allows you to use Any , might pe possible by unpacking the existential container via direct memory access. The Swift ABI is stable at this point, so the existential container layout is guaranteed not to change in the future, however not recommending going that route unless absolutely necessary.

Maybe someone that stumbles this question and has experience in the ABI layout code can provide the code for it.

What I would do is cast Any to all supported types. Why would you cast Int as Any when you know what type is it? anyway?

var value: Any = Int(2) as Any

switch value {

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM