这种 Nullable<T> 行为与隐式转换运算符的理由是啥

Posted

技术标签:

【中文标题】这种 Nullable<T> 行为与隐式转换运算符的理由是啥【英文标题】:What is the justification for this Nullable<T> behavior with implicit conversion operators这种 Nullable<T> 行为与隐式转换运算符的理由是什么 【发布时间】:2012-04-28 07:13:08 【问题描述】:

我在Nullable 和隐式转换之间的交互中遇到了一些有趣的行为。我发现为引用类型从值类型提供隐式转换,它允许将 Nullable 类型传递给需要引用类型的函数,而我却期望出现编译错误。下面的代码演示了这一点:

static void Main(string[] args)

    PrintCatAge(new Cat(13));
    PrintCatAge(12);
    int? cat = null;
    PrintCatAge(cat);


private static void PrintCatAge(Cat cat)

    if (cat == null)
        System.Console.WriteLine("What cat?");
    else
        System.Console.WriteLine("The cat's age is 0 years", cat.Age);


class Cat

    public int Age  get; set; 
    public Cat(int age)
    
        Age = age;
    

    public static implicit operator Cat(int i)
    
        System.Console.WriteLine("Implicit conversion from " + i);
        return new Cat(i);
    

输出:

The cat's age is 13 years
Implicit conversion from 12
The cat's age is 12 years
What cat?

如果从Cat 中删除转换代码,那么您会得到预期的错误:

Error 3 The best overloaded method match for 'ConsoleApplication2.Program.PrintCatAge(ConsoleApplication2.Program.Cat)' has some invalid arguments

Error 4 Argument 1: cannot convert from 'int?' to 'ConsoleApplication2.Program.Cat

如果您使用 ILSpy 打开可执行文件,则生成的代码如下

int? num = null;
Program.PrintCatAge(num.HasValue ? num.GetValueOrDefault() : null);

在一个类似的实验中,我删除了转换并向PrintCatAge 添加了一个重载,它接受一个 int(不可为空)来查看编译器是否会执行类似的操作,但它不会。

我明白发生了什么,但我不明白这样做的理由。这种行为对我来说是出乎意料的,而且看起来很奇怪。我在 MSDN 上的转换文档或 Nullable&lt;T&gt; 中没有成功找到对此行为的任何引用。

然后我提出的问题是,这是故意的吗?是否有解释为什么会发生这种情况?

【问题讨论】:

有趣!一个类似的意外行为是 ((object)(int?)null) == null 为真。可以为空的 int 可以装箱到空引用。 像这样铸造一个空值:(int?)null 被优化掉了。但是, (null as int?) 会导致实际创建 int?临时变量。无论哪种方式,我都希望在这些情况下null == null @usr:这是意料之中的; CLR 具有用于装箱空值的特殊代码。实际上根本不可能得到一个盒装的Nullable&lt;T&gt; 等效的 VB.NET 代码在编译时确实会在 PrintCatAge(cat) 上报错:“Option Strict On 不允许来自 'Integer?' 的隐式转换?到'UserQuery.Cat'。”并且在运行时使用Option Strict Off,它会失败并显示“InvalidOperationException:Nullable object must have a value。”。 【参考方案1】:

我之前说过(1)这是一个编译器错误,(2)这是一个新错误。第一个陈述是准确的;第二个是我在匆忙准时到达公共汽车时感到困惑。 (我想到的这个错误对我来说是新的,它是一个更复杂的错误,涉及提升转换和提升增量运算符。)

这是一个长期存在的已知编译器错误。 Jon Skeet 前段时间第一次引起了我的注意,我相信在某个地方有一个 *** 问题;我不记得在哪里随手。也许乔恩会。

所以,错误。让我们定义一个“提升”运算符。如果运算符从不可为空的值类型 S 转换为不可为空的值类型 T,那么还有一个从 S? 转换的“提升”运算符?到 T?,这样一个空的 S?转换为空 T?和一个非空 S?转换为 T?通过展开 S?到 S,将 S 转换为 T,并将 T 包装为 T?。

规范说 (1) only 存在提升运算符的情况是 S 和 T 都是不可为空的值类型,以及 (2) 提升和非提升的转换运算符同时考虑它们是否是适用的转换候选者,如果两者都适用,则适用转换的源和目标类型(提升或未提升)用于确定最佳源类型、最佳目标类型,并最终获得所有适用转化的最佳转化。

不幸的是,该实现完全违反了所有这些规则,并且这样做的方式是我们无法在不破坏许多现有程序的情况下进行更改。

首先,我们违反了提升运算符存在的规则。如果 S 和 T 都是不可为空的值类型,或者如果 S 是不可为空的值类型并且 T 是可以分配空值的任何类型,则实现认为存在提升的运算符:引用类型,可空值类型或指针类型。在所有这些情况下,我们都会生成一个提升的运算符。

在您的特定情况下,我们通过检查 null 将可空类型转换为引用类型 Cat 来提升为可空。如果源不为空,那么我们正常转换;如果是,那么我们生成一个空 Cat。

其次,当这些候选者之一是提升运算符时,我们彻底违反了关于如何确定适用候选者的最佳来源和目标类型的规则,并且我们还违反了关于确定哪个是最佳运算符的规则。

简而言之,如果不破坏真正的客户,就无法解决这个问题,因此我们可能会将这种行为奉为 Roslyn。我会考虑在某个时候在我的博客中记录编译器的确切行为,但如果我是你,我不会在等待那一天时屏住呼吸。

当然,对于这些错误,我们深表歉意。

【讨论】:

我以为只有 Jon Skeet 在通勤时回答问题。 最大的不同是 Jon 可以在通勤时继续发帖。所有那些摄像机也有wifi热点?真的不知道。 太棒了!好吧,不是有错误的事实,但至少我不必试图弄清楚为什么这种行为是有意义的:) 感谢您的详尽解释。事实证明,这是一个非常难以搜索的行为,因此我可能在其他地方错过了有关此问题的现有解释,我并不感到惊讶。我在评估相关错误报告时偶然发现了一些生产代码中的情况,并且完全感到困惑。跨度> @Thomas:不客气。您在搜索网络或规范时要使用的关键字是“C# 提升可空用户定义的转换运算符”之类的。

以上是关于这种 Nullable<T> 行为与隐式转换运算符的理由是啥的主要内容,如果未能解决你的问题,请参考以下文章

为啥这种显式转换的结果与隐式转换的结果不同?

为啥 Nullable<T> 不匹配作为通用约束的引用类型 [重复]

Nullable<T> 的内存占用是多少

非 Nullable 类型的通用约束

Nullable不是泛型类, 但方法可以是泛型的;

如何让核心动画显式动画的行为方式与隐式动画完全相同?