Software Testing Blog

C#: Non-nullable Reference Types

Today on ATBG, a language design question from reader Staffan:

In C# 1.0 the language supported nullable reference types and non-nullable value types. Nullable value types were added in C# 2.0. How difficult would it be to add non-nullable reference types to the language now?

That’s a fairly frequently asked question which I have never blogged about, so thanks for asking. I, of course, no longer speak for the C# team, but I can certainly tell you about some of the problems the team would face if tasked with implementing this feature.

Several years ago, a team in Microsoft Research made a version of C# called Spec# that does support non-nullable reference types using ! as a type modifier. That is, just as int? is a nullable value type, string! is a non-nullable reference type. And it makes programming much more exciting! So we know that in theory it can be done in a C#-like language. And we also know that failure to check for a null reference is an extremely common source of crashing bugs in many languages, C# included. Sir Tony famously said:

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Sir Tony’s estimate of a billion dollars is, in my opinion, way too low.

So, it seems that non-nullable reference types would be an awesome idea that would save time, effort and money for developers and users. Why not implement the feature in C# for real, instead of leaving it in a research language?

As I often say, in theory, theory and practice are the same, but in practice, they’re different. This is no exception. There are many pragmatic objections to adding non-nullable reference types now. Just a few:

Objection #1:

If the libraries were updated to use the feature, then every existing C# program in the world would have a breaking change. Consider this program fragment:

string result = string.Join(separator, values);

which calls a method with signature

static string Join(string separator, params string[] values);

Now suppose we update that to

static string! Join(string! separator, params string![]! values);

That is, the method takes a non-null separator and a non-null array of non-null strings. What happens to our program fragment?

Let’s assume that there is an implicit reference conversion from string! to string, so the assignment to result succeeds. Is there also an implicit reference conversion from string to string! ? That seems wrong; either the conversion should throw, or it should somehow pick a canonical non-null string. Both seem problematic. Moreover, are we suggesting that there be a covariant array conversion from string[] to string![]! ? Should we have to scan the entire array looking for nulls in order to do the conversion? Or should we simply disallow these conversions?

No matter what, it seems likely that this program fragment is going to cause a breaking change; the author of the code will need to do work to ensure that the compiler is satisfied that the separator and values are not null.

If the BCL team doesn’t use the new annotations, then there seems to be little point in doing the feature at all; but if they do use the annotations, then every existing non-trivial program gets a breaking change. The C# team works very, very hard to ensure that there are as few breaking changes as possible. A third way would be to implement two versions of every BCL method, one with annotations and one without. Why build one when you can have two at twice the price? And then you have to work out if adding all those new methods introduces an ambiguity into any existing program, because that’s another breaking change.

And of course, every other .NET language would have to be upgraded to understand non-nullable reference types. This alone seems like enough of a problem to not add the feature.

Objection #2:

We want a type system to tell us facts about our programs. The whole point of static typing is to make logical deductions at compile time that reduce or eliminate type safety violations at runtime. C# succeeds at this admirably well; when you make a field of type string, then that field is always 100% guaranteed to have either a reference to a valid string, or null. (Assuming of course that the program is verifiable; if you use unsafe code to mess with memory you shouldn’t be touching, then all bets are off; that’s what “unsafe” means.) So suppose the C# language allows you to make a field of type string! – can the language give you that same ironclad guarantee, that this field is never, ever observed to be null? The memory allocator initializes all fields of reference type to null before the constructor runs, so let’s think about constructors.

class C
{
  public string! str;
  public C (string! s)
  {
    Console.WriteLine(this.str.Length);
    // I can take the length of a non-nullable string, right?
    this.str = s;
  }
}

That program would have to be illegal. The compiler would have to do a flow analysis and determine that the non-nullable field is accessed before it was assigned its value. (And of course the compiler would have to determine that all non-nullable fields were assigned by the constructor, but that is trivially done; the compiler already has to do that for fields of any value type.)

So, we add that flow analysis. Are we done? No.

class C
{
  public string! str;
  public C (string! s)
  {
    M(this);
    this.str = s;
  }
}

