2012-03-14 3 views
12

в документации по VS2005 Guidelines for Overloading Equals() and Operator == (C# Programming Guide) государства в части не рекомендуетсяКогда необходимо переопределить класс .NET Equals()? Когда это не так?

Перекрытие оператор == в не неизменных типов.

В новой документации .NET Framework 4 Guidelines for Implementing Equals and the Equality Operator (==) опускает этот оператор, хотя одно сообщение в Содержимое Сообщества повторяет это утверждение и ссылается на устаревшую документацию.

кажется, что имеет смысл переопределить Equals(), по крайней мере для некоторых тривиальных изменяемых классов, таких как

public class ImaginaryNumber 
{ 
    public double RealPart { get; set; } 
    public double ImaginaryPart { get; set; } 
} 

В математике, два мнимых чисел, которые имеют ту же самую действительную часть и ту же мнимую часть являются фактически равным в момент времени, когда выполняется равенство. Неверно утверждать, что они не равны, что произойдет, если отдельные объекты с тем же RealPart и ImaginaryPart будут Equals() не переопределены.

С другой стороны, если переопределить Equals(), следует также переопределить GetHashCode(). Если ImaginaryNumber, который переопределяет Equals() и GetHashCode(), помещается в HashSet, а изменяемый экземпляр меняет его значение, этот объект больше не будет найден в HashSet.

Был ли MSDN некорректным, чтобы удалить руководство относительно не переопределения Equals() и operator== для неизменяемых типов?

Можно ли переопределить Equals() для изменяемых типов, где эквивалентность всех свойств «в реальном мире» означает, что сами объекты равны (как с ImaginaryNumber)?

Если разумно, то как лучше всего иметь дело с потенциальной изменчивостью, в то время как экземпляр объекта участвует в HashSet или что-то еще, что полагается на GetHashCode() не меняется?

UPDATE

Просто наткнулся на этот in MSDN

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

+4

Я не думаю, что это разумно для 'ImaginaryNumber' быть изменяемый тип. –

+2

В действительности, вы должны, вероятно, реализовать ImaginaryNumber как неизменяемый тип значения (struct). – Joe

+0

Действительно? Зачем? Все остальные номера изменяемы. –

ответ

11

Я понял, что хотел бы, чтобы Equals означал две разные вещи, в зависимости от контекста. После взвешивания входа здесь, а также here, я остановился на следующем для моей конкретной ситуации:

Я не перекрывая Equals() и GetHashCode(), а не сохранения общего, но ни в коем случае повсеместного соглашения, что Equals() означает тождественное равенство для классов, и что Equals() означает равенство значений для структур. Самым большим драйвером этого решения является поведение объектов в хешированных коллекциях (Dictionary<T,U>, HashSet<T>, ...), если я отклонился от этого соглашения.

Это решение оставило меня до сих пор отсутствует понятие ценности равенства (как discussed on MSDN)

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

Типичный случай, требующий концепции равенства ценности (или, как я называю это «эквивалентность»), находится в модульных тестах.

Учитывая

public class A 
{ 
    int P1 { get; set; } 
    int P2 { get; set; } 
} 

[TestMethod()] 
public void ATest() 
{ 
    A expected = new A() {42, 99}; 
    A actual = SomeMethodThatReturnsAnA(); 
    Assert.AreEqual(expected, actual); 
} 

тест потерпит неудачу, потому что Equals() тестирует равенство ссылок.

Конечно, единичный тест может быть изменен для проверки каждого свойства по отдельности, но это переводит концепцию эквивалентности из класса в тестовый код для класса.

Чтобы сохранить эти знания, инкапсулированный в классе, а также обеспечить последовательную основу для тестирования эквивалентности, я определил интерфейс, что мои объекты реализации

public interface IEquivalence<T> 
{ 
    bool IsEquivalentTo(T other); 
} 

реализация обычно следует этой схеме:

public bool IsEquivalentTo(A other) 
{ 
    if (object.ReferenceEquals(this, other)) return true; 

    if (other == null) return false; 

    bool baseEquivalent = base.IsEquivalentTo((SBase)other); 

    return (baseEquivalent && this.P1 == other.P1 && this.P2 == other.P2); 
} 

Конечно, если бы у меня было достаточно классов с достаточными свойствами, я мог бы написать помощник, который строит дерево выражений через отражение для реализации IsEquivalentTo().

Наконец, я реализовал метод расширения, который проверяет эквивалентность двух IEnumerable<T>:

static public bool IsEquivalentTo<T> 
    (this IEnumerable<T> first, IEnumerable<T> second) 

If T орудие IEquivalence<T> этот интерфейс используется, в противном случае используется Equals(), для сравнения элементов последовательности. Разрешение возврата Equals() позволяет ему работать, например. с ObservableCollection<string> в дополнение к моим бизнес-объектам.

Теперь утверждение в моем единичном тесте

Assert.IsTrue(expected.IsEquivalentTo(actual)); 
+0

это настолько ленивее, чем думать о генерации хэш-кода и беспокоиться о столкновениях и побочных эффектах и ​​бла-бла-бла. и это хорошо :) –

