C#可以将值类型比较为null


85

我今天遇到了这个问题,不知道为什么C#编译器没有抛出错误。

Int32 x = 1;
if (x == null)
{
    Console.WriteLine("What the?");
}

我对x怎么可能为null感到困惑。特别是由于此分配肯定会引发编译器错误:

Int32 x = null;

x是否有可能成为null,Microsoft是否只是决定不将此检查放入编译器,还是完全错过了?

更新:在弄乱编写本文的代码之后,突然编译器提出了警告,该表达式永远不会为真。现在我真的迷路了。我将对象放到一个类中,现在警告已经消失了,但还有一个问题,值类型最终是否可以为null。

public class Test
{
    public DateTime ADate = DateTime.Now;

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}

9
您也可以写if (1 == 2)。执行代码路径分析不是编译器的工作;这就是静态分析工具和单元测试的目的。
Aaronaught

有关警告为何消失的信息,请参阅我的回答。不-它不能为null。
马克·格雷夫

1
同意(1 == 2),我更想知道这种情况(1 ==空)
Joshua Belden

谢谢大家的回应。现在一切都有意义。
Joshua Belden

关于警告或无警告问题:如果有问题的结构是所谓的“简单类型”,如int,编译器会生成不错的警告。对于简单类型,==运算符由C#语言规范定义。对于其他(非简单类型)结构,编译器会忘记发出警告。有关将struct与null比较的详细信息,请参见错误的编译器警告。对于不是简单类型的结构,==必须通过属于opeartor ==该结构成员的方法来重载运算符(否则==不允许使用)。
杰普·斯蒂格·尼尔森

Answers:


119

这是合法的,因为运算符重载解析有一个唯一的最佳运算符可供选择。有一个==运算符,它需要两个可为null的int。int local可以转换为可为null的int。null文字可转换为可为null的int。因此,这是==运算符的合法用法,并且始终会导致错误。

同样,我们也允许您说“ if(x == 12.6)”,这也永远是假的。int local可以转换为双精度型,文字可以转换为双精度型,并且显然它们永远不会相等。



5
@James :(我撤消了我先前删除的错误注释。)默认情况下还定义了用户定义的相等运算符的用户定义值类型会为其生成一个提升的用户定义的相等运算符。提升的用户定义的相等运算符适用于您陈述的原因:所有值类型都可以隐式转换为它们对应的可为null的类型,如null文字也是如此。这是认为,一用户定义的值型的情况下缺乏用户定义的比较运算符比得上null文本。
埃里克·利珀特

3
@James:当然,您可以实现自己的运算符==和运算符!=,它们可以使用可空结构。如果存在,那么编译器将使用它们,而不是自动为您生成它们。(顺便说一句,令我遗憾的是,针对不可为空的操作数的无意义提升运算符的警告不会产生警告;这是我们尚未解决的编译器错误。)
Eric Lippert,2010年

2
我们想要我们的警告!我们应得的。
Jeppe Stig Nielsen

3
@JamesDunne:定义astatic bool operator == (SomeID a, String b)并用其标记Obsolete怎么办?如果第二个操作数是未类型的文字null,那么它将比需要使用提升运算符的任何形式都更好,但是如果它SomeID?等于a null,那么提升运算符将获胜。
2013年

17

这不是错误,因为有(int?)转换;在给出的示例中,它确实会生成警告:

表达式的结果始终为“ false”,因为类型为“ int”的值永远不会等于类型为“ int?”的“ null”

如果您检查IL,就会完全看到它删除了无法访问的分支-在发行版本中不存在。

但是请注意,它不会对带有相等运算符的自定义结构生成此警告。它曾经在2.0中使用过,但在3.0编译器中却没有。该代码仍被删除(因此它知道该代码不可访问),但是不会生成警告:

using System;

struct MyValue
{
    private readonly int value;
    public MyValue(int value) { this.value = value; }
    public static bool operator ==(MyValue x, MyValue y) {
        return x.value == y.value;
    }
    public static bool operator !=(MyValue x, MyValue y) {
        return x.value != y.value;
    }
}
class Program
{
    static void Main()
    {
        int i = 1;
        MyValue v = new MyValue(1);
        if (i == null) { Console.WriteLine("a"); } // warning
        if (v == null) { Console.WriteLine("a"); } // no warning
    }
}

使用IL(用于Main)-请注意,除(可能会有副作用)以外的所有内容MyValue(1)均已删除:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] valuetype MyValue v)
    L_0000: ldc.i4.1 
    L_0001: stloc.0 
    L_0002: ldloca.s v
    L_0004: ldc.i4.1 
    L_0005: call instance void MyValue::.ctor(int32)
    L_000a: ret 
}

这基本上是:

private static void Main()
{
    MyValue v = new MyValue(1);
}

1
最近也有人内部向我报告了此情况。我不知道为什么我们停止发出该警告。我们已将其输入为错误。
Eric Lippert


5

比较永远不可能成立的事实并不意味着它是非法的。但是,不可以,值类型可以是null


1
但是,值类型可以是等于null。考虑int?,它是的语法糖Nullable<Int32>,是一种值类型。类型变量int?肯定可以等于null
格雷格,

