In this chapter, we're going to build upon the foundation established in the previous chapter by looking at D's user-defined types, its support for object-oriented programming, and some peripherally related features. By the end of the chapter, we'll be finished with the basics of D and ready for more advanced topics. Here's our checklist:
This section shows how to define custom types using enum
, union
, struct
, and class
. The latter two will be the focus for most of the remainder of the chapter.
The anonymous enumeration in D declares a set of immutable values. A major difference from C is that it's possible to specify the underlying type. When no fields are explicitly assigned a value, the underlying type of an enum
defaults to int
. Note that user-defined type declarations in D do not require a semicolon at the end:
enum {top, bottom, left, right} // type is int enum : ubyte {red, green, blue, alpha} // type is ubyte
The members of each will be initialized with sequential values starting at 0
. In the second declaration, the underlying type is explicitly set to ubyte
by appending a colon and the type name to the enum
keyword. enum values aren't restricted to integrals, or even just the basic types. Any type that D supports, be it one of the derived data types or even a user-defined type, can back an enum. Where possible, the compiler will infer the type:
enum {one = "One", two = "Two"} // type is immutable(char)[]
An anonymous enum with only one member is eligible for some special treatment:
enum {author = "Mike Parker"} enum author = "Mike Parker"; enum string author = "Mike Parker";
As shown in the second line, the braces can be dropped. The third line explicitly specifies a type, but in this form there's no colon. An enum
declared without braces is called a manifest constant.
An anonymous enum does not create a new type, but a named enumeration does:
enum Side {top, bottom, left, right} enum Color : ubyte {red, green, blue, alpha}
The name is used as a namespace and the members are accessed via the dot operator. Printing the typeid
of one of these produces a fully-qualified name that includes the enum
name (Side
or Color
, in this case). All user-defined types get this treatment.
Named enums have properties. .init
equates to the value of the first member of the enum
; its .sizeof
is that of the underlying type. The type-specific properties .min
and .max
return the lowest and highest member values respectively.
One special feature designed to work specifically with enums is the final switch
statement. For example, to switch on a value of the Side
type:
auto s = Side.bottom; final switch(s) { case Side.top: writeln("On top"); break; case Side.bottom: writeln("On the bottom"); break; case Side.left: writeln("On the left"); break; case Side.right: writeln("On the right"); break; }
A big benefit is that the compiler will give you an error if you forget to add a case for one of the enum
members. It also adds a default case that asserts if a non-member value somehow slips through, making it an error to add a default case. If two or more members have the same value, only one need appear in a final switch.
For the most part, unions in D work as they do in C:
union One { int a = 10; double b; }
The members of a union
share the same memory, which is large enough to hold the biggest type. In this declaration, the biggest type is double
, so an instance of One
takes up 8 bytes. D diverges from C in terms of initialization. Every variable in D is by default initialized and a union
instance is no exception. By default, the first member is initialized to its .init
value if not explicitly initialized, as in the previous example. It is an error to initialize other members.
To explicitly initialize an instance of a union
, you can use the name:value
syntax with braces. Given the nature of unions, it's an error to initialize more than one field:
One o2 = { b:22.0 };
Unions are great for compatibility with C, but there is an alternative in the standard library that offers better type safety. The module std.variant
exposes the Variant
type. See http://dlang.org/phobos/std_variant.html for details.
D's implementations of the struct
and class
types are a major source of misunderstanding for C-family programmers new to D. Here are declarations of each:
struct MyStruct { int a, b; int calculate() { return a + b; } } class MyClass { int a, b; int calculate() { return a + b; } }
The declarations have the same syntax. The biggest difference is hidden here:
MyStruct ms; MyClass mc;
In D, a struct
is a value type. It's enough to declare an instance for it to be created on the stack and ready to go. At this point, a call to ms.calculate
will successfully compile. The same cannot be said for mc
. A
class
is a reference type. It isn't enough to declare an instance, as that only produces an uninitialized reference, or handle. It is a common error for new D programmers coming from C++ to try using uninitialized class references. Before mc.calculate
can be called, an instance must be allocated:
mc = new MyClass; auto mc2 = new MyClass;
This has implications for how instances are passed to and returned from functions. Since a struct
is a value type, instances are copied. ms.sizeof
is 8 bytes, the size of two ints
, which means passing ms
to a function will cause 8 bytes to be copied:
void modMS(MyStruct ms1, ref MyStruct ms2, MyStruct* ms3) { ms1.a = 1; // Modifies local copy ms2.a = 2; // Modifies original ms3.a = 3; // Ditto. }
Since the first argument is passed by value, any modifications to ms1
only affect the function's copy, but modifications to ms2
and ms3
will be reflected in the original variable. It's different with mc
:
void modMC(MyClass mc) { mc.a = 1; // Modifies original. }
As MyClass
is a reference type, there's no need to declare a pointer or ref
parameter to modify the original variable. mc.sizeof
is 4
on 32-bit architectures and 8
on 64-bit. This is the size of the reference, not the size of the instance itself.
When a class
or struct
instance is instantiated, it gets its own copy of any member variables. If a member variable is declared as static
, only one thread-local copy of it exists. Each member is by default initialized to its .init
value unless an initialization value is specified. Member functions are normal functions that accept a hidden this
argument as the first parameter (accessible inside the function scope), representing the instance. Taking the address of a member function produces a delegate. Static member functions have no this
parameter and taking the address of one yields a function pointer:
struct MembersOnly { static int x; int y; int z = 10; // Initialized to 10 for all instances static void printX() { writeln(x); } void printYZ() { writefln("%s, %s", this.y, z); // this.y is the same as y } }
Non-static members may only be accessed using an instance name as a namespace:
MembersOnly mo; writeln(mo.z); mo.printYZ();
Static members must be accessed using the type name as a namespace:
MembersOnly.x += 1; MembersOnly.printX();
18.116.118.229