为什么“struct Nullable<T>”不是一个结构体?

7
基本上,为什么以下内容在C#中无效?我可以找到很多好的用途,事实上可以通过创建自己的可空结构体类来修复它,但是为什么和如何C#规范(因此编译器)会阻止它?
以下是我所说的部分示例。
struct MyNullable<T> where T : struct
{
    public T Value;
    public bool HasValue;

    // Need to overide equals, as well as provide static implicit/explit cast operators
}

class Program
{
    static void Main(string[] args)
    {
        // Compiles fine and works as expected
        MyNullable<Double> NullableDoubleTest;
        NullableDoubleTest.Value = 63.0;

        // Also compiles fine and works as expected
        MyNullable<MyNullable<Double>> NullableNullableTest;
        NullableNullableTest.Value.Value = 63.0;

        // Fails to compile...despite Nullable being a struct
        // Error: The type 'double?' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'ConsoleApplication1.MyNullable<T>'
        MyNullable<Nullable<Double>> MyNullableSuperStruct;
    }
}

1
你在问为什么无法创建一个可空的可空变量?也许如果你尝试用文字来表达这个问题,而不仅仅是代码,你就会明白为什么这并没有多大意义了。 - Cody Gray
@Cody:我认为这与那个问题没有任何关系。这个问题似乎是关于Nullable<T>上的“magic”约束,该约束要求参数是非空值类型。 - Ben Voigt
@CodyGray 我认为那个问题的答案回答了这个问题,但两个问题是不同的。因此,答案是Nullable在运行时方面是“特殊的”。 - Andrew T Finnell
@CodyGray 我不同意并投票重新开放。如果有什么不同,那就是这是对之前的SO问题的扩展。结构体是值类型,因此Nullable<T>也是值类型,但运行时对其进行了不同的处理。这就是这个问题要问的。为什么Nullable<T>既是值类型/结构体,又不是值类型/结构体。 - Andrew T Finnell
1
Nullable<double?> 也不起作用。这是因为奇怪的变形行为,装箱转换将可空结构转换为 System.Nullable 对象。这不符合您的约束条件。这也是您无法将可空类型用作约束的原因。 - Hans Passant
显示剩余8条评论
2个回答

9
这是一个struct结构体。它只是不能满足值类型泛型类型参数约束。来自语言规范的10.1.5节:
“值类型约束指定用于该类型参数的类型参数必须是非空值类型。所有非空结构类型、枚举类型和具有值类型约束的类型参数都满足此约束。请注意,尽管分类为值类型,但可空类型(§4.1.10)不满足值类型约束。”
所以,“where T : struct”并不意味着你认为它意味着什么。
基本上,为什么以下内容在C#中无效?
因为where T : struct只能被非空值类型的T所满足。Nullable<TNonNullableValueType>不满足此约束。
编译器为什么会阻止这种情况发生,如何防止它发生?
为了与规范一致。通过执行语法和语义分析,并确定您提供的泛型类型参数T不满足泛型类型约束where T : struct来防止它发生。
我可以通过创建自己的可空结构体类来解决它,但是你的版本并没有解决它。它基本上与Nullable完全相同,除了你不会得到编译器的特殊处理,并且你将导致一些装箱,而编译器的实现不会装箱。
我可以找到很多很好的用途。

真的吗?比如说?请记住,Nullable<T> 的基本思想是拥有一个存储位置,可以包含 T 或表示“值缺失”。嵌套它的意义是什么?也就是说,Nullable<Nullable<T>> 的意义是什么?这甚至没有概念上的意义。这可能就是为什么它被禁止的原因,但我只是在猜测(Eric Lippert已经确认了这个猜测是正确的)。例如,int?? 是什么?它表示一个存储位置,表示值缺失或者是一个 int?,而 int? 本身又表示值缺失或者是一个 int 的存储位置?有什么用处呢?


我认为OP的真正问题是:“为什么10.1.5包括请注意,尽管被归类为值类型,但可空类型(§4.1.10)不满足值类型约束条件。”? - Gabe
@Jason,我无法访问TypedPageCommand中的代码,因此无法在其中的每个TArg1使用情况周围使用Nullable。 TypedPageCommand稍后会调用带有TArg1和TArg2作为参数的委托函数。 - NtscCobalt
3
你的猜测是正确的。在该特性最初的设计中,“int??”、“int???”等都是完全合法的,实际上编译器中仍有少量代码留存于原型中,以处理这些情况。当然,现在这些代码只用于错误恢复的路径。 - Eric Lippert
1
@NtscCobalt 这篇博客文章非常好地解释了决策过程:http://blogs.msdn.com/b/somasegar/archive/2005/08/11/450640.aspx - phoog
4
当时这是一个引起了很多争议的决定;正如文章中soma所指出的那样,最终决定在.NET v2.0的船期相当晚才做出。故事的道义当然是要从一开始就将nullability和non-nullability设计到您的类型系统中,而不是后来再试图添加上去。下次设计类型系统时,请记住这一点。 - Eric Lippert
显示剩余13条评论

2
结构约束不允许使用可空类型的一个原因是我们希望能够在泛型方法中使用 T?。如果结构体允许可空值类型,编译器将不得不禁止使用 T?
在其他情况下,可空类型必须在编译器中进行特殊处理:
  • null 关键字必须隐式转换为可空类型;这对于值类型来说是不可能的。
  • 可空值类型可以与 null 关键字进行比较;而非可空值类型,此比较始终返回 false。
  • 可空值类型可以使用 ?? 运算符;而非可空值类型则不能。

@Jason,或许使用Nullable<T>作为类型参数要求T为结构体是有道理的,因为Nullable<T>被定义为struct Nullable<T>:where T struct { T Value; bool HasValue;}。逻辑上可能会令人困惑,但除了规范之外,编译器没有理由阻止它。 - NtscCobalt
@phoog:这里很容易混淆。如果你有一个 Nullable<int> w,并且 w.HasValue 为 false,则 w.GetType() 将抛出异常。现在考虑 Nullable<Nullable<int>> x = w,其中 w 是这样的,使得 w.HasValuefalse。那么 x.Valuew 的一个副本,因此 x.Value.GetType() 将抛出异常,因为 w.GetType() 抛出异常。但是,x.HasValue 为 true,但 x.Value 表示 Nullable<int> 的一个 null 实例。 - jason
@Jason,哦,嗯抱歉我不知道Nullable<T>被装箱为(Object)(this.Value)。这是有道理的。我实际上假设对ValueType.GetValue()的调用在编译时被替换为typeof(ValueType)。 - NtscCobalt
@NtscCobalt:不,编译器做不到那样!这里会发生什么:Base b = new Derived(); Console.WriteLine(b.GetType())。如果编译器能够执行您建议的替换,它将返回打印“Base”而不是“Derived”。 - jason
1
@Jason:问题的根本在于有人决定在(null)和(一个 Nullable<T>,其 HasValue 字段为 false)上调用 Object.Equals 应该返回 true。去掉这种愚蠢的想法,调用 GetType()Nullable<int> 上将返回一个 Nullable<int>,无论它是否有值。为了允许传递到代码中,该代码将期望一个装箱的 T 或一个 null,Nullable<T> 类型可以包括一个 AsObjectOrNull 属性,该属性将返回 Value 强制转换为 Object 或者 null。嵌套的 Nullable<Nullable<T>> 没有问题;该属性将... - supercat
显示剩余15条评论

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