+1

Я хотел бы, чтобы .NET определил два набора тестов виртуального равенства (или включил параметр, указывающий, какой тип равенства следует протестировать). Наличие объекта инкапсулирует данные, удерживая ссылку на изменяемый тип экземпляра объекта, который никогда не будет подвергаться действиям, которые могут его мутировать, является очень распространенным шаблоном; объект, к которому относится эта ссылка, должен предоставить тест равенства, который имел бы смысл в этом сценарии. – supercat

+0

@supercat и EricJ Я согласен. Кажется, это недостающая функция в .Net. – ToolmakerSteve

11

MSDN от документации о не перегружать == для изменяемых типов неправильно. Существует абсолютно ничего неправильного для изменяемых типов для реализации семантики равенства. Теперь два элемента могут быть равны, даже если они будут меняться в будущем.

Опасности, связанные с изменяемыми типами и равенством, обычно обнаруживаются, когда они используются в качестве ключа в хеш-таблице или позволяют изменяемым членам участвовать в функции GetHashCode.

+0

Поскольку '==' фактически представляет два разных оператора в C#, обсуждается, действительно ли какой-либо тип класса должен перегружать его, поскольку не всегда может быть ясно, является ли он эталонным тестом равенства или вызовом перегрузки '=='.Я бы также предположил, что единственное понятие равенства, универсально применимое ко всем объектам, эквивалентно, и отдельные объекты класса могут быть эквивалентны, если нет средств, которыми они могут когда-либо отличаться. Хотя некоторые «изменчивые» типы отвечают этому критерию, вещи, которые могут быть независимо мутированы, не делают. – supercat

+0

Re «Два предмета могут быть теперь равны, даже если они будут меняться в будущем». Проблема в том, что если вы изменяете == для изменяемого типа, никто не может безопасно использовать объект этого типа в качестве словарного ключа. Это большая проблема. Его укусил меня, даже для тех типов, которые я написал себе, поэтому я должен знать лучше. Это серьезный недостаток в разработке Microsoft. Единственный безопасный ответ на mutables, как обсуждают EricJ и supercat в другом ответе, заключается в том, чтобы оставить Equals отдельно и определить свою собственную функцию равенства с другим именем. Раздражает, потому что теперь вы должны использовать другую функцию при сравнении ваших объектов. – ToolmakerSteve

0

Я не понимаю ваших опасений относительно GetHashCode относительно HashSet. GetHashCode просто возвращает число, которое помогает HashSet внутренне хранить и искать значения. Если хэш-код объекта изменяется, объект не удаляется с HashSet, он просто не будет сохранен в наиболее оптимальном положении.

EDIT

Благодаря @Erik J я вижу точку.

HashSet<T> представляет собой коллекцию исполнения и для достижения этой производительности он полностью полагается на GetHashCode, являющийся постоянным для жизни коллекции. Если вам нужна эта производительность, вам необходимо следовать этим правилам. Если вы не можете , то вам придется переключиться на что-то другое, как List<T>

+0

Если вы изменили хэш-код объекта после его добавления в HashSet, 'myHashSet.Contains (myMutatedObject)' возвращает false. Однако сам HashSet по-прежнему содержит объект (теперь мутированный). Таким образом, позволяя изменять значение хэша, разрывает контракт HashSet ('Contains' breaks). –

+0

Я вижу момент сейчас, спасибо! –

6

Отъезд Guidelines and rules for GetHashCode по Eric Lippert.

Правило: целое число, возвращенное GetHashCode никогда не должны изменяться, пока объект содержится в структуре данных, которая зависит от оставшегося хэш-кода стабильной

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

+1

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

Смежные вопросы