问题 对象等于 - 什么是纯对象或不重写等于的引用类型的基本逻辑?


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

我一直在使用反射器并进行调查 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 但是通过描述,如果两个对象不是同一个实例而且不是null,它将调用 object.Equals 方法再次,我进入一个循环(我在谈论 纯物)。

当我说纯物体时我的意思是这样的:

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

通过文档,这应该调用 Object.Equals 得到一个 recusive stackoverflow。我想也许文档是错误的,这检查 参考平等 对于基本对象 - 但我想确定。

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


7773
2017-11-17 16:39


起源

顺便说一句,你不需要再使用反射器,你可以使用 referencesource.microsoft.com - dcastro
不幸的是,2.0 SSCLI没有RH.Equals的“ecall”。这似乎是一个较新的结构。无法看看这种方法的作用。可能,它会检查是否 obj.GetType() == typeof(object) 并将比较短路。 - usr
我们只会确定何时发布.NET CLR源代码(似乎是预定的)。或者,当汉斯帕斯特注意到这个问题时。 - usr
描述在哪里 RuntimeHelpers.Equals来自?我没有把它从Reflector中拿出来而且我没有看到它 referencesource.microsoft.com/#mscorlib/system/runtime/... 当在该方法上使用intellisence时我不明白。 - Chris
有人送汉斯蝙蝠信号! - dcastro


答案:


MSDN的页面上 object.Equals(object) 详细介绍了这一点。具体而言,引用类型的默认实现是引用相等。 “继承人备注”部分中的表格是最直接的。

参考平等;相当于调用Object.ReferenceEquals。

MSDN的页面上 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()调用

真正

所以我从中榨了一下 汉斯帕斯特给出的回答 Math.Pow()...

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

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) 简直不对。


9
2017-11-17 16:49



本文不涉及RuntimeHelpers.Equals。 - usr
问题是“对于不重写Equals的纯对象或引用类型的基本逻辑是什么?”。 RuntimeHelpers是一个实现细节。 - Jude M
我赞同这两个答案,因为他们的立场根本就没有回答这个问题。这是downvote的主要情况。没有证据的琐碎猜测没有带来清晰度。 - usr
我赞成这一点。我认为这涵盖了足够有用的基础。当CLR最终开源时,我们可以再看看。 - usr
我赞成并接受了这个答案。非常感谢您的帮助。我们需要等待CLR开源来验证这一点 - 但这已经足够了。 - Amir Popovich


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

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

没有递归。

NB。的文档 ReferenceHelper.Equals 说:

真正 如果o1参数与o2参数的实例相同,或两者都是 空值, 或者如果 o1.Equals(o2) 回报 真正; 除此以外,

(来自消息来源。)

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


4
2017-11-17 16:50



我写了相同的答案,但RH.Equals的文档声明它确实调用了Object.Equals,它明确地调用了RH.Equals。 - usr
所以你是说基本上这个评论是错误的 o1.Equals(o2)? - Chris
@usr阅读那句话 ReferenceHelps.Equals 再次:注意它以“如果o1参数与o2参数的实例相同,则为true”:它检查引用相等性 第一。 - Richard
@Chris:看我以前的评论。 - Richard
我认为@usr是对的,代码可能会检查第一个参数是否类型 Object 如果是的话,不打电话 object.Equals 如果引用比较失败,则返回false。 - dcastro


我认为这个页面的其他地方有一些混乱。请注意 数字3和4之间有区别!。另一个容易被误解的观点是 base.Equals 实例方法(#1)调用 RuntimeHelpers.Equals 版本,和  它自己的静态方法 Object.ReferenceEquals

  1. 虚拟布尔 ((目的)这个)。等于(目的)
    [链接到源]

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

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

  2. 静态布尔 的Object.Equals(对象,对象)
    [链接到源]

    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. 静态布尔 Object.ReferenceEquals(对象,对象)
    [链接到源]

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

    结果是最简单的运行时代码。通常最终内联两个引用类型的句柄值的简单CPU比较。不调用用户定义的 Equals 覆盖并且不试图以任何方式将非引用类型等同起来。也就是说,没有两种值类型,blittable原语,枚举等等等。

  4. 静态布尔 RuntimeHelpers.Equals(对象,对象)
    [链接到源]

    [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. 覆盖bool ((值类型)这个)。等于(目的)
    [链接到源]

    (未显示)
    无论如何,可能会受到JIT拦截。可能会结束 runtimecallablewrapper.cpp

这里还有更多要讨论的内容。一个主要因素是很多行为受到特殊JIT处理的严重影响或拦截,其中一些可能取决于在运行时遇到的实例是否可能是值类型,或者JIT是否可以将其排除在外。我也不是这些问题的专家,所以请随意评论和/或纠正。让我知道是否有关于JIT结果的更多细节的兴趣,我可能会稍微扩展一下。


0
2018-04-06 07:43