与 null 的比较在表达式 expr == null 和 expr != null 中均为 true。

10

我看到了一些非常奇怪的东西,无法解释。我猜测这是C#的某些边缘情况,我不熟悉,或者是运行时/发射器中的错误?

我有以下方法:

public static bool HistoryMessageExists(DBContext context, string id)
{
    return null != context.GetObject<HistoryMessage>(id);
}

在测试我的应用程序时,我发现它表现不佳 - 它对于我知道不存在于我的数据库中的对象返回true。因此,我停在了这个方法上,在即时窗口里运行了以下命令:

context.GetObject<HistoryMessage>(id)
null
null == context.GetObject<HistoryMessage>(id)
true
null != context.GetObject<HistoryMessage>(id)
true

GetObject 的定义如下:

public T GetObject<T>(object pk) where T : DBObject, new()
{
    T rv = Connection.Get<T>(pk);

    if (rv != null)
    {
        rv.AttachToContext(this);
        rv.IsInserted = true;
    }

    return rv;
}

有趣的是,将表达式转换为object后,比较操作会被正确地执行:
null == (object)context.GetObject<HistoryMessage>(id)
true
null != (object)context.GetObject<HistoryMessage>(id)
false

没有等号运算符覆盖。

编辑:事实证明存在一个运算符重载,之前的说法是错误的。但是为什么在内部方法通用的GetObject中,当rvHistoryMessage类型时,等式会正确计算呢?

public class HistoryMessage : EquatableIdentifiableObject
{
    public static bool HistoryMessageExists(DBContext context, string id)
    {
        var rv = context.GetObject<HistoryMessage>(id);
        bool b = rv != null;
        return b;
    }

    public static void AddHistoryMessage(DBContext context, string id)
    {
        context.InsertObject(new HistoryMessage { Id = id });
    }
}

public abstract partial class EquatableIdentifiableObject : DBObject, IObservableObject
{
    public event PropertyChangedEventHandler PropertyChanged;

    [PrimaryKey]
    public string Id { get; set; }

    //...
}

public abstract partial class EquatableIdentifiableObject
{
    //...

    public static bool operator ==(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
    {
        if (ReferenceEquals(self, null))
        {
            return ReferenceEquals(other, null);
        }

        return self.Equals(other);
    }

    public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
    {
        if (ReferenceEquals(self, null))
        {
            return !ReferenceEquals(other, null);
        }

        return !self.Equals(other);
    }
}

public abstract class DBObject
{
    [Ignore]
    protected DBContext Context { get; set; }

    [Ignore]
    internal bool IsInserted { get; set; }

    //...
}

这里发生了什么?

2
HistoryMessage 实现了相等运算符吗? - Lasse V. Karlsen
1
甚至在HistoryMessage的任何基类中也没有吗? - Lasse V. Karlsen
2
无论如何,你能给我们展示“HistoryMessage”吗? - Lasse V. Karlsen
1
如果您反编译该项目,就可以发现是否实际上存在运算符重载。尝试使用免费的反编译器dotPeek。如果有基本引用检查,则比较应该由ceq指令实现;如果存在运算符重载,则会看起来像call SomeClass.op_Equality。您还可以在LINQPad中发现这一点,这是我尝试过的地方。 - Lasse V. Karlsen
3
此行为的另一个原因可能是竞态条件或者在 GetObject 中存在的 bug,导致连续调用返回不同的结果。将 context.GetObject<HistoryMessage>(id) 缓存到本地变量中,然后检查是否为 null 并查看问题是否仍然存在。 - InBetween
显示剩余14条评论
2个回答

3
As you have already explained, the == operator did not work for your type due to an incorrect overload. However, when casting to object, the == operator worked correctly because it used object's implementation of == instead of EquatableIdentifiableObject's implementation.
In the method GetObject, the operator evaluates correctly because it does not use EquatableIdentifiableObject's implementation of ==. In C#, generics are resolved at runtime and not at compile time. Note that == is static and not virtual. This means that the type T is resolved at runtime, but the call to == must be resolved at compile time. At compile time, when the compiler resolves ==, it does not know to use EquatableIdentifiableObject's implementation of ==. Therefore, if the DBObject type has a defined implementation of ==, it will be used instead. If DBObject does not define ==, then the implementation of the first base class that does so (up to object) will be used.
A few additional comments about EquatableIdentifiableObject's implementation of ==:
- The previous implementation was incorrect. - You could replace this part:
if (ReferenceEquals(self, null))
{
     return ReferenceEquals(other, null);
}

关于:

// If both are null, or both are the same instance, return true.
if (object.ReferenceEquals(h1, h2))
{
    return true;
}
  • 将其替换为更健壮的方式会更好。
public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
{
    ...
}

使用:

public static bool operator !=(EquatableIdentifiableObject self, EquatableIdentifiableObject other)
{
    return !(self == other);
}

您定义 == 签名的方式有些误导性。第一个参数的名称是 self ,第二个参数的名称是 other 。如果 == 是实例方法,那就可以接受。由于它是静态方法,名称 self 有点误导。更好的名称可能是 o1 o2 或类似这样的名称,以便将两个操作数视为更平等的对象。

1
谢谢解释。我猜运算符不是虚拟的,因为它们是静态的。你有什么想法为什么运算符被实现为静态的吗?是因为空值边缘情况吗? - Léo Natan
请看这里:https://dev59.com/zHI-5IYBdhLWcg3wF0Qc - Ladi
顺便说一句,如果您编写自己的通用方法并希望从比较的虚拟方面受益,则可以通过覆盖虚拟“Equals”并调用它来实现。 - Ladi
实际上,我已经覆盖了它。这就是为什么我最初认为我没有运算符重载(不是很喜欢)。看来我在这里找到更多不喜欢它们的理由。 - Léo Natan

1

正如你所知,operator ==(...) 可以有多个重载。其中一些可以是 C# 内置的重载,而其他的可以是用户定义的运算符。

如果你在 Visual Studio 中将鼠标悬停在 !=== 符号上,它会显示重载决议选择的是哪个重载(在 VS2013 中,只有当所选重载实际上是用户定义的重载时才会显示,在 VS2015 中,我认为它会在所有情况下都显示)。

==绑定(即调用哪个重载)在编译时静态固定。它没有任何动态或虚拟的东西。因此,如果你有:

public T SomeMethod<T>() where T : SomeBaseClass
{
  T rv = ...;

  if (rv != null)
  {

然后,使用哪个!=重载将在编译时通过常规重载解析(包括==的一些特殊规则)进行固定。 rv具有类型T,该类型已知为等于或派生自SomeBaseClass的引用类型。 因此,根据此选择最佳重载。 如果SomeBaseClass未定义(或“继承”)适当的重载,则可能是operator!=(object,object)重载(内置)。

因此,在运行时,即使实际替换T的类型恰好是更具体的类型SomeEqualityOverloadingClass(当然满足约束条件),也不意味着将在运行时发生新的重载解析!

这与virtual方法.Equals(object)不同。

在C#中,泛型不像模板那样工作,也不像dynamic那样。

如果您真的想要dynamic重载解析(在运行时而不是在编译时进行绑定),则允许说if ((dynamic)rv!= null)


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