More on Non-nullable Reference Types
I was thinking about this some more today, and it occurred to me that a very simple construct could achieve what is being asked for without making any changes to the language or tools. Nullable value types (structs) were first implemented with an explicit wrapper class; it wasn't until later that the ValueType? shorthand appeared. With implicit operators, we could basically do the same for non-nullable reference types:
This does not get rid of NullReferenceExceptions, but it does bake in null checks into the code and makes for some interesting syntax:
It's not perfect. I used a struct because it's a value type that can never be null, so we don't have to worry about the nullity of the NotNull<T> itself. However, all structs have a default constructor that cannot be overridden, which means that if default(Nullable<T>) is used then the internal _value field will not be properly initialized. So the Dereference() method is implemented such that it will throw a NullReferenceException if _value in this case.
So what does this actually achieve? Well, a couple of things. First, it serves to document the method prototype. Obviously the caller is expecting a non-null value and/or ensures that the return value is not null. Second, it does enforce null checks as close to the call site as possible, which should prevent null references from making it too far. This is beneficial in that it would prevent NullReferenceExceptions from occurring within third-party code that may not have debugging enabled.
When dealing with anything more complicated than a single number or character, we need pointers, even with fancy managed languages like C# and Java. And unless they have something to point to, uninitialized references are here to stay. Syntactic candy and helper classes only go so far. I still contend that design-by-contract is the way to go.
public struct NotNull<T> : IEquatable<T>, IEquatable<NotNull<T>> where T: class { private T _value; public NotNull(T value) { if (value == null) { throw new ArgumentNullException("value"); } _value = value; } public T Dereference() { if (_value == null) { throw new NullReferenceException(); } return _value; } public override bool Equals(object other) { if (ReferenceEquals(null, other)) { return false; } if (other is NonNullable<T>) { return Equals((NotNull<T>)other); } if (other is T) { return Equals((T)other); } return false; } public bool Equals(NotNull<T> other) { return Equals(other._value); } public bool Equals(T other) { if (ReferenceEquals(null, other)) { return false; } return Equals(Dereference(), other); } public int GetHashCode() { return Dereference().GetHashCode(); } public static bool operator ==(NotNull<T> left, NotNull<T> right) { return Equals(left, right); } public static bool operator !=(NotNull<T> left, NotNull<T> right) { return !Equals(left, right); } public static implicit operator NotNull<T>(T value) { return new NotNull<T>(value); } public static implicit operator T(NotNull<T> notNullValue) { return notNullValue.Dereference(); } }
This does not get rid of NullReferenceExceptions, but it does bake in null checks into the code and makes for some interesting syntax:
public static void Main(string[] args) { if (args.Length < 1) throw new Exception(); // Implicit conversion to NotNull<string> will throw at call site // if args[0] is null Method(args[0]); } public static NotNull<string> Method(NotNull<string> stringValue) { // Implicit conversion from NotNull<string> to string will // call NotNull<T>.Dereference(), which will throw in the case // that default(NotNull<string>) was passed in. string foo = stringValue; return foo + "bar"; }
It's not perfect. I used a struct because it's a value type that can never be null, so we don't have to worry about the nullity of the NotNull<T> itself. However, all structs have a default constructor that cannot be overridden, which means that if default(Nullable<T>) is used then the internal _value field will not be properly initialized. So the Dereference() method is implemented such that it will throw a NullReferenceException if _value in this case.
So what does this actually achieve? Well, a couple of things. First, it serves to document the method prototype. Obviously the caller is expecting a non-null value and/or ensures that the return value is not null. Second, it does enforce null checks as close to the call site as possible, which should prevent null references from making it too far. This is beneficial in that it would prevent NullReferenceExceptions from occurring within third-party code that may not have debugging enabled.
When dealing with anything more complicated than a single number or character, we need pointers, even with fancy managed languages like C# and Java. And unless they have something to point to, uninitialized references are here to stay. Syntactic candy and helper classes only go so far. I still contend that design-by-contract is the way to go.
Comments
Post a Comment