简体   繁体   中英

How to determine if two generic type values are equal?

Update* I am so sorry... my sample code contained an error which resulted in a lot of answers I didn't understand. In stead of

Console.WriteLine("3. this.Equals   " + (go1.Equals(go2)));

I meant to write

Console.WriteLine("3. this.Equals   " + (go1.Equals(sb2)));

I'm trying to figure out how I can successfully determine if two generic type values are equal to each other. Based on Mark Byers' answer on this question I would think I can just use value.Equals() where value is a generic type. My actual problem is in a LinkedList implementation, but the problem can be shown with this simpler example.

class GenericOjbect<T> {
    public T Value { get; private set; }
    public GenericOjbect(T value) {
        Value = value;
    }
    public bool Equals(T value) {
        return (Value.Equals(value));
    }
}

Now I define an instance of GenericObject<StringBuilder> containing new StringBuilder("StackOverflow") . I would expect to get true if I call Equals(new StringBuilder("StackOverflow") on this GenericObject instance, but I get false .

A sample program showing this:

using System;
using System.Text;

class Program
{
    static void Main()
    {
        var sb1 = new StringBuilder("StackOverflow");
        var sb2 = new StringBuilder("StackOverflow");

        Console.WriteLine("StringBuilder compare");
        Console.WriteLine("1. ==            " + (sb1 == sb2));
        Console.WriteLine("2. Object.Equals " + (Object.Equals(sb1, sb2)));
        Console.WriteLine("3. this.Equals   " + (sb1.Equals(sb2)));

        var go1 = new GenericOjbect<StringBuilder>(sb1);
        var go2 = new GenericOjbect<StringBuilder>(sb2);

        Console.WriteLine("\nGenericObject compare");
        Console.WriteLine("1. ==            " + (go1 == go2));
        Console.WriteLine("2. Object.Equals " + (Object.Equals(go1, sb2)));
        Console.WriteLine("3. this.Equals   " + (go1.Equals(sb2)));
        Console.WriteLine("4. Value.Equals  " + (go1.Value.Equals(sb2.Value)));
    }
}

For the three methods of comparing two StringBuilder objects, only the StringBuilder.Equals instance method (the third line) returns true . This is what I expected. But when comparing the GenericObject objects, its Equals() method (the third line) returns false . Interestingly enough, the fourth compare method does return true . I'd think the third and fourth comparison are actually doing the same thing.

I would have expected true . Because in the Equals() method of the GenericObject class, both value and Value are of type T which in this case is a StringBuilder . Based on Mark Byers' answer in this question , I would've expected the Value.Equals() method to be using the StringBuilder's Equals() method. And as I've shown, the StringBuilder's Equal() method does return true .

I've even tried

public bool Equals(T value) {
    return EqualityComparer<T>.Default.Equals(Value, value);
}

but that also returns false.

So, two questions here:

  1. Why doesn't the code return true ?
  2. How could I implement the Equals method so it does return true ?

As suggested in Marc Gravell's answer , the problem is with StringBuilder Equals(object) implementation that is different to the one in Equals(StringBuilder) .

Then, you can ignore the problem because your code will work with any other coherently-implemented classes, or you can use dynamic to fix the problem (again as suggested by Mark Gravell).

But, given that you are not using C# 4 (so no dynamic), you can try in this way:

public bool Equals(T value)
{
   // uses Reflection to check if a Type-specific `Equals` exists...
   var specificEquals = typeof(T).GetMethod("Equals", new Type[] { typeof(T) });
   if (specificEquals != null &&
       specificEquals.ReturnType == typeof(bool))
   {
       return (bool)specificEquals.Invoke(this.Value, new object[]{value});
   }
   return this.Value.Equals(value);
}

Your code looks fine. The problem here is that StringBuilder has a confusing set of Equals that are contradictory. In particular, Equals(StringBuilder) disagrees with Equals(object), even when the object is a StringBuilder.

All that EqualityComparer<T> needs is a sane Equals(object) implementation. The interface ( IEquatable<T> ) is optional. Unfortunately StringBuilder doesn't have this (at least, by comparison to Equals(StringBuilder), which your third test is using).

But in general, the advice is: use EqualityComparer<T> ; this supports:

  • nullable-of-T with standard "lifted" rules
  • IEquatable-of-T
  • object.Equals

Line 3 with the generic object is not calling your custom written method. Instead, it is calling the base Object.Equals(object) . To call your custom method, you need to pass in a T not a GenericObject<T> . Something like: go1.Equals(go2.Value)

As Eric Lippert says in answer to this question - Overload resolution is performed at compile time.

If you take a look at StringBuilder 's implementation you will notice it overloads Equals and does not override it. This is basically the root of the problem as to why StringBuilder.Equals does not work as you expected in your example.

Take the following 2 classes as example. Overloader is analogous to StringBuilder in the example as it overloads Equals . Overrider is very similar except it it overrides Equals instead.

public class Overloader
{
  public string Str {get;private set;}
  public Overloader (string str) {Str = str;}

  public bool Equals( Overloader str )
  {
    return this.Str.Equals( str );
  }
}

public class Overrider
{
  public string Str {get;private set;}
  public Overrider (string str) {Str = str;}

  public override bool Equals( object obj )
  {
    if ( obj is Overrider )
    {
      return this.Str.Equals( (obj as Overrider).Str );
    }
    return base.Equals( obj );
  }
}

I have slightly modified your GenericObject<T> class in my example:

class GenericOjbect<T>
{
  public T Value {get;private set;}
  public GenericOjbect( T val ) {Value = val;}

  public bool Equals( T val )
  {
    return Value.Equals( val );
  }

  public override bool Equals( object obj )
  {
    if ( obj is T )
    {
      return this.Equals( ( T )obj );
    }
    if (obj != null && obj is GenericOjbect<T> )
    {
      return this.Equals( ( obj as GenericOjbect<T> ).Value );
    }
    return base.Equals( obj );
  }
}

In this sample program you will see that Overloader (or Stringbuilder for that matter) will return false. However, Overrider returns true.

class Program
{
  static void Main( string[] args )
  {
    var goOverloader1 = new GenericOjbect<Overloader>( new Overloader( "StackOverflow" ) );
    var goOverloader2 = new GenericOjbect<Overloader>( new Overloader( "StackOverflow" ) );

    var goOverrider1 = new GenericOjbect<Overrider>( new Overrider( "StackOverflow" ) );
    var goOverrider2 = new GenericOjbect<Overrider>( new Overrider( "StackOverflow" ) );

    Console.WriteLine( "Overrider  : {0}", goOverloader1.Equals( goOverloader2 ) ); //False
    Console.WriteLine( "Overloader : {0}", goOverrider1.Equals( goOverrider2 ) ); //True
  }
}

Referencing Eric Lippert again - Overload resolution is performed at compile time. Meaning that the compiler basically looks at your GenericObject<T>.Equals( T val ) like this:

public bool Equals( T val )
{
  return Value.Equals( (Object) val );
}

To anwser your question How to determine if two generic type values are equal? . There's two things you possibly could do.

  1. If you own all the objects that will be wrapped in GenericObject<T> ensure they all at least override Equals .
  2. You could perform some reflection magic in your GenericObject<T>.Equals(T val) to manually perform late binding.

You can either implement IEquatable<T> , or implement a comparer class that implements IEqualityComparer<T> .

Make sure that value you check for equality is immutable and is set only at initialization of the class.

Another consideration would be to implement IComparer<T> , when you implement this one, you don't have to worry about the hash-code, and thus, can be implemented for mutable types/fields as well.

Once you'll properly implement IEquatable<T> in your class, your questions will be solved.

Update: Calling return EqualityComparer<T>.Default.Equals(Value, value); would basically return same result since there is no IEqualityComparer<T> implemented...

Compare the output of typeof() first, so you make sure you are comparing the same type of objects, then write an Equals method on X class which takes another instance of X class, and compare all properties... once you find something different, return false, else keep going till you return true.

Cheers :)

To elaborate on Gideon's answer (please upvote his, not mine): the method you defined has signature

bool GenericOjbect<T>::Equals( T )

While your code is calling

bool GenericOjbect<T>::Equals( GenericOjbect<T> )

which is inherited (not overridden) from Object .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM