Equality Operator And Value Types In C#

Background

This article is in continuation of the series of articles regarding how Equality works in .NET. The purpose is to give the developers a more clear understanding on how .NET handles Equality for different types. We have already seen equality for primitive types, how reference types work, and we also discussed separately how equality works differently for String type. Following are some points which we were able to understand so far.

Key Points Learned So Far

  • By default, the virtual 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 a chance that one or both of the parameters can be null, other than that it behaves identically to the virtual 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 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 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 make 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 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 objects using the == operator.

The Person type definition looks like, 

  1. public struct Person {  
  2.     private string _name;  
  3.     public string Name {  
  4.         get {  
  5.             return _name;  
  6.         }  
  7.     }  
  8.     public Person(string name) {  
  9.         _name = name;  
  10.     }  
  11.     public override string ToString() {  
  12.         return _name;  
  13.     }  

If we write the following code in Main and build the project, what will we see,

  1. class Program {  
  2.     static void Main(String[] args) {  
  3.         Person p1 = new Person("Ehsan Sajjad");  
  4.         Person p2 = new Person("Ehsan Sajjad");  
  5.         Console.WriteLine(p1.Equals(p2));  
  6.         Console.WriteLine(p1 == p2);  
  7.         Console.ReadKey();  
  8.     }  
  9. }   

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, 

  1. public static bool operator == (Person p1, Person p2) {}   

After adding the above code in the Person struct we will be able to compile the code written in the Main method of the Program, but note that this will not compile still as the overload has return type bool as per signatures but we are returning nothing, this is just to give an 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, 

  1. class Program {  
  2.     static void Main(string[] args) {  
  3.         Tuple < intint > tuple1 = Tuple.Create(1, 2);  
  4.         Tuple < intint > tuple2 = Tuple.Create(1, 2);  
  5.         Console.WriteLine(ReferenceEquals(tuple1, tuple2));  
  6.         Console.WriteLine(tuple1.Equals(tuple2));  
  7.         Console.WriteLine(tuple1 == tuple2);  
  8.         Console.Read();  
  9.     }   

As you can see we are instantiating two tuples containing same values i.e. 1 and 3, Tuple is a generic class which comes prebuilt in 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 a couple of fields that is probably what you want equality to mean, so as the two Tuples have the same value the Equals method says that they are equal, but Microsoft didn’t provide 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 in 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.

Comparing == Operator and Object.Equals Method

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 both (overload == operator and override the Equals method).
  • For Non-primitive Value Types the 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.

So after all the above points and discussion, we can conclude that a lot of the time the operator and the method gives the same result in practice, but since the syntax of the operator is so much more convenient 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 the preferable but instead Equals method is.

Up Next
    Ebook Download
    View all
    Learn
    View all