Implementing equality for reference objects using IEquatable and the == operator: summary

We have looked at implementing IEquatable and overriding various equality-related methods and operators in various other posts on this blog. You can look at this page and scroll down to the section called “Comparison and equality” to view all of them.

In this post we’ll put all of these together and implement a joined equality strategy for our reference type.

We’ll start with a simple Animal class:

public class Animal
{
	public string Name { get; set; }
	public int NumberOfLegs { get; set; }
}

Let’s follow what we saw before about IEquatable and implement it along with object.GetHashCode and object.Equals. We say that two animals are equal if their names and number of legs are the same:

public class Animal : IEquatable<Animal>
{
	public string Name { get; set; }
	public int NumberOfLegs { get; set; }

	public bool Equals(Animal other)
	{
		if (other == null) return false;
		return (other.Name == Name && other.NumberOfLegs == NumberOfLegs);
	}

	public override bool Equals(object obj)
	{
		if (obj == null) return false;
		if (obj is Animal)
		{
			Animal other = (Animal)obj;
			return this.Equals(other);
		}
		return false;
	}

	public override int GetHashCode()
	{
		return NumberOfLegs * Name.GetHashCode();
	}
}

Let’s perform some tests:

Animal animalOne = new Animal()
{
	Name = "Fiffy",
	NumberOfLegs = 4
};
Animal animalTwo = new Animal()
{
	Name = "Fiffy",
	NumberOfLegs = 4
};
Animal animalThree = new Animal()
{
	Name = "Fiffy",
	NumberOfLegs = 6
};
Debug.WriteLine(animalOne.Equals(animalTwo));
Debug.WriteLine(animalOne.Equals(animalThree));
Debug.WriteLine(animalTwo.Equals(animalThree));

…gives

True
False
False

…which looks fine. Let’s now declare animalOne as an Object:

object animalOne = new Animal()
{
	Name = "Fiffy",
	NumberOfLegs = 4
};

This will yield the same output. We’ll now override the == and != operators:

public class Animal : IEquatable<Animal>
{
	public string Name { get; set; }
	public int NumberOfLegs { get; set; }

	public static bool operator ==(Animal animalOne, Animal animalTwo)
	{
		return object.Equals(animalOne, animalTwo);
	}

	public static bool operator !=(Animal animalOne, Animal animalTwo)
	{
		return !object.Equals(animalOne, animalTwo);
	}

	public bool Equals(Animal other)
	{
		if (other == null) return false;
		return (other.Name == Name && other.NumberOfLegs == NumberOfLegs);
	}

	public override bool Equals(object obj)
	{
		if (obj == null) return false;
		if (obj is Animal)
		{
			Animal other = (Animal)obj;
			return this.Equals(other);
		}
		return false;
	}

	public override int GetHashCode()
	{
		return NumberOfLegs * Name.GetHashCode();
	}
}

Do you remember how we overrode == and != from this post? In the case of the Animal class we simply call object.Equals. Why? Because the compiler will then direct the == and != calls to our overridden object.Equals method which already satisfies equality.

Let’s test this code with the same three Animal objects:

Debug.WriteLine(animalOne == animalTwo);
Debug.WriteLine(animalOne == animalThree);
Debug.WriteLine(animalTwo == animalThree);

This will print true, false, false like before. Let’s now consider a class called Giraffe which has an extra property: NeckLength. We consider two giraffes to be equal if all Animal properties are equal and their neck lengths are the same. Here comes a possible implementation of all equality-related methods for the Giraffe class:

public class Giraffe : Animal, IEquatable<Giraffe>
{
	public int NeckLength { get; set; }
		
	public bool Equals(Giraffe other)
	{
		if (!base.Equals(other)) return false;
		return NeckLength == other.NeckLength;
	}

	public override bool Equals(object obj)
	{
		if (!base.Equals(obj)) return false;
		Giraffe giraffe = (Giraffe)obj;
		return this.Equals(giraffe);
	}

	public override int GetHashCode()
	{
		return base.GetHashCode() * NeckLength.GetHashCode();
	}
}

Let’s perform some tests:

Giraffe giraffeOne = new Giraffe()
{
	Name = "Jack",
	NeckLength = 2,
	NumberOfLegs = 4
};
Giraffe giraffeTwo = new Giraffe()
{
	Name = "Jack",
	NeckLength = 2,
	NumberOfLegs = 4
};
Giraffe giraffeThree = new Giraffe()
{
	Name = "Jack",
	NeckLength = 3,
	NumberOfLegs = 4
};
Debug.WriteLine(giraffeOne.Equals(giraffeTwo));
Debug.WriteLine(giraffeOne.Equals(giraffeThree));
Debug.WriteLine(giraffeTwo.Equals(giraffeThree));

Prints true, false, false so it looks correct. Notice how we didn’t provide any extra overloads for the == and != operators. Can you guess if the below code returns the correct results?

Debug.WriteLine(giraffeOne == giraffeTwo);
Debug.WriteLine(giraffeOne == giraffeThree);
Debug.WriteLine(giraffeTwo == giraffeThree);

It will also return true, false, false. Why? The == and != overrides in the Animal superclass make sure that object.Equals is called. As we have implemented object.Equals in Giraffe as well we are safe without providing any extra logic for == and != in the derived class.

There’s in fact a simplified version for all this. If you have overridden the object.Equals and object.GetHashCode methods then there’s no need for the strongly typed IEquatable interface:

public class Animal
{
	public string Name { get; set; }
	public int NumberOfLegs { get; set; }

	public static bool operator ==(Animal animalOne, Animal animalTwo)
	{
		return object.Equals(animalOne, animalTwo);
	}

	public static bool operator !=(Animal animalOne, Animal animalTwo)
	{
		return !object.Equals(animalOne, animalTwo);
	}

	public override bool Equals(object obj)
	{
		if (obj == null) return false;
		if (obj is Animal)
		{
			Animal other = (Animal)obj;
			return (other.Name == Name && other.NumberOfLegs == NumberOfLegs);
		}
		return false;
	}

	public override int GetHashCode()
	{
		return NumberOfLegs * Name.GetHashCode();
	}
}

public class Giraffe : Animal
{
	public int NeckLength { get; set; }
		
	public override bool Equals(object obj)
	{
		if (!base.Equals(obj)) return false;
		Giraffe giraffe = (Giraffe)obj;
		return this.NeckLength == giraffe.NeckLength;
	}

	public override int GetHashCode()
	{
		return base.GetHashCode() * NeckLength.GetHashCode();
	}
}

View all various C# language feature related posts here.

Advertisement

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.

%d bloggers like this: