Object Equals - whats the basic logic for pure objects or reference types that don't override Equals?

后端 未结 3 625
广开言路
广开言路 2021-02-14 02:03

I got here after reading this and I didn\'t find a relevant answer - So please don\'t mark this as a duplicate until you read the whole question.

I\'ve been using a refl

相关标签:
3条回答
  • 2021-02-14 02:35

    MSDN's page on object.Equals(object) covers this in some detail. Specifically, the default implementation for reference types is reference equality. The table in the section "Notes for Inheritors" is the most direct.

    Reference equality; equivalent to calling Object.ReferenceEquals.

    MSDN's page on RuntimeHelpers.Equals(object,object) does say that Object.Equals(Object) is called in the case that its arguments are not reference equal and neither is null. This is demonstrably false; the behavior actually exhibited is that RuntimeHelpers.Equals(object,object) never calls Object.Equals(Object).

    For example, this LINQPad script:

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

    prints the output below:

    False

    False

    Bar.Equals() called

    False

    False

    Baz.Equals() called

    False

    False

    Qux.Equals() called

    True

    False

    So I cribbed a little from an answer Hans Passant gave about Math.Pow()...

    This is the relevant code from \clr\src\vm\ecall.cpp in 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()
    

    This is the code for the function in \clr\src\vm\comobject.cpp to which it is mapped:

    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
    

    I see the reference comparison, null checks, value type exclusion, type match check, and a bitwise equality comparison. I don't see how Object.Equals(Object) is ever called. I believe that the documentation for RuntimeHelpers.Equals(object,object) is simply incorrect.

    0 讨论(0)
  • 2021-02-14 02:35

    Object.Equals is virtual. Types override it to have different behaviour.

    The default implementation, as you note, calls to an MethodImplOptions.InternalCall method (ie. it is part of the .NET runtime's internals). This method performs reference equality by directly looking at the reference (essentially it does a C/C++ pointer comparison).

    There is no recursion.

    NB. The documentation for ReferenceHelper.Equals says:

    true if the o1 parameter is the same instance as the o2 parameter, or if both are null, or if o1.Equals(o2) returns true; otherwise, false.

    (Emphasis from the source.)

    But this would imply that a.Equals(b) where Object.ReferenceEquals(a, b) is false and neither are null, then Object.Equals(object) calls ReferenceHelper.Equals(object, object) calls Object.Equals(object), …. This seems to be a documentation error (runtime behaviour is not recursive for types not overriding Equals(object) and then called for different objects resulting in a false reference equality result).

    0 讨论(0)
  • 2021-02-14 02:40

    I think there is some confusion elsewhere on this page. Please notice that there's a difference between numbers 3 and 4!. Another point that is easily mistaken is that the base.Equals instance method (#1) calls RuntimeHelpers.Equals version, and not its own static method Object.ReferenceEquals.

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

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

      This is the instance method in the Object base class. As noted above, this avoids infinite recursion by calling the RuntimeHelpers version which cannot be overridden.

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

      Results in the simplest runtime code. Usually ends up inlined with a trivial CPU comparison of the handle values for the two reference types. Does not call user-defined Equals overrides and makes no attempt to equate non reference-types by any means. That is, no two value types, blittable primitives, enums, etc. will ever equate.

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

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

      Notice the extern keyword: there is no IL code; this jumps directly to a CLR-internal code. Also beware that this is a newslot static method, so you must qualify this with "R̲u̲n̲t̲i̲m̲e̲H̲e̲l̲p̲e̲r̲s̲.Equals" at any call site or you'll get the very different behavior of the instance method (#2) Object.Equals.

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

      (Not shown)
      Probably subject to JIT interception anyway. May end up at runtimecallablewrapper.cpp.

    There's much more to discuss here. A major factor is that a lot of the behavior is heavily influenced or intercepted by special JIT processing some of which might depend on whether an instance encountered at runtime can possibly be a value type, or whether the JIT can rule that out. I'm no expert on these matters either, so feel free to comment and/or correct. Let me know if there's interest in more detail on the JIT results, and I might be able to expand this a bit.

    0 讨论(0)
提交回复
热议问题