Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c#

.net

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 reflector and looked into Object.Equals.What I saw is:

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

And RuntimeHelpers.Equals looks like this:

// 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);

Now I can't see the implementation of RuntimeHelpers.Equals but by the description, if both objects aren't the same instance and aren't null it will call the object.Equals method again and I'd get into a loop (I'm talking about pure objects).

When I say pure objects I mean something like this:

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

By documentation this should call Object.Equals and get a recusive stackoverflow. I guess maybe the documentation is wrong and this checks reference equality for basic objects - but I wanted to be sure.

Bottom line:
When comparing two pure objects(e.g. not casting a string into on object) via an Equals call - how does it determine if they are equal? - What happens if I don't override the Equals method and I call Equals on two objects?
P.s. is there anyway that I can see the RuntimeHelpers.Equals source code?

like image 249
Amir Popovich Avatar asked Nov 17 '14 16:11

Amir Popovich


People also ask

Does Object have an Equals method?

The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y , this method returns true if and only if x and y refer to the same object ( x == y has the value true ).

Do all objects have an Equals () method?

Every Object in Java includes an equals() and a hashcode() method, but they must be overridden to work properly. To understand how overriding works with equals() and hashcode() , we can study their implementation in the core Java classes. Below is the equals() method in the Object class.

What is the reason for overriding Equals () method?

We can override the equals method in our class to check whether two objects have same data or not.

Can we override the Equals method in Java?

You can override the equals method on a record, if you want a behavior other than the default. But if you do override equals , be sure to override hashCode for consistent logic, as you would for a conventional Java class.


2 Answers

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.

like image 144
Jude Melancon Avatar answered Sep 25 '22 02:09

Jude Melancon


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).

like image 38
Richard Avatar answered Sep 25 '22 02:09

Richard