C#idiomatic C#
equalitycsharp/fundamentals/expressions

Language

C# Equality comparisons

Learn how C# compares values and references with ==, !=, Equals, GetHashCode, and ReferenceEquals for classes, structs, records, and tuples. Covers the equivalence contract, polymorphic equality in class hierarchies, and records with collection members.

  • page csharp/fundamentals/expressions/equality
  • updated 08/18/2026

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. Value types usually compare data, and reference types usually compare identity. Type authors can change those defaults, 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. 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 System.Object.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 System.Object.Equals test whether two variables point to the same object.

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

sample · csharpdocs/csharp/fundamentals/expressions/snippets/equality/Program.cs

csharp
// <ClassEquality>
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
// </ClassEquality>

// <StructEquality>
var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

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

// <RecordEquality>
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
// </RecordEquality>

// <RecordStructEquality>
var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

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

// <TupleEquality>
var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

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

// <ReferenceEqualsDemo>
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
// </ReferenceEqualsDemo>

// <RecordWithCollectionProblem>
var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True
// </RecordWithCollectionProblem>


// ── Type declarations ────────────────────────────────────────────────────────

class Order(int id, string name)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
}

struct Point(int x, int y)
{
    public int X { get; } = x;
    public int Y { get; } = y;
}

record Person(string First, string Last);

record struct Dimension(double Width, double Height);

class Document(string title)
{
    public string Title { get; } = title;
}


record Playlist(string Name, List<string> Tracks);

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

sample · csharpdocs/csharp/fundamentals/expressions/snippets/equality/Program.cs

csharp
// <ClassEquality>
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
// </ClassEquality>

// <StructEquality>
var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

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

// <RecordEquality>
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
// </RecordEquality>

// <RecordStructEquality>
var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

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

// <TupleEquality>
var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

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

// <ReferenceEqualsDemo>
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
// </ReferenceEqualsDemo>

// <RecordWithCollectionProblem>
var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True
// </RecordWithCollectionProblem>


// ── Type declarations ────────────────────────────────────────────────────────

class Order(int id, string name)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
}

struct Point(int x, int y)
{
    public int X { get; } = x;
    public int Y { get; } = y;
}

record Person(string First, string Last);

record struct Dimension(double Width, double Height);

class Document(string title)
{
    public string Title { get; } = title;
}


record Playlist(string Name, List<string> Tracks);

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 System.Object.Equals and System.Object.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:

sample · csharpdocs/csharp/fundamentals/expressions/snippets/equality/Program.cs

csharp
// <ClassEquality>
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
// </ClassEquality>

// <StructEquality>
var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

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

// <RecordEquality>
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
// </RecordEquality>

// <RecordStructEquality>
var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

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

// <TupleEquality>
var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

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

// <ReferenceEqualsDemo>
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
// </ReferenceEqualsDemo>

// <RecordWithCollectionProblem>
var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True
// </RecordWithCollectionProblem>


// ── Type declarations ────────────────────────────────────────────────────────

class Order(int id, string name)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
}

struct Point(int x, int y)
{
    public int X { get; } = x;
    public int Y { get; } = y;
}

record Person(string First, string Last);

record struct Dimension(double Width, double Height);

class Document(string title)
{
    public string Title { get; } = title;
}


record Playlist(string Name, List<string> Tracks);

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

Use Object.ReferenceEquals to test identity directly

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

sample · csharpdocs/csharp/fundamentals/expressions/snippets/equality/Program.cs

csharp
// <ClassEquality>
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
// </ClassEquality>

// <StructEquality>
var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

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

// <RecordEquality>
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
// </RecordEquality>

// <RecordStructEquality>
var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

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

// <TupleEquality>
var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

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

// <ReferenceEqualsDemo>
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
// </ReferenceEqualsDemo>

// <RecordWithCollectionProblem>
var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True
// </RecordWithCollectionProblem>


// ── Type declarations ────────────────────────────────────────────────────────

class Order(int id, string name)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
}

struct Point(int x, int y)
{
    public int X { get; } = x;
    public int Y { get; } = y;
}

record Person(string First, string Last);

record struct Dimension(double Width, double Height);

class Document(string title)
{
    public string Title { get; } = title;
}


record Playlist(string Name, List<string> Tracks);

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.

Types can define different equality semantics

