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 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>.

Quest Research

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.

Completing the Quest

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.


Posted

in

by

Comments

One response to “Implementing IEquatable and IComparable”

  1. Treasure Hunter Devlog 7 – Draggable Windows – WeirdBeard's Blog Avatar

    […] quest, I turned my attention to the research tree. The first thing I created was a custom sortable ResearchItem class. That was straightforward, so next up I wanted a draggable panel to display the research […]

    Like

Send a Missive

This site uses Akismet to reduce spam. Learn how your comment data is processed.