D's structs are a good foundation to create a dynamic type. We'll create a tagged union to serve as a starting dynamic type. This is the technique used to implement the library jsvar
type that we used in a previous chapter. Here, we'll only support int
and string
.
We will execute the following steps to create a tagged dynamic type:
opAssign
.The following is the code to create a tagged dynamic type:
struct Dynamic { enum Type { integer, string } private Type type; private union { int integer; string str; } this(T)(T t) { // easy construction this = t; } // assignment of our supported types Dynamic opAssign(inti) { type = Type.integer; integer = i; return this; } Dynamic opAssign(string i) { type = Type.string; str = i; return this; } // getting our types back out into static variables // here, we used the coercion option instead of throwing if // we had the wrong type int asInteger() { final switch(type) { case Type.integer: return integer; case Type.string: import std.conv; return to!int(str); } } string asString() { final switch(type) { case Type.string: return str; case Type.integer: import std.conv; return to!string(integer); } } // we may also implement other operators for more integration }
Dynamic types are usually implemented using the tagged union technique used here. A union is a group of variables which share a block of overlapped memory. Unless all the variables of the union are valid simultaneously (which we'll look at in the following recipe), it is important to know which member is currently active. That's where the type
member comes into play.
The other parts of the code are things we've already seen. The opAssign
function implements the =
operator with other types. The constructor implements by declaring the struct with an argument list of other types. The asInteger
and asString
methods are just regular functions.
There's one interesting bit we use in those methods: the final switch. A final switch is guaranteed at compile time to cover every member of the enum
. With an enum
, this means the build will fail if a member is not handled by a case, making the final switch resilient to changes. If a new member is added to the enum
, the compiler will help you remember to add a handling case to each and every switch that uses it.
Phobos' std.variant
provides a tagged union, Variant
, that uses a large union as well as a pointer to an even larger type, making it capable of holding almost anything. Variant
doesn't provide weak typing or full operator overloading. However, since it can hold and offer nearly any type with proper runtime checking, you may use it when you need a generic runtime variable, such as an array of mixed type elements.
Variant
uses method templates and generated function pointers to handle the types rather than writing out a list manually. We'll look at these techniques in depth in Chapter 9, Code Generation, and Chapter 8, Reflection.
18.117.138.104