【问题标题】:Object Equals - whats the basic logic for pure objects or reference types that don't override Equals?Object Equals - 纯对象或不覆盖 Equals 的引用类型的基本逻辑是什么?
【发布时间】:2015-01-14 15:28:26
【问题描述】:

我是在阅读 this 后来到这里的,但我没有找到相关的答案 - 所以请在阅读整个问题之前不要将其标记为重复。

我一直在使用反射器并查看了Object.Equals。我看到的是:

[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public virtual bool Equals(object obj)
{
    return RuntimeHelpers.Equals(this, obj);
}

而RuntimeHelpers.Equals 看起来像这样:

// System.Runtime.CompilerServices.RuntimeHelpers
/// <summary>Determines whether the specified <see cref="T:System.Object" /> instances are considered equal.</summary>
/// <returns>true if the <paramref name="o1" /> parameter is the same instance as the <paramref name="o2" /> parameter, or if both are null, or if o1.Equals(o2) returns true; otherwise, false.</returns>
/// <param name="o1">The first object to compare. </param>
/// <param name="o2">The second object to compare. </param>
[SecuritySafeCritical]
[MethodImpl(MethodImplOptions.InternalCall)]
public new static extern bool Equals(object o1, object o2);

现在我看不到RuntimeHelpers.Equals 的实现,但根据描述,如果两个对象不是同一个实例并且不为空,它将再次调用object.Equals 方法,我会进入一个循环(我说的是纯对象)。

当我说纯对象时,我的意思是这样的:

object pureObj1 = new object();
object pureObj2 = new object();
bool areEql = pureObj1.Equals(pureObj2);

根据文档,这应该调用 Object.Equals 并获得 recusive stackoverflow。我想可能是文档有误,这会检查基本对象的引用相等性 - 但我想确定一下。

底线:
通过Equals 调用比较两个纯对象(例如,不将字符串转换为对象)时 - 它如何确定它们是否相等? - 如果我不覆盖Equals 方法并且我在两个对象上调用Equals 会发生什么?
附:无论如何我可以看到RuntimeHelpers.Equals源代码吗?

【问题讨论】:

  • 顺便说一句,没有必要再使用反射器了,你可以使用referencesource.microsoft.com
  • 不幸的是,2.0 SSCLI 没有 RH.Equals 的“ecall”。这似乎是一个较新的结构。无法查看此方法的作用。可能,它会检查obj.GetType() == typeof(object) 是否会短路比较。
  • 我们只会确定何时发布 .NET CLR 源代码(似乎已确定)。或者,当 Hans Passant 注意到这个问题时。
  • RuntimeHelpers.Equals的描述从何而来?我没有从 Reflector 中得到它,也没有在 referencesource.microsoft.com/#mscorlib/system/runtime/… 中看到它,并且在对该方法使用智能时我没有得到它。
  • 有人向汉斯发送蝙蝠信号!

标签: c# .net


【解决方案1】:

MSDN's page on object.Equals(object) 详细介绍了这一点。具体来说,引用类型的默认实现是引用相等。 “继承者须知”一节中的表格是最直接的。

引用相等;相当于调用 Object.ReferenceEquals。

MSDN's page on RuntimeHelpers.Equals(object,object) 确实说 Object.Equals(Object) 在其参数不是引用相等且也不为 null 的情况下被调用。这显然是错误的;实际表现出的行为是 RuntimeHelpers.Equals(object,object) 从不调用 Object.Equals(Object)。

例如,这个 LINQPad 脚本:

void Main()
{
    object left = new Foo();
    object right = new Foo();
    left.Equals(right).Dump();
    RuntimeHelpers.Equals( left, right ).Dump();
    left = new Bar();
    right = new Bar();
    left.Equals(right).Dump();
    RuntimeHelpers.Equals( left, right ).Dump();
    left = new Baz();
    right = new Baz();
    left.Equals(right).Dump();
    RuntimeHelpers.Equals( left, right ).Dump();
    left = new Qux();
    right = new Qux();
    left.Equals(right).Dump();
    RuntimeHelpers.Equals( left, right ).Dump();
}

private class Foo {}

private class Bar {
    public override bool Equals(object obj) { 
        "Bar.Equals() called".Dump();
        return base.Equals(obj);
    }
}

private class Baz {
    public override bool Equals(object obj) { 
        "Baz.Equals() called".Dump();
        return RuntimeHelpers.Equals( this, obj );
    }
}

private class Qux {
    public override bool Equals(object obj) { 
        "Qux.Equals() called".Dump();
        return true;
    }
}

打印下面的输出:

错误

错误

Bar.Equals() 调用

错误

错误

Baz.Equals() 调用

错误

错误

Qux.Equals() 调用

是的

错误

所以我抄了一点an answer Hans Passant gave about Math.Pow()...

这是来自SSCLI2.0中\clr\src\vm\ecall.cpp的相关代码

FCFuncStart(gObjectFuncs)
    FCIntrinsic("GetType", ObjectNative::GetClass, CORINFO_INTRINSIC_Object_GetType)
    FCFuncElement("InternalGetHashCode", ObjectNative::GetHashCode)
    FCFuncElement("InternalEquals", ObjectNative::Equals)
    FCFuncElement("MemberwiseClone", ObjectNative::Clone)
FCFuncEnd()

这是 \clr\src\vm\comobject.cpp 中映射到的函数的代码:

FCIMPL2(FC_BOOL_RET, ObjectNative::Equals, Object *pThisRef, Object *pCompareRef)
{
    CONTRACTL
    {
        THROWS;
        DISABLED(GC_NOTRIGGER);
        INJECT_FAULT(FCThrow(kOutOfMemoryException););
        MODE_COOPERATIVE;
        SO_TOLERANT;          
    }
    CONTRACTL_END;
    
    if (pThisRef == pCompareRef)    
        FC_RETURN_BOOL(TRUE);

    // Since we are in FCALL, we must handle NULL specially.
    if (pThisRef == NULL || pCompareRef == NULL)
        FC_RETURN_BOOL(FALSE);

    MethodTable *pThisMT = pThisRef->GetMethodTable();

    // If it's not a value class, don't compare by value
    if (!pThisMT->IsValueClass())
        FC_RETURN_BOOL(FALSE);

    // Make sure they are the same type.
    if (pThisMT != pCompareRef->GetMethodTable())
        FC_RETURN_BOOL(FALSE);

    // Compare the contents (size - vtable - sink block index).
    BOOL ret = memcmp(
        (void *) (pThisRef+1), 
        (void *) (pCompareRef+1), 
        pThisRef->GetMethodTable()->GetBaseSize() - sizeof(Object) - sizeof(int)) == 0;

    FC_GC_POLL_RET();

    FC_RETURN_BOOL(ret);
}
FCIMPLEND

我看到了引用比较、空值检查、值类型排除、类型匹配检查和按位相等比较。我不知道Object.Equals(Object) 是如何被调用的。我相信RuntimeHelpers.Equals(object,object) 的文档是不正确的。

【讨论】:

  • 本文不涉及 RuntimeHelpers.Equals。
  • 问题是“纯对象或不覆盖 Equals 的引用类型的基本逻辑是什么?”。 RuntimeHelpers 是一个实现细节。
  • 我对这两个答案都投了反对票,因为就他们的立场而言,他们根本不回答这个问题。这是投反对票的主要情况。没有证据的琐碎猜测不会带来清晰。
  • 我对此表示赞同。我认为这涵盖了足够有用的基础。当 CLR 最终开源时,我们可以再看看。
  • 我赞成并接受了这个答案。非常感谢您的帮助。我们需要等待 CLR 开源来验证这一点——但这已经足够了。
【解决方案2】:

Object.Equals 是虚拟的。类型会覆盖它以具有不同的行为。

您注意到,默认实现调用MethodImplOptions.InternalCall 方法(即,它是.NET 运行时内部的一部分)。此方法通过直接查看引用来执行引用相等(本质上它是进行 C/C++ 指针比较)。

没有递归。

注意。 ReferenceHelper.Equals 的文档说:

true 如果 o1 参数与 o2 参数是同一实例,或者如果两者都是 null,或者如果 o1.Equals(o2) 返回 true; strong> 否则,false。

(强调来自源。)

但这意味着a.Equals(b) 其中Object.ReferenceEquals(a, b) 为假,null 也不是,然后Object.Equals(object) 调用ReferenceHelper.Equals(object, object) 调用Object.Equals(object),...。这似乎是一个文档错误(对于未覆盖 Equals(object) 的类型,运行时行为不是递归的,然后调用不同的对象导致 false 引用相等结果)。

【讨论】:

  • 我写了同样的答案,但 RH.Equals 的文档指出它确实调用了 Object.Equals,而后者显然调用了 RH.Equals。
  • 那么你是说关于它调用o1.Equals(o2)的评论基本上是错误的?
  • @usr 再次从ReferenceHelps.Equals 中读取该句子:注意它以“如果o1 参数与o2 参数的实例相同,则为true”开头:它检查引用相等性first i>.
  • @Chris:见我之前的评论。
  • 我认为@usr 是对的,代码可能会检查第一个参数是否为Object 类型,如果是,则不调用object.Equals,如果引用比较失败则返回false。跨度>
【解决方案3】:

我认为此页面的其他地方有些混乱。请注意数字 3 和 4 之间存在差异!。还有一点容易误会是base.Equals实例方法(#1)调用RuntimeHelpers.Equals版本,不是自己的静态方法Object.ReferenceEquals。

  1. virtual bool ((Object)​this​).Equals(Object)
    [link to source]

    [__DynamicallyInvokable]
    public virtual bool Equals(object obj) => RuntimeHelpers.Equals(this, obj);
    

    这是Object 基类中的实例方法。如上所述,这通过调用不能被覆盖的RuntimeHelpers 版本来避免无限递归。

  2. static bool Object.Equals(Object, Object)
    [link to source]

    public static bool Equals(Object objA, Object objB)
    {
       if (objA == objB)
           return true;
    
       if (objA == null || objB == null)
           return false;
    
       return objA.Equals(objB);
    }
    
  3. static bool Object.ReferenceEquals(Object, Object)
    [link to source]

    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    [NonVersionable, __DynamicallyInvokable]
    public static bool ReferenceEquals(Object objA, Object objB)
    {
        return objA == objB;
    }
    

    产生最简单的运行时代码。通常以对两种引用类型的句柄值进行简单的 CPU 比较来结束内联。不调用用户定义的Equals 覆盖并且不尝试以任何方式等同于非引用类型。也就是说,没有两个值类型、blittable 基元、枚举等永远等同。

  4. static bool RuntimeHelpers.Equals(Object, Object)
    [link to source]

    [MethodImpl(MethodImplOptions.InternalCall), SecuritySafeCritical]
    public new static extern bool Equals(object o1, object o2);
    

    注意extern关键字:没有IL代码;这直接跳转到 CLR 内部代码。另请注意,这是一个 newslot 静态方法,因此您必须在任何调用站点上使用“R̲u̲n̲t̲i̲m̲e̲H̲e̲l̲p̲e̲r̲s̲.Equals”对其进行限定,否则您将获得实例方法的非常不同的行为(#2 )Object.Equals.

  5. override bool ((ValueType)​this​).Equals(Object)
    [link to source]

    (未显示)
    无论如何都可能受到 JIT 拦截。最终可能是runtimecallablewrapper.cpp。

这里还有很多要讨论的。一个主要因素是许多行为受到特殊 JIT 处理的严重影响或拦截,其中一些可能取决于运行时遇到的实例是否可能是值类型,或者 JIT 是否可以排除这种情况。我也不是这些问题的专家,所以请随时发表评论和/或更正。如果有兴趣了解更多关于 JIT 结果的详细信息,请告诉我,我或许可以扩展一下。

【讨论】:

    猜你喜欢
    • 2016-07-12
    • 2013-07-05
    • 1970-01-01
    • 1970-01-01
    • 2012-06-22
    • 1970-01-01
    • 2011-09-25
    • 2017-02-05
    相关资源
    最近更新 更多