Что не так с определением оператора ==, но не с определением Equals() или GetHashCode()?

Для кода ниже

public struct Person
{
    public int ID;
    public static bool operator ==(Person a, Person b) { return  a.Equals(b); }
    public static bool operator !=(Person a, Person b) { return !a.Equals(b); }
}

Почему компилятор дает мне эти предупреждения? Что неправильно с тем, чтобы не определять методы ниже?

warning CS0660: 'Person' defines operator == or operator != but
    does not override Object.Equals(object o)

warning CS0661: 'Person' defines operator == or operator != but
    does not override Object.GetHashCode()

Ответ 1

EDIT. Этот ответ был исправлен, среди прочего, отметить, что пользовательские типы значений не генерируют ==, а также указывают на проблемы с производительностью ValueType.Equals.


В общем, переопределение одного, но не все, вызывает смущение. Пользователь не ожидает ни переопределения, ни того и другого, с той же семантикой.

Microsoft рекомендации для этого состояния (среди прочего):

  • Внедрите метод GetHashCode всякий раз, когда вы реализуете метод Equals. Это позволяет синхронизировать Equals и GetHashCode.

  • Переопределите метод Equals всякий раз, когда вы реализуете оператор равенства (==) и делаете их одинаковыми.

В вашем случае у вас есть веская причина отложить до Equals (компилятор не выполняет автоматически ==) и переопределяет только те два (==/!=). Однако проблема с производительностью по-прежнему сохраняется, поскольку ValueType.Equals использует отражение:

"Переопределить метод Equals для определенного типа для улучшения эффективности метода и более точно представляют собой концепцию равенство для типа."

Таким образом, по-прежнему рекомендуется переопределить все (==/!=/Equals) в конце. Разумеется, производительность может не иметь значения для этой тривиальной структуры.

Ответ 2

Мое предположение: вы получаете это предупреждение, потому что компилятор не знает, что вы используете метод Equals in ==

Предположим, что у вас есть эта реализация

public struct  Person
{
    public int ID;
    public static bool operator ==(Person a, Person b) { return Math.Abs(a.ID - b.ID) <= 5; }
    public static bool operator !=(Person a, Person b) { return Math.Abs(a.ID - b.ID) > 5; }
}

Тогда

 Person p1 = new Person() { ID = 1 };
 Person p2 = new Person() { ID = 4 };

 bool b1 = p1 == p2;
 bool b2 = p1.Equals(p2);

b1 будет true, но b2 false

- EDIT -

Теперь предположим, что вы хотите сделать это

Dictionary<Person, Person> dict = new Dictionary<Person, Person>();
dict.Add(p1, p1);
var x1 = dict[p2]; //Since p2 is supposed to be equal to p1 (according to `==`), this should return p1

Но это создаст исключение, подобное KeyNotFound

Но если вы добавите

public override bool Equals(object obj)
{
    return Math.Abs(ID - ((Person)obj).ID) <= 5; 
}
public override int GetHashCode()
{
    return 0;
}

вы получите то, что хотите.

Компилятор просто предупреждает вас, что вы можете столкнуться с аналогичными условиями

Ответ 3

В рамках Framework существует общее ожидание, что определенные операции всегда должны давать одинаковый результат. Причина в том, что определенные операции (в частности, сортировка и поиск, составляющие значительную часть любого приложения) зависят от этих разных операций, производящих значимые и последовательные результаты. В этом случае вы нарушаете пару таких предположений:

  • Если существует действительная операция == между a и b, она должна давать тот же результат, что и a.Equals(b)
  • Аналогично, если существует действительная операция != между a и b, она должна давать тот же результат, что и !a.Equals(b)
  • Если существуют два объекта a и b, для которых a == b, тогда a и b должны выдавать один и тот же ключ при сохранении в хэш-таблице.

Первые два, ИМО, очевидны; если вы определяете, что означает, что для двух объектов равны, вы должны включить все способы проверки того, чтобы два объекта были равны. Обратите внимание, что компилятор не выполняет (в общем, не может), что вы фактически следуете этим правилам. Он не собирается выполнять сложный анализ кода тела ваших операторов, чтобы убедиться, что они уже имитируют Equals, потому что в худшем случае это может быть эквивалентно решение проблемы прекращения.

Тем не менее, он может проверять случаи, когда вы, скорее всего, нарушаете эти правила, в частности, вы предоставляете пользовательские операторы сравнения и не предоставляете собственный метод Equals. Предположение здесь состоит в том, что вы бы не беспокоились о предоставлении операторов, если вы не хотели, чтобы они делали что-то особенное, и в этом случае вы должны были предоставить настраиваемое поведение для всех методов, которые должны быть синхронизированы.

Если вы реализовали Equals как нечто отличное от ==, компилятор не стал бы жаловаться; вы бы достигли предела того, насколько жестко С# хочет попытаться помешать вам сделать что-то глупое. Он был готов помешать вам случайно ввести тонкие ошибки в ваш код, но это позволит вам сделать это специально, если это то, что вы хотите.

Третье предположение связано с тем, что многие внутренние операции в Framework используют некоторый вариант хеш-таблицы. Если у меня есть два объекта, которые по моему определению "равны", тогда я должен это сделать:

if (a == b)
{
    var tbl = new HashTable();
    tbl.Add(a, "Test");

    var s = tbl[b];
    Debug.Assert(s.Equals("Test"));
}

Это основное свойство хэш-таблиц, которое может вызвать очень странные проблемы, если это внезапно не соответствует действительности.

Ответ 4

Все, что вам нужно сделать, это добавить другого члена в вашу структуру, скажем, имя файла.

Итак, если у вас есть два человека с идентификатором 63, но разные имена, равны или нет?

Все зависит от того, какое определение "того же" вы хотите реализовать.

Используйте лучший пример struct, напишите аппетитное приложение, чтобы выполнить различные методы, и посмотрите, что произойдет, когда вы измените определения равенства и/или эквивалентности, если они не все в шаге, вы получите такие вещи, как! ( a == b)!= (a!= b), что может быть правдой, но если вы не переопределите все методы, которые используют ваш код, задаются вопросом, каковы ваши намерения.

В основном компилятор говорит вам, чтобы быть хорошим гражданином и сделать ваше намерение понятным.

Ответ 5

Вероятно, потому что метод Equals() по умолчанию не будет достаточным для реальной системы (например, в вашем классе он должен сравнить поле ID).

Ответ 6

Если вы переопределяете Equals и GetHashCode, вам даже не нужно будет переопределять операторы и использовать более чистый подход. Отредактировано: он должен работать, поскольку это структура.

Ответ 7

Прочитайте страницы MSDN.

CS0660

CS0661

Компилятор в основном говорит: "Поскольку вы говорите, что знаете, как сравнивать свой объект, вы должны сделать так, чтобы он все время сравнивался".