Edit

C# Equality comparisons

Tip

This article is part of the Fundamentals section for developers who already know at least one programming language and are learning C#. If you're new to programming, start with the Get started tutorials first.

Coming from another language? In Java, == on objects and JavaScript === on objects test identity, not content. C# classes work the same way by default. In Python, == calls __eq__ and tests content by default , similar to how C# records compare. C# structs also compare by value when you call Equals.

C# distinguishes two kinds of equality. Value equality means two instances are equal when their data matches. Reference equality means two variables are equal only when they point to the same object in memory. This condition is also called identity. The kind of type gives you the best first clue about the default equality behavior: value types usually compare data, and reference types usually compare identity. Defaults aren't destiny, but that mental model prevents subtle bugs where two objects that look identical aren't considered equal, or where a mutation through one variable silently changes what another variable sees.

Value types, reference types, and equality defaults

Every type in C# is either a value type or a reference type. A value type holds its data directly in the variable. A reference type holds a reference to an object. When you assign a reference-type variable to another variable, both variables refer to the same object. This article uses that distinction as a quick refresher. For more information about value types and reference types, see Type system overview.

The default equality behavior usually follows the kind of type:

  • Built-in numeric types and enums are value types. Two int variables are equal when their numeric values match.
  • Structs are value types. A plain struct uses value equality when you call Equals.
  • Tuples are value types. Two tuples are equal when all their element values match.
  • Classes are reference types. A plain class uses reference equality, so == and Equals test whether two variables point to the same object.

A plain class shows reference equality. Two separate objects with the same data aren't equal, but two variables that refer to the same object are equal:

var order1 = new Order(42, "Shoes");
var order2 = new Order(42, "Shoes");

Console.WriteLine(order1 == order2);               // => False
Console.WriteLine(order1.Equals(order2));          // => False
Console.WriteLine(ReferenceEquals(order1, order2)); // => False

Order order3 = order1;
Console.WriteLine(order1 == order3);               // => True

A plain struct shows value equality through Equals. Two struct instances are equal when their fields match:

var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

Console.WriteLine(pt1.Equals(pt2)); // => True

Plain structs don't get a predefined == operator. Writing p1 == p2 on a plain struct compiles only if the struct declares its own operator ==. If you need operator comparisons for a struct, define == and != as a pair and keep them consistent with Equals and GetHashCode.

Tuples are value types too. Two tuples are equal when every element value matches. Element names in a named tuple are a compile-time convenience and aren't considered during comparison. Only positions and values matter:

var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

Console.WriteLine(t1 == t2); // => True

For more information about tuple syntax and deconstruction, see Tuples and deconstruction.

Types can define different equality semantics

Defaults aren't destiny. Some types define equality semantics that differ from the type-kind default, and your own types can do the same when their data should determine equality.

Common exceptions and customizations include:

  • Records generate value equality and include ==/!= operators. The next section shows how the record modifier gives value equality to both record classes and record structs.
  • Strings are classes, but == and Equals compare string content, not identity.
  • Your own classes and structs can define value equality when their data should determine equality.

Equality is woven through these related members:

  • ==: the equality operator. Most types use this as the primary equality check. Its behavior depends on whether the type has a built-in or user-defined == operator.
  • !=: the inequality operator. When a type defines a user-defined == operator, it must also define !=.
  • Equals: a virtual method inherited by every type. You can override it to change equality semantics for a type.
  • GetHashCode: a virtual method used by hash-based collections. When two values are equal, their hash codes must also be equal.
  • ReferenceEquals: a static method that always tests identity.

Use records for value equality

Use the record modifier to give a data-focused type value equality when the type can be a record. The compiler generates Equals, GetHashCode, and ==/!= members that compare every declared property value.

A record class is still a reference type, but it compares values instead of identity:

var person1 = new Person("Ada", "Lovelace");
var person2 = new Person("Ada", "Lovelace");

Console.WriteLine(person1 == person2);               // => True
Console.WriteLine(person1.Equals(person2));          // => True
Console.WriteLine(ReferenceEquals(person1, person2)); // => False

