IComparable, IEquatable and IDisposable
Three interfaces the framework looks for: implement them and sorting, equality and using blocks start working on your own types.
int CompareTo(T? other) | bool Equals(T? other) | void Dispose()Some interfaces are special because library code goes looking for them. Implement one and framework methods change behaviour on your type. Each one also binds you to a contract that nothing checks at compile time.
IComparable<T>: ordering
The interface is IComparable<in T> and its single member is int CompareTo(T? other). The angle brackets name the type you compare against; a later stage covers that notation. Return a negative number if this instance sorts before other, zero if they sort in the same place, and a positive number if it sorts after. Array.Sort and List<T>.Sort call it for you. The contract: the ordering must be consistent, so if A is before B and B is before C then A is before C.
IEquatable<T>: equality
IEquatable<T> declares bool Equals(T? other). List<T>.Contains, IndexOf and Remove use it, as does Dictionary<TKey, TValue>. The contract is the hard part: also override Equals(object?) and GetHashCode, and keep all three in agreement. Two equal values must return the same hash code.
IDisposable: clean-up
IDisposable declares void Dispose(), which releases a file handle, socket or similar. The using statement calls it for you, even if an error occurs. Its contract: a second call to Dispose must do nothing and must not throw.
Example
Release[] releases =
{
new Release(2, 1),
new Release(1, 9),
new Release(2, 0)
};
// Array.Sort finds IComparable<Release> and calls CompareTo.
Array.Sort(releases);
foreach (Release release in releases)
{
Console.WriteLine(release.Label);
}
// This call lands on Equals(Release?) from IEquatable<Release>.
Console.WriteLine(new Release(2, 0).Equals(new Release(2, 0)));
sealed class Release : IComparable<Release>, IEquatable<Release>
{
public Release(int major, int minor)
{
Major = major;
Minor = minor;
}
public int Major { get; }
public int Minor { get; }
public string Label => $"{Major}.{Minor}";
// Negative sorts first, zero sorts level, positive sorts later.
public int CompareTo(Release? other)
{
if (other is null)
{
return 1;
}
int byMajor = Major.CompareTo(other.Major);
return byMajor != 0 ? byMajor : Minor.CompareTo(other.Minor);
}
public bool Equals(Release? other) =>
other is not null && Major == other.Major && Minor == other.Minor;
// Kept in step with Equals(Release?), as the contract requires.
public override bool Equals(object? obj) => Equals(obj as Release);
public override int GetHashCode() => HashCode.Combine(Major, Minor);
}
// Output:
// 1.9
// 2.0
// 2.1
// TrueWhen to use it
- Sort a list of your own version or money objects with Array.Sort instead of a hand-written loop.
- Use a composite key type in a Dictionary and have lookups actually find it.
- Wrap a file or connection in a type that a using block closes even when the body throws.
More examples
IDisposable and the three ways to call Dispose
Any type holding a file, socket or database connection implements IDisposable. Seeing the try/finally that using expands into explains why Dispose still runs when the block throws, and the guard flag is what keeps a repeated call harmless.
// The using statement: Dispose runs when the block ends, error or not.
using (Session session = new Session("orders"))
{
session.Run("select 1");
}
// The using declaration, C# 8 shorthand. Dispose runs at the end of the
// enclosing scope instead of a block of its own:
// using Session session = new Session("orders");
//
// The longhand both forms compile into:
// Session session = new Session("orders");
// try
// {
// session.Run("select 1");
// }
// finally
// {
// session.Dispose();
// }
sealed class Session : IDisposable
{
private readonly string _name;
private bool _disposed;
public Session(string name)
{
_name = name;
Console.WriteLine($"opened {_name}");
}
public void Run(string command)
{
// Added in .NET 7. Throws ObjectDisposedException when the flag is set.
ObjectDisposedException.ThrowIf(_disposed, this);
Console.WriteLine($"{_name}: {command}");
}
public void Dispose()
{
if (_disposed)
{
return; // a second call must do nothing and must not throw
}
_disposed = true;
Console.WriteLine($"closed {_name}");
}
}
// Output:
// opened orders
// orders: select 1
// closed ordersEquality that a Dictionary can rely on
A dictionary uses the hash code to pick a bucket and Equals to confirm the match. Build both from the same properties whenever a type will be used as a key or searched for in a list, or lookups fail in ways that look like data loss.
var stock = new Dictionary<Sku, int>
{
[new Sku("LAMP", 2)] = 7
};
// Found because Equals and GetHashCode agree on what makes two Skus the same.
Console.WriteLine(stock[new Sku("LAMP", 2)]);
var basket = new List<Sku> { new Sku("LAMP", 2) };
Console.WriteLine(basket.Contains(new Sku("LAMP", 2)));
sealed class Sku : IEquatable<Sku>
{
public Sku(string code, int variant)
{
Code = code;
Variant = variant;
}
public string Code { get; }
public int Variant { get; }
public bool Equals(Sku? other) =>
other is not null && Code == other.Code && Variant == other.Variant;
public override bool Equals(object? obj) => Equals(obj as Sku);
// Same inputs as Equals, so equal objects always share a hash code.
public override int GetHashCode() => HashCode.Combine(Code, Variant);
}
// Output:
// 7
// TrueThe shorthand: let a record write the equality members
Records arrived in C# 9 and are the short way to get the hand-written equality from the previous example. Write the members yourself only when equality means something other than all properties matching, such as comparing on an id alone.
// A record implements IEquatable<T> and writes Equals, GetHashCode,
// == and != for you, from the properties you declared.
record Sku(string Code, int Variant);
Sku a = new Sku("LAMP", 2);
Sku b = new Sku("LAMP", 2);
Console.WriteLine(a.Equals(b));
Console.WriteLine(a == b);
// IEquatable<Sku> is implemented, so this cast compiles.
IEquatable<Sku> equatable = a;
Console.WriteLine(equatable.Equals(b));
// Output:
// True
// True
// True
Discussion