Equality Operator (==) With Inheritance And Generics In C#

Background

In the previous post, we learned that the == operator does not work for non-primitive value types unless we overload the operator for that value type, and we saw how we can overload the equality operator to enable == operator comparison for non-primitive value types. We also compare the == operator and Object. Equals method to see how their behavior differs for primitive types, value types, and reference types.

Comparison of == operator and Object.Equals

Here is a comparison of both.

  • For Primitive Types e.g. int, float, long, bool, etc., both the == operator and Object. The equals method will compare the values i.e. 1 is equal to 1, but 1 is not equal to 0.
  • For most of the Reference Types, both the == operator and Object. The 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 objects to be compared as parameters.

Previous Posts in the Series

You might also want to read the previous posts in this series. The following are the links to the previous content related to it.

Introduction

In this post, we will be seeing why it is not always a good option to use the == operator for doing equality. We will be seeing how the== operator does not work well with inheritance and generics and will see that the better option is to rely on the virtual Equals method.

Equality Operator and Inheritance Problem

Let’s create some example code that will illustrate how the == operator behaves when inheritance is in play in our code. So, let’s declare two string-type variables, and we will check the result once again by doing an equality check in 4 different ways that are available in C#.

using System;
public class Program
{
    public static void Main(string[] args)
    {
        string str = "Ehsan Sajjad";
        string str1 = string.Copy(str);
        Console.WriteLine(ReferenceEquals(str, str1));
        Console.WriteLine(str.Equals(str1));
        Console.WriteLine(str == str1);
        Console.WriteLine(object.Equals(str, str1));
    }
}

Output

Outputvalue

The following is the output of the above-executed code.

First, we are checking if both string variables have reference to the same string object using the ReferenceEquals method, next, we check using the instance method Equals of String type; on the third line, we are again checking for equality but this time using the == operator and lastly we are checking using the static Equals method of Object so that we can compare the result of all these 4 techniques. We should be able to tell what would be the result from the previous posts which we have done so far.

  • ReferenceEquals will for sure return false as both are references to different objects, not the same object.
  • The Equals method of String type will also return true as both strings are identical (i.e. same sequence or characters).
  • == Operator will also return true as both string values are equal.
  • virtual Equals call will also return true as the overridden implementation of String would be called, and it checks the equality of values of string.

All the above until now makes sense. Now, we will modify the above example a little. The only change we will do is instead of using variables of type String, we will use variables of type Object, and we will see how the output differs from the above code. Here is the code after converting it to use Object type instead of String.

using System;
public class Program
{
    public static void Main(string[] args)
    {
        Object str = "Ehsan Sajjad";
        Object str1 = string.Copy((string)str);
        Console.WriteLine(ReferenceEquals(str, str1));
        Console.WriteLine(str.Equals(str1));
        Console.WriteLine(str == str1);
        Console.WriteLine(object.Equals(str, str1));
    }
}

Output

Output

So we can see the result is different than what we had before when we were using variables of type String. The other three ways methods are still returning the same result, but the == operator equality check is now returning false instead of true, stating that the two strings are not equal, contradicting the fact that they are equal actually, and it’s also conflicting with the result of other two methods.

Why is it that?

The reason behind this is that the == operator is equivalent to a static method, and a static method cannot be a virtual method, what is actually happening when comparing using == operator here is that we are trying to compare two variables of type Object, we know that they are of type String in actual, but the compiler is not aware of that, and we know that for non-virtual methods, it is decided at compile-time that which implementation needs to be invoked and as the variables have been declared of type Object the compiler will emit code for comparing instances of type Object and if you navigate the source code for Object you will see that there is no overload for == operator for it, so what will happen here is that == operator will do what it always do for comparing two reference types when there is no overload found for that type (i.e. it will check for reference equality) and as these two string objects are separate instances, the reference equality will be evaluated to false saying that the two objects are not equal.

Object Equals Method Should be Preferred?

The above problem will never come with the other two Equals methods as they are virtual, and the specific type will provide the override for it, and calling them will always call the overridden implementation to correctly evaluate the equality of them, so in the above case, overridden methods of String type will be called.

For the static Equals method, we already discussed in one of the previous posts that it internally calls the same virtual Equals method. It is just there where we want to avoid NRE (Null Reference Exception), and there is a chance that the object on which we will be calling the Equals method could be null.

Equals Method The Right Way

