可空引用类型:如何指定“T?”类型不限于类或结构

Posted

技术标签:

【中文标题】可空引用类型:如何指定“T?”类型不限于类或结构【英文标题】:Nullable reference types: How to specify "T?" type without constraining to class or struct 【发布时间】:2019-09-22 07:39:44 【问题描述】:

我想创建一个具有T 类型成员的泛型类。 T 可以是类、可为空的类、结构或可为空的结构。所以基本上什么都有。这是一个显示我的问题的简化示例:

#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));

由于使用了新的#nullable enable 功能,我收到以下警告:Program.cs(11,23): warning CS8653: A default expression introduces a null value when 'T' is a non-nullable reference type.

这个警告对我来说很有意义。然后我尝试通过在属性和构造函数参数中添加? 来修复它:

#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));

但现在我得到了两个错误:

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.

但是,我不想添加约束。我不在乎 T 是类还是结构。

一个明显的解决方案是将违规成员包装在#nullable disable 指令下。但是,就像#pragma warning disable 一样,除非有必要,否则我想避免这样做。是否有其他方法可以在不禁用可空性检查或 CS8653 警告的情况下编译我的代码?

$ dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   3.0.100-preview4-011223
 Commit:    118dd862c8

【问题讨论】:

我没有更仔细地研究这个问题,但我假设T? 值类型和引用类型之间的差异根本不由编译器处理。为什么?我可以它只会增加大量的复杂性,但我也猜想您需要来自实际编译器人员的输入才能确定。值类型的T?Nullable&lt;T&gt; 处理,而C# 8 中引用类型的T? 由具有属性的T 处理。基本上,我认为这根本不被支持。 嗨@Andent,欢迎来到SO。您实际上是在打开一个设置,告诉编译器 not 允许将null 分配给T 类型的变量(当T 是引用类型时),然后使用@987654341 @ which 可以为引用类型返回 null。这里没有什么神秘之处。你必须选择一条路径,比如 Neo... :-) 您可能正在考虑同时制作Box&lt;T&gt; where T : classValueBox&lt;T&gt; where T : struct。我认为目前没有办法通过T?Nullable&lt;T&gt; 统一泛型类型/方法。 从根本上说,可空类型和泛型不能很好地混合。根据我的经验,这是设计中最棘手的部分。 我认为您在这里有一些相互矛盾的目标。您想拥有default 框的概念,但对于引用类型,还有什么是合适的default?对于与使用可空引用类型直接冲突的引用类型,默认值为 null。也许您需要将 T 限制为可以默认构造的类型 (new())。 【参考方案1】:

如果您使用的是 C# 9,该怎么办

在 C# 9 中,您可以在不受约束的类型参数上使用T? 来指示当 T 是引用类型时该类型始终可以为空。事实上,原始问题中的示例在将? 添加到属性和构造函数参数后“正常工作”。请参阅以下示例以了解您对 Box&lt;T&gt; 的不同类型参数的预期行为。

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();

如果您使用 C# 8 该怎么办

在 C# 8 中,无法在不受约束的类型参数(即未知的引用类型或值类型)上放置​​可为空的注释。

正如 cmets 关于这个问题的讨论,您可能需要考虑具有默认值的 Box&lt;string&gt; 在可空上下文中是否有效,并可能相应地调整您的 API 表面。也许类型必须是 Box&lt;string?&gt; 才能使包含默认值的实例有效。但是,在某些情况下,您需要指定属性、方法返回或参数等仍然可以为 null,即使它们具有不可为空的引用类型。如果您属于该类别,您可能希望使用与可空性相关的属性。

MaybeNull 和 AllowNull 属性已被引入 .NET Core 3 以处理这种情况。

这些属性的一些具体行为仍在不断发展,但基本思想是:

[MaybeNull] 表示某事物的输出(读取字段或属性、方法返回等)可能是null[AllowNull] 表示某事物(写入字段或属性、方法参数等)的 输入 可以是 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();
    

请参阅https://devblogs.microsoft.com/dotnet/try-out-nullable-reference-types 了解更多信息,尤其是“the issue with T?”部分。

【讨论】:

令人着迷。这是我的第一次尝试,但没有成功,我也不知道为什么。事实证明,如果我将您的示例更改为使用“default(T)”而不是“default”,我将得到相同的 CS8653,这是我完全没想到的。我应该认为它们在语义上是相同的——事实上,我认为纯默认值只有在它们存在时才是合法的! 如果我将您的示例更改为省略 Value 上的 [MaybeNull],我根本不会收到任何警告,这对我来说甚至更陌生。并调用 Box.CreateDefault().Value.Length 在运行时抛出 NRE! 在这种情况下缺少default 的警告是一个错误。追踪于github.com/dotnet/roslyn/issues/38339。我更新了我的答案以更详细地介绍,并希望能更清楚地了解您所看到的行为。 C# 9 提供的解决方案仍然不能令人满意,因为它将int? 强制转换为int 而不是Nullable&lt;int&gt;... :'( 当@JonSkeet 称其为“设计中最棘手的部分”时,您就会知道这确实是一个棘手的问题!就我个人而言,我认为我们在语言上走错了路,我相信不可为空的引用类型应该是完整类型。事实上,在 C#9 中,当 T 不受约束(甚至被约束为 notnull)时,语法“T?”表示“对应于 T 的可默认类型”而不是“对应于 T 的可空类型”,因为它在语言中的其他任何地方都是如此。但是,如果您将 T 限制为类或结构,那么“T?”再次开始表示“对应于T的可空类型”。【参考方案2】:

Jeff Mercado 在 cmets 中提出了一个很好的观点:

我认为您在这里有一些相互矛盾的目标。您想要默认框的概念,但对于引用类型,还有什么是合适的默认值?对于与使用可空引用类型直接冲突的引用类型,默认值为 null。也许您需要将 T 限制为可以默认构造的类型(new())。

例如,T = stringdefault(T) 将是null,因为在运行时stringstring? 之间没有区别。这是当前语言功能的限制。

我通过为每个案例创建单独的CreateDefault 方法来解决这个问题:

#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);

这种似乎类型对我来说是安全的,但代价是更丑陋的呼叫站点(CreateDefaultBox.ReferenceTypeNullable&lt;object&gt;() 而不是Box&lt;object?&gt;.CreateDefault())。在我发布的示例类中,我将完全删除方法并直接使用Box 构造函数。哦,好吧。

【讨论】:

【参考方案3】:
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?

【讨论】:

这不会编译。您不能只将! 任何地方 - 它出现在表达式的末尾,而不是类型的末尾。 哇,真的是 Jon skeet!我认为你是对的,但我希望它有助于找到正确的方法 我认为这对这里没有帮助。 OP 希望 Value 可以为空,据我所知,无法在 C# 8 中表达这一点。

以上是关于可空引用类型:如何指定“T?”类型不限于类或结构的主要内容,如果未能解决你的问题,请参考以下文章

C#中 ?? ? ?: ?.?[ ]

C#中的?和??的用法

可空类型是引用类型吗?

C#中的??是啥意思

在C#中??和?分别是什么意思?

具有泛型返回类型的可空引用类型