C#idiomatic C#
equality-operatorscsharp/language-reference/operators

Language

Equality operators - test if two objects are equal or not

C# equality operators test if two objects are equal or not equal. You can define equality operators for your types for custom comparisons for equality. Learn how to implement value equality correctly in sealed types and unsealed class hierarchies.

  • page csharp/language-reference/operators/equality-operators
  • updated 08/19/2026

The == (equality) and != (inequality) operators check if their operands are equal or not. Value types are equal when their contents are equal. Reference types are equal when the two variables refer to the same storage.


ms.custom: "updateeachrelease"

The C# language reference documents the most recently released version of the C# language. It also contains initial documentation for features in public previews for the upcoming language release.

The documentation identifies any feature first introduced in the last three versions of the language or in current public previews.

You can use the is pattern matching operator as an alternative to an == test when you test against a constant value. The is operator uses the default equality semantics for all value and reference types.

Equality operator ==

The equality operator == returns true if its operands are equal, false otherwise.

Value types equality

Operands of the built-in value types are equal if their values are equal:

sample · csharpdocs/csharp/language-reference/operators/snippets/shared/EqualityOperators.csValueTypesEquality

csharp
namespace operators;

public static class EqualityOperators
{
    public static void Examples()
    {
        Console.WriteLine("Value types:");
        ValueTypesEquality();

        Console.WriteLine("Strings:");
        StringEquality();

        Console.WriteLine("Reference types:");
        ReferenceTypesEquality.Main();

        Console.WriteLine("Record types:");
        RecordTypesEquality.Main();

        Console.WriteLine("Delegate types:");
        DelegateEquality();
        IdenticalLambdasNotEqual();

        Console.WriteLine("Non equality:");
        NonEquality();
    }

    private static void ValueTypesEquality()
    {
        // <SnippetValueTypesEquality>
        int a = 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a == b);  // output: True

        char c1 = 'a';
        char c2 = 'A';
        Console.WriteLine(c1 == c2);  // output: False
        Console.WriteLine(c1 == char.ToLower(c2));  // output: True
        // </SnippetValueTypesEquality>
    }

    private static void StringEquality()
    {
        // <SnippetStringEquality>
        string s1 = "hello!";
        string s2 = "HeLLo!";
        Console.WriteLine(s1 == s2.ToLower());  // output: True

        string s3 = "Hello!";
        Console.WriteLine(s1 == s3);  // output: False
        // </SnippetStringEquality>
    }

    // Rationale for the structure of the next snippet.
    // A method cannot contain a class definition. Thus, a standard way to include snippet doesn't work.
    // We want snippets to be interactive. Thus, the whole snippet has a structure of the console program.
    // (Running the code without the ReferenceTypesEquality class doesn't produce any output in the interactive Output control.)

    // <SnippetReferenceTypesEquality>
    public class ReferenceTypesEquality
    {
        public class MyClass
        {
            private int id;

            public MyClass(int id) => this.id = id;
        }

        public static void Main()
        {
            var a = new MyClass(1);
            var b = new MyClass(1);
            var c = a;
            Console.WriteLine(a == b);  // output: False
            Console.WriteLine(a == c);  // output: True
        }
    }
    // </SnippetReferenceTypesEquality>

    private static void NonEquality()
    {
        // <SnippetNonEquality>
        int a = 1 + 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a != b);  // output: True

        string s1 = "Hello";
        string s2 = "Hello";
        Console.WriteLine(s1 != s2);  // output: False

        object o1 = 1;
        object o2 = 1;
        Console.WriteLine(o1 != o2);  // output: True
        // </SnippetNonEquality>
    }

    private static void DelegateEquality()
    {
        // <SnippetDelegateEquality>
        Action a = () => Console.WriteLine("a");

        Action b = a + a;
        Action c = a + a;
        Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
        Console.WriteLine(b == c);  // output: True
        // </SnippetDelegateEquality>

        // <SnippetCheckReceiver>
        var o1 = new object();
        var o2 = new object();
        var d1 = o1.ToString;
        var d2 = o2.ToString;
        Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
        Console.WriteLine(d1 == d2);  // output: False (different receivers)
        // </SnippetCheckReceiver>
    }

    private static void IdenticalLambdasNotEqual()
    {
        // <SnippetIdenticalLambdas>
        Action a = () => Console.WriteLine("a");
        Action b = () => Console.WriteLine("a");

        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a + b == a + b);  // output: True
        Console.WriteLine(b + a == a + b);  // output: False
        // </SnippetIdenticalLambdas>
    }

    // <RecordTypesEquality>
    public class RecordTypesEquality
    {
        public record Point(int X, int Y, string Name);
        public record TaggedNumber(int Number, List<string> Tags);

        public static void Main()
        {
            var p1 = new Point(2, 3, "A");
            var p2 = new Point(1, 3, "B");
            var p3 = new Point(2, 3, "A");

            Console.WriteLine(p1 == p2);  // output: False
            Console.WriteLine(p1 == p3);  // output: True

            var n1 = new TaggedNumber(2, new List<string>() { "A" });
            var n2 = new TaggedNumber(2, new List<string>() { "A" });
            Console.WriteLine(n1 == n2);  // output: False
        }
    }
    // </RecordTypesEquality>
}

Two operands of the same enum type are equal if the corresponding values of the underlying integral type are equal.

User-defined struct types don't support the == operator by default. To support the == operator, a user-defined struct must overload it.

C# tuples have built-in support for the == and != operators. For more information, see the Tuple equality section of the Tuple types article.

Reference types equality

By default, reference-type operands, excluding records, are equal if they refer to the same object:

sample · csharpdocs/csharp/language-reference/operators/snippets/shared/EqualityOperators.csReferenceTypesEquality

csharp
namespace operators;

public static class EqualityOperators
{
    public static void Examples()
    {
        Console.WriteLine("Value types:");
        ValueTypesEquality();

        Console.WriteLine("Strings:");
        StringEquality();

        Console.WriteLine("Reference types:");
        ReferenceTypesEquality.Main();

        Console.WriteLine("Record types:");
        RecordTypesEquality.Main();

        Console.WriteLine("Delegate types:");
        DelegateEquality();
        IdenticalLambdasNotEqual();

        Console.WriteLine("Non equality:");
        NonEquality();
    }

