locked
An IEquatable<T> object cast to IEquatable<explicittype> results in wrong Equals override called.

    Question

  • I've got an object, Person, that supports IEquatable<Person>. It implements
    bool Equals(Person obj)
    as well as overriding
    bool Equals(object obj)

    I've got a container type that holds a member object of generic type T, that supports IEquatable<T>, and a method, DoComparisons(T obj) to compare the member object to the object passed in.

    In the method, if I call:

    member.Equals(obj);

    Then the Equals(Person obj) overload is called,

    but if I cast the member to IEquatable<Person> and then call

    ((IEquatable<Person>)member).Equals(obj);

    Then the Equals(object obj) overload is called.

    My question is why? The type T should have been resolved to Person when I created my container, why does the first call result in the correct method being called on the interface, whereas when the object is cast to IEquatable<Person> it does not? Surely both should be doing the same thing?

    Here's the full code:





    Code Snippet

    using System;
    using System.Collections.Generic;
    using System.Text;

    namespace EquatableTest
    {
        public class Program
        {
            static void Main(string[] args)
            {
                Container<Person> container = new Container<Person>(new Person("Gary Evans"));
                Person person = new Person("Gary Evans");
                container.DoComparisons(person);
            }
        }

        public class Person : IEquatable<Person>
        {
            private string firstName;
            private string lastName;

            public Person(string fullName)
            {
                string[] names = fullName.Split(' ');
                this.firstName = names[0];
                this.lastName = names[names.Length - 1];
            }

            #region IEquatable<Person> Members

            public bool Equals(Person other)
            {
                Console.Write("Equals(Person other) called.");
                return ToString() == other.ToString();
            }

            #endregion

            public override string ToString()
            {
                return string.Concat(firstName, " ", lastName);
            }

            public override int GetHashCode()
            {
                return ToString().GetHashCode();
            }

            public override bool Equals(object obj)
            {
                Console.Write("Equals(object other) called.");
                Person person = obj as Person;
                return ((person != null) && (ToString() == person.ToString()));
            }
        }

        public class Container<T> where T : IEquatable<T>
        {
            private T member;
            public Container(T t)
            {
                member = t;
            }

            public void DoComparisons(T obj)
            {
                Console.Write("Calling member.Equals() cast as IEquatable<T>. ");
                bool result = member.Equals(obj);
                Console.WriteLine(" Result:" + result.ToString());

                Console.Write("Calling member.Equals() cast as IEquatable<Person>. ");
                result = ((IEquatable<Person>)member).Equals(obj);
                Console.WriteLine(" Result:" + result.ToString());
            }
        }
    }



    Thursday, April 19, 2007 1:39 PM

Answers

  • The problem, I believe, is that you are in the mindset that generics work like C++ templates where the actual code generation doesn't occur until you associate a type with the value.  But this is not the case.  For reference types the generated code uses object as the actual type for reference types.  All reference types will ultimately use the exact same method implementation.  The type-checking occurs at compile-time.  At runtime it is irrelevant.  Value types will each get their own implementation.  Therefore when the DoComparisons method is converted to a call it will map to

     

    ((IEquatable<Person>)member).Equals(object) 

     

    which will ultimately call

     

    IEquatable.Equals(object)

     

    and not

     

    ((IEquatable<Person>)member).Equals(person)

     

    This works because IEquatable<T> ulimately has a version that accepts an object value.  This might not be the exact technical behavior but it logically (to me) works that way.

     

    To get what you want you should instead use the following code

     

    ((IEquatable<T>)member).Equals(obj);

     

    This will map to a call to

     

    IEquatable<T>.Equals(T)

     

    which is what you want to happen.  Besides your code would ultimately probably fail at runtime if you passed anything other than Person as the type.

     

    Michael Taylor - 4/19/07

    http://p3net.mvps.org

     

    Thursday, April 19, 2007 6:45 PM
    Moderator
  • Typecasting is a compile-time thing.  So what you are doing is forcing the above code to do is treat member as type IEquatable<Person>.  Fine.  Now the compiler needs to match the Equals method to a signature.  It has two choices: Equals(Person) or Equals(object).  Which version can T obj map to?  Well Person only works if T were Person.  But object works in all cases.  Therefore the compiler choses (whether reasonable or not) the second overload at compile time.

     

    Now if you instead use IEquatable<T> as the cast then the compiler has the choice between Equals(T) and Equals(object).  Now which one?  Well since T works in all cases for obj then it is used because it is more strongly typed than object.

     

    Michael Taylor - 4/19/07

    http://p3net.mvps.org

     

    Thursday, April 19, 2007 10:30 PM
    Moderator

