Comparing objects is a very common action in any program we write; in fact, it is likely one of the actions we perform most frequently, since every time we use an if expression or a switch we are comparing.
But comparison doesn't stop there. If we're filtering or ordering, we'll also compare two elements.
Table of Contents
1 - Difference between == and equals in C#
When we compare, we either use ==
or the equals
method, and even though both may seem to give the same result, this is not the case.
- Note: We have other options, such as using
Object.Equals(element1, element2)
orObject.ReferenceEquals(e1, e2);
.
The result will depend on whether we are working with value types or reference types.
Also, remember that you can overwrite operators, but in this example, we'll cover everything without overwriting.
1.1 - Comparing value types
When using value types, that is, primitive types or structs, if we use the ==
operator, what we do is compare the value of the element, returning true or false.
int value1 = 1;
int value2 = 1;
bool sonIguales = value1 == value2; //true
Keep in mind that if you create a struct
, by default you will not be able to compare them using the == operator
: you must override this operator.
public struct ElementoPorValor
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
public static bool operator ==(ElementoPorValor c1, ElementoPorValor c2)
{
return c1.Equals(c2);
}
public static bool operator !=(ElementoPorValor c1, ElementoPorValor c2)
{
return !c1.Equals(c2);
}
}
Now we can use the == operator
, but as you've noticed, we're calling the .Equals
method, which for value types first checks the type, then checks that each value is equal.
ElementoPorValor elemento1 = new ElementoPorValor()
{
Valor1 = 1,
Valor2 = 2
};
ElementoPorValor elemento2 = new ElementoPorValor()
{
Valor1 = 1,
Valor2 = 2
};
bool sonIguales = elemento1 == elemento2; //true
bool sonIgualesOpt2 = elemento1.Equals(elemento2);//true
1.2 - Reference types
With reference types, both the == operator
and the .Equals
method check if the object reference is the same:
ElementoReferencia elemento1 = new ElementoReferencia()
{
Valor1 = 1,
Valor2 = 2
};
ElementoReferencia elemento2 = new ElementoReferencia()
{
Valor1 = 1,
Valor2 = 2
};
ElementoReferencia elemento1Copia = elemento1;
bool dosPrimeros = elemento1 == elemento2; //false;
bool laCopia = elemento1 == elemento1Copia; //true
bool dosPrimeros2 = elemento1.Equals(elemento2); //false;
bool laCopia2 = elemento1.Equals(elemento1Copia); //true
Of course, as in the previous case, we can also override the == operator
to make it work as we want:
public class ElementoReferencia
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
public static bool operator ==(ElementoReferencia c1, ElementoReferencia c2)
{
return c1.Valor1 == c2.Valor1 && c1.Valor2 == c2.Valor2;
}
public static bool operator !=(ElementoReferencia c1, ElementoReferencia c2)
{
return c1.Valor1 != c2.Valor1 || c1.Valor2 != c2.Valor2;
}
}
//And now this comparison
bool dosPrimeros = elemento1 == elemento2; //previously false, now returns true
2 - The IEquatable Interface
We've seen the most common ways to compare two objects, but what if our use case is a bit more specific? For example, our object has 3 properties but for some reason we only want to compare two of them.
public class ElementoEspecial
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
public DateTime Fecha { get; set; }
}
As we've seen before, we can override the == operator
, but I personally don't recommend stopping there only.
What I recommend is implementing the IEquatable<T>
interface, which is used by the Object
type. This way, we simply override the behavior of the equals
method.
Before continuing, note that equals
acts over object bool Equals(object obj)
and to add ours, we can do so without using IEquatable
by creating bool Equals(T object)
, but to make the code clearer for future developers, you should override IEquatable
.
This way, by modifying both the == operator
and equals
, the code is clear, and if you also invoke .Equals
from the == operator
, you create important consistency in execution, which can help you avoid more than one problem.
public class ElementoIEquatable : IEquatable<ElementoIEquatable>
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
public DateTime Fecha { get; set; }
public static bool operator ==(ElementoIEquatable c1, ElementoIEquatable c2)
{
return c1.Equals(c2);
}
public static bool operator !=(ElementoIEquatable c1, ElementoIEquatable c2)
{
return !c1.Equals(c2);
}
//Compare objects but ignore the date.
public bool Equals(ElementoIEquatable other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Valor1 == other.Valor1 && Valor2 == other.Valor2;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((ElementoIEquatable)obj);
}
public override int GetHashCode()
{
return HashCode.Combine(Valor1, Valor2);
}
}
3 - The IComparer and IComparable Interfaces
If what we want is to compare objects in a way where one element is greater than another, we won't do this with IEquatable
, because, as we've seen, only true or false is returned.
Instead, we'll use the IComparer
or IComparable
interface. Both seem the same but are slightly different.
First, let's go with the common aspects: the result. Both will create a method that returns an integer, and this integer has 3 possibilities:
-1
if the first element, or the current one, is considered "greater".- 0 if both are equal
- 1 if the second element, or the one provided, is considered "greater"
How you define "greater" is up to the logic you want to design in the method.
When implementing the IComparable<T> interface, you need to implement the CompareTo(T obj)
method, and when implementing IComparer<T>
, you implement Compare(T x, T y)
.
public class ElementoIComparer : IComparer<ElementoIComparer>, IComparable<ElementoIComparer>
{
public int Valor1 { get; set; }
public int Valor2 { get; set; }
//ignore the date in the comparison
public DateTime Fecha { get; set; }
public int Compare(ElementoIComparer x, ElementoIComparer y)
{
if (ReferenceEquals(x, y)) return 0;
if (ReferenceEquals(null, y)) return 1;
if (ReferenceEquals(null, x)) return -1;
int valor1Comparison = x.Valor1.CompareTo(y.Valor1);
if (valor1Comparison != 0) return valor1Comparison;
return x.Valor2.CompareTo(y.Valor2);
}
public int CompareTo(ElementoIComparer other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
int valor1Comparison = Valor1.CompareTo(other.Valor1);
if (valor1Comparison != 0) return valor1Comparison;
return Valor2.CompareTo(other.Valor2);
}
}
As you can see, in both cases we are ignoring the date in the comparison.
3.1 - When to use IComparable
Implementing icomparable
allows us, if we have a list of elements, to sort it using linq, based on the logic we have in the CompareTo
method.
ElementoIComparar elemento1 = new ElementoIComparar(1, 2, DateTime.UtcNow);
ElementoIComparar elemento2 = new ElementoIComparar(2, 2, DateTime.UtcNow);
ElementoIComparar elemento3 = new ElementoIComparar(1, 2, DateTime.UtcNow);
List<ElementoIComparar>listaComparar = new List<ElementoIComparar>()
{
elemento1, elemento2, elemento3
};
List<ElementoIComparar> listaOrdenada = listaComparar.OrderBy(x => x).ToList();
// elemento2 is the last one as the first value has 2
Assert.AreEqual(elemento2, listaOrdenada.Last());
As we see, elemento2
has moved to the last position.
3.2 - When to Use IComparer
When you want to make a comparison, but not necessarily with the current object. Let me explain: implementing the IComparer<T>
interface creates the Compare(T x, T y)
method, meaning you can input any T
; you're not comparing with a specific element.
Personally, I think this comparison should be placed in an abstract class, helper, etc., because even though it is executed in an object, it doesn't interact with it.
Conclusion
- In this post we've seen how to compare objects in C# using different methods.
- How to modify the
== operator
and the equals method to change comparisons. - How to implement the
IComparable
andIComparer
interfaces.
If there is any problem you can add a comment bellow or contact me in the website's contact form