Как определить, равны ли два общих значения типа?
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
.