User-defined types are used everywhere in D to group data, model objects, provide compile-time checks, and more. Here, you'll create a simple vector type with a length and direction to look at some basic capabilities.
Whenever you create a user-defined collection in D, the first decision to make is whether it should be a class, struct, mixin template, or union. Mixin templates are great for code reuse. They define code that can be copied (or mixed in) to another type, with parameterization. Unions are for the cases when you need the same block of memory to have multiple types, and are the least common in typical D code. Classes and structs are the backbone of user-defined types in D, and they have the most in common. The key difference is polymorphic inheritance; if you need it, you probably want a class. Otherwise, structs are lighter weight and give maximum flexibility. Using them, you can precisely define the layout of each byte with no hidden data, overload all operators, use deterministic destruction (the RAII idiom from C++), and use both reference or value semantics depending on your specific needs. D's structs also support a form of subtyping, though not virtual functions, which you'll see in Chapter 6, Wrapped Types.
Let's summarize as follows:
Struct |
Class |
---|---|
This offers precise control over memory layout |
This offers virtual functions and inheritance |
This is ideal for lightweight wrappers of other types |
This is always a reference type |
This offers deterministic destruction |
This is usually managed by the garbage collector |
Since your vector type will not need virtual functions, it will be a struct.
Let's look at creating a vector type using the following steps:
struct
variable with a name. This declaration can appear anywhere; but, in your case, you want it to be generally accessible. So, it should go in the top-level scope of your module. Unlike C++, there is no need to put a semicolon at the end of the struct
definition, as shown in the following code:struct Vector {}
struct Vector { float magnitude; float direction; }
(x, y)
coordinates. The complete code is as follows:struct Vector { // the data float magnitude; float direction; // the methods /// create a Vector from an (x, y) point static Vector fromPoint(float[2] point) { import std.math; Vector v; float x = point[0]; float y= point[1]; v.magnitude = sqrt(x ^^ 2 + y ^^ 2); v.direction = atan2(y, x); return v; }}} /// converts to an (x,y) point. returns in an array. float[2] toPoint() const { import std.math; float x = cos(direction) * magnitude; float y = sin(direction) * magnitude; return [x, y]; } /// the addition operator Vector opBinary(string op : "+")(Vector rhs) const { auto point = toPoint(), point2 = rhs.toPoint(); point[0] += point2[0]; point[1] += point2[1];];]; return Vector.fromPoint(point);); } }
auto origin = Vector(0, 0); import std.math; auto result = origin + Vector(1.0, PI); import std.stdio; writeln("Vector result: ", result); writeln(" Point result: ", result.toPoint());
It will print Vector(1.0, 3.14)
and [-1, 0]
, showing the vector sum as magnitude and direction, and then x, y. Your run may have slightly different results printed due to differences in how your computer rounds off the floating point result.
Structs are aggregate types that can contain data members and function methods. All members and methods are defined directly inside the struct, between the opening and closing braces. Data members have the same syntax as a variable declaration: a type (which can be inferred, if there is an initializer), a name, and optionally, an initializer. Initializers must be evaluated at compile time. When you declare a struct, without an explicit initializer, all members are set to the value of their initializers inside the struct
definition.
Methods have the same syntax as functions at module scope, with two differences; they can be declared static and they may have const
, immutable
, or inout
attached, which applies to the variable this
. The this
variable is an automatically declared variable that represents the current object instance in a method. The following recipe on immutability will discuss these keywords in more detail.
Operator overloading in D is done with methods and special names. In this section, you defined opBinary
, which lets you overload the binary operators such as the addition and subtraction operators. It is specialized only on the +
operator. It is also possible to overload casting, assignment, equality checking, and more.
At the usage point, you declared a vector with auto
, using the automatically defined constructor.
Finally, when you write the result, you use the automatic string formatting that prints the name and the values, in the same way as the automatic constructor. It is also possible to take control of this by implementing your own toString
method.
3.145.73.207