Tracks
/
C#
C#
/
Syllabus
/
Equality
Eq

Equality in C#

5 exercises

About Equality

The coding exercise illustrates a number of properties of equality in C#:

Object.Equals()

Topics covered by the coding exercise

  • Simple types (strings and primitives) are typically tested for equality with the == and !=. This is considered more idiomatic than using the Equals() method which is also available with these types. Java programmers should be alert, when dealing with strings, to the fact that == compares by value in C# but by reference in Java when returning to their former language.
  • Reference types (Instances of classes) are compared using the Equals() method inherited from object. If your goal with the equality test is to ensure that two objects are the exact same instance then relying on object's implementation will suffice. If not, you need to override object.Equals().
  • If you know that all the instances of your class are created in one place, say characters in some game or simulation then reference equality is sufficient. However, it is likely that multiple instances of the same real-world entity will be created (for example, from a database, by user input, via a web request). In this case values that uniquely identify the entity must be tested for equality. Therefore Equals() must be overridden.
  • An overridden Equals() will contain equality tests on members of simple types using == and reference types with recursive calls to Equals().
class StatusBar
{
    private readonly int width = 200, height = 20;

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return width == (other as StatusBar).width && height == (other as StatusBar).height;
    }
}
class Window
{
    private readonly string title = "Main";
    private readonly StatusBar statusBar = new StatusBar();

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
    }
}

  • The static method object.ReferenceEquals() is used to compare two objects to detect if they are one and the same instance. This provides clarity and is a necessity where Equals() and == have been overridden/overloaded.
var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true

Ancillary topics

  • In addition to public override bool Equals(object obj) IDEs typically generate the overload protected bool Equals(FacialFeatures other) for use when inheritance is involved. A derived class can call the base classe's Equals() and then add its own test.
  • Do not use == unless you have overloaded the == operator, as well as the Equals() method in your class (see the operator-overloading exercise) or you care only that the references are equal.
  • Equality tests in structs are dealt with in the structs exercise.
  • Equality of tuples is dealt with in the tuples exercise.
  • Many developers rely on their IDEs to provide implementation of equality methods as these take care of all the minutiae of equality. For instance, JetBrains' RIDER (v 2020.1) generates the following equality methods for a class:
protected bool Equals(T other)
{
    return field1 == other.field1 && field2.Equals(other.field2);
}

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((T) obj);
}
  • Be careful if you decide to improve on the code generated by your IDE. That code is generally bullet-proof against nulls and mistyped objects and fairly well optimized.
  • Tests for the equality of delegates is not specifically discussed in the exercise.
  • There are no built in equality tests for arrays nor most collections. LINQ (discussed in later exercises) provides SequenceEquals() but in the absence of LINQ it is a matter of iterating through both collections and comparing items individually.
  • For a discussion of how to use == and != with your own classes see the operator-overloading exercise.

Object.GetHashCode()

  • object.GetHashCode() returns a hash code in the form of a 32 bit integer. The hash code is used by dictionary and set classes such as Dictionary<T> and HashSet<T> to store and retrieve objects in a performant manner. In the case of dictionaries the hashing relates to the keys.
  • There is an expectation amongst C# developers that if you override Equals() you will also override GetHashCode(). There is a relationship between Equals() and GetHashCode() that must hold true for correct behavior of dictionary and hash set classes and any others that use a hash code. You are expected to implement the method so that no traps are laid for maintainers who might add a hash code based collection at a later time.
  • The relationship between hash code and equality is that if two objects are equal (Equal() returns true) then GetHashCode() for the two objects must return the same value. This does not apply in the reverse direction. It is not symmetrical. Picture a lookup function that first goes to a "bucket" based on the hash code and then picks out the particular item using the equality test.
  • The easiest way to create a hashcode is to call HashCode.Combine() passing in the values used in the equality test (or a subset). Bear in mind the more information you provide to Combine() the more performant the hash implementation is likely to be.
public class Assessment
{
    private int rating;
    private Person boss;

    public override int GetHashCode()
    {
        return HashCode.Combine(rating, boss);
    }
}
  • The values used in the equality test must be stable while the hashed collection is in use. If you add an object to the collection with one set of values and then change those values the hash code will no longer point to the correct "bucket". In practice this means that the object should be immutable. Other approaches run the risk of creating gotchas for maintainers. Immutability is discussed in other exercises.
  • It is possible that you can design a better hashcode than that produced by the library routines but either it's because you have a detailed understanding of the data's characteristics or because it is a very simple collection where values can be used directly without hashing. It may not be worth the extra effort.

Performance Enhancements

To improve performance slightly, especially where objects belong to collections you can add an overloaded member public bool Equals(T other).

This will save a certain amount of type checking for reference types and will save a boxing step for value types as they will not need to be converted to an object (boxed) as an argument to public override bool Equals(object other).

If you add the interface IEquatable<T> to your class this will require the overload to be implemented. Unless your code contains routines that take objects of type IEquatable<T> (and presumably do something interesting relating to equality irrespective of the implementing class) there is really no other compelling to include the interface.

IEqualityComparer<T>

If you have a class that can be uniquely identified in two different ways, say a Person class that has a SSID and a unique email address then .NET provides a means to allow two different collections to use different hash-code and equality tests. Each can have an different implementation of IEqualityComparer<T> with its own an Equals() and a GetHashCode() method. You can have a dictionary keyed on SSID and another keyed on email address.

Where IEqualityComparer<T> is in play you would typically still override Equals() and GetHashCode() on your item class to avoid problems outside the collection classes.

One consideration when using IEqualityComparer<T> is that private methods etc. will not be available for the equality test.

If only one hashed collection is in play then it may be better to avoid IEqualityComparer<T> and encapsulate equality and hash code in the object itself. It is not ideal, in the first place, that a key dependency such as that between object and hashed collection cannot be enforced by the compiler but with the hashing and comparison logic elsewhere it would be even easier for a maintainer to make changes to a class's members without regard to the consequences for the collection.

Note on floating-point equality

One primitive that can challenge the unwary coder is testing the equality of floating-point values. This is discussed in the about.md document for the floating-point-numbers concept.

Equality and Inheritance

This article shows some of the decisions that have to be made with regard to equality and inheritance. They will only concern you if you are manipulating instances of a class and a derived class and need to test the equality between the two.

Edit via GitHub The link opens in a new window or tab

Learn Equality

Practicing is locked

Unlock 4 more exercises to practice Equality