The above problem we saw is one of the reasons we might want to use the Equals method for checking the Equality of two variables rather than using the equality operator. Methods work well with inheritance than the equality operator.

== Operator and Reference Equals

Another thing to note is that casting the operands of the equality operator to an object before comparing will always give the same result as calling the ReferenceEquals method. Some developers write that way to check for reference equality. We should be careful when doing reference equality that way because someone else reading that code in the future might not understand or not be aware that casting to an object causes the comparison to be based on reference. But if we explicitly call the ReferenceEquals method, that would clarify the intent that we want to do reference equality, and it would clear all the doubts that what the code needs to do.

== Operator and Generics Issue

Another reason to avoid the == operator is if we are using generics in our code. To illustrate that, let’s create a simple method that takes two generic parameters of type T and compares them to check if both the objects are equal and print the result on the Console, the code for which would be,

static void Equals<T>(T a, T b)  
 {      
 Console.WriteLine(a == b); 
  }   

The above example is obviously pretty simple, and one can easily tell what it is actually doing. We are using the equality operator to compare the two objects of type T, but if you try to build the above example in Visual Studio, it wouldn’t build, and a compile-time error would come saying.

Error CS0019 : Operator '==' cannot be applied to operands of type 'T' and 'T'

Explanation

The above error we are seeing is because the T could be any type, it can be a reference type or a value type, or a primitive type, and there is no guarantee that the type parameter is being passed provided implementation of the == operator for itself.

In C# generics, there is no way to apply a constraint on the generic type or method which could force the past type parameter to provide the overload implementation of the == operator; we can make the above code build successfully by putting the class constraint on type T like,

static void Equals<T>(T a, T b) where T : class  
 {      
 Console.WriteLine(a == b); 
  }   

So we have put a constraint on generic type T to be a reference type, due to which we are now able to compile the code successfully, as the == operator can always be used on reference types with no problems, and it will check for reference equality of two objects of reference type.

Temporary Solution

We are able to build the code now, but there is a problem because what if we need to pass value type parameters on primitive types to the generic Equals method? Then we will not be able to do it as we had put the restriction to this method to only be called for reference types.

Let’s write some code in the main method, which would create two identical strings, as we did a couple of times in previous posts as well.

using System;
class Program
{
    static void Main(string[] args)
    {
        Object str = "Ehsan Sajjad";
        Object str1 = string.Copy((string)str);
        Equals(str, str1);
    }
    static void Equals<T>(T a, T b) where T : class
    {
        Console.WriteLine(a == b);
    }
}

Guess what would be the output of the above code when we run it. It will evaluate the two strings are equal or not. If we recall what we saw before was that for String type, the == operator overload defined by String compares the values of the objects, so the above should print true on the Console, but if we run the code, we will see the opposite result that it printed false,

Output

So, the above code has given us an unexpected result, as we were expecting True to be printed on Console. The question which comes to mind instantly is why it is evaluating to false, it looks like the == operator is checking the two objects for Reference Equality instead of values equality, but the question is why it is doing reference equality.

This is happening because even though the compiler knows that it can apply the == operator to whatever the type T is being passed, In the above case, String and it has == operator overload, but the compiler does not know whether the generic type which is being used when using the method overloads the == operator or not, so it assumes that T wouldn’t overload it and it compiles the code considering that the == operator is called on instances of type Object which clearly happened that it checked for Reference Equality.

This is very important to keep in mind when using the == operator with generics. The == operator will not use the equality operator overload defined by the type T, and it will consider it as Object.

Again Object.Equals to Rescue

Now let’s change our generic method to use the Equals method instead of the equality operator, which would be,

static void Equals<T>(T a, T b) where T      
    {        
      Console.WriteLine(object.Equals(a,b));   
       }   

We have removed the class constraint as an Object. Equals can be called on any type, and we are using the static method for the same reason that if one of the parameters is passed null,our code wouldn’t fail and will work as expected, and now this method will work for both values types and reference types.

Now, if we run the code again, we will see that it printed the result as expected because of the object. Equals will call the appropriate overridden implementation of the Equals method at run-time as a static method will call the virtual Equals method, and we see the expected result True as both string values are equal.

Summary

  • == Operator doesn’t work well with Inheritance and might give you unexpected results when used with inheritance because the == operator is not virtual, so wherever possible virtual Equals or static Equals methods should be preferred.
  • == Operator also doesn’t work as expected when used in Generic class or methods and Equals method should be used with generic to avoid unexpected behavior in the program.


Similar Articles