In this article, we delve into the similarities and differences between the equality (==
) operator and the Equals method in C#.
Now, let’s explore these concepts further!
Equality Operator And Equals Method in Different Scenarios
Let’s review some examples to understand how ==
operator and Equals
method behaves in different cases. For a comprehensive go-through, we will also be exploring the usage of the ReferenceEquals
method in the examples.
Built-In Value Types
Built-in value types, such as int
, float
, bool
, and others, utilize the equality (==
) operator to directly compare their values. The equality operator examines whether the values are equal or not.
Before proceeding with the code examples, let’s take a moment to inspect a method that we will use in our snippets for printing our results:
public static string PrintFormattedResult(bool? a, bool? b, bool? c) { var result = $"Reference: {a}, Equality: {b}, Equals: {c}"; Console.WriteLine(result); return result; }
Taking that into account, let’s analyze a code snippet that compares the behaviors of the ReferenceEquals
method, the equality (==
) operator, and the Equals
method:
int firstNumber = 5; int secondNumber = 5; return PrintFormattedResult( ReferenceEquals(firstNumber, secondNumber), // false firstNumber == secondNumber, // true firstNumber.Equals(secondNumber) // true );
We must keep in mind that the ReferenceEquals
method cannot be utilized with value types and will consistently return false
when invoked with value types. However, for built-in value types, both the ==
operator and the Equals
method behaves similarly by returning true
when the values of the operands are equal. Therefore, when comparing built-in value types, we can see that the ==
operator and the Equals
method exhibit similar behavior.
Objects
The equality (==
) operator exhibits different behavior for reference types, such as objects, by default. It checks for reference equality, comparing the memory addresses of the objects to determine if they are the same instance. Even if two different objects have the same content, they will not be considered equal with the ==
operator unless they refer to the same instance. Similarly, the Equals
method, by default, performs reference equality comparisons for reference types.
Let’s explore how two objects behave when we compare them using the ==
operator and the Equals
method:
object firstObject = new[] { 1, 2, 3 }; object secondObject = new[] { 1, 2, 3 }; return PrintFormattedResult( ReferenceEquals(firstObject, secondObject), // false firstObject == secondObject, // false firstObject.Equals(secondObject) // false );
The ReferenceEquals
method returns false
because these two objects have different memory addresses. The equality (==
) operator also returns false
for the same reason. Though the contents of both objects are the same, the Equals
method returns false
as the references of both objects are not identical.
Strings
Now, let’s study the behavior of the ==
operator and the Equals
method when the comparison is made between two strings:
string firstWord = "pqr"; string secondWord = "pqr"; return PrintFormattedResult( ReferenceEquals(firstWord, secondWord), // true firstWord == secondWord, // true firstWord.Equals(secondWord) // true );
String interning in .NET causes the ReferenceEquals
method to return true
. String interning is a process where the runtime maintains a pool of unique string instances. When creating a string, the runtime actively checks if an identical string already exists in the pool. If it does, the new string reference points to the existing string instance instead of creating a new one.
C# performs this optimization for conserving memory and improving performance. For string
, the equality (==
) operator, inequality (!=
) operator and the Equals
method perform a case-sensitive, ordinal comparison by default. Hence, both the ==
operator and the Equals
method returns true
when a comparison is made between firstWord
and secondWord
. Besides, we can pass a StringComparison
argument with the Equals
method for altering its sorting rules. To learn more about this, you may visit Default ordinal comparisons.
Object and String
With that in mind, let’s take a closer look at how the ==
operator and the Equals
method behave when we compare an object
with a string
:
object thirdObject = new string(new[] { 'x', 'y', 'z' }); string thirdWord = "xyz"; return PrintFormattedResult( ReferenceEquals(thirdObject, thirdWord), // false thirdObject == thirdWord, // false thirdObject.Equals(thirdWord) // true );
The ReferenceEquals
method returns false
because thirdObject
and thirdWord
have different memory references. We can see that thirdObject
is a string
boxed as an object
and thirdWord
is simply a string
. When we compare these two, the ==
operator defined on the object
gets invoked, rather than the string
. This leads to a reference comparison and a false
value is returned. To achieve the behavior of ==
operator defined on the string
class, we have to cast the thirdObject
to a string
:
Console.WriteLine((string) thirdObject == thirdWord); // true
We get a return value of true
once we cast the thirdObject
to string
and compare it with thirdWord
.
Finally, the thirdObject
being a string
calls the override of the Equals
method defined in the string
class, which performs the default case-sensitive ordinal comparison. This allows for an accurate comparison between the two string values.
Classes
For user-defined types (e.g., classes, structs, and records), the behavior of the ==
operator depends on how equality is defined for the type. By default, the ==
operator compares reference equality for classes. However, we can override the ==
operator or implement the Equals()
method to define custom equality comparison logic for our types:
var firstEmployee = new Employee("Hermione Granger", 5000); var secondEmployee = new Employee("Hermione Granger", 5000); return PrintFormattedResult( ReferenceEquals(firstEmployee, secondEmployee), // false firstEmployee == secondEmployee, // false firstEmployee.Equals(secondEmployee) // false );
The ReferenceEquals
method returns false
because firstEmployee
and secondEmployee
have different memory addresses. The equality (==
) operator also performs reference equality for class types and hence returns false
. Lastly, the Equals
method defaults to performing reference equality, resulting in a false
return. Again, it is possible to override the Equals
method to enable a more meaningful comparison of equality.
Structs
That being said, let’s analyze the behavior of the ==
operator and the Equals
method when the comparison is made between two struct types:
var firstCar = new Car("Audi R8", 3715); var secondCar = new Car("Audi R8", 3715); return PrintFormattedResult( ReferenceEquals(firstCar, secondCar), // false firstCar == secondCar, // true firstCar.Equals(secondCar) // true );
In C#, structs are value types, which means they store their instances directly with their values instead of referring to memory locations. When using the ReferenceEquals
method to compare two struct instances, it will always return false
because the method verifies if the two references point to the same memory location, which is not applicable for structs.
Next, we encounter the ==
operator, which is not automatically defined for struct types. As a result, we have to override the ==
operator on the Car
class in a way that replicates the behavior of the Equals
method:
public override bool Equals(object? obj) { if (obj is not Car other) return false; return Model == other.Model && Weight == other.Weight; } public override int GetHashCode() => (Model, Weight).GetHashCode(); public static bool operator ==(Car carLeft, Car carRight) => carLeft.Equals(carRight); public static bool operator !=(Car carLeft, Car carRight) => !(carLeft == carRight);
Finally, the Equals
method compares the values of each field in the struct to determine equality. Since all the fields have identical values, the method returns true
.
Records
With the class
and struct
types covered, let’s learn how the ==
operator and the Equals
method behave when we compare two record
types:
var firstLaptop = new Laptop("ASUS TUF A15", 1499); var secondLaptop = new Laptop("ASUS TUF A15", 1499); return PrintFormattedResult( ReferenceEquals(firstLaptop, secondLaptop), // false firstLaptop == secondLaptop, // true firstLaptop.Equals(secondLaptop) // true );
The ReferenceEquals
method returns false
because firstLaptop
and secondLaptop
have different memory addresses. The equality (==
) operator, by default, performs a memberwise comparison for record types. As all the members are identical, it returns true
in this case. Lastly, the Equals
method also has behavior similar to the equality (==) operator, and outputs will also be similar.
Nullable Types
When dealing with nullable types (e.g., int?
, float?
, etc.), we can use the equality (==
) operator to compare nullable values. It returns true
if both operands are null
or have the same underlying value:
int? firstNullableNumber = null; int? secondNullableNumber = null; return PrintFormattedResult( ReferenceEquals(firstNullableNumber, secondNullableNumber), // true firstNullableNumber == secondNullableNumber, // true firstNullableNumber.Equals(secondNullableNumber) // true );
The ReferenceEquals
method returns true
here, both arguments are null, indicating that they refer to the same null reference. Next, the equality (==
) operator evaluates to true
because null
represents the absence of a value, and two null
values are considered equal. Finally, when both nullable instances have null values, the Equals
method considers them equal because they represent the absence of a value.
Conclusion
In this article, we have learned about the different behaviors of the equality operator and the Equals method in various scenarios. Moreover, this knowledge will enable us to make more informed decisions regarding when to utilize the equality operator and the Equals
method in our code.