ReferenceEquals confirms that person1 and person2 are different objects in memory, while == and Equals return True because the compiler-generated equality compares property values.

The same compiler generation applies to record struct types:

var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

Console.WriteLine(dim1 == dim2);      // => True
Console.WriteLine(dim1.Equals(dim2)); // => True

Record types generate the whole equality set for their own type. Both record class and record struct types override Equals and GetHashCode. They also generate == and != operators, plus a typed Equals method for the record type. Unlike a plain struct, a record struct therefore supports == and != automatically. For more information about record types and their equality semantics, see Records.

Implement equality yourself when a type can't be a record

Important

This section shows how to implement by hand the equality behavior that the compiler generates when you add record to a type. If your type can be a record, use record instead. It generates all these members for you. Implement them manually only when your type can't be a record.

When a class or struct represents a value, such as a color or a measurement, the equality members for that type must agree. The easiest way to achieve this consistency is to declare the type as a record. If the type can't be a record, such as when it must derive from a non-record class, implement the equality members yourself. The language enforces that user-defined == and != operators must be declared as a pair. If you provide those operators, compiler warning CS0660 means the type also needs an Object.Equals override. Warning CS0661 means the type also needs an Object.GetHashCode override.

In a complete manual implementation, provide these members:

  • == and != operators. Add them as a pair because the compiler requires a type that overloads one to overload the other.
  • An override of Equals. This override changes equality semantics for the type and keeps object-level equality consistent.
  • An override of GetHashCode. Objects that are equal must return the same hash code. Without this pairing, the type behaves incorrectly in hash-based collections such as Dictionary<TKey,TValue> or HashSet<T>. See GetHashCode for guidance on a correct implementation.
  • Optionally, a typed Equals method by implementing IEquatable<T>. You often see this written as Equals(T?) in docs: T is a type parameter, a placeholder for the current type, and ? is a nullable annotation that says the argument can be null. This typed method can avoid extra conversions when callers already have the same type, but it's a secondary optimization.

The following example starts with the Equals and GetHashCode overrides, plus the optional typed Equals member, so you can see their effect before the == and != operators are added. HashCode.Combine is a library helper that builds one hash code from the same values used by Equals:

class Color : IEquatable<Color>
{
    public Color(int r, int g, int b)
    {
        R = r;
        G = g;
        B = b;
    }

    public int R { get; }
    public int G { get; }
    public int B { get; }

    public bool Equals(Color? other) =>
        other is not null && R == other.R && G == other.G && B == other.B;

    public override bool Equals(object? obj) => obj is Color other && Equals(other);
    public override int GetHashCode() => HashCode.Combine(R, G, B);
}

At this point, Equals reflects value equality, but == still tests identity for the class because the type hasn't declared == and != operators. Plain structs likewise still don't have a predefined == operator unless you declare one:

var red1 = new Color(255, 0, 0);
var red2 = new Color(255, 0, 0);

Console.WriteLine(red1.Equals(red2)); // => True
Console.WriteLine(red1 == red2);      // => False  (no == overload; identity check)

Adding == and != operators is the remaining step when you need operator comparisons. This article intentionally stops before the full operator implementation so the first pass can focus on the equality contract. The operator-focused follow-up shows the completed shape. For the operator syntax, see Equality operators in the language reference.

Use Object.ReferenceEquals to test identity directly

ReferenceEquals always tests identity regardless of how a type overrides Equals or overloads ==. Use it as an identity diagnostic when you need to confirm whether two variables point to the exact same object:

var doc1 = new Document("Report");
var doc2 = new Document("Report");
var doc3 = doc1;

Console.WriteLine(ReferenceEquals(doc1, doc2)); // => False
Console.WriteLine(ReferenceEquals(doc1, doc3)); // => True

A common use is inside an Equals override to short-circuit the full comparison: when both arguments are the same reference, they're always equal without checking individual fields.

Note

Advanced detail: when variables are typed as an interface, == checks whether the interface variables refer to the same object. A call to Equals still runs the underlying object's implementation.

See also