    private static void ValueTypesEquality()
    {
        // <SnippetValueTypesEquality>
        int a = 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a == b);  // output: True

        char c1 = 'a';
        char c2 = 'A';
        Console.WriteLine(c1 == c2);  // output: False
        Console.WriteLine(c1 == char.ToLower(c2));  // output: True
        // </SnippetValueTypesEquality>
    }

    private static void StringEquality()
    {
        // <SnippetStringEquality>
        string s1 = "hello!";
        string s2 = "HeLLo!";
        Console.WriteLine(s1 == s2.ToLower());  // output: True

        string s3 = "Hello!";
        Console.WriteLine(s1 == s3);  // output: False
        // </SnippetStringEquality>
    }

    // Rationale for the structure of the next snippet.
    // A method cannot contain a class definition. Thus, a standard way to include snippet doesn't work.
    // We want snippets to be interactive. Thus, the whole snippet has a structure of the console program.
    // (Running the code without the ReferenceTypesEquality class doesn't produce any output in the interactive Output control.)

    // <SnippetReferenceTypesEquality>
    public class ReferenceTypesEquality
    {
        public class MyClass
        {
            private int id;

            public MyClass(int id) => this.id = id;
        }

        public static void Main()
        {
            var a = new MyClass(1);
            var b = new MyClass(1);
            var c = a;
            Console.WriteLine(a == b);  // output: False
            Console.WriteLine(a == c);  // output: True
        }
    }
    // </SnippetReferenceTypesEquality>

    private static void NonEquality()
    {
        // <SnippetNonEquality>
        int a = 1 + 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a != b);  // output: True

        string s1 = "Hello";
        string s2 = "Hello";
        Console.WriteLine(s1 != s2);  // output: False

        object o1 = 1;
        object o2 = 1;
        Console.WriteLine(o1 != o2);  // output: True
        // </SnippetNonEquality>
    }

    private static void DelegateEquality()
    {
        // <SnippetDelegateEquality>
        Action a = () => Console.WriteLine("a");

        Action b = a + a;
        Action c = a + a;
        Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
        Console.WriteLine(b == c);  // output: True
        // </SnippetDelegateEquality>

        // <SnippetCheckReceiver>
        var o1 = new object();
        var o2 = new object();
        var d1 = o1.ToString;
        var d2 = o2.ToString;
        Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
        Console.WriteLine(d1 == d2);  // output: False (different receivers)
        // </SnippetCheckReceiver>
    }

    private static void IdenticalLambdasNotEqual()
    {
        // <SnippetIdenticalLambdas>
        Action a = () => Console.WriteLine("a");
        Action b = () => Console.WriteLine("a");

        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a + b == a + b);  // output: True
        Console.WriteLine(b + a == a + b);  // output: False
        // </SnippetIdenticalLambdas>
    }

    // <RecordTypesEquality>
    public class RecordTypesEquality
    {
        public record Point(int X, int Y, string Name);
        public record TaggedNumber(int Number, List<string> Tags);

        public static void Main()
        {
            var p1 = new Point(2, 3, "A");
            var p2 = new Point(1, 3, "B");
            var p3 = new Point(2, 3, "A");

            Console.WriteLine(p1 == p2);  // output: False
            Console.WriteLine(p1 == p3);  // output: True

            var n1 = new TaggedNumber(2, new List<string>() { "A" });
            var n2 = new TaggedNumber(2, new List<string>() { "A" });
            Console.WriteLine(n1 == n2);  // output: False
        }
    }
    // </RecordTypesEquality>
}

As the preceding example shows, user-defined reference types support the == operator by default. However, a reference type can overload the == operator. If a reference type overloads the == operator, use the System.Object.ReferenceEquals method to check if two references of that type refer to the same object.

Record types equality

Record types support the == and != operators that by default provide value equality semantics. That is, two record operands are equal when both of them are null or corresponding values of all fields and automatically implemented properties are equal.

sample · csharpdocs/csharp/language-reference/operators/snippets/shared/EqualityOperators.csRecordTypesEquality

csharp
namespace operators;

public static class EqualityOperators
{
    public static void Examples()
    {
        Console.WriteLine("Value types:");
        ValueTypesEquality();

        Console.WriteLine("Strings:");
        StringEquality();

        Console.WriteLine("Reference types:");
        ReferenceTypesEquality.Main();

        Console.WriteLine("Record types:");
        RecordTypesEquality.Main();

        Console.WriteLine("Delegate types:");
        DelegateEquality();
        IdenticalLambdasNotEqual();

        Console.WriteLine("Non equality:");
        NonEquality();
    }

    private static void ValueTypesEquality()
    {
        // <SnippetValueTypesEquality>
        int a = 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a == b);  // output: True