Now M, whatever it is, can observe the state of str before its value is assigned. This has to be illegal as well.  Are we done? No.

abstract class B
{
  public B()
  {
    Console.WriteLine(this.M().Length);
  }
  public abstract string! M();
}
class C : B
{
  public string! str;
  public override string! M() { return this.str; }
  public C (string! s)
  {
    this.str = s;
  }
}

Calling virtual methods in a base class constructor is foolish but legal. Somehow this would have to be made illegal. But where? Every line seems perfectly reasonable.

Clearly we’re going down a bad path here. The problem is that the field is being initialized too late. Field initializers run before constructor bodies, so let’s require that every non-nullable field have an initializer.

class C : B
{
  public string! str = "";
  public override string! M() { return this.str; }
  public C (string! s)
  {
    this.str = s;
  }
}

This is code generated as this pseudo-C#:

class C : B
{
  public string! str;
  public override string! M() { return this.str; }
  public void C_constructor(string! s)
  {
    this.str = "";
    this.B_constructor();
    this.str = s;
  }
}

And now we’re fine, right?  No matter what we do in either the C or B constructor bodies, the field is never observed to be in its non-initialized state. Field initializers are already restricted from using this in any way, so they’re not going to observe the uninitialized state of any other field.

Are we done?  No.

class C
{
  public static C Factory()
  {
    return new C("hello");
  }
  public string! str = "";
  public C (string! s)
  {
    this.str = s;
  }
  ~C() { Console.WriteLine(this.str.Length); }
}

What if a thread abort exception is thrown after the memory allocator returns memory for the new object, but before the field initializer runs? Such an object is still finalizable! Of course, you should never touch a managed object in a destructor because that object might have been destructed already. But my point is that the finalizer thread can observe the uninitialized null state of a supposedly non-nullable reference type field.

Is this an important scenario? Probably not. We already know that destructors are weird. But like I said before, the type system absolutely guarantees that even in crazy situations like this, string fields always contain a valid string reference or null; we can’t easily make a similar ironclad guarantee for non-nullable reference types without adding all kinds of new gear to the compiler, and probably to the runtime as well.

We could keep on playing whack-a-mole here for a long time, looking for crazy ways that non-nullable fields could be observed to be null. I haven’t even considered all the weird situations you can get into with static constructors and I’m sure there are more.

In short: C# was designed with the principle that any object fresh from the allocator meets the requirements of the type system. That’s not true in a world with non-nullable reference types, and so it would be a lot of design work to patch up the resulting holes.

And again, every .NET language would have to go through a similar process. And the .NET verifier would have to be updated as well, to seek out situations where the non-nullability constraint could be observed to be violated.

Objection #3:

We can do an adequate job of taming nullability with code contracts. Since VS 2010, Microsoft has supplied a library and tools for adding annotations to your programs that describe non-nullability (and other invariants). There is then an outside-the-compiler type analyzer that tries to prove that your program meets its contracts. Is it as seamless an experience as having the non-nullability built into the compiler’s type system? Certainly not. Is it good enough to solve real-world problems at a reasonable price? Yes, it is. The existence of a reasonable, low-cost solution with a few rough edges is points against a difficult, expensive solution with a slightly smaller number of rough edges.

So, long story short: non-nullable reference types is a great idea, but as a practical manner, the objections to implementing it now are enormous. Non-nullability is the sort of thing you want baked into a type system from day one, not something you want to retrofit in 12 years later. Keep that in mind the next time you design a new type system!

