Implementing IEquatable and IComparable
The Path of Discovery Recently, I created a ResearchItem class for my game, and I realized I wanted to sort them in a specific, non-alphabetical way. In the past when […]
What's a game dev that doesn't make the gaming world a better place?
The Path of Discovery Recently, I created a ResearchItem class for my game, and I realized I wanted to sort them in a specific, non-alphabetical way. In the past when […]
Recently, I created a ResearchItem
class for my game, and I realized I wanted to sort them in a specific, non-alphabetical way. In the past when I wanted to control the sorting process I created an int
SortOrder
field. That I easily controlled how my items were sorted. There are several ways to accomplish this and I settled on implementing IEquatable<T>
and IComparable<T>
.
I started by researching List<T>.Sort()
which led me to IComparable<T>
which led me to IEquatable<T>
. The IEquatable<T>
docs page from Microsoft stated: “The IEquatable<T>
interface is used by generic collection objects such as Dictionary<TKey, TValue>
, List<T>
, and LinkedList<T>
when testing for equality in such methods as Contains
, IndexOf
, LastIndexOf
, and Remove
. It should be implemented for any object that might be stored in a generic collection.“
I knew a List<ResearchItem>
was in my future so this just made sense. Now for full disclosure, I had lots of cobwebs to clear away as it’s been a few year since I last implemented IEquatable<T>
or IComparable<T>
. Rather than bore you with more details of my research, I’ll cut to the “‘X’ marks the spot” moment.
If you’re interested in what I used for sources you can find a list at the end of this post.
For IEquatable<T>
, I needed to implement Equals(T other)
, Equals(object obj)
, GetHashCode()
, and op_Equality
(‘==’), and op_Inequality
(‘!=’).
For IComparable<T>
, I needed to implement CompareTo(T other)
, op_GreaterThan
(‘>’), op_LessThan
(‘<‘), op_GreaterThanOrEquals
, (‘>=’), op_LessThanOrEquals
(‘<=’).
Luckily, these all fed off each other and several checks refer back to Equals(T other)
, thereby saving me time and effort.
Edit (17 Apr 2020): In the public bool Equals(Researchitem other)
method I changed if (other == null) return false;
to if (other is null) return false;
. It turns out that using the ==
operator will (of course) invoke the overriden operator. By using is
the code works as intended when comparing against null
objects.
public class ResearchItem : IEquatable<ResearchItem>, IComparable<ResearchItem> { #region Ctors public ResearchItem(string name, string description, float cost, ExplorationZoneType zoneType, int sortOrder) { Name = name; Description = description; Cost = cost; Zone = zoneType; SortOrder = sortOrder; } #endregion Ctors #region Properties public string Name { get; set; } public string Description { get; set; } public float Cost { get; set; } public ExplorationZoneType Zone { get; set; } public int SortOrder { get; set; } #endregion Properties #region IEquatable<T> Implementation public bool Equals(ResearchItem other) { if (other is null) return false; // ReferenceEquals ensures identity equality // Use ReferenceEquals ONLY for reference types // Don't use for value types and only sometimes for strings // sources 6, 8 if (ReferenceEquals(this, other)) return true; return string.Equals(Name, other.Name) && string.Equals(Description, other.Description) && Cost == other.Cost && Zone == other.Zone && SortOrder == other.SortOrder; } // The 'as' keyword returns null if not convertable (source 5) public override bool Equals(object obj) => Equals(obj as ResearchItem); public override int GetHashCode() { // Use `unchecked` so if results overflows it is truncated // source 7 unchecked { // Computing hashCode from source 4 var hashCode = 13; hashCode = ComputeHash(hashCode, (int)Cost); hashCode = ComputeHash(hashCode, Name?.GetHashCode() ?? 0); hashCode = ComputeHash(hashCode, Description?.GetHashCode() ?? 0); hashCode = ComputeHash(hashCode, (int)Zone); hashCode = ComputeHash(hashCode, SortOrder); return hashCode; } } // == and != from source 1 public static bool operator ==(ResearchItem x, ResearchItem y) => x.Equals(y); public static bool operator !=(ResearchItem x, ResearchItem y) => !x.Equals(y); public int ComputeHash(int currentHash, int value) => (currentHash * 397) ^ value; #endregion IEquatable<T> Implementation #region IComparable<T> Implementation public int CompareTo(ResearchItem other) { if (other == null) return 1; return SortOrder.CompareTo(other.SortOrder); } // >, <, >=, <= from source 2 public static bool operator >(ResearchItem op1, ResearchItem op2) => op1.CompareTo(op2) == 1; public static bool operator <(ResearchItem op1, ResearchItem op2) => op1.CompareTo(op2) == -1; public static bool operator >=(ResearchItem op1, ResearchItem op2) => op1.CompareTo(op2) >= 0; public static bool operator <=(ResearchItem op1, ResearchItem op2) => op1.CompareTo(op2) <= 0; #endregion IComparable<T> Implementation }
If you look closely all the comparisons used the Equals(T other)
method, thereby ensuring a consistent comparison for ResearchItem
. The downside I see is that I have to update Equals(T other)
and GetHashCode()
whenever I update ResearchItem
itself. I briefly looked into using Reflection for those methods, however, my quick look implied a performance hit. Maybe in the future I’ll look into it and see just how bad the performance hit would be for a Unity game.
Do you see anything I could/should change with my code? How do you handle sorting your custom objects? Drop me a comment and let me know.
1 Comment »