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 »