        char c1 = 'a';
        char c2 = 'A';
        Console.WriteLine(c1 == c2);  // output: False
        Console.WriteLine(c1 == char.ToLower(c2));  // output: True
        // </SnippetValueTypesEquality>
    }

    private static void StringEquality()
    {
        // <SnippetStringEquality>
        string s1 = "hello!";
        string s2 = "HeLLo!";
        Console.WriteLine(s1 == s2.ToLower());  // output: True

        string s3 = "Hello!";
        Console.WriteLine(s1 == s3);  // output: False
        // </SnippetStringEquality>
    }

    // Rationale for the structure of the next snippet.
    // A method cannot contain a class definition. Thus, a standard way to include snippet doesn't work.
    // We want snippets to be interactive. Thus, the whole snippet has a structure of the console program.
    // (Running the code without the ReferenceTypesEquality class doesn't produce any output in the interactive Output control.)

    // <SnippetReferenceTypesEquality>
    public class ReferenceTypesEquality
    {
        public class MyClass
        {
            private int id;

            public MyClass(int id) => this.id = id;
        }

        public static void Main()
        {
            var a = new MyClass(1);
            var b = new MyClass(1);
            var c = a;
            Console.WriteLine(a == b);  // output: False
            Console.WriteLine(a == c);  // output: True
        }
    }
    // </SnippetReferenceTypesEquality>

    private static void NonEquality()
    {
        // <SnippetNonEquality>
        int a = 1 + 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a != b);  // output: True

        string s1 = "Hello";
        string s2 = "Hello";
        Console.WriteLine(s1 != s2);  // output: False

        object o1 = 1;
        object o2 = 1;
        Console.WriteLine(o1 != o2);  // output: True
        // </SnippetNonEquality>
    }

    private static void DelegateEquality()
    {
        // <SnippetDelegateEquality>
        Action a = () => Console.WriteLine("a");

        Action b = a + a;
        Action c = a + a;
        Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
        Console.WriteLine(b == c);  // output: True
        // </SnippetDelegateEquality>

        // <SnippetCheckReceiver>
        var o1 = new object();
        var o2 = new object();
        var d1 = o1.ToString;
        var d2 = o2.ToString;
        Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
        Console.WriteLine(d1 == d2);  // output: False (different receivers)
        // </SnippetCheckReceiver>
    }

    private static void IdenticalLambdasNotEqual()
    {
        // <SnippetIdenticalLambdas>
        Action a = () => Console.WriteLine("a");
        Action b = () => Console.WriteLine("a");

        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a + b == a + b);  // output: True
        Console.WriteLine(b + a == a + b);  // output: False
        // </SnippetIdenticalLambdas>
    }

    // <RecordTypesEquality>
    public class RecordTypesEquality
    {
        public record Point(int X, int Y, string Name);
        public record TaggedNumber(int Number, List<string> Tags);

        public static void Main()
        {
            var p1 = new Point(2, 3, "A");
            var p2 = new Point(1, 3, "B");
            var p3 = new Point(2, 3, "A");

            Console.WriteLine(p1 == p2);  // output: False
            Console.WriteLine(p1 == p3);  // output: True

            var n1 = new TaggedNumber(2, new List<string>() { "A" });
            var n2 = new TaggedNumber(2, new List<string>() { "A" });
            Console.WriteLine(n1 == n2);  // output: False
        }
    }
    // </RecordTypesEquality>
}

As the preceding example shows, the equality of reference-type members is compared using their specific equality implementations.

String equality

Two string operands are equal when both of them are null or both string instances are of the same length and have identical characters in each character position:

sample · csharpdocs/csharp/language-reference/operators/snippets/shared/EqualityOperators.csStringEquality

csharp
namespace operators;

public static class EqualityOperators
{
    public static void Examples()
    {
        Console.WriteLine("Value types:");
        ValueTypesEquality();

        Console.WriteLine("Strings:");
        StringEquality();

        Console.WriteLine("Reference types:");
        ReferenceTypesEquality.Main();

        Console.WriteLine("Record types:");
        RecordTypesEquality.Main();

        Console.WriteLine("Delegate types:");
        DelegateEquality();
        IdenticalLambdasNotEqual();

        Console.WriteLine("Non equality:");
        NonEquality();
    }

    private static void ValueTypesEquality()
    {
        // <SnippetValueTypesEquality>
        int a = 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a == b);  // output: True

        char c1 = 'a';
        char c2 = 'A';
        Console.WriteLine(c1 == c2);  // output: False
        Console.WriteLine(c1 == char.ToLower(c2));  // output: True
        // </SnippetValueTypesEquality>
    }

    private static void StringEquality()
    {
        // <SnippetStringEquality>
        string s1 = "hello!";
        string s2 = "HeLLo!";
        Console.WriteLine(s1 == s2.ToLower());  // output: True

        string s3 = "Hello!";
        Console.WriteLine(s1 == s3);  // output: False
        // </SnippetStringEquality>
    }

    // Rationale for the structure of the next snippet.
    // A method cannot contain a class definition. Thus, a standard way to include snippet doesn't work.
    // We want snippets to be interactive. Thus, the whole snippet has a structure of the console program.
    // (Running the code without the ReferenceTypesEquality class doesn't produce any output in the interactive Output control.)

    // <SnippetReferenceTypesEquality>
    public class ReferenceTypesEquality
    {
        public class MyClass
        {
            private int id;

            public MyClass(int id) => this.id = id;
        }

        public static void Main()
        {
            var a = new MyClass(1);
            var b = new MyClass(1);
            var c = a;
            Console.WriteLine(a == b);  // output: False
            Console.WriteLine(a == c);  // output: True
        }
    }
    // </SnippetReferenceTypesEquality>

    private static void NonEquality()
    {
        // <SnippetNonEquality>
        int a = 1 + 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a != b);  // output: True

        string s1 = "Hello";
        string s2 = "Hello";
        Console.WriteLine(s1 != s2);  // output: False

        object o1 = 1;
        object o2 = 1;
        Console.WriteLine(o1 != o2);  // output: True
        // </SnippetNonEquality>
    }

    private static void DelegateEquality()
    {
        // <SnippetDelegateEquality>
        Action a = () => Console.WriteLine("a");

        Action b = a + a;
        Action c = a + a;
        Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
        Console.WriteLine(b == c);  // output: True
        // </SnippetDelegateEquality>

        // <SnippetCheckReceiver>
        var o1 = new object();
        var o2 = new object();
        var d1 = o1.ToString;
        var d2 = o2.ToString;
        Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
        Console.WriteLine(d1 == d2);  // output: False (different receivers)
        // </SnippetCheckReceiver>
    }

    private static void IdenticalLambdasNotEqual()
    {
        // <SnippetIdenticalLambdas>
        Action a = () => Console.WriteLine("a");
        Action b = () => Console.WriteLine("a");

        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a + b == a + b);  // output: True
        Console.WriteLine(b + a == a + b);  // output: False
        // </SnippetIdenticalLambdas>
    }

    // <RecordTypesEquality>
    public class RecordTypesEquality
    {
        public record Point(int X, int Y, string Name);
        public record TaggedNumber(int Number, List<string> Tags);

        public static void Main()
        {
            var p1 = new Point(2, 3, "A");
            var p2 = new Point(1, 3, "B");
            var p3 = new Point(2, 3, "A");

            Console.WriteLine(p1 == p2);  // output: False
            Console.WriteLine(p1 == p3);  // output: True

            var n1 = new TaggedNumber(2, new List<string>() { "A" });
            var n2 = new TaggedNumber(2, new List<string>() { "A" });
            Console.WriteLine(n1 == n2);  // output: False
        }
    }
    // </RecordTypesEquality>
}

