【问题标题】:Comparison to null evaluates to true for both expr == null and expr != null对于 expr == null 和 expr != null,与 null 的比较结果为 true
【发布时间】:2016-06-01 08:02:04
【问题描述】:

我看到一些非常奇怪的东西,我无法解释。我猜是我不熟悉的 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 中正确评估相等性,其中rv 在这种情况下属于HistoryMessage 类型。

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; }

    //...
}

这是怎么回事?

【问题讨论】:

  • HistoryMessage 是否实现了相等运算符?
  • 甚至不在HistoryMessage的任何基类上?
  • 无论如何,你能告诉我们HistoryMessage吗?
  • 如果你反编译项目,你可以发现实际上是否存在运算符重载。尝试dotPeek 获取免费的反编译器。如果有一个基本的参考检查,比较应该由ceq指令实现,如果有操作符重载,它看起来像call SomeClass.op_Equality。你也可以在LINQPad 中发现它,我在那里尝试过。
  • 此行为的另一个原因可能是赛车条件或GetObject 中的错误导致连续调用返回不同的结果。将context.GetObject&lt;HistoryMessage&gt;(id) 缓存到局部变量,然后然后检查null 是否相等,看看问题是否仍然存在。

标签: c# .net null


【解决方案1】:
  • 正如您已经澄清的那样,== 运算符对您的类型失败,因为您的重载不正确。
  • 当转换为对象时,== 运算符可以正常工作,因为它使用的是 object's 实现的 ==,而不是 EquatableIdentifiableObject's
  • GetObject 方法中,运算符的计算正确,因为它不是==EquatableIdentifiableObject's 实现。在 C# 中,泛型是在运行时解析的(至少在此处相关的意义上)而不是在编译时解析。请注意,== 是静态的而不是虚拟的。所以T 类型是在运行时解析的,但是对== 的调用必须在编译时解析。在编译器解析== 时,它不会知道使用EquatableIdentifiableObject's 实现==。由于类型 T 有这个约束:where T : DBObject, new()DBObject's 实现(如果有的话)将被使用。如果DBObject 没有定义==,那么将使用第一个定义的基类(直到object)。

关于EquatableIdentifiableObject's 实现== 的更多cmets:

  • 你可以替换这部分:
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 这个名字有点误导人。更好的名称将是 o1o2 或类似的名称,以便在更平等的基础上处理两个操作数。

【讨论】:

  • 感谢您的解释。我猜操作员不是虚拟的,因为它们是静态的。知道为什么运算符被实现为静态的吗?是因为 null 边缘情况吗?
  • 顺便说一句,如果您编写自己的通用方法并希望从比较的虚拟方面受益,您可以通过覆盖虚拟 Equals 并调用它来实现。
  • 实际上,我确实将其覆盖了。这就是为什么我最初认为我没有运算符重载(不是一个大粉丝)。似乎在这里我找到了不喜欢它们的更多理由。
【解决方案2】:

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

如果您在 Visual Studio 中将鼠标悬停在 !=== 符号上,它将显示重载决议选择的重载(直到 VS2013,它只会在选择的重载实际上是用户时显示-定义一个,在 VS2015 中它会在我相信的所有情况下显示)。

==绑定(即调用哪个重载)在编译时静态修复。这不是动态或虚拟的。所以如果你有:

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

  if (rv != null)
  {

然后将在编译时通过通常的重载决议(包括== 的一些特殊规则)修复要使用的!= 的哪个重载。 rv 具有 T 类型,已知它是 SomeBaseClass 的引用类型或派生自 SomeBaseClass。因此,基于此选择最佳过载。如果SomeBaseClass 没有定义(或“继承”)适当的重载,那可能是operator !=(object, object) 重载(内置)。

在运行时,即使T 的实际替换恰好是更具体的类型SomeEqualityOverloadingClass(当然满足约束),这并不意味着新的重载决议将在运行时发生-时间!

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

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

如果你真的想要dynamic 重载解析(绑定 在运行时而不是在编译时),可以说if ((dynamic)rv != null)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-14
    • 1970-01-01
    相关资源
    最近更新 更多