4
votes

Consider the following contrived class:

public sealed class Test<T>
{
    public void SetItem(T item)
    {
        _item = item;
    }

    public T GetItem()
    {
        return _item;
    }

    T _item; // warning CS8618: Non-nullable field '_item' is uninitialized.
}            // Consider declaring the field as nullable.

Assume that it must work with the following code:

var test1 = new Test<int>();
test1.SetItem(1);
Console.WriteLine(test1.GetItem());

var test2 = new Test<string>();
test2.SetItem("One");
Console.WriteLine(test2.GetItem());

Further assume that nullable is enabled when compiling.

This implementation yields a compiler warning (which turns into an error if, like us, you have "warnings as errors" enabled):

warning CS8618: Non-nullable field '_item' is uninitialized. Consider declaring the field as nullable.

My question is simple:

What is the best way to avoid this warning?

Things I have tried:

1) Declaring _item as nullable:

T? _item; // Error: A nullable type parameter must be known to be a value type or non-nullable reference type

2) Declaring the class as:

public sealed class Test<T> where T: struct

This avoids the warning, but now the line var test2 = new Test<string>(); will not compile

3) Declaring the class as:

public sealed class Test<T> where T: class
...
T? _item;

This also avoids the warning, but now the line var test1 = new Test<int>(); will not compile.

So what I have at the moment is this:

public sealed class Test<T>
{
    public void SetItem(T item)
    {
        _item = item;
    }

    public T GetItem()
    {
        return _item;
    }

    #pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
    T _item;
    #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
}

Is this really the best option I have?

Note: I've seen this related question, but it doesn't answer my question since the answer says to use where T : struct, which - as noted above - does not solve the issue.


[Edited after receiving the correct answer]

So it turns out that for a generic type, there is no way of using nullable to express the case that "if T is a nullable type, then it can be null".

However, Resharper DOES allow you do to this, so I'm using a combination of both.

The class looks like this when using Resharper support as well as nullable:

public sealed class Test<T>
{
    public void SetItem([CanBeNull] T item)
    {
        _item = item;
    }

    [CanBeNull] public T GetItem()
    {
        return _item;
    }

    [CanBeNull] T _item = default!;
}

Now when I write code like this:

var test = new Test<string>();
Console.WriteLine(test.GetItem().Length); // Warning: Possible null reference exception.

Resharper warns me about test.GetItem() possibly returning a null - even though the compiler doesn't.

(It's a shame that the C# compiler gives me no way to do this, but at least we have Resharper!)


[Edit after further consideration]

If I just use the class like this:

public sealed class Test<T>
{
    public void SetItem(T item)
    {
        _item = item;
    }

    public T GetItem()
    {
        return _item;
    }

    T _item = default!;
}

Then it becomes the responsibility of the instantiating code to supply the correct nullable type of T if needed, like so:

var test = new Test<string?>();

And now the compiler itself will warn you if you attempt to dereference a possibly-null reference:

var test = new Test<string?>(); // Note "string?"
Console.WriteLine(test.GetItem().Length); // Warning about test.GetItem() possibly null.

This is definitely preferable to using Resharper annotations like I did in my second edit, since that is actually wrong because it means that you can't say that the values are NOT null.


[Hopefully final clarifying edit]

The actual class does not allow _item to be accessed unless it was explicitly set, so it's more like this:

public sealed class Test<T>
{
    public void SetItem(T item)
    {
        _wasSet = true;
        _item = item;
    }

    public T GetItem()
    {
        if (!_wasSet)
            throw new InvalidOperationException("No item set.");

        return _item;
    }

    T _item = default!;

    bool _wasSet;
}

Because of the way the real class is used, the item to be set is not available at the time that the object is instantiated, so it's not possible to set the default in the constructor.

(The real class is something someone has written to provide a queue where you can access the first AND the second items in the queue without dequeueing them, and _item is in fact a separate value used for the front of the queue, with the remainder of the queue being stored in a Queue<T>. That's not the way I'd have done it, really, but that's what I'm working with...)

1
What would you expect to happen if someone called var test = new Test<string>(); string x = test.GetItem();? Basically your code is lying about the nullability - so it makes sense that there's a warning. It would be best if you could accept the item in your constructor.Jon Skeet
@JonSkeet In that case, I'd expect it to return null (which of course it does). The problem is just that it's not possible to express the case that "if type T is a reference type, then it could be null". (The class I showed is quite contrived - the real code is more complex and I'm trying to convert it to nullable without having to make too many changes.)Matthew Watson
In my experience, using nullability along with generics is going to be a major headache, precisely because of the problems you've seen in the code. There doesn't seem to be any good way to specify non-nullability for T members of a generic type and still support both structs and classes. Your best option is to design the class to allow null, which means you're going to have to force the compiler to accept your code.Lasse V. Karlsen
@LasseV.Karlsen I think my most recent edit seems to solve the issue completely. I avoid the warning, but the compiler still warns me if the type is nullable when I dereference it. This is also how List<T> works with, for example, List<string?>Matthew Watson
Well, what if you do: var x = new Test<string>(); var y = x.GetItem().Length;? That compiles, no warnings (because T is not T? so there is a promise here you're not getting null, but you are because of default!). Basically, GetItem() returns T, not T?, so it shouldn't return null, but the default value is default!, which is basically "null, don't complain!".Lasse V. Karlsen

1 Answers

5
votes

As a possible option, you use combination of default literal and null-forgiving operator, like

private T _item = default!;