String equality comparisons are case-sensitive ordinal comparisons. For more information about string comparison, see How to compare strings in C#.

Delegate equality

Two delegate operands of the same run-time type are equal when both of them are null or their invocation lists are the same length and have equal entries in each position:

sample · csharpdocs/csharp/language-reference/operators/snippets/shared/EqualityOperators.csDelegateEquality

csharp
namespace operators;

public static class EqualityOperators
{
    public static void Examples()
    {
        Console.WriteLine("Value types:");
        ValueTypesEquality();

        Console.WriteLine("Strings:");
        StringEquality();

        Console.WriteLine("Reference types:");
        ReferenceTypesEquality.Main();

        Console.WriteLine("Record types:");
        RecordTypesEquality.Main();

        Console.WriteLine("Delegate types:");
        DelegateEquality();
        IdenticalLambdasNotEqual();

        Console.WriteLine("Non equality:");
        NonEquality();
    }

    private static void ValueTypesEquality()
    {
        // <SnippetValueTypesEquality>
        int a = 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a == b);  // output: True

        char c1 = 'a';
        char c2 = 'A';
        Console.WriteLine(c1 == c2);  // output: False
        Console.WriteLine(c1 == char.ToLower(c2));  // output: True
        // </SnippetValueTypesEquality>
    }

    private static void StringEquality()
    {
        // <SnippetStringEquality>
        string s1 = "hello!";
        string s2 = "HeLLo!";
        Console.WriteLine(s1 == s2.ToLower());  // output: True

        string s3 = "Hello!";
        Console.WriteLine(s1 == s3);  // output: False
        // </SnippetStringEquality>
    }

    // Rationale for the structure of the next snippet.
    // A method cannot contain a class definition. Thus, a standard way to include snippet doesn't work.
    // We want snippets to be interactive. Thus, the whole snippet has a structure of the console program.
    // (Running the code without the ReferenceTypesEquality class doesn't produce any output in the interactive Output control.)

    // <SnippetReferenceTypesEquality>
    public class ReferenceTypesEquality
    {
        public class MyClass
        {
            private int id;

            public MyClass(int id) => this.id = id;
        }

        public static void Main()
        {
            var a = new MyClass(1);
            var b = new MyClass(1);
            var c = a;
            Console.WriteLine(a == b);  // output: False
            Console.WriteLine(a == c);  // output: True
        }
    }
    // </SnippetReferenceTypesEquality>

    private static void NonEquality()
    {
        // <SnippetNonEquality>
        int a = 1 + 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a != b);  // output: True

        string s1 = "Hello";
        string s2 = "Hello";
        Console.WriteLine(s1 != s2);  // output: False

        object o1 = 1;
        object o2 = 1;
        Console.WriteLine(o1 != o2);  // output: True
        // </SnippetNonEquality>
    }

    private static void DelegateEquality()
    {
        // <SnippetDelegateEquality>
        Action a = () => Console.WriteLine("a");

        Action b = a + a;
        Action c = a + a;
        Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
        Console.WriteLine(b == c);  // output: True
        // </SnippetDelegateEquality>

        // <SnippetCheckReceiver>
        var o1 = new object();
        var o2 = new object();
        var d1 = o1.ToString;
        var d2 = o2.ToString;
        Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
        Console.WriteLine(d1 == d2);  // output: False (different receivers)
        // </SnippetCheckReceiver>
    }

    private static void IdenticalLambdasNotEqual()
    {
        // <SnippetIdenticalLambdas>
        Action a = () => Console.WriteLine("a");
        Action b = () => Console.WriteLine("a");

        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a + b == a + b);  // output: True
        Console.WriteLine(b + a == a + b);  // output: False
        // </SnippetIdenticalLambdas>
    }

    // <RecordTypesEquality>
    public class RecordTypesEquality
    {
        public record Point(int X, int Y, string Name);
        public record TaggedNumber(int Number, List<string> Tags);

        public static void Main()
        {
            var p1 = new Point(2, 3, "A");
            var p2 = new Point(1, 3, "B");
            var p3 = new Point(2, 3, "A");

            Console.WriteLine(p1 == p2);  // output: False
            Console.WriteLine(p1 == p3);  // output: True

            var n1 = new TaggedNumber(2, new List<string>() { "A" });
            var n2 = new TaggedNumber(2, new List<string>() { "A" });
            Console.WriteLine(n1 == n2);  // output: False
        }
    }
    // </RecordTypesEquality>
}

sample · csharpdocs/csharp/language-reference/operators/snippets/shared/EqualityOperators.csSnippetCheckReceiver

csharp
namespace operators;

public static class EqualityOperators
{
    public static void Examples()
    {
        Console.WriteLine("Value types:");
        ValueTypesEquality();

        Console.WriteLine("Strings:");
        StringEquality();

        Console.WriteLine("Reference types:");
        ReferenceTypesEquality.Main();

        Console.WriteLine("Record types:");
        RecordTypesEquality.Main();

        Console.WriteLine("Delegate types:");
        DelegateEquality();
        IdenticalLambdasNotEqual();

        Console.WriteLine("Non equality:");
        NonEquality();
    }

    private static void ValueTypesEquality()
    {
        // <SnippetValueTypesEquality>
        int a = 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a == b);  // output: True