Types can define equality semantics that differ from the default behavior. The most common reason is to implement value equality. If you create a type that represents data, such as a bank account, a product in inventory, or a user in a system, consider instances with the same values as equal. Choose record types for implementing value equality, and the compiler generates all the necessary equality members for you.

  • ==: the equality operator. Most types use this operator 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 !=.
  • System.Object.Equals: a virtual method inherited by every type. You can override it to change equality semantics for a type.
  • System.Object.GetHashCode: a virtual method used by hash-based collections. When two values are equal, their hash codes must also be equal.
  • System.Object.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 System.Object.Equals, System.Object.GetHashCode, and ==/!= members that compare every declared property value.

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

sample · csharpdocs/csharp/fundamentals/expressions/snippets/equality/Program.cs

csharp
// <ClassEquality>
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
// </ClassEquality>

// <StructEquality>
var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

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

// <RecordEquality>
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
// </RecordEquality>

// <RecordStructEquality>
var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

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

// <TupleEquality>
var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

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

// <ReferenceEqualsDemo>
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
// </ReferenceEqualsDemo>

// <RecordWithCollectionProblem>
var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True
// </RecordWithCollectionProblem>


// ── Type declarations ────────────────────────────────────────────────────────

class Order(int id, string name)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
}

struct Point(int x, int y)
{
    public int X { get; } = x;
    public int Y { get; } = y;
}

record Person(string First, string Last);

record struct Dimension(double Width, double Height);

class Document(string title)
{
    public string Title { get; } = title;
}


record Playlist(string Name, List<string> Tracks);

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

The same compiler generation applies to record struct types:

sample · csharpdocs/csharp/fundamentals/expressions/snippets/equality/Program.cs

csharp
// <ClassEquality>
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
// </ClassEquality>

// <StructEquality>
var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

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

// <RecordEquality>
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
// </RecordEquality>

// <RecordStructEquality>
var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

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

// <TupleEquality>
var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

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

// <ReferenceEqualsDemo>
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
// </ReferenceEqualsDemo>

// <RecordWithCollectionProblem>
var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True
// </RecordWithCollectionProblem>


// ── Type declarations ────────────────────────────────────────────────────────

class Order(int id, string name)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
}

struct Point(int x, int y)
{
    public int X { get; } = x;
    public int Y { get; } = y;
}

record Person(string First, string Last);

record struct Dimension(double Width, double Height);

class Document(string title)
{
    public string Title { get; } = title;
}


record Playlist(string Name, List<string> Tracks);

Record types generate the whole equality set for their own type. Both record class and record struct types override System.Object.Equals and System.Object.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.

Records with reference-type members

Record equality uses the members' own equality semantics. Each property or field is compared by using its own Equals method. For most scalar values, such as int, string, or DateTime, this approach compares the values of the record members. The subtlety arises with common mutable collections such as List<T> or T[]: these types compare by reference, so two record instances that contain different list objects with the same content are not considered equal by the synthesized record equality.

sample · csharpdocs/csharp/fundamentals/expressions/snippets/equality/Program.cs

csharp
// <ClassEquality>
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
// </ClassEquality>

// <StructEquality>
var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

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

// <RecordEquality>
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
// </RecordEquality>

// <RecordStructEquality>
var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

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

// <TupleEquality>
var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

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

// <ReferenceEqualsDemo>
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
// </ReferenceEqualsDemo>

// <RecordWithCollectionProblem>
var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True
// </RecordWithCollectionProblem>


// ── Type declarations ────────────────────────────────────────────────────────

class Order(int id, string name)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
}

struct Point(int x, int y)
{
    public int X { get; } = x;
    public int Y { get; } = y;
}

record Person(string First, string Last);

record struct Dimension(double Width, double Height);

class Document(string title)
{
    public string Title { get; } = title;
}


record Playlist(string Name, List<string> Tracks);

playlist1 and playlist2 are separate List<string> instances. Even though their contents match, Equals returns false.

When you need record equality to reflect collection contents, you have a few options:

  • Implement IEquatable<T> on the record and override Equals to use System.Linq.Enumerable.SequenceEqual for the collection members.
  • Use a collection type with value equality — for example, a custom IEqualityComparer<T> or a type whose own Equals compares elements.
  • Design around identity: if the record represents an entity rather than a pure value, reference equality for its collection members might be intentional.

See also