All replies

  • The problem, I believe, is that you are in the mindset that generics work like C++ templates where the actual code generation doesn't occur until you associate a type with the value.  But this is not the case.  For reference types the generated code uses object as the actual type for reference types.  All reference types will ultimately use the exact same method implementation.  The type-checking occurs at compile-time.  At runtime it is irrelevant.  Value types will each get their own implementation.  Therefore when the DoComparisons method is converted to a call it will map to

     

    ((IEquatable<Person>)member).Equals(object) 

     

    which will ultimately call

     

    IEquatable.Equals(object)

     

    and not

     

    ((IEquatable<Person>)member).Equals(person)

     

    This works because IEquatable<T> ulimately has a version that accepts an object value.  This might not be the exact technical behavior but it logically (to me) works that way.

     

    To get what you want you should instead use the following code

     

    ((IEquatable<T>)member).Equals(obj);

     

    This will map to a call to

     

    IEquatable<T>.Equals(T)

     

    which is what you want to happen.  Besides your code would ultimately probably fail at runtime if you passed anything other than Person as the type.

     

    Michael Taylor - 4/19/07

    http://p3net.mvps.org

     

    Thursday, April 19, 2007 6:45 PM
    Moderator

  • Thanks for the reply.

    I understand  how generics differ to C++, and understand that for reference types there is only one copy of the generic type. Where you say

    "Therefore when the DoComparisons method is converted to a call it will map to

     

    ((IEquatable<Person>)member).Equals(object) "


    but this isn't what happens. When I don't explicitly cast anything, the call is mapped to:


    ((IEquatable<T>)member).Equals(T rhs)


    And this is intuitive behaviour for me. The generic type has correctly inferred which method to call based on its generic type parameters. It's only where I explicitly cast the member to IEquatable<Person> that the Equals(object) is called.


    What my question is is why this explicit casts breaks/changes the ability for the correct overload to be inferred.

    The output of my program is:

    Calling member.Equals() cast as IEquatable<T>. Equals(Person other) called. Result: True
    Calling member.Equals() cast as IEquatable<Person>. Equals(object other) called. Result: True

    (Note that in real life casting to a Person is obviously a stupid thing to do, but I came across this as I was debugging something else, and just wondered why this behaviour is occuring).

    Hope you can shed some light on this!

    Cheers,
    Gary

    Thursday, April 19, 2007 8:10 PM
  • Typecasting is a compile-time thing.  So what you are doing is forcing the above code to do is treat member as type IEquatable<Person>.  Fine.  Now the compiler needs to match the Equals method to a signature.  It has two choices: Equals(Person) or Equals(object).  Which version can T obj map to?  Well Person only works if T were Person.  But object works in all cases.  Therefore the compiler choses (whether reasonable or not) the second overload at compile time.

     

    Now if you instead use IEquatable<T> as the cast then the compiler has the choice between Equals(T) and Equals(object).  Now which one?  Well since T works in all cases for obj then it is used because it is more strongly typed than object.

     

    Michael Taylor - 4/19/07

    http://p3net.mvps.org

     

    Thursday, April 19, 2007 10:30 PM
    Moderator
  • The penny's finally dropped for me on this, and I understand what's going on now thanks to your explanation.

    I think the bit I missed was that the cast is compile time, and so it's forcing the choice of overload too early. Your explanation's made this really clear now!

    Thanks for your time with this,
    Gary
    Friday, April 20, 2007 8:28 AM