为什么尝试向空值添加不会引发InvalidOperationException异常?

8
int? x = null;
x = x + 1;  // Works, but x remains null

我期望编译器会尝试将x转换为int,但显然它没有这样做。
由280Z28编辑:将NullReferenceException更改为InvalidOperationException,因为这是Nullable<T>.ValueHasValue为false时抛出的异常。
7个回答

10

这符合提升的二元运算符规范。根据 §7.2.7:

对于二元运算符

+ - * / % & | ^ << >>

如果操作数和结果类型都是非空值类型,则该运算符存在升级形式。升级形式是通过为每个操作数和结果类型添加一个 ? 修饰符来构造的。如果一个或两个操作数为 null,则提升的运算符将产生 null 值(bool? 类型的 & 和 | 运算符是例外,在 §7.10.3 中有描述)。否则,提升的运算符将解开操作数,应用底层运算符,然后包装结果。

理由是:你应该将可空类型的 null 视为“我不知道该值是什么”。 “我不知道” 加一 的结果是什么?“我不知道”。因此,结果应为 null


你的逻辑表述得很好,但是它真的应该抛出一个异常。把 + 看作运行 number.Plus(2) 的语法更酷的方式。如果 number 为空,或者它可以轻易地导致意料之外的行为,那么它肯定应该抛出异常。 - Niels Brinch
更糟糕的是,如果你有一个数字并将 null 加到它上面,它将变成 null。 - Niels Brinch

5

可空类型实际上永远不会是 null 引用。它们始终是对象引用。它们的内部类重写了 === 运算符。如果它们与 null 进行比较,它们将返回 HasValue 属性的值。


1
第一句话是正确的。中间两句话是错误的。Nullables永远不是对象引用。Nullable<T>结构体没有覆盖相等运算符。没有覆盖赋值运算符这样的事情。最后一句话是正确的。 - Eric Lippert
你是对的——不是赋值运算符,而是从int类型进行的隐式转换。它们确实是不同的运算符,但最终效果是相同的。感谢你指出这一点。 - rusty

2
这是因为编译器会为可空类型创建一个“提升”的运算符 - 在这种情况下,它类似于以下内容:
public static int? operator +(int? a, int? b)
{
    return (a == null || b == null) ? (int?)null : a.Value + b.Value
}

我认为,如果您尝试将结果分配给非空值,编译器将被迫使用非空重载并将x转换为int。

e.g. int i = x + 1;  //throws runtime exception

实际上 int i = x + 1 是一个错误的写法。如果 x 没有值,int i = (int)(x + 1) 将抛出异常。 - Dolphin

2

既然你已经将它声明为可空类型,为什么希望编译器将其转换为int类型呢?编译器正在执行你告诉它要做的事情,null + 1 = null。

在尝试添加int之前,您必须显式地进行转换或检查x.HasValue。


0

很遗憾,它并没有。在x = X + 1中的X与第一行中的null相同,因此您正在将1添加到null,其结果为null。

由于它是可空int,您可以使用x.HasValue来检查它是否具有值,然后使用x.Value获取实际的int值。


0
无论变量x是否为null,这并不是关键。
关键在于,在你进行加法运算时,你何时见过NullReferenceException
以下示例也不会引发NullReferenceException,而且是完全有效的。
string hello = null;
string world = "world";
string hw = hello+world;

只有在尝试访问一个空对象的成员时,才会出现NullReferenceException


-1

int?类型永远不可能是null,因为它是一个结构体。结构体存储在栈上,而栈不能很好地处理null。

请参见什么是NullPointerException,我该如何修复它?

另外,可空类型有2个非常有用的属性:HasValue和Value

这段代码:

        if (x != null)
        {
            return (int) x;
        }

应该重构为:

        if (x.HasValue)
        {
            return x.Value;
        }

这是完全不正确的。首先,值类型并不是“存储在堆栈上”。值类型存储在内存管理器认为合适的堆栈或堆上。其次,在堆栈上有一个空引用是完全合法的。“string s = null;”就是一个在堆栈上的空引用,没有问题。 - Eric Lippert
1
我很感激您的纠正。当我刚开始学习.NET时,我记得曾经这样写代码:DateTime x = null; 然后收到了这个错误提示:“编译器无法将null赋值给值类型;null只能被分配给引用类型。结构体是值类型。” 我误解了这个信息。这个错误提示是在谈论null字面量的限制,而不是值类型本身。感谢您给我上的这个应该受到的教训。 - fremis

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