Не редко возникает необходимость добавить к типу данных, созданных вами, возможности для сравнения экземпляров друг с другом или с другими объектами. В C#, так уж повелось, это не самая простая операция, чреватая и ошибками и избыточным кодом. Связано это с тем, как мне кажется, что в различных источниках приведены самые разные "паттерны" реализации сравнения и люди, начитавшись (кто-то больше, кто-то меньше) иногда смешивают подходы в одном примере с подходами в другом да добавляют ещё и что-то от себя лично :) Таким образом "паттерны" размножаются, служа пищей для размышления и источником вдохновения для следующего поколения программистов, которые читают доставшийся им код, другие публикации и придумывают свои "паттерны".
Так же, свою руку к путанице, на первый взгляд, приложили и сами разработчики дотнета, описав "запутанные" правила, по которым должно быть реализовано сравнение (смотрите Notes to Implementers /* Даже адрес по ссылке заканчивается на "АК-47", даже на два :о) */). Но это только так кажется, на самом деле правила весьма просты и, наоборот, позволяют просто и безошибочно реализовать сравнения.
Большой вклад по привлечению к обозначенной проблеме широкой общественности внёс Чистяков Влад (VladD2) в своей статье "Багодром: Реализация операторов сравнения", наглядно продемонстрировав, на сколько неприятны могут быть последствия плохой реализации и предложив надёжный "паттерн" решения. Ниже я лишь где-то упрощу, а где-то усложню предложенный способ (обратите внимание, что предложенный в статье способ следует использовать только для reference-типов), а так же предложу способ, который следует применять с value-типами.
Тренироваться будем на следующих типах, к которым сначала добавим возможность сравнения на равенство, а затем на больше-меньше.
using System;
internal struct MyValue
{
public MyValue(int first, int? second) : this() {
First = first;
Second = second;
}
public int First { get; private set; }
public int? Second { get; private set; }
}
internal sealed class MyData
{
public MyData(string name, MyValue value) {
Name = name ?? String.Empty;
Value = value;
}
public string Name { get; private set; }
public MyValue Value { get; private set; }
}
Мы переопределим методы Equals(object) и GetHashCode(), добавим реализацию IEquatable<>, а так же переопределим операторы == и !=. Причём собственно реализация сравнения будет находиться лишь в двух методах: Object::GetHashCode() и IEquatable<T>::Equals(T) и всё остальные вызовы будут переадресованы туда и их реализация и будет "паттерном", не зависящим от логики сравнения.
internal struct MyValue : IEquatable<MyValue>
{
public MyValue(int first, int? second) : this() {
First = first;
Second = second;
}
public int First { get; private set; }
public int? Second { get; private set; }
public override bool Equals(object obj) {
return obj is MyValue && Equals((MyValue)obj);
}
public override int GetHashCode() {
return First.GetHashCode() ^ Second.GetHashCode();
}
#region IEquatable<MyValue> Members
public bool Equals(MyValue other) {
return First == other.First && Second == other.Second;
}
#endregion IEquatable<MyValue> Members
public static bool operator ==(MyValue left, MyValue right) {
return left.Equals(right);
}
public static bool operator !=(MyValue left, MyValue right) {
return !(left == right);
}
}
internal sealed class MyData : IEquatable<MyData>
{
public MyData(string name, MyValue value) {
Name = name ?? String.Empty;
Value = value;
}
public string Name { get; private set; }
public MyValue Value { get; private set; }
public override bool Equals(object obj) {
return Equals(obj as MyData);
}
public override int GetHashCode() {
return Name.GetHashCode() ^ Value.GetHashCode();
}
#region IEquatable<MyData> Members
public bool Equals(MyData other) {
return other != null
&& Name == other.Name
&& Value == other.Value;
}
#endregion IEquatable<MyData> Members
public static bool operator ==(MyData left, MyData right) {
return Equals(left, right);
}
public static bool operator !=(MyData left, MyData right) {
return !(left == right);
}
}
Разница между реализацией в значимом и ссылочном типах следующая:
- Проверка типа аргумента в значимом типе в реализации Object::Equals(object) "двойная" - сначала посредством is, а потом при приведении типа, но по-другому никак. С сылочным типом ситуация лучше - тут используется оператор as, который одновременно и проверяет тип и делает приведение типа. Причём, если тип аргумента не подходящий, as вернёт null, а эта ситуация рассматривается в следующем пункте.
- При реализации IEquatable<T>::Equals(T) в ссылочном типе всегда проверяйте аргумент на null и возвращайте false если проверка удалась, ибо "x.Equals(null) returns false" (Notes to Implementers).
- Реализация оператора "==" проста - в значимом типе мы просто вызываем экземплярный типизированный Equals, а в ссылочном - статический Object::Equals(object, object), который сначала проверит аргументы на null, а потом вызовет тот же самый экземплярный типизированный Equals (только если оба аргумента не пустые). Почему-то разработчики часто забывают об этом удобнейшем методе и самостоятельно выписывают велосипеды a-la
if(ReferenceEquals(left, null)) {или
return ReferenceEquals(right, null);
} else if(ReferenceEquals(right, null)) {
return false;
}//if
if((object)left == null) {что совершенно не требуется.
return (object)right == null;
} else if((object)right == null) {
return false;
}//if
return RefenrenceEquals(other, this) || other != null
&& Name == other.Name
&& Value == other.Value;
}
Для завершения раздела о реализации равенства скажу, что сравнение незапечатанных (обратили внимание, что класс MyData обозначен как sealed?) ссылочных типов достойно отдельной статьи.
Наконец, для упрощения написания этих шаблонных методов я использую сниппеты. Первый добавляет реализацию для значимого типа:
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Implements IEquatable<> interface for a value type</Title>
<Shortcut>eqstruct</Shortcut>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal Editable="false">
<ID>TypeName</ID>
<Function>ClassName()</Function>
</Literal>
<Literal Editable="false">
<ID>NotImplementedException</ID>
<Function>SimpleTypeName(global::System.NotImplementedException)</Function>
</Literal>
</Declarations>
<Code Language="CSharp">
<![CDATA[public override bool Equals(object obj) {
return obj is $TypeName$ && Equals(($TypeName$)obj);
}
public override int GetHashCode() {
throw new $NotImplementedException$();
}
#region IEquatable<$TypeName$> Members
public bool Equals($TypeName$ other) {
// Add you equals logic
//return $end$;
throw new $NotImplementedException$();
}
#endregion IEquatable<$TypeName$> Members
public static bool operator ==($TypeName$ left, $TypeName$ right) {
return left.Equals(right);
}
public static bool operator !=($TypeName$ left, $TypeName$ right) {
return !(left == right);
}]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
Второй - для ссылочного типа:
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>Implements IEquatable<> interface for a reference type</Title>
<Shortcut>eqclass</Shortcut>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal Editable="false">
<ID>TypeName</ID>
<Function>ClassName()</Function>
</Literal>
<Literal Editable="false">
<ID>NotImplementedException</ID>
<Function>SimpleTypeName(global::System.NotImplementedException)</Function>
</Literal>
</Declarations>
<Code Language="CSharp">
<![CDATA[public override bool Equals(object obj) {
return Equals(obj as $TypeName$);
}
public override int GetHashCode() {
throw new $NotImplementedException$();
}
#region IEquatable<$TypeName$> Members
public bool Equals($TypeName$ other) {
// Add you equals logic
//return other != null && $end$;
throw new $NotImplementedException$();
}
#endregion IEquatable<$TypeName$> Members
public static bool operator ==($TypeName$ left, $TypeName$ right) {
return Equals(left, right);
}
public static bool operator !=($TypeName$ left, $TypeName$ right) {
return !(left == right);
}]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
Реализация сравнения на больше-меньше будет заметно легче - в ней нет подводных камней, достаточно всё сделать аккуратно. Следующий пост постараюсь посвятить ей.
Комментариев нет:
Отправить комментарий