Как определить, равны ли два общих значения типа?

Update * Мне очень жаль... мой пример кода содержал ошибку, которая привела к множеству ответов, которые я не понял. Вместо

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

Я хотел написать

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

Я пытаюсь выяснить, как я могу успешно определить, являются ли два общих значения типа равными друг другу. Основываясь на ответе Марка Байера на этом вопросе, я бы подумал, что могу просто использовать value.Equals(), где значение - это общий тип. Моя фактическая проблема заключается в реализации LinkedList, но проблема может быть показана с помощью этого более простого примера.

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

Теперь я определяю экземпляр GenericObject<StringBuilder>, содержащий new StringBuilder("StackOverflow"). Я ожидал бы получить true, если я вызову Equals(new StringBuilder("StackOverflow") в этом экземпляре GenericObject, но получаю false.

Пример программы, показывающей это:

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

Для трех методов сравнения двух объектов StringBuilder только экземпляр StringBuilder.Equals(третья строка) возвращает true. Это то, чего я ожидал. Но при сравнении объектов GenericObject его метод Equals() (третья строка) возвращает false. Интересно, что четвертый метод сравнения возвращает true. Я думаю, что третье и четвертое сравнение действительно делают то же самое.

Я бы ожидал true. Поскольку в методе Equals() класса GenericObject оба value и value имеют тип T, который в этом случае является StringBuilder. Основываясь на ответе Марка Байера в на этом вопросе, я бы ожидал, что метод value.Equals() будет использовать метод StringBuilder Equals(). И, как я показал, метод StringBuilder Equal() возвращает true.

Я даже пробовал

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

но также возвращает false.

Итак, здесь есть два вопроса:

  • Почему код не возвращается true?
  • Как я мог реализовать метод Equals, чтобы он возвращал true?

Ответ 1

Как было предложено в ответе Марка Гравелла, проблема заключается в реализации StringBuilder Equals(object), которая отличается от версии в Equals(StringBuilder).

Затем вы можете игнорировать проблему, потому что ваш код будет работать с любыми другими когерентно реализованными классами, или вы можете использовать динамическое решение проблемы (опять же, как это предложил Марк Гравелл).

Но, учитывая, что вы не используете С# 4 (так что нет динамического), вы можете попробовать таким образом:

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

Ответ 2

Ваш код выглядит отлично. Проблема здесь в том, что StringBuilder имеет запутанное множество Equals, которые противоречат друг другу. В частности, Equals (StringBuilder) не согласуется с Equals (object), даже если объект StringBuilder.

Все, что требуется EqualityComparer<T> , - это правильная реализация Equals (object). Интерфейс (IEquatable<T>) является необязательным. К сожалению, StringBuilder не имеет этого (по крайней мере, по сравнению с Equals (StringBuilder), который использует ваш третий тест).

Но в целом совет: use EqualityComparer<T>; это поддерживает:

  • nullable-of-T со стандартными "отмененными" правилами.
  • IEquatable-оф-Т
  • Object.equals

Ответ 3

Строка 3 с общим объектом не вызывает ваш собственный письменный метод. Вместо этого он вызывает базу Object.Equals(object). Чтобы вызвать свой собственный метод, вам необходимо передать T не a GenericObject<T>. Что-то вроде: go1.Equals(go2.Value)

Ответ 4

Как говорит Эрик Липперт в ответ на это question - Разрешение перегрузки выполняется во время компиляции.

Если вы посмотрите на реализацию StringBuilder, вы заметите, что она перегружает Equals и не отменяет ее. Это в основном корень проблемы, почему StringBuilder.Equals не работает так, как вы ожидали в вашем примере.

В качестве примера возьмем следующие 2 класса. Overloader в примере аналогичен StringBuilder, поскольку он перегружает Equals. Overrider очень похож, за исключением того, что вместо этого он переопределяет Equals.

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

Я немного изменил ваш класс GenericObject<T> в моем примере:

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

В этой примерной программе вы увидите, что Overloader (или StringBuilder, если на то пошло) вернет false. Однако Overrider возвращает 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
  }
}

Ссылка на Eric Lippert снова - Разрешение перегрузки выполняется во время компиляции. Это означает, что компилятор в основном смотрит на ваш GenericObject<T>.Equals( T val ) следующим образом:

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

Чтобы ответить на ваш вопрос Как определить, равны ли два общих значения типа?. Там вы можете сделать две вещи.

  • Если вы владеете всеми объектами, которые будут обернуты в GenericObject<T>, убедитесь, что они, по крайней мере, переопределяют Equals.
  • Вы можете выполнить некоторую магию отражения в своем GenericObject<T>.Equals(T val) для ручного выполнения позднего связывания.

Ответ 5

Вы можете либо реализовать IEquatable<T>, либо реализовать класс сравнения, который реализует IEqualityComparer<T>.

Убедитесь, что value, который вы проверяете на равенство, является неизменным и устанавливается только при инициализации класса.

Еще одно соображение - реализовать IComparer<T>, когда вы реализуете это, вам не нужно беспокоиться о хеш-коде, и, следовательно, может быть реализовано также для изменяемых типов/полей.

Как только вы правильно выполните IEquatable<T> в вашем классе, ваши вопросы будут решены.

Обновление: Вызов return EqualityComparer<T>.Default.Equals(Value, value); будет в основном возвращать тот же результат, поскольку не реализовано IEqualityComparer<T>...

Ответ 6

Сначала сравните вывод типаof(), поэтому вы убедитесь, что вы сравниваете объекты одного и того же типа, затем пишите метод Equals в классе X, который принимает другой экземпляр класса X и сравнивает все свойства... как только вы найти что-то другое, вернуть false, иначе продолжайте идти, пока не вернетесь.

Приветствия:)

Ответ 7

Разрабатывать ответ Гидеона (пожалуйста, повысьте его, а не мой): метод, который вы определили, имеет подпись

bool GenericOjbect<T>::Equals( T )

Пока ваш код вызывает

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

который наследуется (не переопределяется) из Object.