【问题标题】:C# okay with comparing value types to nullC# 可以将值类型与 null 进行比较
【发布时间】:2010-12-30 15:29:04
【问题描述】:

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

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

我对 x 怎么可能为空感到困惑。特别是因为这个赋值肯定会引发编译器错误:

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?");
        }
    }
}

【问题讨论】:

  • 你也可以写if (1 == 2)。执行代码路径分析不是编译器的工作;这就是静态分析工具和单元测试的用途。
  • 关于警告消失的原因,请参阅我的回答;不 - 它不能为空。
  • 同意了(1 == 2),我比较好奇情况(1 == null)
  • 感谢所有回复的人。现在一切都说得通了。
  • 关于警告或无警告问题:如果所讨论的结构是所谓的“简单类型”,如int,编译器会生成很好的警告。对于简单类型,== 运算符由 C# 语言规范定义。对于其他(非简单类型)结构,编译器忘记发出警告。有关详细信息,请参阅Wrong compiler warning when comparing struct to null。对于非简单类型的结构,== 运算符必须由该结构成员的opeartor == 方法重载(否则不允许使用==)。

标签: c# null


【解决方案1】:

编译器将允许您将任何实现== 的结构与空值进行比较。它甚至允许您将 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);

被翻译为假:

IL_001e:  ldc.i4.0

【讨论】:

    【解决方案2】:

    我认为关于为什么编译器接受这一点的最佳答案是针对泛型类。考虑以下类...

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

    如果编译器不接受与null 进行值类型的比较,那么它本质上会破坏这个类,并在其类型参数上附加一个隐式约束(即它只适用于非基于值的类型) .

    【讨论】:

      【解决方案3】:

      比较永远不会为真这一事实并不意味着它是非法的。尽管如此,不,值类型永远可以是null

      【讨论】:

      • 但是值类型可以等于null。考虑int?,它是Nullable&lt;Int32&gt; 的语法糖,它是一个值类型。 int? 类型的变量当然可以等于 null
      • @Greg:是的,它可以等于 null,假设您所指的“等于”是 == 运算符的结果。需要注意的是,该实例实际上并非为空。
      【解决方案4】:

      [已编辑:将警告变为错误,并使运算符明确表示可为空而不是字符串破解。]

      根据@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
      • 你是绝对正确的。我应该澄清一下我的建议打破了可空值的使用......在我正在工作的代码库中无论如何都被认为是有罪的(不需要的装箱等)。 ;)
      【解决方案5】:

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

      这就是为什么只有一个警告。

      【讨论】:

      • 这是不正确的,有两个原因。首先,当其参数之一是引用类型时, == 与 Object.Equals 的语义不同。其次,null 不是类型。如果您想了解引用相等运算符的工作原理,请参阅规范的第 7.9.6 节。
      • "null 字面量(第 9.4.4.6 节)计算结果为 null 值,用于表示不指向任何对象或数组的引用,或者没有值。null 类型有单个值,它是空值。因此,类型为空类型的表达式只能计算为空值。无法显式编写空类型,因此无法在声明的类型中使用它。” -- 这是来自 ECMA 的引用。你在说什么?另外,您使用哪个版本的 ECMA?我没有看到 7.9.6。
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

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

        表达式的结果始终为“假”,因为“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);
        }
        

        【讨论】:

        【解决方案8】:

        这是合法的,因为运算符重载决议有一个唯一的最佳运算符可供选择。有一个 == 运算符,它采用两个可为空的整数。 int local 可转换为可为空的 int。 null 文字可转换为可为 null 的 int。因此,这是 == 运算符的合法用法,并且总是会导致错误。

        同样,我们也允许您说“if (x == 12.6)”,这也总是错误的。 int local 可以转换为 double,literal 可以转换为 double,显然它们永远不会相等。

        【讨论】:

        • @James:(我收回了我之前的错误评论,我已经删除了。)用户定义的值类型默认也定义了一个用户定义的相等运算符为他们生成的提升的用户定义的相等运算符。提升的用户定义的相等运算符适用于您陈述的原因:所有值类型都可以隐式转换为其相应的可为空类型,空文字也是如此。 不是,一个缺少用户定义的比较运算符的用户定义的值类型与空字面量是可比的。
        • @James:当然,您可以实现自己的 operator == 和 operator != 采用可为空的结构。如果这些存在,那么编译器将使用它们而不是自动为您生成它们。 (顺便说一句,我很遗憾对不可为空的操作数的无意义提升运算符的警告不会产生警告;这是编译器中的一个错误,我们还没有解决。)
        • 我们需要我们的警告!我们应得的。
        • @JamesDunne:定义一个static bool operator == (SomeID a, String b) 并用Obsolete 标记它怎么样?如果第二个操作数是无类型文字 null,这将比任何需要使用提升运算符的形式更好的匹配,但如果它是一个恰好等于 nullSomeID?,则提升运算符将获胜。
        【解决方案9】:

        不,Int32 x 永远不会变成 null

        如果您将 int 与 null 进行比较 然后是比较运算符 需要两个 int?s 是适用的。

        "Why a comparison of a value type with null is a warning?" 文章会帮助你。

        【讨论】:

          【解决方案10】:

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

          旁注:可以让可为空的 Int32 使用 Int32 吗? x 代替。

          【讨论】:

            猜你喜欢
            • 2021-02-11
            • 2012-02-08
            • 1970-01-01
            • 2013-09-14
            • 2013-05-04
            • 2023-03-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多