Background
This article is in continuation of the series of articles regarding how Equality works in .NET, the purpose is to have the developers more clear understanding on how .NET handles equality for different types. We have already seen equality for primitive types, reference types works, we also discussed separately how equality works differently for String
type. Following are some points which we were able to understand until now.
Key Points Learned So Far
- By default, the
virtual Object.Equals
method does reference equality for reference types and value equality for value types, but for value types, it uses reflection which is a performance overhead for value types and any type can override
Object.Equals
method to change the logic of how it checks for equality e.g. String, Delegate
and Tuple
do this for providing value equality, even though these are reference types. Object
class also provides a static
Equals
method which can be used when there is chance that one or both of the parameters can be null
, other than that it behaves identical to the virtual
Object.Equals
method. - There is also a
static ReferenceEquals
method which provides a guaranteed way to check for reference equality. IEquatable<T>
interface can be implemented on a type to provide a strongly typed Equals
method which also avoids boxing for value types. It is implemented for primitive numeric types but unfortunately Microsoft has not been very proactive implementing for other value types in the FCL( Framework Class Library )
. - For
Value Types
using == operator
gives us the same result as calling Object.Equals
but underlying mechanism of == operator
is different in IL( Intermediate Language )
as compared to Object.Equals
, so the Object.Equals
implementation provided for that primitive type is not called, instead an IL
instruction ceq
gets called which says that compare the two values that are being loaded on the stack right now and perform equality comparison using CPU registers. - For
Reference Types
, == operator
and Object.Equals
method call both work differently behind the scenes which can be verified by inspecting the IL code generated. It also uses ceq
instruction which do the comparison of memory addresses.
If the above points do not makes sense to you, it would be better to read it from the start, following are the links to the previous content related to it:
Equality Operator for Value Types
We have already learned that what the Equality operator does for both the primitive types and reference types. One case that we haven’t tested yet is that what happens for the non-primitive value types. This time, we will be focusing on the value types.
We will be using the same example that we used before, so we will declare a Person
type as struct
and we will compare two instances to see if they are equal not using Object.Equals
method which we did previously and we know that it does the value comparison which is very in-efficient as for value types it uses reflection to iterate through the fields and check for equality of each one, but instead we will compare two Person
type
objects using the == operator
.
The Person
type definition looks like:
public struct Person
{
public string Name { get; set; }
public Person(string name)
{
Name = name;
}
public override string ToString()
{
return Name;
}
}
If we write the following code in Main
and build the project, what will we see:
class Program
{
static void Main(String[] args)
{
Person p1 = new Person("Ehsan Sajjad");
Person p2 = new Person("Ehsan Sajjad");
Console.WriteLine(p1.Equals(p2));
Console.WriteLine(p1 == p2);
Console.ReadKey();
}
}
When we try to build the above program, the first line where we are comparing p1
and p2
using Equals
method will have no problem, but the build fails on line 2 where we are using ==
operator with the following error message:
Operator ‘==’ cannot be applied to operands of type `Person` and `Person`
So, the above makes one thing clear to us that the Equality operator does nothing for the non-primitive value types, for using the equality operator for non-primitive value types we need to provide an operator overload for the type.
Let’s modify the above example to add the equality operator overload for the Person struct
, we will be now specifying what the ==
operator should do for two Person
objects being compared, following is the syntax to provide the overload for ==
operator if we want to provide the implementation so that what the operator should do when used for two objects of type Person
:
public static bool operator ==(Person p1, Person p2)
{
}
After adding the above code in the Person struct
, we would be able to compile the code written in Main
method of Program
, but note that this would not compile still as the overload has return type bool
as per signatures but we are returning nothing, this is just to give idea how to write ==
operator overloaded implementation for a user defined Value Type.
We saw in one of the previous posts that String
overloads the equality operator to make sure that it does the same thing as Equals
method, so whenever you are defining a new type make sure that it does the same thing with both the method and the operator, if we provide either of them, it is generally a good thing to do. Following is an example code that will help us understand why it is a good practice:
class Program
{
static void Main(string[] args)
{
Tuple<int int,int> tuple1 = Tuple.Create(1, 2);
Tuple<int int,int> tuple2 = Tuple.Create(1, 2);
Console.WriteLine(ReferenceEquals(tuple1, tuple2));
Console.WriteLine(tuple1.Equals(tuple2));
Console.WriteLine(tuple1 == tuple2);
Console.Read();
}
}
As you can see, we are instantiating two tuples containing same values i.e. 1
and 2
, Tuple
is a generic class which comes within Framework Class Libraries provided by Microsoft which simply provides a way to group couple of values together in a single object. The Tuple.Create(1, 2);
is a nicer way to instantiate a new tuple saving developer to explicitly write the generic types in the code.
Now we are comparing the tuples to see if they are equal or not, Tuple
is a reference type, so we are checking equality using ReferenceEquals
check to confirm that we are dealing with two separate instances, next we are comparing if they are equal using the Equals
method, and lastly we are comparing using equality operator aka ==
operator. Let’s run this program and following is the output of the above program:
For some of you, the result might be a surprise, we can see that ReferenceEquals
has returned false
which means that both are different instances, but the == operator
and Equals
method have returned the opposite result, the == operator
says that both are not equal while the Equals
method is saying that they are equal.
What’s actually happening above is that Tuple
overrides the Equals
method in a way that it checks for value equality for the objects. Microsoft figured that when you are dealing with a type whose purpose is just to encapsulate couple of fields that is probably what you want equality to mean, so as the two Tuple instances have the same value the Equals
method says that they are equal, but Microsoft
didn’t provided the overload for == operator
and that means that == operator
has just done what it is meant to be doing and will always do for reference type that does not provide an overload and checks reference equality and has returned False
in this case as both are different instances.
I am pretty sure that has confused you, of course the behavior is confusing and it did confuse me as well when I was digging in to it. Almost no one is going to expect this kind of behavior and it is strongly recommended to not add this kind of behavior is any type we define.
If you override
the Equality, then it is much better to provide the == operator
overload to make sure that method and the operator always gives the same result and if you implement the IEquatable<T>
interface then you should do same for that as well.
Comparison of == Operator and Object.Equals Method
Finally, let’s quickly see how the ==
operator and Equals
method differ as far as their behavior is concerned:
- For Primitive Types,
e.g.
int
, float
, long
, bool
etc., both the == operator
and Object.Equals
method will compare the values, i.e., 1
is equal to but 1
, but 1
is not equal to 0
- For most of the
Reference Types
, both the == operator
and Object.Equals
method will by default compare the references, you can modify this behavior by overloading the ==
operator or overriding the Object.Equals
method but if you want the behavior of both to be consistent and don’t want to surprise other developers and yourself, you must do the both (overload ==
operator and override the Equals
method). - For Non-primitive Value Types, the
Object.Equals
method will do the value equality using Refection which is slow and this is overridden behavior of course, but the equality operator is by default not available for value types unless you overload the ==
operator for that type which we saw in the example above. - There is also another minor difference that for reference types the
virtual Equals
method cannot work if the first parameter is null
but that is trivial, as a workaround the static Equals
method can be used which takes both the objects to be compared as parameter.
Summary
So after all the above points and discussion, we can conclude that a lot of the time, the operator and the method give the same result in practice, but since the syntax of the operator is so much convenient that developers most of the time prefer the operator. In the next post, we will be discussing what are the situations where ==
operator might not be preferable but instead Equals
method would be preferable.