I want to create a generic class that has a member of type T
. T
may be a class, a nullable class, a struct, or a nullable struct. So basically anything. This is a simplified example that shows my problem:
#nullable enable class Box<T> { public T Value { get; } public Box(T value) { Value = value; } public static Box<T> CreateDefault() => new Box<T>(default(T)); }
Due to using the new #nullable enable
feature I get the following warning: Program.cs(11,23): warning CS8653: A default expression introduces a null value when 'T' is a non-nullable reference type.
This warning makes sense to me. I then tried to fix it by adding a ?
to the property and constructor parameter:
#nullable enable class Box<T> { public T? Value { get; } public Box(T? value) { Value = value; } public static Box<T> CreateDefault() => new Box<T>(default(T)); }
But now I get two errors instead:
Program.cs(4,12): error CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint. Program.cs(6,16): error CS8627: A nullable type parameter must be known to be a value type or non-nullable reference type. Consider adding a 'class', 'struct', or type constraint.
However, I don't want to add a constraint. I don't care if T
is a class or a struct.
An obvious solution is to wrap the offending members under a #nullable disable
directive. However, like #pragma warning disable
, I'd like to avoid doing that unless it's necessary. Is there another way in getting my code to compile without disabling the nullability checks or the CS8653 warning?
$ dotnet --info .NET Core SDK (reflecting any global.json): Version: 3.0.100-preview4-011223 Commit: 118dd862c8
You can declare nullable types using Nullable<t> where T is a type. Nullable<int> i = null; A nullable type can represent the correct range of values for its underlying value type, plus an additional null value. For example, Nullable<int> can be assigned any value from -2147483648 to 2147483647, or a null value.
Nullable types represent value-type variables that can be assigned the value of null. You cannot create a nullable type based on a reference type. (Reference types already support the null value.) So, no they're not reference types.
Nullable reference types aren't new class types, but rather annotations on existing reference types. The compiler uses those annotations to help you find potential null reference errors in your code. There's no runtime difference between a non-nullable reference type and a nullable reference type.
NullReferenceException. Nullable reference types includes three features that help you avoid these exceptions, including the ability to explicitly mark a reference type as nullable: Improved static flow analysis that determines if a variable may be null before dereferencing it.
In C# 9, you can use T?
on an unconstrained type parameter to indicate that the type is always nullable when T is a reference type. In fact, the example in the original question "just works" after adding ?
to the property and constructor parameter. See the following example to understand what behaviors you may expect for different kinds of type arguments to Box<T>
.
var box1 = Box<string>.CreateDefault(); // warning: box1.Value may be null box1.Value.ToString(); var box2 = Box<string?>.CreateDefault(); // warning: box2.Value may be null box2.Value.ToString(); var box3 = Box<int>.CreateDefault(); // no warning box3.Value.ToString(); var box4 = Box<int?>.CreateDefault(); // warning: 'box4.Value' may be null box4.Value.Value.ToString();
In C# 8, it is not possible to put a nullable annotation on an unconstrained type parameter (i.e. that is not known to be of a reference type or value type).
As discussed in the comments on this question, you will probably need to take some thought as to whether a Box<string>
with a default value is valid or not in a nullable context and potentially adjust your API surface accordingly. Perhaps the type has to be Box<string?>
in order for an instance containing a default value to be valid. However, there are scenarios where you will want to specify that properties, method returns or parameters, etc. could still be null even though they have non-nullable reference types. If you are in that category, you will probably want to make use of nullability-related attributes.
The MaybeNull and AllowNull attributes have been introduced to .NET Core 3 to handle this scenario.
Some of the specific behaviors of these attributes are still evolving, but the basic idea is:
[MaybeNull]
means that the output of something (reading a field or property, a method return, etc.) could be null
.[AllowNull]
means that the input to something (writing a field or property, a method parameter, etc.) could be null
.#nullable enable using System.Diagnostics.CodeAnalysis; class Box<T> { // We use MaybeNull to indicate null could be returned from the property, // and AllowNull to indicate that null is allowed to be assigned to the property. [MaybeNull, AllowNull] public T Value { get; } // We use only AllowNull here, because the parameter only represents // an input, unlike the property which has both input and output public Box([AllowNull] T value) { Value = value; } public static Box<T> CreateDefault() { return new Box<T>(default); } public static void UseStringDefault() { var box = Box<string>.CreateDefault(); // Since 'box.Value' is a reference type here, [MaybeNull] // makes us warn on dereference of it. _ = box.Value.Length; } public static void UseIntDefault() { // Since 'box.Value' is a value type here, we don't warn on // dereference even though the original property has [MaybeNull] var box = Box<int>.CreateDefault(); _ = box.Value.ToString(); } }
Please see https://devblogs.microsoft.com/dotnet/try-out-nullable-reference-types for more information, particularly the section "the issue with T?".
Jeff Mercado raised a good point in the comments:
I think you have some conflicting goals here. You want to have the notion of a default box but for reference types, what else is an appropriate default? The default is null for reference types which directly conflicts with using nullable reference types. Perhaps you will need to constrain T to types that could be default constructed instead (new()).
For example, default(T)
for T = string
would be null
, since at runtime there is no distinction between string
and string?
. This is a current limitation of the language feature.
I have worked around this limation by creating separate CreateDefault
methods for each case:
#nullable enable class Box<T> { public T Value { get; } public Box(T value) { Value = value; } } static class CreateDefaultBox { public static Box<T> ValueTypeNotNull<T>() where T : struct => new Box<T>(default); public static Box<T?> ValueTypeNullable<T>() where T : struct => new Box<T?>(null); public static Box<T> ReferenceTypeNotNull<T>() where T : class, new() => new Box<T>(new T()); public static Box<T?> ReferenceTypeNullable<T>() where T : class => new Box<T?>(null); }
This seems type safe to me, at the cost of more ugly call sites (CreateDefaultBox.ReferenceTypeNullable<object>()
instead of Box<object?>.CreateDefault()
). In the example class I posted I'd just remove the methods completely and use the Box
constructor directly. Oh well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With