Если в вашем типе paramID нет переопределенных equals и GetHashCode, и это скорее класс, чем структура, то будет действовать значение равенства по умолчанию, и каждый paramID будет равен только самому себе.
Вы, вероятно, хотите что-то вроде:
public class ParamID : IEquatable<ParamID> // IEquatable makes this faster
{
private readonly string _first; //not necessary, but immutability of keys prevents other possible bugs
private readonly string _second;
public ParamID(string first, string second)
{
_first = first;
_second = second;
}
public bool Equals(ParamID other)
{
//change for case-insensitive, culture-aware, etc.
return other != null && _first == other._first && _second == other._second;
}
public override bool Equals(object other)
{
return Equals(other as ParamID);
}
public override int GetHashCode()
{
//change for case-insensitive, culture-aware, etc.
int fHash = _first.GetHashCode();
return ((fHash << 16) | (fHash >> 16)) ^ _second.GetHashCode();
}
}
Для запрошенного объяснения я собираюсь сделать другую версию ParamID, в которой сравнение строк выполняется без учета регистра и порядкового номера, а не на основе культуры (форма, которая подходит для некоторых машиночитаемых кодов (например, соответствующие ключевые слова) в нечувствительном к регистру компьютерном языке или нечувствительных к регистру идентификаторах, таких как языковые теги), но не для чего-то понятного человеку (например, он не поймет, что «SS» является нечувствительным к регистру соответствием «ß»). Эта версия также учитывает { «A», «B»} для соответствия {«B», «A»} - то есть не имеет значения, какой путь имеют строки. Делая другую версию с другими правилами, можно иметь возможность касаться некоторые из соображений дизайна, которые вступают в игру.
Давайте начнем с нашего класса, содержащего только два поля, которые являются его состоянием:
public class ParamID
{
private readonly string _first; //not necessary, but immutability of keys prevents other possible bugs
private readonly string _second;
public ParamID(string first, string second)
{
_first = first;
_second = second;
}
}
На данный момент, если мы сделаем следующее:
ParamID x = new ParamID("a", "b");
ParamID y = new ParamID("a", "b");
ParamID z = x;
bool a = x == y;//a is false
bool b = z == x;//b is true
Поскольку по умолчанию ссылочный тип равен только самому себе. Зачем? Ну, во-первых, иногда это именно то, чего мы хотим, а во-вторых, не всегда понятно, чего еще мы можем хотеть, если программист не определит, как работает равенство.
Также обратите внимание, что если бы ParamID был структурой, то он имел бы равенство, определенное так же, как вы хотели. Тем не менее, реализация будет довольно неэффективной, а также ошибочной, если она будет содержать десятичную дробь, поэтому в любом случае всегда полезно явно реализовать равенство.
Первое, что мы собираемся сделать, чтобы придать этому другому понятию равенства, это переопределить IEquatable<ParamID>
. Это не является строго необходимым (и не существовало до .NET 2.0), но:
- Это будет более эффективно во многих случаях использования, в том числе когда ключ к
Dictionary<TKey, TValue>
.
- Следующий шаг легко сделать с этим в качестве отправной точки.
Теперь есть четыре правила, которым мы должны следовать при реализации концепции равенства:
- Объект должен всегда быть равным самому себе.
- Если X == Y и X! = Z, то позже, если состояние ни одного из этих объектов не изменилось, X == Y и X! = Z по-прежнему.
- Если X == Y и Y == Z, то X == Z.
- Если X == Y и Y! = Z, то X! = Z.
В большинстве случаев вы в конечном итоге будете следовать всем этим правилам, даже не задумываясь об этом, вам просто нужно проверить их, если вы особенно странны и сообразительны в своей реализации. Правило 1 также может быть использовано для повышения производительности в некоторых случаях:
public class ParamID : IEquatable<ParamID>
{
private readonly string _first; //not necessary, but immutability of keys prevents other possible bugs
private readonly string _second;
public ParamID(string first, string second)
{
_first = first;
_second = second;
}
public bool Equals(ParamID other)
{
if(other == null)
return false;
if(ReferenceEquals(this, other))
return true;
if(string.Compare(_first, other._first, StringComparison.InvariantCultureIgnoreCase) == 0 && string.Compare(_second, other._second, StringComparison.InvariantCultureIgnoreCase) == 0)
return true;
return string.Compare(_first, other._second, StringComparison.InvariantCultureIgnoreCase) == 0 && string.Compare(_second, other._first, StringComparison.InvariantCultureIgnoreCase) == 0;
}
}
Первое, что мы сделали, это посмотрим, сравниваем ли мы равенство с нулем. Мы почти всегда хотим возвращать false в таких случаях (не всегда, но исключения очень, очень редки, и если вы точно не знаете, что имеете дело с таким исключением, вы почти наверняка нет), и, конечно, мы не хочу генерировать исключение NullReferenceException.
Следующее, что мы делаем, это проверяем, сравнивается ли объект с самим собой. Это чисто оптимизация. В этом случае, вероятно, это пустая трата времени, но это может быть очень полезно с более сложными тестами на равенство, поэтому здесь стоит указать на этот трюк. Это использует правило о том, что идентичность влечет за собой равенство, то есть любой объект равен самому себе (Айн Рэнд, казалось, думал, что это было как-то глубоко).
Наконец, рассмотрев эти два особых случая, мы получаем фактическое правило равенства. Как я уже говорил выше, мой пример считает два объекта равными, если они имеют одинаковые две строки, в любом порядке, для регистрозависимых порядковых сравнений без учета регистра, поэтому у меня есть немного кода, чтобы разобраться с этим.
(Обратите внимание, что порядок, в котором мы сравниваем составные части, может влиять на производительность. Не в этом случае, а с классом, который содержит как int, так и строку, мы сначала сравнили бы int, потому что это быстрее, и, следовательно, возможно,найдите ответ false
, прежде чем мы даже посмотрим на строки)
Теперь у нас есть хорошая основа для переопределения метода Equals
, определенного в object
:
public override bool Equals(object other)
{
return (other as ParamID);
}
Так как as
вернет ссылку ParamID
, если other
является ParamID
и пустым для чего-либо еще (включая, если нулевое было тем, что мы были переданы в первую очередь), и так как мы уже обрабатываем сравнение сnull, все готово.
Попробуйте скомпилировать на этом этапе, и вы получите предупреждение о том, что вы переопределили Equals
, но не GetHashCode
(то же самое верно, если вы сделали этонаоборот).
GetHashCode используется словарем (и другими хеш-коллекциями, такими как HashTable и HashSet), чтобы решить, где разместить ключ внутри.Он возьмет хеш-код, повторно хэширует его до меньшего значения таким образом, который является его бизнесом, и использует его для помещения объекта во внутреннее хранилище.
Из-за этого понятно, почему следующееэто плохая идея, если ParamID не доступен только для чтения во всех полях:
ParamID x = new ParamID("a", "b");
dict.Add(x, 33);
x.First = "c";//x will now likely never be found in dict because its hashcode doesn't match its position!
Это означает, что к хеш-кодам применяются следующие правила:
- Два объекта, считающиеся равными, должны иметь одинаковыехэш-код.(Это жесткое правило, у вас будут ошибки, если вы его сломаете).
- Хотя мы не можем гарантировать уникальность, чем больше будет распространено возвращаемых результатов, тем лучше.(Мягкое правило, у вас будет лучшая производительность, чем лучше у вас).
- (ну, 2½.) Хотя это и не строгое правило, если мы примем такой сложный подход к пункту 2 выше, что он будет длиться вечночтобы вернуть результат, эффект nett будет хуже, чем если бы у нас был хэш худшего качества.Поэтому мы хотим постараться быть достаточно быстрыми, если сможем.
Несмотря на последнее замечание, редко стоит отмечать результаты.Коллекции, основанные на хеше, обычно запоминают значение сами, поэтому это бесполезно делать в объекте.
Для первой реализации, поскольку наш подход к равенству зависел от подхода по умолчанию к равенству строк, мыможет использовать строки по умолчанию хэш-код.Для моей другой версии я буду использовать другой подход, который мы рассмотрим позже:
public override int GetHashCode()
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(_first) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(_second);
}
Давайте сравним это с первой версией.В обоих случаях мы получаем хэш-коды составных частей.Если бы значения были целыми числами, символами или байтами, мы бы работали с самими значениями, но здесь мы опираемся на работу, проделанную при реализации той же логики для этих частей.В первой версии мы используем GetHashCode
из string
, но поскольку «a» имеет хеш-код, отличный от «A», который здесь не работает, поэтому мы используем класс, который создает хеш-код, игнорирующий эту разницу.
Другое большое различие между ними состоит в том, что в первом случае мы смешиваем биты больше с ((fHash << 16) | (fHash >> 16))
.Причина этого состоит в том, чтобы избежать дублирования хэшей.Мы не можем создать идеальный хеш-код, где каждый отдельный объект имеет свое значение, потому что существует только 4294967296 возможных значений хеш-кода, но гораздо больше возможных значений для ParamID (включая ноль, который рассматривается как имеющий хэш-код 0).(Есть случаи, когда возможны хеши префектов, но они вызывают другие проблемы, чем здесь).Из-за этого несовершенства мы должны думать не только о том, какие ценности возможны, но какие вероятны.Как правило, сдвиг битов, как мы делали в первой версии, позволяет избежать общих значений, имеющих одинаковый хэш.Мы не хотим, чтобы {"A", "B"} хэшировали так же, как {"B", "A"}.
Это интересный эксперимент по созданию намеренно плохого GetHashCode, который всегда возвращает 0,это будет работать, но вместо того, чтобы быть близко к O (1), словари будут O (n), и плохой, поскольку O (n) идет на это!
Вторая версия не делает этого, потому что у нее разные правила, поэтому мы действительно хотим считать значения одинаковыми, но за то, что они переключаются как равные, и, следовательно, с тем же хеш-кодом.
Другая большая разница заключается в использовании StringComparer.OrdinalIgnoreCase
. Это экземпляр StringComparer
, который, помимо других интерфейсов, реализует IEqualityComparer<string>
и IEqualityComparer
. Об интерфейсах IEqualityComparer<T>
и IEqualityComparer
есть две интересные вещи.
Во-первых, все коллекции на основе хеша (например, словарь) используют их, просто если они не передадут экземпляр единицы в свой конструктор, они будут использовать DefaultEqualityComparer, который вызывает методы Equals и GetHashCode, которые мы описали выше.
Другое, это то, что он позволяет нам игнорировать Equals и GetHashCode, упомянутые выше, и предоставлять их из другого класса. У этого есть три преимущества:
Мы можем использовать их в случаях (строка - классический случай), где существует более одного вероятного определения «равно».
Мы можем игнорировать это автором класса и предоставить свое собственное.
Мы можем использовать их, чтобы избежать конкретной атаки. Эта атака основана на ситуации, когда вводимые вами данные будут хэшироваться кодом, на который вы атакуете. Вы выбираете ввод таким образом, чтобы преднамеренно предоставлять объекты, которые отличаются, но хэшируют одно и то же. Это означает, что плохая производительность, о которой мы говорили ранее и которую можно было избежать, поражает, и она может быть настолько плохой, что превращается в атаку отказа в обслуживании. Предоставляя различные реализации IEqualityComparer со случайными элементами для хеш-кода (но одинаковые для каждого экземпляра компаратора), мы можем каждый раз изменять алгоритм достаточно, чтобы предотвратить атаку. Использование для этого редко (это должно быть что-то, что будет основываться исключительно на хэше, основанном исключительно на внешнем входе, который достаточно велик для того, чтобы плохая производительность действительно повредила), но жизненно важен, когда он появляется.
Наконец-то. Если мы переопределим Equals, мы можем или не захотим переопределить == и! = Тоже. Может быть полезно, чтобы они ссылались только на идентичность (бывают моменты, когда это то, что нас больше всего волнует), но может быть полезно, чтобы они ссылались на другую семантику (`" abc "==" ab "+" c " пример переопределения).
В итоге:
Равенство ссылочных объектов по умолчанию - это тождество (равное только себе).
Равенство типов значений по умолчанию - это простое сравнение всех полей (но с низкой производительностью).
Мы можем изменить концепцию равенства для наших классов в любом случае, но это ДОЛЖНО включать как Equals, так и GetHashCode *
Мы можем переопределить это и дать другую концепцию равенства.
Словарь, HashSet, ConcurrentDictionary и т. Д. Все зависит от этого.
Хеш-коды представляют собой отображение всех значений объекта на 32-разрядное число.
Хеш-коды должны быть одинаковыми для объектов, которые мы считаем равными.
Хеш-коды должны хорошо распределяться.
* Между прочим, анонимные классы имеют простое сравнение, подобное сравнению с типами значений, но имеют более высокую производительность, что соответствует почти любому случаю, когда мы должны заботиться о хэш-коде анонимного типа.