        char c1 = 'a';
        char c2 = 'A';
        Console.WriteLine(c1 == c2);  // output: False
        Console.WriteLine(c1 == char.ToLower(c2));  // output: True
        // </SnippetValueTypesEquality>
    }

    private static void StringEquality()
    {
        // <SnippetStringEquality>
        string s1 = "hello!";
        string s2 = "HeLLo!";
        Console.WriteLine(s1 == s2.ToLower());  // output: True

        string s3 = "Hello!";
        Console.WriteLine(s1 == s3);  // output: False
        // </SnippetStringEquality>
    }

    // Rationale for the structure of the next snippet.
    // A method cannot contain a class definition. Thus, a standard way to include snippet doesn't work.
    // We want snippets to be interactive. Thus, the whole snippet has a structure of the console program.
    // (Running the code without the ReferenceTypesEquality class doesn't produce any output in the interactive Output control.)

    // <SnippetReferenceTypesEquality>
    public class ReferenceTypesEquality
    {
        public class MyClass
        {
            private int id;

            public MyClass(int id) => this.id = id;
        }

        public static void Main()
        {
            var a = new MyClass(1);
            var b = new MyClass(1);
            var c = a;
            Console.WriteLine(a == b);  // output: False
            Console.WriteLine(a == c);  // output: True
        }
    }
    // </SnippetReferenceTypesEquality>

    private static void NonEquality()
    {
        // <SnippetNonEquality>
        int a = 1 + 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a != b);  // output: True

        string s1 = "Hello";
        string s2 = "Hello";
        Console.WriteLine(s1 != s2);  // output: False

        object o1 = 1;
        object o2 = 1;
        Console.WriteLine(o1 != o2);  // output: True
        // </SnippetNonEquality>
    }

    private static void DelegateEquality()
    {
        // <SnippetDelegateEquality>
        Action a = () => Console.WriteLine("a");

        Action b = a + a;
        Action c = a + a;
        Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
        Console.WriteLine(b == c);  // output: True
        // </SnippetDelegateEquality>

        // <SnippetCheckReceiver>
        var o1 = new object();
        var o2 = new object();
        var d1 = o1.ToString;
        var d2 = o2.ToString;
        Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
        Console.WriteLine(d1 == d2);  // output: False (different receivers)
        // </SnippetCheckReceiver>
    }

    private static void IdenticalLambdasNotEqual()
    {
        // <SnippetIdenticalLambdas>
        Action a = () => Console.WriteLine("a");
        Action b = () => Console.WriteLine("a");

        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a + b == a + b);  // output: True
        Console.WriteLine(b + a == a + b);  // output: False
        // </SnippetIdenticalLambdas>
    }

    // <RecordTypesEquality>
    public class RecordTypesEquality
    {
        public record Point(int X, int Y, string Name);
        public record TaggedNumber(int Number, List<string> Tags);

        public static void Main()
        {
            var p1 = new Point(2, 3, "A");
            var p2 = new Point(1, 3, "B");
            var p3 = new Point(2, 3, "A");

            Console.WriteLine(p1 == p2);  // output: False
            Console.WriteLine(p1 == p3);  // output: True

            var n1 = new TaggedNumber(2, new List<string>() { "A" });
            var n2 = new TaggedNumber(2, new List<string>() { "A" });
            Console.WriteLine(n1 == n2);  // output: False
        }
    }
    // </RecordTypesEquality>
}

For more information, see the Delegate equality operators section of the C# language specification.

Delegates that come from evaluating semantically identical lambda expressions aren't equal, as the following example shows:

sample · csharpdocs/csharp/language-reference/operators/snippets/shared/EqualityOperators.csIdenticalLambdas

csharp
namespace operators;

public static class EqualityOperators
{
    public static void Examples()
    {
        Console.WriteLine("Value types:");
        ValueTypesEquality();

        Console.WriteLine("Strings:");
        StringEquality();

        Console.WriteLine("Reference types:");
        ReferenceTypesEquality.Main();

        Console.WriteLine("Record types:");
        RecordTypesEquality.Main();

        Console.WriteLine("Delegate types:");
        DelegateEquality();
        IdenticalLambdasNotEqual();

        Console.WriteLine("Non equality:");
        NonEquality();
    }

    private static void ValueTypesEquality()
    {
        // <SnippetValueTypesEquality>
        int a = 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a == b);  // output: True

        char c1 = 'a';
        char c2 = 'A';
        Console.WriteLine(c1 == c2);  // output: False
        Console.WriteLine(c1 == char.ToLower(c2));  // output: True
        // </SnippetValueTypesEquality>
    }

    private static void StringEquality()
    {
        // <SnippetStringEquality>
        string s1 = "hello!";
        string s2 = "HeLLo!";
        Console.WriteLine(s1 == s2.ToLower());  // output: True

        string s3 = "Hello!";
        Console.WriteLine(s1 == s3);  // output: False
        // </SnippetStringEquality>
    }

    // Rationale for the structure of the next snippet.
    // A method cannot contain a class definition. Thus, a standard way to include snippet doesn't work.
    // We want snippets to be interactive. Thus, the whole snippet has a structure of the console program.
    // (Running the code without the ReferenceTypesEquality class doesn't produce any output in the interactive Output control.)

    // <SnippetReferenceTypesEquality>
    public class ReferenceTypesEquality
    {
        public class MyClass
        {
            private int id;

            public MyClass(int id) => this.id = id;
        }

        public static void Main()
        {
            var a = new MyClass(1);
            var b = new MyClass(1);
            var c = a;
            Console.WriteLine(a == b);  // output: False
            Console.WriteLine(a == c);  // output: True
        }
    }
    // </SnippetReferenceTypesEquality>

    private static void NonEquality()
    {
        // <SnippetNonEquality>
        int a = 1 + 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a != b);  // output: True

        string s1 = "Hello";
        string s2 = "Hello";
        Console.WriteLine(s1 != s2);  // output: False

        object o1 = 1;
        object o2 = 1;
        Console.WriteLine(o1 != o2);  // output: True
        // </SnippetNonEquality>
    }

    private static void DelegateEquality()
    {
        // <SnippetDelegateEquality>
        Action a = () => Console.WriteLine("a");

        Action b = a + a;
        Action c = a + a;
        Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
        Console.WriteLine(b == c);  // output: True
        // </SnippetDelegateEquality>

        // <SnippetCheckReceiver>
        var o1 = new object();
        var o2 = new object();
        var d1 = o1.ToString;
        var d2 = o2.ToString;
        Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
        Console.WriteLine(d1 == d2);  // output: False (different receivers)
        // </SnippetCheckReceiver>
    }

    private static void IdenticalLambdasNotEqual()
    {
        // <SnippetIdenticalLambdas>
        Action a = () => Console.WriteLine("a");
        Action b = () => Console.WriteLine("a");

        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a + b == a + b);  // output: True
        Console.WriteLine(b + a == a + b);  // output: False
        // </SnippetIdenticalLambdas>
    }

    // <RecordTypesEquality>
    public class RecordTypesEquality
    {
        public record Point(int X, int Y, string Name);
        public record TaggedNumber(int Number, List<string> Tags);

        public static void Main()
        {
            var p1 = new Point(2, 3, "A");
            var p2 = new Point(1, 3, "B");
            var p3 = new Point(2, 3, "A");

            Console.WriteLine(p1 == p2);  // output: False
            Console.WriteLine(p1 == p3);  // output: True

            var n1 = new TaggedNumber(2, new List<string>() { "A" });
            var n2 = new TaggedNumber(2, new List<string>() { "A" });
            Console.WriteLine(n1 == n2);  // output: False
        }
    }
    // </RecordTypesEquality>
}

