Equality might seem straightforward, but it tends to get fuzzier the longer you think about it. When it comes to programming, there are two general thoughts around equality.
The first is the idea of precise equality. Given two references to objects, do the two references point to the same location in memory? Programming languages are suited for this kind of comparison, and .NET comes with many prebuilt operators to handle this case.
The second form is logical equality. Anyone who has worked with databases understands the idea of identifiers and their use case for distinguishing sameness. Developers define reasons for equality in code, but the logic can be composed of smaller precise equality comparisons.
In this post, we’ll see how we can implement the IEqualityComparer
interface in C# and how it can help us define logical comparisons. Additionally, we’ll see how we can use our implementation in code.
The Player Class
We first need to define a class that needs comparison with another instance of the class. For this post, we’ll be using a Player
class with two properties.
public class Player
{
public DateTimeOffset LastLogin { get; set; }
= DateTimeOffset.UtcNow;
public string Username { get; set; }
}
Looking at our Player
class, we may start to craft a rule for our application.
All players are distinguished by their
Username
It seems like a good rule. Let’s see how we can implement the IEqualityComparer
to make sure we can work with collections of Player
instances.
The Setup
Let’s set up a simple collection with four Player
instances, but two unique players.
static void Main(string[] args)
{
var players = new[]
{
new Player {Username = "khalidabuhakmeh"},
new Player {Username = "nicoleabuhakmeh"},
new Player {Username = "khalidabuhakmeh"},
new Player {Username = "nicoleabuhakmeh"},
};
}
As we can see from the code sample, there are two unique players: khalidabuhakmeh
and nicoleabuhakmeh
. Our next step is to implement the IEqualityComparer
. Let’s first look at the interface itself.
namespace System.Collections.Generic
{
public interface IEqualityComparer<in T>
{
bool Equals([AllowNull] T x, [AllowNull] T y);
int GetHashCode([DisallowNull] T obj);
}
}
As we can see, to satisfy the interface, we need to implement two methods: Equals
and GetHashCode
. Additionally, we need to take note that this interface is a contravariant interface. Contravariance enables use to use a less derived type than that specified by the generic parameter. In our case, we could pass in an object
instance instead of a Player
instance.
private sealed class PlayerEqualityComparer : IEqualityComparer<Player>
{
public bool Equals(Player x, Player y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
if (x.GetType() != y.GetType()) return false;
return x.Username == y.Username;
}
public int GetHashCode(Player obj)
{
return (obj.Username != null ? obj.Username.GetHashCode() : 0);
}
}
Let’s walk through each line of the Equals
method:
- If it is the same instance, then it is the same object. Precise equality means logical equality.
- If either
Player
instance isnull
, then we cannot compare the two usernames. The two players cannot be equal. - Comparing types is optional and depends on your inheritance structure, but in this case, if the two players aren’t the same type, then they are not equal.
- Finally, we compare the
Username
for each player instance.
As a matter of style, we could encapsulate the equality comparer in our Player
class and expose the IEqualityComparer
through a static property.
public class Player
{
public DateTimeOffset LastLogin { get; set; } = DateTimeOffset.UtcNow;
public string Username { get; set; }
private sealed class PlayerEqualityComparer : IEqualityComparer<Player>
{
public bool Equals(Player x, Player y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
if (x.GetType() != y.GetType()) return false;
return x.Username == y.Username;
}
public int GetHashCode(Player obj)
{
return (obj.Username != null ? obj.Username.GetHashCode() : 0);
}
}
public static IEqualityComparer<Player> Comparer { get; } = new PlayerEqualityComparer();
}
Finally, we can use the equality comparer to find the distinct players in our collection.
class Program
{
static void Main(string[] args)
{
var players = new[]
{
new Player {Username = "khalidabuhakmeh"},
new Player {Username = "nicoleabuhakmeh"},
new Player {Username = "khalidabuhakmeh"},
new Player {Username = "nicoleabuhakmeh"},
};
// using our custom comparer
var unique = players
.Distinct(Player.Comparer)
.ToList();
Console.WriteLine("Unique Players");
unique.ForEach(p =>
{
Console.WriteLine($"- {p.Username} (last seen {p.LastLogin:d})");
});
}
}
We get the following result from running our application.
Unique Players
- khalidabuhakmeh (last seen 04/30/2020)
- nicoleabuhakmeh (last seen 04/30/2020)
Conclusion
When dealing with comparison, it is essential to understand the difference between precise comparison and logical comparison. Luckily for us, .NET has the IEqualityComparer
that is used by LINQ. As we saw above, it doesn’t take much code. As our model evolves, we’ll likely have to extend our comparers, so always thinking about what “equal” means now and what it could mean in the future is a valuable exercise.
I hope you found this post helpful, and please leave a comment.