Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

c# Indicate nullability of generic member

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:

  1. The compiler should warn me if I write any methods for Fred that blindly assume that 'Value' will never be null
  2. 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.
  3. 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?

like image 463
Betty Crokker Avatar asked Oct 16 '22 02:10

Betty Crokker


People also ask

What C is used for?

C programming language is a machine-independent programming language that is mainly used to create many types of applications and operating systems such as Windows, and other complicated programs such as the Oracle database, Git, Python interpreter, and games and is considered a programming foundation in the process of ...

What is the full name of C?

In the real sense it has no meaning or full form. It was developed by Dennis Ritchie and Ken Thompson at AT&T bell Lab. First, they used to call it as B language then later they made some improvement into it and renamed it as C and its superscript as C++ which was invented by Dr.

What is C in C language?

What is C? C is a general-purpose programming language created by Dennis Ritchie at the Bell Laboratories in 1972. It is a very popular language, despite being old. C is strongly associated with UNIX, as it was developed to write the UNIX operating system.

Is C language easy?

C is a general-purpose language that most programmers learn before moving on to more complex languages. From Unix and Windows to Tic Tac Toe and Photoshop, several of the most commonly used applications today have been built on C. It is easy to learn because: A simple syntax with only 32 keywords.


1 Answers

In System.Diagnostics.CodeAnalysis there is an attribute AllowNullAttribute that specifies that null is allowed as an input even if the corresponding type disallows it. I would use this attribute in your final sample to:

  • Decorate field Value. This will allow us to remove null-forgiving operator in the assignment Value = default. Compiler will not warn us about Possible null reference assignment, because now it knows that null value can be assigned to the property Value.
  • Decorate argument T value of the method SetValue. It will allow to pass null value to the method SetValue without getting compiler warning Cannot convert null literal to non-nullable reference type. (Currently if we pass null value to the method SetValue we will get this warning)

Here is final sample with suggested changes:

class Fred<T>
{
    // AllowNull attribute says that a null value
    // can be assigned to the field Value.
    [AllowNull]
    private T Value;

    public Fred()
    {
        // Now we can delete null-forgiving operator, because compiler knows
        // that null value can be assigned to the field Value.
        Value = default;
    }

    // AllowNull attribute says that a null value
    // can be passed to the method SetValue.
    public void SetValue([AllowNull] 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();

        // Compiler does not warn us "Cannot convert null literal to
        // non-nullable reference type" because it knows that a null
        // value can be passed to the method SetValue.
        fredGeorge.SetValue(null);

        Fred<float> fredFloat = new Fred<float>();
        float f = fredFloat.GetValue();
    }
}

If we use a regular property instead of the field Value with a pair of methods GetValue and SetValue then we can rewrite final sample in a clearer way:

class Fred<T>
{
    // Here we tell that:
    // 1) a null value can be assigned;
    // 2) a null value can be returned.
    [AllowNull, MaybeNull]
    public T Value { get; set; }

    public Fred()
    {
        // Compiler does not warn us "Possible null reference assignment".
        // It knows that a null value can be assigned. It is correct.
        // We can delete null-forgiving operator.
        Value = default;
    }

    public string Describe()
    {
        // If we delete null checking, then we get a warning "Dereference of
        // a possibly null reference". It is correct. Compiler helps us to avoid
        // NullReferenceException.
        return (Value == null) ? "null" : (Value.ToString() ?? "ToString is null");
    }
}

class George
{
    George()
    {
        Fred<George> fredGeorge = new Fred<George>();

        // Compiler warns us "Converting null literal or possible null
        // value to non-nullable type". It is correct.
        // We should use nullable reference type George?.
        George g = fredGeorge.Value;

        // Compiler does not warn us "Cannot convert null literal to
        // non-nullable reference type". It knows that a null value
        // can be passed to the method SetValue. It is correct.
        fredGeorge.Value = null;

        Fred<float> fredFloat = new Fred<float>();
        float f = fredFloat.Value;
    }
}
like image 127
Iliar Turdushev Avatar answered Nov 10 '22 00:11

Iliar Turdushev