Understanding immutability
Here, you'll look at how to use immutability in your functions and data types. Immutability helps us to write code that is easier to understand and maintain because it limits the places where things can change.
Getting ready
First, write a function. Then, look at it and determine what it needs to do. Does it just look at the data passed to it? Does it store or return a reference to data passed in? We'll use these facts about how the function uses its arguments to determine the best-fit qualifiers.
How to do it…
The use of const
and immutable
is slightly different on free functions and object methods.
Writing functions
If you are accepting a value type, const
and immutable
aren't very important.
If you are borrowing a value—going to look at it, but not store it nor modify it—use the in
keyword and, if it is a character string, use char[]
instead of string
(string
is an alias for immutable(char)[])
:
void foo(in char[] lookAtThis) { /* inspect lookAtThis */ }
If you are going to store a reference, it is best to take immutable data, if possible as follows:
void foo(immutable(ubyte)[] data) { stored = data; }
If you are going to modify the data, but not store it, use scope
, but not const
(in
is shorthand for scope const
), as follows:
void foo(scope char[] changeTheContents) { /* change it */ }
If you are not going to modify or store the contents, but will return a reference to it, use inout
as follows:
inout(char)[] substring(inout(char)[] haystack, size_t start, size_t end) { return haystack[start .. end]; }
If you are going to change the value itself (not just the contents it references), use ref
as follows:
void foo(ref string foo) { /* change foo itself */ }
Writing object methods
When writing object methods, all of the preceding functions still apply, in addition to putting a qualifier on the this
parameter. The qualifier for this goes either before or after the function as follows:
int foo() const { return this.member; } /* this is const */ const int foo() { return this.member; } /* same as above */
Since the second form can be easily confused with returning a const
value (the correct syntax for that is const(int) foo() { …}
), the first form is preferred. Put qualifiers on this at the end of the function.
How it works…
D's const
qualifiers is different than that of C++ in two key ways: D has immutable
qualifiers, which means the data will never change, and D's const
and immutable
qualifiers are transitive, that is, everything reachable through a const
/immutable
reference is also const
/immutable
. There is no escape like the mutable
keyword of C++.
These two differences result in a stronger guarantee, which is useful, especially when storing data.
When storing data, you generally want either immutable or mutable data—const
usually isn't very useful on a member variable; although it prevents your class from modifying it, it doesn't prevent other functions from modifying it. Immutable means nobody will ever modify it. You can store that with confidence that it won't change unexpectedly. Of course, mutable member data is always useful to hold the object's own private state.
The guarantee that the data will never change is the strength of immutable data. You can get all the benefits of a private copy, knowing that nobody else can change it, without the cost of actually making a copy. The const
and immutable
qualifiers are most useful on reference types such as pointers, arrays, and classes. They have relatively little benefit on value types such as scalars (int
, float
, and so on) or structs because these are copied when passed to functions anyway.
When inspecting data, however, you don't need such a strong guarantee. That's where const
comes in. The const
qualifier means you will not modify the data, without insisting that nobody else can modify it. The in
keyword is a shorthand that expands to scope const
. The scope
parameters aren't fully implemented as of the time of this writing, but it is a useful concept nonetheless. A scope
parameter is a parameter where you promise that no reference to it will escape. You'll look at the data, but not store a reference anywhere. When combined with const
, you have a perfect combination for input data that you'll look at. Other than that you have the short and convenient in
keyword.
When you do return a reference to const
data, it is important that the constancy is preserved, and this should be easy. This is where D's inout
keyword is used. Consider the standard C function strstr
:
char *strstr(const char *haystack, const char *needle);
This function returns a pointer to haystack
where it finds needle
, or null if needle
is not found. The problem with this prototype is that the const
character attached to haystack
is lost on the return value. It is possible to write to constant data through the pointer returned by strstr
, breaking the type system.
In C++, the solution to this is often to duplicate the function, one version that uses const
, and one version that does not. D aims to fix the system, keeping the strong constancy guarantee that C loses and avoiding the duplication that C++ requires. The appropriate definition for a strstr
style function in D will be as follows:
inout(char)* strstr(inout(char)* haystack, in char* needle);
The inout
method is used on the return value, in place of const
, and is also attached to one or more parameters, or the this
reference. Inside the function, the inout(T)
data is the same as const(T)
data. In the signature, it serves as a wildcard that changes based on the input data. If you pass a mutable haystack, it will return a mutable pointer. A const
haystack returns a const
pointer. Also, an immutable
haystack will return an immutable
pointer. One function, three uses.
D also has the ref
function parameters. These give a reference to the variable itself, as shown in the following code:
void foo(int a) { a = 10; } void bar(ref int a) { a = 10; } int test = 0; foo(test); assert(test == 0); bar(test); assert(test == 10);
In this example, the variable test
is passed to foo
normally. Changes to a
inside the function is not seen outside the function.
Note
If a
was a pointer, changes to a
will not be seen, but changes to *a
will be visible. That's why const
and immutable
are useful there.
With the function bar
, on the other hand, it takes the parameter by reference. Here, the changes made to a
inside the function are seen at the call site; test
becomes 10.
Tip
Some guides recommend passing structs to a function by ref
for performance reasons rather than because they want changes to be seen at the call site. Personally, I do not recommend this unless you have profiled your code and have identified the struct copy as a performance problem. Also, you cannot pass a struct
literal as ref
, because there is no outer variable for it to update. So, ref
limits your options too.