将结构体与null进行比较

39

可能是重复问题:
C#中比较值类型和null是否可以

我在多线程环境下开发 Windows 应用程序时,有时会遇到异常 "Invoke 或 BeginInvoke 无法在控件的窗口句柄创建之前调用。" 所以我想只需添加这行代码:

if(this.Handle != null)
{
   //BeginInvokeCode
}

但是那并没有解决问题。所以我进一步研究后发现,IntPtr(Form.Handle 所属的类型)是一个不能为 null 的结构体。以下是最终有效的修复方法:

if(this.Handle != IntPtr.Zero)
{
   //BeginInvokeCode
}

接着我意识到,当我检查它是否为null时,为什么它还可以编译通过呢?所以我决定自己试一下:

    public struct Foo { }

然后:

    static void Main(string[] args)
    {
        Foo f = new Foo();
        if (f == null) { }
    }

然后,果然编译出错并提示“错误 1 运算符“==”不能应用于类型为“ConsoleApplication1.Foo”和“”的操作数”。好吧,我开始查看IntPtr的元数据,并将IntPtr结构中的所有内容添加到我的Foo结构中(ISerializable,ComVisible),但仍未解决问题。最后,当我添加了==和!=的运算符重载时,它就起作用了:

[Serializable]
[ComVisible(true)]
public struct Foo : ISerializable
{
    #region ISerializable Members

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        throw new NotImplementedException();
    }

    #endregion

    public override bool Equals(object obj)
    {
        return base.Equals(obj);
    }

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

    public static bool operator ==(Foo f1, Foo f2) { return false; }
    public static bool operator !=(Foo f1, Foo f2) { return false; }
}

这最终编译成功了:

    static void Main(string[] args)
    {
        Foo f = new Foo();
        if (f == null) { }
    }

我的问题是为什么?如果你覆盖了 == 和 !=,为什么可以将其与 null 进行比较? == 和 != 的参数仍然是不可空的 Foo 类型,那么为什么突然允许这样做呢?


3
好问题!顺便问候一下来自布鲁克林的朋友 :) - Stan R.
6个回答

20

看起来问题在于微软引入可空类型时,使得每个结构体都可以隐式转换为其可空类型(foo?),因此代码

if( f == null)

等同于

if ( (Nullable<foo>)f == (Nullable<foo>)null) 
自 MSDN 指出“任何为值类型存在的用户定义运算符也可以被可空类型使用”时,重写 operator== 时,允许编译隐式类型转换,因为现在您已经有了一个用户定义的 == 运算符,从而免费获得了可空性质的重载操作。
一旁说一句:
似乎在您的示例中,存在某些编译器优化。 编译器发出唯一暗示进行测试的 IL 代码是:
ldc.i4.0
ldc.i4.0
ceq
stloc.1   //where there is an unused boolean local
请注意,如果您将“main”更改为“

”,则可能会影响HTML的结构。
Foo f = new Foo();
object b = null;
if (f == b) { Console.WriteLine("?"); }

它不再编译。但是如果您封装该结构体:

Foo f = new Foo();
object b = null;
if ((object)f == b) { Console.WriteLine("?"); }

如果编译成功并生成了中间语言(IL),并且按预期运行(结构体不为空);


8

这与序列化或COM无关 - 所以从公式中除去这一点是值得的。例如,下面是一个简短但完整的程序,演示了问题:

using System;

public struct Foo
{
    // These change the calling code's correctness
    public static bool operator ==(Foo f1, Foo f2) { return false; }
    public static bool operator !=(Foo f1, Foo f2) { return false; }

    // These aren't relevant, but the compiler will issue an
    // unrelated warning if they're missing
    public override bool Equals(object x) { return false; }
    public override int GetHashCode() { return 0; }
}

public class Test
{
    static void Main()
    {
        Foo f = new Foo();
        Console.WriteLine(f == null);
    }
}

我认为这段代码之所以能编译成功,是因为从null字面量到Nullable<Foo>存在隐式转换,并且你可以合法地这样做:

Foo f = new Foo();
Foo? g = null;
Console.WriteLine(f == g);

有趣的是,这种情况只在==被重载时发生——Marc Gravell以前就发现了这一点。我不知道这实际上是编译器bug,还是转换、重载等方式中有非常微妙的东西。
在某些情况下(例如int、decimal),编译器会警告你隐式转换的问题,但在其他情况下(例如Guid),它不会。

@Jon,我也不需要重写Equals或GetHashCode来模拟这个行为。 - Stan R.
1
@Stan:不,我只是想让它编译时没有警告。我正准备将这个编辑到答案中 :) - Jon Skeet

4

我想到的可能是,你重载了 == 运算符,这使得编译器需要在以下两个选择之间进行选择:

public static bool operator ==(object o1, object o2)

并且

public static bool operator ==(Foo f1, Foo f2)

有两个选择,可以将左侧转换为对象并使用前者。但是如果您尝试基于您的代码运行某些内容,则不会进入运算符重载。没有运算符选择,编译器显然正在进行进一步的检查。


请注意,如果没有重载,第一种形式仍然适用 - 但编译器会禁止它。我认为你肯定沿着正确的方向前进,但这里有更深层次的魔法。 - Jon Skeet
2
选择实际上是(obj, obj),(Foo,Foo)和(Foo?,Foo?)之间的选择——在(Foo,Foo)上定义等号运算符也会自动定义提升到可为空版本。 - Eric Lippert
@Eric - 解释得非常好,非常清晰易懂。谢谢。 - David M
@Jon - 我在一本书中发现了完全相同的情况 - 它在《C#深入》的第126页上 :) - David M

1

我相信当你重载一个运算符时,你明确地订阅了这样一个观念,即你将处理特定运算符所需的所有逻辑。因此,如果它被调用,处理运算符重载方法中的 null 是你的责任。在这种情况下,正如你可能已经注意到的那样,如果与 null 进行比较,重载的方法永远不会被调用。

真正有趣的是,根据Henks answer here,我查看了反编译器中的以下代码。

Foo f1 = new Foo();
if(f1 == null)
{
  Console.WriteLine("impossible");
}

Console.ReadKey();

这就是反射器所显示的内容。

Foo f1 = new Foo();
Console.ReadKey();

编译器会清除它,因此重载运算符方法甚至不会被调用。


1
当然,结构体参数永远不会实际为空,这就是他的问题。 - Lasse V. Karlsen
没错,但我认为编译器不会处理它,也就是说,就编译器而言,你重载了运算符。此外,该方法甚至从未被调用。 - Stan R.

1

结构体没有定义重载运算符"=="或"!=",这就是为什么你会得到原始错误的原因。一旦在你的结构体中添加了重载运算符,比较就是合法的(从编译器的角度来看)。作为运算符重载的创建者,你有责任处理这个逻辑(显然微软在这种情况下错过了这一点)。

根据你的结构体实现方式(以及它所代表的内容),与null的比较可能是完全有效的,这就是为什么这是可能的。


不,我们没有错过这个。 (尽管似乎没有报告警告是一个错误。)根据规范,这种行为是正确的。 - Eric Lippert

-1

2
我认为他不需要阅读那些内容,因为那不是他问题的焦点。 - Stan R.

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