1
@Greg:是的,它可以等于null,假设您所指的“等于”是==运算符的结果。需要特别注意的是,该实例实际上并非为null。
亚当·罗宾逊


1

值类型不能为null,尽管它可以等于null(consider Nullable<>)。在您的情况下,int变量和null被隐式转换为Nullable<Int32>并进行比较。


0

我怀疑您的特定测试只是在编译器生成IL时被优化,因为该测试永远不会为假。

注意:是否有可能将Int32设为可为null的Int32?x代替。


0

我猜这是因为“ ==”是语法糖,实际上代表了对System.Object.Equals接受System.Object参数的方法的调用。ECMA规范中的Null是一种特殊类型,它当然是从派生的System.Object

这就是为什么只有警告。


这是不正确的,原因有两个。首先,==与Object不具有相同的语义。当其参数之一是引用类型时等于。第二,null不是类型。如果您想了解引用相等运算符如何工作,请参见规范的7.9.6节。
Eric Lippert

“空文字(第9.4.4.6节)求值为空值,该空值用于表示未指向任何对象或数组或没有值的引用。空类型只有一个值,即空值值,因此类型为null类型的表达式只能求值为null值。无法显式编写null类型,因此无法在声明的类型中使用它。” -这是ECMA的报价。你在说什么?您还使用哪个版本的ECMA?我看不到我的7.9.6。
维塔利2009年

0

[编辑:将警告变成错误,并使运算符明确可空而不是字符串hack。]

根据@supercat在以上注释中的巧妙建议,以下运算符重载使您可以生成有关将自定义值类型与null进行比较的错误。

通过实现与您的类型的可为空的版本进行比较的运算符,比较中使用null将匹配该操作符的可为空的版本,这使您可以通过Obsolete属性生成错误。

在Microsoft退还编译器警告之前,我要使用此替代方法,谢谢@supercat!

public struct Foo
{
    private readonly int x;
    public Foo(int x)
    {
        this.x = x;
    }

    public override string ToString()
    {
        return string.Format("Foo {{x={0}}}", x);
    }

    public override int GetHashCode()
    {
        return x.GetHashCode();
    }

    public override bool Equals(Object obj)
    {
        return x.Equals(obj);
    }

    public static bool operator ==(Foo a, Foo b)
    {
        return a.x == b.x;
    }

    public static bool operator !=(Foo a, Foo b)
    {
        return a.x != b.x;
    }

    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo a, Foo? b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo a, Foo? b)
    {
        return true;
    }
    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo? a, Foo b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo? a, Foo b)
    {
        return true;
    }
}

除非我有所遗漏,否则您的方法将导致编译器误Foo a; Foo? b; ... if (a == b)...以为,即使这样的比较应该是完全合理的。我之所以提出“字符串入侵”的原因是,它允许进行上述比较,但会冒犯于if (a == null)。代替使用的string,一个可替代的其他任何类型的参考比ObjectValueType; 如果需要的话,可以用一个永远不会被调用的私有构造函数来定义一个哑类,并为其赋予权利ReferenceThatCanOnlyBeNull
超级猫

你是绝对正确的。我应该澄清一下,我的建议打破了对可为空值的使用……在我正在工作的代码库中,无论如何,它们都被认为是有罪的(不需要的装箱等)。;)
yoyo 2015年

0

我认为关于编译器为什么接受此方法的最佳答案是针对通用类。考虑以下课程...

public class NullTester<T>
{
    public bool IsNull(T value)
    {
        return (value == null);
    }
}

如果编译器不接受针对null值类型的比较,那么它将本质上破坏此类,并在其类型参数上附加隐式约束(即,它仅适用于基于非值的类型)。


0

编译器将允许您比较实现 ==null的。它甚至允许您将int与null进行比较(尽管会得到警告)。

但是,如果您反汇编代码,则会在编译代码时看到比较已解决。因此,例如,以下代码(在其中Foo实现的结构==):

public static void Main()
{
    Console.WriteLine(new Foo() == new Foo());
    Console.WriteLine(new Foo() == null);
    Console.WriteLine(5 == null);
    Console.WriteLine(new Foo() != null);
}

生成此IL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       45 (0x2d)
  .maxstack  2
  .locals init ([0] valuetype test3.Program/Foo V_0)
  IL_0000:  nop
  IL_0001:  ldloca.s   V_0
  IL_0003:  initobj    test3.Program/Foo
  IL_0009:  ldloc.0
  IL_000a:  ldloca.s   V_0
  IL_000c:  initobj    test3.Program/Foo
  IL_0012:  ldloc.0
  IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                           valuetype test3.Program/Foo)
  IL_0018:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_001d:  nop
  IL_001e:  ldc.i4.0
  IL_001f:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_0024:  nop
  IL_0025:  ldc.i4.1
  IL_0026:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_002b:  nop
  IL_002c:  ret
} // end of method Program::Main

如你看到的:

Console.WriteLine(new Foo() == new Foo());

转换为:

IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                               valuetype test3.Program/Foo)

鉴于:

Console.WriteLine(new Foo() == null);

转换为false:

IL_001e:  ldc.i4.0
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.