Inequality operator !=

The inequality operator != returns true if its operands aren't equal, and false otherwise. For the operands of the built-in types, the expression x != y produces the same result as the expression !(x == y). For more information about type equality, see the Equality operator section.

The following example demonstrates how to use the != operator:

sample · csharpdocs/csharp/language-reference/operators/snippets/shared/EqualityOperators.csNonEquality

csharp
namespace operators;

public static class EqualityOperators
{
    public static void Examples()
    {
        Console.WriteLine("Value types:");
        ValueTypesEquality();

        Console.WriteLine("Strings:");
        StringEquality();

        Console.WriteLine("Reference types:");
        ReferenceTypesEquality.Main();

        Console.WriteLine("Record types:");
        RecordTypesEquality.Main();

        Console.WriteLine("Delegate types:");
        DelegateEquality();
        IdenticalLambdasNotEqual();

        Console.WriteLine("Non equality:");
        NonEquality();
    }

    private static void ValueTypesEquality()
    {
        // <SnippetValueTypesEquality>
        int a = 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a == b);  // output: True

        char c1 = 'a';
        char c2 = 'A';
        Console.WriteLine(c1 == c2);  // output: False
        Console.WriteLine(c1 == char.ToLower(c2));  // output: True
        // </SnippetValueTypesEquality>
    }

    private static void StringEquality()
    {
        // <SnippetStringEquality>
        string s1 = "hello!";
        string s2 = "HeLLo!";
        Console.WriteLine(s1 == s2.ToLower());  // output: True

        string s3 = "Hello!";
        Console.WriteLine(s1 == s3);  // output: False
        // </SnippetStringEquality>
    }

    // Rationale for the structure of the next snippet.
    // A method cannot contain a class definition. Thus, a standard way to include snippet doesn't work.
    // We want snippets to be interactive. Thus, the whole snippet has a structure of the console program.
    // (Running the code without the ReferenceTypesEquality class doesn't produce any output in the interactive Output control.)

    // <SnippetReferenceTypesEquality>
    public class ReferenceTypesEquality
    {
        public class MyClass
        {
            private int id;

            public MyClass(int id) => this.id = id;
        }

        public static void Main()
        {
            var a = new MyClass(1);
            var b = new MyClass(1);
            var c = a;
            Console.WriteLine(a == b);  // output: False
            Console.WriteLine(a == c);  // output: True
        }
    }
    // </SnippetReferenceTypesEquality>

    private static void NonEquality()
    {
        // <SnippetNonEquality>
        int a = 1 + 1 + 2 + 3;
        int b = 6;
        Console.WriteLine(a != b);  // output: True

        string s1 = "Hello";
        string s2 = "Hello";
        Console.WriteLine(s1 != s2);  // output: False

        object o1 = 1;
        object o2 = 1;
        Console.WriteLine(o1 != o2);  // output: True
        // </SnippetNonEquality>
    }

    private static void DelegateEquality()
    {
        // <SnippetDelegateEquality>
        Action a = () => Console.WriteLine("a");

        Action b = a + a;
        Action c = a + a;
        Console.WriteLine(object.ReferenceEquals(b, c));  // output: False
        Console.WriteLine(b == c);  // output: True
        // </SnippetDelegateEquality>

        // <SnippetCheckReceiver>
        var o1 = new object();
        var o2 = new object();
        var d1 = o1.ToString;
        var d2 = o2.ToString;
        Console.WriteLine(object.ReferenceEquals(d1, d2));  // output: False
        Console.WriteLine(d1 == d2);  // output: False (different receivers)
        // </SnippetCheckReceiver>
    }

    private static void IdenticalLambdasNotEqual()
    {
        // <SnippetIdenticalLambdas>
        Action a = () => Console.WriteLine("a");
        Action b = () => Console.WriteLine("a");

        Console.WriteLine(a == b);  // output: False
        Console.WriteLine(a + b == a + b);  // output: True
        Console.WriteLine(b + a == a + b);  // output: False
        // </SnippetIdenticalLambdas>
    }

    // <RecordTypesEquality>
    public class RecordTypesEquality
    {
        public record Point(int X, int Y, string Name);
        public record TaggedNumber(int Number, List<string> Tags);

        public static void Main()
        {
            var p1 = new Point(2, 3, "A");
            var p2 = new Point(1, 3, "B");
            var p3 = new Point(2, 3, "A");

            Console.WriteLine(p1 == p2);  // output: False
            Console.WriteLine(p1 == p3);  // output: True

            var n1 = new TaggedNumber(2, new List<string>() { "A" });
            var n2 = new TaggedNumber(2, new List<string>() { "A" });
            Console.WriteLine(n1 == n2);  // output: False
        }
    }
    // </RecordTypesEquality>
}

Equality in class hierarchies

Records handle inheritance correctly without manual work. The compiler-generated equality checks both runtime type and all declared properties, so it automatically satisfies the symmetry and transitivity requirements. Prefer record over a manual unsealed hierarchy when value equality is the goal.

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

Here is a minimal manual implementation for a value type that can't be a record:

sample · csharpdocs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.csColorDefinition

csharp
// <IEquatableUsage>
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)
// </IEquatableUsage>

// <HierarchyUsage>
Shape circle1 = new Circle("red", 5.0);
Shape circle2 = new Circle("red", 7.0);
Shape circle3 = new Circle("red", 5.0);
Shape shape1  = new Shape("red");

Console.WriteLine(circle1.Equals(circle2)); // => False  (Radius differs)
Console.WriteLine(circle1.Equals(circle3)); // => True
Console.WriteLine(circle1.Equals(shape1));  // => False  (different runtime types)
// </HierarchyUsage>

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

