Plugging in Equality and Order
A type’s default equating or comparison implementation typically reflects what is
most “natural” for that type. Sometimes, however, the default behavior is not what
you want. You might need a dictionary whose string-type key is treated case-
insensitively. Or you might want a sorted list of customers, sorted by each customer’s
postcode. For this reason, the .NET Framework also defines a matching set of “plug-
in” protocols. The plug-in protocols achieve two things:
• They allow you to switch in alternative equating or comparison behavior.
• They allow you to use a dictionary or sorted collection with a key type that’s
not intrinsically equatable or comparable.
The plug-in protocols consist of the following interfaces:
IEqualityComparer and IEqualityComparer<T>
• Performs plug-in equality comparison and hashing
• Recognized by Hashtable and Dictionary
IComparer and IComparer<T>
• Performs plug-in order comparison
• Recognized by the sorted dictionaries and collections; also, Array.Sort
Each interface comes in generic and nongeneric forms. The IEqualityComparer in-
terfaces also have a default implementation in a class called EqualityComparer.
In addition, Framework 4.0 adds two new interfaces called IStructuralEquatable
and IStructuralComparable, which allow the option of structural comparisons on
classes and arrays.
IEqualityComparer and EqualityComparer
An equality comparer switches in nondefault equality and hashing behavior, pri-
marily for the Dictionary and Hashtable classes.
Recall the requirements of a hashtable-based dictionary. It needs answers to two
questions for any given key:
• Is it the same as another?
• What is its integer hash code?
An equality comparer answers these questions by implementing the
IEqualityComparer interfaces:
public interface IEqualityComparer<T>
{
bool Equals (T x, T y);
int GetHashCode (T obj);
}
public interface IEqualityComparer // Nongeneric version
{
bool Equals (object x, object y);
int GetHashCode (object obj);
}
To write a custom comparer, you implement one or both of these interfaces (im-
plementing both gives maximum interoperability). As this is somewhat tedious, an
alternative is to subclass the abstract EqualityComparer class, defined as follows:
public abstract class EqualityComparer<T> : IEqualityComparer,
IEqualityComparer<T>
{
public abstract bool Equals (T x, T y);
public abstract int GetHashCode (T obj);
bool IEqualityComparer.Equals (object x, object y);
int IEqualityComparer.GetHashCode (object obj);
public static EqualityComparer<T> Default { get; }
}
EqualityComparer implements both interfaces; your job is simply to override the two
abstract methods.
The semantics for Equals and GetHashCode follow the same rules for object.Equals
and object.GetHashCode, In the following example, we define
a Customer class with two fields, and then write an equality comparer that matches
both the first and last names:
public class Customer
{
public string LastName;
public string FirstName;
public Customer (string last, string first)
{
LastName = last;
FirstName = first;
}
}
public class LastFirstEqComparer : EqualityComparer <Customer>
{
public override bool Equals (Customer x, Customer y)
{
return x.LastName == y.LastName && x.FirstName == y.FirstName;
}
public override int GetHashCode (Customer obj)
{
return (obj.LastName + ";" + obj.FirstName).GetHashCode();
}
}
To illustrate how this works, we’ll create two customers:
Customer c1 = new Customer ("Bloggs", "Joe");
Customer c2 = new Customer ("Bloggs", "Joe");
Because we haven’t overridden object.Equals, normal reference type equality se-
mantics apply:
Console.WriteLine (c1 == c2); // False
Console.WriteLine (c1.Equals (c2)); // False
The same default equality semantics apply when using these customers in a
Dictionary without specifying an equality comparer:
var d = new Dictionary<Customer, string>();
d [c1] = "Joe";
Console.WriteLine (d.ContainsKey (c2)); // False
Now with the custom equality comparer:
var eqComparer = new LastFirstEqComparer();
var d = new Dictionary<Customer, string> (eqComparer);
d [c1] = "Joe";
Console.WriteLine (d.ContainsKey (c2)); // True
In this example, we would have to be careful not to change the customer’s
FirstName or LastName while it was in use in the dictionary. Otherwise, its hash code
would change and the Dictionary would break.
EqualityComparer<T>.Default
Calling EqualityComparer<T>.Default returns a general-purpose equality comparer
that can be used as an alternative to the static object.Equals method. The advantage
is that first checks if T implements IEquatable<T> and if so, calls that implementation
instead, avoiding the boxing overhead. This is particularly useful in generic methods:
static bool Foo<T> (T x, T y)
{
bool same = EqualityComparer<T>.Default.Equals (x, y);
...
No comments:
Post a Comment