- Eric

  1. I agree that retro-fitting an existing app with non-nullable can potentially cause a lot of grief and unless some epic force decides to pull null out of every deep and dark corner we’re going to be dealing with it for a long time. But aren’t there stepping stones towards using non-nullable types? One example that comes to mind is there are a lot of cases where null is passed way down a stack but a method marked with non-null might catch a null earlier (eg. private void add(int! a, int! b)). This way the exception is pushed further up the stack and closer to the culprit who’s generating the null in the first place.

  2. CodeContracts are nice but writing CodeContracts.Requires( x != null) everywhere is a bit tiresome. Exclamation mark could be nice sugar for this I believe.

    void foo(string! str)
    {
    }

    equivalent to

    void foo(string str)
    {
    CodeContracts.Requires(str != null);
    }

  3. >We can do an adequate job of taming nullability with code contracts.

    No! Code contracts slow down so much compilation, it is not practical to use it on more than a short public API surface.

    I talked with Anders Hejlsberg just before the release of C#2 / .NET 2, and did my best to advocate non-nullable ref types. Its argument against was the major breaking change in the CTS. I remember also in an interview Anders said its biggest regret is to not have designed non-nullable ref since the inception of .NET and C#.

  4. Forgetting about back comp. What if non nullable ref types behaves like value types about zero value? default(int) is a valid value, so default(string) should be too, writing 0 (default) to all fields.

  5. Hello!

    What if non-nullables will be optional compile-time check, not strong runtime contract? There is similar feature already – the /checked compiler switch.

    If we do so, we could leave clr untouched, the only thing we have to patch is BCL (we have to add [NotNull] attributes on all non-nullable parameters/return values) and compiler (adding syntactic sugar handling “string!” as “[NotNull] string”, automatic annotations based on control flow analysis and explicit checks when casting from nullables to non-nullables).

    If we DO NOT specify compile-time option “no nullables”, all client code just ignores [not null] annotations – no warnings, errors etc, so there will be no breaking changes.

    If we turn checks ON, the annotations should be checked at compile-time and so we get the new shiny nullless code.

    Now the most interesting part (here and below we assuming the checks turned ON): the annotations should be deducted implicitly and automatically.
    As an example, return of the

    public string SayHello() { return “Hello!”; }

    should be annotated as [NotNull] so we can safely do something alike

    var myVar = SayHello().Length;

    now, if SayHello() will be rewritten as

    public string SayHello() { return null; }

    compiler should emit error at SayHello().Length. Of course, we can explicitly annotate SayHello as

    public string! SayHello() { return null; } // or public [NotNull] string SayHello() …

    and got compile-time error right there without need to fix all places where SayHello() is called. Complex cases and casts from nullable to non-nullable should be explicit:

    public string! SayHello() { return (string!)GetNullableString(); }

    Compiler should inject explicit check here so NRE will be thrown at the return of the method if GetNullableString() returns null.
    If we follow these restrictions the flow analysys could be restricted by method boundary.

    And the last thing: we already have at least two working implementations, the jetbrains resharpers annotations and not-null checks from CodeContracts.
    CodeContracts is especially interesting: static verifier does the control flow, deduces null/not null values and allows to supply the annotations together with the assembly. And, of course fires errors on contract failure:)
    Only thing we missing is syntactic sugar part.

    So, is this really hard to bring [NotNull] into c#?

  6. Have you seen Bart De Smet’s proposal mentioned here? Safe Navigation Operator in C#? – Stack Overflow

    Basically, instead of trying to get “the language guarantees that string! x means x is never null”, what we’re actually interested in is “the language guarantees that string! x means x.Length will never crash”. To do that, it might be possible (no idea how *easy* it would be) to do it as syntactic sugar over the Maybe monad. Make T! mean Maybe and make x.f(), where x is of type T!, mean “from _ in x from s in _.f() select s”, with a custom implementation of SelectMany that returns an empty sequence when given a null.

  7. Eric,

    This is my favorite subject. I agree with your assessment that estimating the cost of nulls at a billion dollars is way too low.

    How hard would it be for the CLR to report, as part of a null reference exception, the runtime type of the reference that turned out to be null? I don’t know how many times I’ve muttered under my breath ‘if I just knew if it was that string reference or that custom-type reference, etc…’.

  8. So suppose the C# language allows you to make a field of type string! – can the language give you that same ironclad guarantee, that this field is never, ever observed to be null?
    No, but it doesn’t have to. That is not the real need. Look at the readonly keyword, it is a useful feature. Yet during the constructor’s execution, a readonly field can be observed to change. We consider that it’s ok to trust programmers with not leaking the newly-created instance before the constructor has finished (or they lose the guarantee offered by readonly).
    In the same spirit, you could add non-nullable reference types that can be null only during the constructors. It would be a very useful feature even with that caveat, just like readonly is. Giving a useful tool to programmers, even if they could mess up using it, is better than doing nothing in the name of unnattainable perfection.

    In short: C# was designed with the principle that any object fresh from the allocator meets the requirements of the type system. That’s not true in a world with non-nullable reference types, and so it would be a lot of design work to patch up the resulting holes.
    Well, if you add non-nullable references to C# as a compiler-only feature, like readonly, without any kind of support in the runtime, I don’t think there would be any hole to patch. The only sad point is that the runtime would not be able to optimize the memory representation of the types to reflect the non-nullability of the references, since there is no such guarantee. But I think it is more important to help the programmer find bugs in her code than to worry about missed optimization opportunities.
    Did I miss something?

  9. I wish the C# designers would have / could have put Nullable and the ? type-suffix into C# 1.0, and just made all types non-nullable by default and require you to put a ? on it if you want to make it nullable. In my opinion having a statically typed language but having nullability is absurd: In a dynamically typed language if I have the wrong type of object I will (usually) hit a method-not-found exception somewhere in the internals of the code. In a statically typed language if I pass null where I didn’t intend to, I’ll get a null-reference exception when I try to access a method on the object. But the exception occurs in the same place, at the same time in both the dynamically typed and the statically typed versions of the program! What difference does it make if I it was a method-not-found exception versus a can’t-call-method-on-null exception? I’m not for or against static or dynamic typing, but I’m saying if you’re going to do one or the other then really do one or the other, don’t make the system self-defeating.

  10. I do agree with a previous commenter who said, what programmers really want is the ability to call “str.Length” without it blowing up. The Io language has an interesting feature whereby if you put ? in front of a method name, as in “str ?Length” it will call the method iff the target is non-null, and return null instead. If C# had that syntax sugar, “str.?Length” would mean “(str != null ? str.Length : null)”.

  11. Eric, Objection#1 at the very least seems moot to me. Why would the BCL team need to ensure non-nullability on their *parameters*? The BCL code already works fine, no one cares if it accepts nullable strings, and the BCL team can live with guard if clauses as long as they’re interested in backward compat.

    What *would* be beneficial to consumers is the application of non-nullability to return types where applicable, which would never be a breaking change as long as non-nullable types are assignable to their nullable counterparts. If I’m sticking my brand new non-nullable types into a plain old nullable BCL parameter, that would work, and if some crusty old code is a accepting a nullable string from a method that now returns string!, that would also (still) work fine.

    Assignability in the reverse direction makes no sense, as you noted, and therefore should not be implemented. I fail to see what the problem is.

    1. Because the point of the feature is to provide a proof in the type system that the program has no nullity errors. Look at it this way: if there are non-nullable strings, and a base class library method takes a nullable string, that method is waving a sign that says “you don’t have to prove non-nullability of your string, I can handle it”. The same way that if you had a method that took an int?, you’d expect that passing null to that method would not crash and die horribly.

      Or, let me put that another way: the point of putting nullability checks in the type system is to catch bugs. Adopting your propose would mean that the number of bugs caught gets significantly lower, which makes the feature produce less value without changing its costs. If the net benefit — value minus costs — is negative then doing the feature makes the world worse, not better.

      1. Yes, and waving that flag would be accurate for all BCL methods, since all BCL methods, as currently implemented, *can* accept nullable strings (and other reference types), having had no alternative when they were designed. They probably have branches that deal with cases like the parameter being null, which allow them to function perfectly fine when a regular, “not-non-nullable” string is passed in.

        The BCL folks could, of course, add overloads that accept non-nullable versions of their original parameters, and enjoy the benefits in the implementation of these overloads. I still don’t understand why the implementation of non-nullable types would create a need for library maintainers to eliminate or change the signature of existing methods, which would work just fine with or without this feature.

        I’ll admit that I didn’t understand just how insurmountable Objection #2 was at the time I read this. Hope springs eternal, however :)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Current day month ye@r *