In this contrived C# 8 example:
#nullable enable
class Fred<T>
{
T Value; // If T is a nullable type, Value can be null.
public Fred() { }
public void SetValue(T value) { Value = value; }
public T GetValue() { return Value; }
public string Describe() { return Value.ToString() ?? "oops"; }
}
class George
{
George()
{
Fred<George> fredGeorge = new Fred<George>();
George g = fredGeorge.GetValue();
Fred<float> fredFloat = new Fred<float>();
float f = fredFloat.GetValue();
}
}
I have three design goals:
- The compiler should warn me if I write any methods for Fred that blindly assume that 'Value' will never be null
- The compiler should warn me if I write any methods outside of Fred (say, in George) that blindly assume that 'GetValue' will never return null.
- No compiler warnings (if I have written code that doesn't blindly assume values won't be null)
So this first version isn't bad, I get a warning in Fred that Describe() might be dereferencing a null reference (satisfies goal #1) but I also get a warning that Value is uninitialized in Fred's constructor (violates goal #3) and George compiles without any warnings (violates goal #2). If I make this change:
public Fred() { Value = default; }
George still compiles without warnings (violates goal #2) and I get a different warning in Fred's constructor about a "Possible null reference assignment" (violates goal #3).
I can get rid of the possible null reference assignment by using the null-forgiving operator:
public Fred() { Value = default!; }
And now Fred only has the correct warning (possible dereference in Describe()), but George also compiles without warning (violates goal #2).
If I try to indicate that 'Value' can be null:
T? Value;
I get a compiler error that "A nullable type parameter must be known to be a value type or non-nullable reference type" so that's no good.
If I go back to
T Value;
and add the "MaybeNull" attribute:
[return: MaybeNull]
public T GetValue() { return Value; }
I get two warnings - one in Fred.Describe() warning of a possible null dereference (correct) and one in George warning that fredGeorge.GetValue() might be null (correct). There is no warning about fredFloat.GetValue() being null (correct).
So after adding code to expect null references, what I end up with is this:
class Fred<T>
{
T Value;
public Fred()
{
Value = default!;
}
public void SetValue(T value)
{
Value = value;
}
[return: MaybeNull]
public T GetValue()
{
return Value;
}
public string Describe()
{
return (Value == null) ? "null" : (Value.ToString() ?? "ToString is null");
}
}
class George
{
George()
{
Fred<George> fredGeorge = new Fred<George>();
George? g = fredGeorge.GetValue();
Fred<float> fredFloat = new Fred<float>();
float f = fredFloat.GetValue();
}
}
Is this the correct pattern for this functionality?