// <HierarchyShapeDefinition>
// Shape is an unsealed base class. Making Equals virtual and guarding with GetType()
// ensures a derived instance is never equal to an instance of a different runtime type.
class Shape : IEquatable<Shape>
{
    public string Color { get; }
    public Shape(string color) => Color = color;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // virtual so derived classes can override and augment the comparison
    public virtual bool Equals(Shape? other) =>
        other is not null &&
        GetType() == other.GetType() &&   // reject different runtime types
        Color == other.Color;

    // GetType() is included because equality requires matching runtime types
    public override int GetHashCode() => HashCode.Combine(GetType(), Color);

    public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null;
    public static bool operator !=(Shape? l, Shape? r) => !(l == r);
}
// </HierarchyShapeDefinition>

// <HierarchyCircleDefinition>
class Circle : Shape
{
    public double Radius { get; }
    public Circle(string color, double radius) : base(color) => Radius = radius;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // Calls base.Equals to verify Color and runtime type, then adds Radius
    public override bool Equals(Shape? other) =>
        other is Circle c && base.Equals(c) && Radius == c.Radius;

    public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius);
}
// </HierarchyCircleDefinition>

// <ColorDefinition>
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);
}
// </ColorDefinition>

The implementation provides three required members: Equals(T?) as the core comparison, override Equals(object?) for object-level calls, and override GetHashCode() so hash-based collections work correctly. HashCode.Combine is a library helper that builds one hash from the same values used by Equals. Implementing System.IEquatable (the Equals(T?) overload) is optional but avoids boxing when callers already have the concrete type.

When you also define == and !=, the language requires them as a pair; warnings CS0660 and CS0661 remind you to keep all four members consistent.

With the three members above in place, Equals reflects value equality, but == still tests identity because no == operator has been declared yet:

sample · csharpdocs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.csIEquatableUsage

csharp
// <IEquatableUsage>
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)
// </IEquatableUsage>

// <HierarchyUsage>
Shape circle1 = new Circle("red", 5.0);
Shape circle2 = new Circle("red", 7.0);
Shape circle3 = new Circle("red", 5.0);
Shape shape1  = new Shape("red");

Console.WriteLine(circle1.Equals(circle2)); // => False  (Radius differs)
Console.WriteLine(circle1.Equals(circle3)); // => True
Console.WriteLine(circle1.Equals(shape1));  // => False  (different runtime types)
// </HierarchyUsage>

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

// <HierarchyShapeDefinition>
// Shape is an unsealed base class. Making Equals virtual and guarding with GetType()
// ensures a derived instance is never equal to an instance of a different runtime type.
class Shape : IEquatable<Shape>
{
    public string Color { get; }
    public Shape(string color) => Color = color;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // virtual so derived classes can override and augment the comparison
    public virtual bool Equals(Shape? other) =>
        other is not null &&
        GetType() == other.GetType() &&   // reject different runtime types
        Color == other.Color;

    // GetType() is included because equality requires matching runtime types
    public override int GetHashCode() => HashCode.Combine(GetType(), Color);

    public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null;
    public static bool operator !=(Shape? l, Shape? r) => !(l == r);
}
// </HierarchyShapeDefinition>

// <HierarchyCircleDefinition>
class Circle : Shape
{
    public double Radius { get; }
    public Circle(string color, double radius) : base(color) => Radius = radius;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // Calls base.Equals to verify Color and runtime type, then adds Radius
    public override bool Equals(Shape? other) =>
        other is Circle c && base.Equals(c) && Radius == c.Radius;

    public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius);
}
// </HierarchyCircleDefinition>

// <ColorDefinition>
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);
}
// </ColorDefinition>

A correct implementation must also satisfy the equivalence contract (assume x, y, and z are non-null):

  1. Reflexive: x.Equals(x) returns true.
  2. Symmetric: x.Equals(y) returns the same value as y.Equals(x).
  3. Transitive: if x.Equals(y) and y.Equals(z) are both true, then x.Equals(z) must be true.
  4. Consistent: successive calls to x.Equals(y) return the same value as long as neither object changes.
  5. Null behavior: x.Equals(null) returns false; x.Equals(y) must not throw when called on a non-null x.

Value equality in an unsealed class hierarchy requires more care than in a sealed class to satisfy the symmetric and transitive rules. The hazard is that IEquatable<T>.Equals(T? other) dispatch follows the declared type (the type written in the variable declaration) of the variable, not its runtime type. If Shape declares a non-virtual Equals(Shape? other), a variable typed as Shape that holds a Circle at runtime invokes Shape.Equals—silently ignoring Circle-specific fields. Two Circle objects with different radii can compare as equal when accessed through a Shape variable.

The correct pattern requires two cooperating requirements: make the typed Equals method virtual so each derived class can extend the comparison, and add a GetType() == other.GetType() guard in the base-class implementation so objects of different runtime types are never considered equal.

Base class implementation

sample · csharpdocs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.csHierarchyShapeDefinition

csharp
// <IEquatableUsage>
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)
// </IEquatableUsage>

// <HierarchyUsage>
Shape circle1 = new Circle("red", 5.0);
Shape circle2 = new Circle("red", 7.0);
Shape circle3 = new Circle("red", 5.0);
Shape shape1  = new Shape("red");

Console.WriteLine(circle1.Equals(circle2)); // => False  (Radius differs)
Console.WriteLine(circle1.Equals(circle3)); // => True
Console.WriteLine(circle1.Equals(shape1));  // => False  (different runtime types)
// </HierarchyUsage>

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

// <HierarchyShapeDefinition>
// Shape is an unsealed base class. Making Equals virtual and guarding with GetType()
// ensures a derived instance is never equal to an instance of a different runtime type.
class Shape : IEquatable<Shape>
{
    public string Color { get; }
    public Shape(string color) => Color = color;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // virtual so derived classes can override and augment the comparison
    public virtual bool Equals(Shape? other) =>
        other is not null &&
        GetType() == other.GetType() &&   // reject different runtime types
        Color == other.Color;

    // GetType() is included because equality requires matching runtime types
    public override int GetHashCode() => HashCode.Combine(GetType(), Color);

