D class references may be nullD still is the langauge that produces the fewest Simon rants. Many common problems have great, short solutions in D. But, as with anything worthwhile, it has its corners to point and and rant. What everybody loves most!
Class objects live on the garbage-collected heap. The basics of OO of D are similar to OO in Java and work great. Your class variables are references (pointers, names). If you want polymorphism in any language, you need reference semantics, therefore all this is consistent, good design.
The initial value of any class reference is null. That is also a good choice.
class Color { /* ... */ }
class Porcupine {
Color color; // implicitly null}But D has no way to statically enforce that a reference cannot be null in a given context. You may
assert a reference's non-null-ness, but that happens at runtime. I want nice check at compiletime. Also, it feels slightly dumb to assert for non-null because your operating system already asserts this for you on every usage: The OS will print "segmentation fault" and you run your debugger.
I like to write several short methods, and naturally, some of them take class arguments. I really don't want to assert that my parameters aren't null every time over and over.
Structs in D are value types, they have no such issues. If I define
Point to be a struct with two ints, x and y, then
Point will occupy as much memory as two ints would, and its default value is x = 0, y = 0, because 0 happens to be the default value for int. (Everything in D is default-initialized, which kills a massive class of bugs from C++. If you're absolutely sure that the default initialization kills your performance, you can explicitly write
Point p = void;)
Problem statement: I wish for a class reference that is guaranteed to be non-null. Assigning null to this reference is a compile-time error, no matter how indirect the assignment was. It's an acceptable hack to have it null by default, as long as nobody can ever read/dereference it before it's overwritten with non-null for the first time.
Let's attempt a solution: I could wrap all my nullable D class references in a templated struct, such as the following, where
C is the template parameter.
struct NotNull(C) {
C payload; // we wrap a class reference
@disable this(); // disallow default struct init with payload = null;
this(C c) {
assert(c !is null);
payload = c;
}
alias payload this; // allow this struct to implicitly convert to C
}
Then I can ask for a
NotNull!C everywhere I'd normally ask for a
C. This has 3 downsides:
- It's a nonstandard hack. This problem is really really common in OO and it's worth a language feature, or at least a library solution. The D standard library has Nullable!S for structs, but no NotNull!C for classes.
- It doesn't statically prevent me from instantiating the thing with null, e.g., auto x = NotNull!C(null);. It still passes compilation and only asserts at runtime. The benefit over normal class references is that it explodes already when we create the null reference, not merely when we dereference later, as a normal segfault would.
- It's an abstraction inversion. The non-nullable type is type with simpler behavior, I can call all methods without segfault. The nullable type is the more complex type, I can either call methods on it or must check first for non-nullness. My NotNull implements a simple type in terms of a more complex type. This is bad design.
Who does it better? In
Zig, your types cannot be null, not even pointers, unless you add
?, the nullable type modifier. You may not use a nullable type freely unless you specify what happens in the null case. The language has special, short constructs for exactly this problem.
In general, Zig is designed around easy passing of error conditions because it avoids exceptions and garbage collection.
% is another type modifier, it means "either your type or an error code".
C# gets a mention here for its null-conditional dereferencing operator
?. that calls a method only if the reference is not null. This cures the symptom of having many ugly null checks throughout the codebase, but the original issue still stands -- it's not a compile-time prevention of assigning null to class variables.
-- Simon