Nullable reference types: How to specify “T?” type

2020-02-05 01:46发布

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

3条回答
Viruses.
2楼-- · 2020-02-05 02:14

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.

查看更多
霸刀☆藐视天下
3楼-- · 2020-02-05 02:14
class Box<T> {
    public T! Value { get; }

    public Box(T! value) {
        Value = value;
    }

    public static Box<T> CreateDefault()
        => new default!;
}

What does null! statement mean?

查看更多
聊天终结者
4楼-- · 2020-02-05 02:28

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>
{
    [MaybeNull]
    public T Value { get; }

    public Box([AllowNull] T value) // 1
    {
        Value = value;
    }

    public static Box<T> CreateDefault()
    {
        return new Box<T>(default(T)!); // 2
    }

    public static void UseStringDefault()
    {
        var box = Box<string>.CreateDefault();
        _ = box.Value.Length; // 3
    }

    public static void UseIntDefault()
    {
        var box = Box<int>.CreateDefault();
        _ = box.Value.ToString(); // 4
    }
}

Notes:

  1. The flow analysis attributes currently affect only the calls to the annotated members, not the implementations. This means that if we deleted the [MaybeNull] attribute from the property Value we would not get a warning about assigning the [AllowNull] T value parameter to it. The [AllowNull] in practice just prevents possible-null-argument warnings from being given at the call site.
  2. The compiler's analysis of unconstrained (i.e. not known to be value or reference type) type parameters is limited. At this point this means the compiler gives a warning: 'A default expression introduces a null value when 'T' is a non-nullable reference type.'. Because the compiler can't always determine when a usage of a value of type T is null-safe or not, it instead gives warnings in places where T could be a non-nullable reference type, yet 'null' could be introduced. The most obvious place this happens is with default(T), but it may also happen at the invocation of generic methods whose returns are annotated with [MaybeNull]. For now we handle the problem by suppressing the warning with default(T)!.
  3. This is a simpler case where we give a nullability warning at the dereference of box.Value because of the [MaybeNull] annotation on the property.
  4. In this case the MaybeNull attribute does not cause a warning at the usage, because box.Value is a non-nullable value type.

Please see https://devblogs.microsoft.com/dotnet/try-out-nullable-reference-types for more information, particularly the section "the issue with T?".

查看更多
登录 后发表回答