    public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null;
    public static bool operator !=(Shape? l, Shape? r) => !(l == r);
}
// </HierarchyShapeDefinition>

// <HierarchyCircleDefinition>
class Circle : Shape
{
    public double Radius { get; }
    public Circle(string color, double radius) : base(color) => Radius = radius;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // Calls base.Equals to verify Color and runtime type, then adds Radius
    public override bool Equals(Shape? other) =>
        other is Circle c && base.Equals(c) && Radius == c.Radius;

    public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius);
}
// </HierarchyCircleDefinition>

// <ColorDefinition>
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);
}
// </ColorDefinition>

Key points:

  • virtual typed Equals: each derived class overrides this method to augment the comparison with its own fields.
  • GetType() guard: GetType() == other.GetType() prevents a Circle from equaling a Shape with the same color, and prevents objects of different derived types from equaling each other.
  • GetHashCode includes GetType(): because two objects are equal only when their runtime types match, GetHashCode must hash the runtime type as well as the data fields. Omitting GetType() here causes incorrect behavior in Dictionary<TKey,TValue> and HashSet<T>.
  • == delegates to Equals: keeps operator and method equality consistent.

Derived class implementation

A derived class that adds fields overrides the typed Equals, casts to its own type, calls base.Equals, then compares its own fields:

sample · csharpdocs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.csHierarchyCircleDefinition

csharp
// <IEquatableUsage>
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)
// </IEquatableUsage>

// <HierarchyUsage>
Shape circle1 = new Circle("red", 5.0);
Shape circle2 = new Circle("red", 7.0);
Shape circle3 = new Circle("red", 5.0);
Shape shape1  = new Shape("red");

Console.WriteLine(circle1.Equals(circle2)); // => False  (Radius differs)
Console.WriteLine(circle1.Equals(circle3)); // => True
Console.WriteLine(circle1.Equals(shape1));  // => False  (different runtime types)
// </HierarchyUsage>

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

// <HierarchyShapeDefinition>
// Shape is an unsealed base class. Making Equals virtual and guarding with GetType()
// ensures a derived instance is never equal to an instance of a different runtime type.
class Shape : IEquatable<Shape>
{
    public string Color { get; }
    public Shape(string color) => Color = color;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // virtual so derived classes can override and augment the comparison
    public virtual bool Equals(Shape? other) =>
        other is not null &&
        GetType() == other.GetType() &&   // reject different runtime types
        Color == other.Color;

    // GetType() is included because equality requires matching runtime types
    public override int GetHashCode() => HashCode.Combine(GetType(), Color);

    public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null;
    public static bool operator !=(Shape? l, Shape? r) => !(l == r);
}
// </HierarchyShapeDefinition>

// <HierarchyCircleDefinition>
class Circle : Shape
{
    public double Radius { get; }
    public Circle(string color, double radius) : base(color) => Radius = radius;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // Calls base.Equals to verify Color and runtime type, then adds Radius
    public override bool Equals(Shape? other) =>
        other is Circle c && base.Equals(c) && Radius == c.Radius;

    public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius);
}
// </HierarchyCircleDefinition>

// <ColorDefinition>
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);
}
// </ColorDefinition>

base.Equals(c) enforces the GetType() guard and checks the shared fields. The cast via other is Circle c fails fast when the argument is a Shape of any other derived type.

Usage through a base-type variable

sample · csharpdocs/csharp/language-reference/operators/snippets/EqualityHierarchies/Program.csHierarchyUsage

csharp
// <IEquatableUsage>
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)
// </IEquatableUsage>

// <HierarchyUsage>
Shape circle1 = new Circle("red", 5.0);
Shape circle2 = new Circle("red", 7.0);
Shape circle3 = new Circle("red", 5.0);
Shape shape1  = new Shape("red");

Console.WriteLine(circle1.Equals(circle2)); // => False  (Radius differs)
Console.WriteLine(circle1.Equals(circle3)); // => True
Console.WriteLine(circle1.Equals(shape1));  // => False  (different runtime types)
// </HierarchyUsage>

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

// <HierarchyShapeDefinition>
// Shape is an unsealed base class. Making Equals virtual and guarding with GetType()
// ensures a derived instance is never equal to an instance of a different runtime type.
class Shape : IEquatable<Shape>
{
    public string Color { get; }
    public Shape(string color) => Color = color;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // virtual so derived classes can override and augment the comparison
    public virtual bool Equals(Shape? other) =>
        other is not null &&
        GetType() == other.GetType() &&   // reject different runtime types
        Color == other.Color;

    // GetType() is included because equality requires matching runtime types
    public override int GetHashCode() => HashCode.Combine(GetType(), Color);

    public static bool operator ==(Shape? l, Shape? r) => l?.Equals(r) ?? r is null;
    public static bool operator !=(Shape? l, Shape? r) => !(l == r);
}
// </HierarchyShapeDefinition>

// <HierarchyCircleDefinition>
class Circle : Shape
{
    public double Radius { get; }
    public Circle(string color, double radius) : base(color) => Radius = radius;

    public override bool Equals(object? obj) => Equals(obj as Shape);

    // Calls base.Equals to verify Color and runtime type, then adds Radius
    public override bool Equals(Shape? other) =>
        other is Circle c && base.Equals(c) && Radius == c.Radius;

    public override int GetHashCode() => HashCode.Combine(GetType(), Color, Radius);
}
// </HierarchyCircleDefinition>

// <ColorDefinition>
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);
}
// </ColorDefinition>

Sealed classes are simpler

You can't subclass a sealed class, so compile-time and runtime types always agree. You don't need the GetType() guard or virtual dispatch. The IEquatable<T> pattern shown in Implement equality yourself when a type can't be a record is correct and complete for a sealed class.

Operator overloadability

You can overload the == and != operators in a user-defined type. If you overload one of these two operators, you must also overload the other operator.

You can't explicitly overload the == and != operators in a record type. To change the behavior of the == and != operators for record type T, implement the System.IEquatable.Equals method with the following signature:

csharp
public virtual bool Equals(T? other);

C# language specification

For more information, see the Relational and type-testing operators section of the C# language specification.

For more information about equality of record types, see the Equality members section of the C# language specification.

See also