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

T

taumuon

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:


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

Cheers!
Gary
 
M

Marc Gravell

You have
T obj;
blah
result = ((IEquatable<Person>)member).Equals(obj);
but it doesn't know (at compile time) that obj is a Person, so how
can it know to call .Equals(Person obj).

If you add a cast it might work:
result = ((IEquatable<Person>)member).Equals((Person)obj);

Likewise a constraint on T : Person

Just thoughts; none tested.

Marc
 
T

taumuon

You have
T obj;
blah
result = ((IEquatable<Person>)member).Equals(obj);
but it doesn't know (at compile time) that obj is a Person, so how
can it know to call .Equals(Person obj).

If you add a cast it might work:
result = ((IEquatable<Person>)member).Equals((Person)obj);

Likewise a constraint on T : Person

Just thoughts; none tested.

Marc

Thanks for the reply.

Without casting any object everything works (the Equals(Person)
overload is called) - it doesn't know at compile time that obj is a
person, but at runtime the correct overload is correctly called as
inferred from the generic type.

What I'm wondering is, is why explicitly casting the member to Person
breaks this inferrence (of course I wouldn't cast anything to Person
in real life - that would nullify the point of me having a generic
class, I was just replicating the behaviour I saw in the watch window
whilst debugging a separate issue).

Cheers,
gary
 
M

Marc Gravell

it doesn't know at compile time that obj is a person,

It doesn't need to; it knows, however, that "member" is T, "obj" is T,
and that T : IEquatable<T>; hence "member.Equals(obj)" *can* (and
will) be used at compile-time to detect the Equals(T) option rather
than
Equals(obj). When you specified one (but not both) casts you broke
this relationship, and the only thing left was Equals(object).

For reference, this type of casting inside a generic essentially
defeats
the purpose of generics; likewise, it is recommended for
Equals(object)
to by functionally equivalent to Equals(T) for any supported T; for
simple
cases (only one T) you could just cast
bool override Equals(object obj) { return Equals((Person)obj);}
For more involved cases you may need to inspect obj to pick an
overload
manually.

Marc
 
T

taumuon

It doesn't need to; it knows, however, that "member" is T, "obj" is T,
and that T : IEquatable<T>; hence "member.Equals(obj)" *can* (and
will) be used at compile-time to detect the Equals(T) option rather
than
Equals(obj). When you specified one (but not both) casts you broke
this relationship, and the only thing left was Equals(object).

For reference, this type of casting inside a generic essentially
defeats
the purpose of generics; likewise, it is recommended for
Equals(object)
to by functionally equivalent to Equals(T) for any supported T; for
simple
cases (only one T) you could just cast
bool override Equals(object obj) { return Equals((Person)obj);}
For more involved cases you may need to inspect obj to pick an
overload
manually.

Marc

Hi Marc,

The penny's clicked with this issue now, I had a bit of a mental block
seeing that it was the cast that was forcing it to choose an overload
at compile time rather than at runtime. Thanks for spending your time
on this though!

About the casting defeating the point of generics - I agree! I'd never
put this in "real code" that's what I said as a note at the end of the
last post - I only put this in especially to replicate this issue (I
saw this behaviour originally in the watch window debugging a separate
issue, and wanted to replicate it). Similarly, I would have normally
implemented Equals(object) to return Equals(Person), but I thought I'd
keep them separate in this case to keep the console output cleaner.

Thanks again for your help!
Gary
 
M

Marc Gravell

No problem;
it was the cast that was forcing it to choose an overload
at compile time rather than at runtime

For regular code the overload is *always* decided at compile time;
otherwise you need to use reflection / dynamic invoke.

Marc
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top