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

57

我想创建一个通用类,该类具有类型为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

9
我尚未深入研究这个问题,但我认为编译器没有处理值类型和引用类型之间 T? 的差异。为什么?我可以猜测这只会增加许多复杂性,但我也猜想您需要实际的编译器人员提供输入才能确定。对于值类型,T?Nullable<T> 处理,而对于引用类型,C# 8 中的 T? 由具有属性的 T 处理。基本上,我认为这是不受支持的。 - Lasse V. Karlsen
1
嗨,@Andent,欢迎来到SO。您实际上正在打开一个设置,告诉编译器不要允许将null分配给类型为T的变量(当T是引用类型时),然后使用default(T),这可能会为引用类型返回null。这里没有什么神秘的。您必须选择一条路,就像Neo一样... :-) - JuanR
3
目前看来,您可能想要创建一个Box<T> where T : class和一个ValueBox<T> where T : struct。我认为目前没有将泛型类型/方法统一到T?Nullable<T>上的路径。 - Jonathon Chase
8
基本上,可为空的类型和泛型不太容易混合使用。根据我的经验,这是设计中最棘手的部分。 - Jon Skeet
1
我认为你在这里有一些相互冲突的目标。你想要有一个“默认”框的概念,但对于引用类型来说,还有什么是适当的“默认”呢?对于引用类型来说,“null”就是默认值,这与使用可空引用类型直接冲突。也许你需要将“T”限制为可以进行默认构造的类型(“new()”)。 - Jeff Mercado
显示剩余6条评论
3个回答

26

如果您正在使用C# 9,应该怎么做

在C# 9中,您可以在未受限制的类型参数上使用T?,以指示当T为引用类型时该类型始终可为空。实际上,在将?添加到属性和构造函数参数后,原始问题中的示例就可以正常运行。请参见以下示例,了解您可能期望不同类型参数的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();

如果你正在使用C# 8,该怎么办

在C# 8中,不可能在未受限制的类型参数上放置可空注释(即未知是否为引用类型或值类型)。

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

.NET Core 3引入了 MaybeNullAllowNull 属性来处理此场景。

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

  • [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了解更多信息,特别是"T的问题"部分。

有趣。这是我的第一次尝试,但它没有成功,我不知道为什么。原来,如果我将你的示例更改为使用"default(T)"而不是"default",我会得到相同的CS8653,这完全出乎我的意料。我认为它们在语义上是相同的-实际上,我认为纯默认值只有在它们存在时才合法! - solublefish
如果我将你的示例更改为省略值上的[MaybeNull],则根本不会收到任何警告,这对我来说甚至更奇怪。而调用Box<string>.CreateDefault().Value.Length在运行时引发NRE! - solublefish
1
在这种情况下缺少default警告是一个错误。已跟踪到https://github.com/dotnet/roslyn/issues/38339。我更新了我的答案,以便更详细地说明并希望能够更好地解释您所看到的行为。 - Rikki Gibson
C# 9提供的解决方案仍然不尽如人意,因为它将int?强制转换为int而不是Nullable<int>... :'( - Good Night Nerd Pride
2
当@JonSkeet称其为“设计中最棘手的部分”时,您就知道这确实是一个棘手的问题!就个人而言,我认为我们在语言方面走错了路,我认为非空引用类型应该是完整的类型。事实上,在C#9中,对于未受限制的T(甚至限制为notnull),语法“T?”意味着“对应于T的可默认类型”,而不是像语言中的其他地方一样意味着“对应于T的可空类型”。但是,如果将T约束为类或结构,则“T?”再次开始意味着“对应于T的可空类型”。 - sjb-sjb

4

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

我认为你在这里有一些冲突的目标。你想要有一个默认框的概念,但对于引用类型,还有什么其他适当的默认值呢?引用类型的默认值为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<object>() 而不是 Box<object?>.CreateDefault())。在我发布的示例类中,我会完全删除方法并直接使用 Box 构造函数。嗯。


-14
class Box<T> {
    public T! Value { get; }

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

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

null!语句是什么意思?


5
无法编译。你不能随意地将 ! 放在任何地方 - 它只能出现在表达式的末尾,而不是类型的末尾。 - Jon Skeet
哇,真的是Jon Skeet!我认为你是对的,但我希望它能帮助找到正确的方法。 - evilGenius
4
我认为这对此处没有帮助。楼主想让“Value”可为空,在我所知的C# 8中没有表达这一点的方法。 - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接