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...)
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 SkeetList<T>
works with, for example,List<string?>
– Matthew Watsonvar x = new Test<string>(); var y = x.GetItem().Length;
? That compiles, no warnings (becauseT
is notT?
so there is a promise here you're not gettingnull
, but you are because ofdefault!
). Basically,GetItem()
returnsT
, notT?
, so it shouldn't returnnull
, but the default value isdefault!
, which is basically "null, don't complain!". – Lasse V. Karlsen