CA1036: Override methods on comparable types

Property Value
Rule ID CA1036
Title Override methods on comparable types
Category Design
Fix is breaking or non-breaking Non-breaking
Enabled by default in .NET 8 No

Cause

A type implements the System.IComparable interface and does not override System.Object.Equals or does not overload the language-specific operator for equality, inequality, less-than, or greater-than. The rule does not report a violation if the type inherits only an implementation of the interface.

By default, this rule only looks at externally visible types, but this is configurable.

Rule description

Types that define a custom sort order implement the IComparable interface. The CompareTo method returns an integer value that indicates the correct sort order for two instances of the type. This rule identifies types that set a sort order. Setting a sort order implies that the ordinary meaning of equality, inequality, less-than, and greater-than don't apply. When you provide an implementation of IComparable, you must usually also override Equals so that it returns values that are consistent with CompareTo. If you override Equals and are coding in a language that supports operator overloads, you should also provide operators that are consistent with Equals.

How to fix violations

To fix a violation of this rule, override Equals. If your programming language supports operator overloading, supply the following operators:

  • op_Equality
  • op_Inequality
  • op_LessThan
  • op_GreaterThan
// In C#, implement these operators.
public static bool operator ==(SampleClass? one, SampleClass? other) { }
public static bool operator !=(SampleClass? one, SampleClass? other) { }
public static bool operator <(SampleClass? one, SampleClass? other) { }
public static bool operator >(SampleClass? one, SampleClass? other) { }
' In Visual Basic, implement these operators.

Public Shared Operator =(one As SampleClass, other As SampleClass) As Boolean
    ...
End Operator

Public Shared Operator <>(one As SampleClass, other As SampleClass) As Boolean
    ...
End Operator

Public Shared Operator <(one As SampleClass, other As SampleClass) As Boolean
    ...
End Operator

Public Shared Operator >(one As SampleClass, other As SampleClass) As Boolean
    ...
End Operator

When to suppress warnings

It's safe to suppress a warning from rule CA1036 when the violation is caused by missing operators and your programming language does not support operator overloading. If you determine that implementing the operators does not make sense in your app context, it's also safe to suppress a warning from this rule when it fires on equality operators other than op_Equality. However, you should always override op_Equality and the == operator if you override Object.Equals.

Suppress a warning

If you just want to suppress a single violation, add preprocessor directives to your source file to disable and then re-enable the rule.

#pragma warning disable CA1036
// The code that's violating the rule is on this line.
#pragma warning restore CA1036

To disable the rule for a file, folder, or project, set its severity to none in the configuration file.

[*.{cs,vb}]
dotnet_diagnostic.CA1036.severity = none

For more information, see How to suppress code analysis warnings.

Configure code to analyze

Use the following option to configure which parts of your codebase to run this rule on.

You can configure this option for just this rule, for all rules it applies to, or for all rules in this category (Design) that it applies to. For more information, see Code quality rule configuration options.

Include specific API surfaces

You can configure which parts of your codebase to run this rule on, based on their accessibility. For example, to specify that the rule should run only against the non-public API surface, add the following key-value pair to an .editorconfig file in your project:

dotnet_code_quality.CAXXXX.api_surface = private, internal

Examples

The following code contains a type that correctly implements IComparable. Code comments identify the methods that satisfy various rules that are related to Equals and the IComparable interface.

// Valid ratings are between A and C.
// A is the highest rating; it is greater than any other valid rating.
// C is the lowest rating; it is less than any other valid rating.

public class RatingInformation : IComparable, IComparable<RatingInformation>
{
    public string Rating { get; private set; }

    public RatingInformation(string rating)
    {
        ArgumentNullException.ThrowIfNull(rating);

        string v = rating.ToUpper(CultureInfo.InvariantCulture);
        if (v.Length != 1 ||
            string.Compare(v, "C", StringComparison.Ordinal) > 0 ||
            string.Compare(v, "A", StringComparison.Ordinal) < 0)
        {
            throw new ArgumentException("Invalid rating value was specified.", nameof(rating));
        }

        Rating = v;
    }

    public int CompareTo(object? obj)
    {
        if (obj == null)
        {
            return 1;
        }

        if (obj is RatingInformation other)
        {
            return CompareTo(other);
        }

        throw new ArgumentException("A RatingInformation object is required for comparison.", nameof(obj));
    }

    public int CompareTo(RatingInformation? other)
    {
        if (other is null)
        {
            return 1;
        }

        // Ratings compare opposite to normal string order,
        // so reverse the value returned by String.CompareTo.
        return -string.Compare(Rating, other.Rating, StringComparison.OrdinalIgnoreCase);
    }

    public static int Compare(RatingInformation left, RatingInformation right)
    {
        if (object.ReferenceEquals(left, right))
        {
            return 0;
        }
        if (left is null)
        {
            return -1;
        }
        return left.CompareTo(right);
    }

    // Omitting Equals violates rule: OverrideMethodsOnComparableTypes.
    public override bool Equals(object? obj)
    {
        if (obj is RatingInformation other)
        {
            return CompareTo(other) == 0;
        }

        return false;
    }

    // Omitting getHashCode violates rule: OverrideGetHashCodeOnOverridingEquals.
    public override int GetHashCode()
    {
        char[] c = Rating.ToCharArray();
        return (int)c[0];
    }

    // Omitting any of the following operator overloads
    // violates rule: OverrideMethodsOnComparableTypes.
    public static bool operator ==(RatingInformation left, RatingInformation right)
    {
        if (left is null)
        {
            return right is null;
        }
        return left.Equals(right);
    }
    public static bool operator !=(RatingInformation left, RatingInformation right)
    {
        return !(left == right);
    }
    public static bool operator <(RatingInformation left, RatingInformation right)
    {
        return (Compare(left, right) < 0);
    }
    public static bool operator >(RatingInformation left, RatingInformation right)
    {
        return (Compare(left, right) > 0);
    }
}
Imports System.Globalization

Public Class RatingInformation
    Implements IComparable
    Implements IComparable(Of RatingInformation)

    Public Sub New(rating As String)
        ArgumentNullException.ThrowIfNull(rating)

        Dim v As String = rating.ToUpper(CultureInfo.InvariantCulture)
        If (v.Length <> 1 Or
            String.Compare(v, "C", StringComparison.Ordinal) > 0 Or
            String.Compare(v, "A", StringComparison.Ordinal) < 0) Then
            Throw New ArgumentException("Invalid rating value was specified.", NameOf(rating))
        End If

        Me.Rating = v
    End Sub

    Public ReadOnly Property Rating As String

    Public Function CompareTo(obj As Object) As Integer Implements IComparable.CompareTo
        If (obj Is Nothing) Then Return 1
        If (TypeOf obj IsNot RatingInformation) Then Return 0
        Dim other As RatingInformation = DirectCast(obj, RatingInformation)
        Return CompareTo(other)
    End Function

    Public Function CompareTo(other As RatingInformation) As Integer Implements IComparable(Of RatingInformation).CompareTo
        If (other Is Nothing) Then Return 1
        ' Ratings compare opposite To normal String order,
        ' so reverse the value returned by String.CompareTo.
        Return -String.Compare(Rating, other.Rating, StringComparison.OrdinalIgnoreCase)
    End Function

    Public Shared Operator =(one As RatingInformation, other As RatingInformation) As Boolean
        If (one Is Nothing) Then Return (other Is Nothing)
        If (other Is Nothing) Then Return False
        Return (one.Rating = other.Rating)
    End Operator

    Public Shared Operator <>(one As RatingInformation, other As RatingInformation) As Boolean
        If (one Is Nothing) Then Return (other IsNot Nothing)
        If (other Is Nothing) Then Return True
        Return (one.Rating <> other.Rating)
    End Operator

    Public Shared Operator <(one As RatingInformation, other As RatingInformation) As Boolean
        If (one Is Nothing) Then Return (other IsNot Nothing)
        If (other Is Nothing) Then Return False
        Return (one.Rating < other.Rating)
    End Operator

    Public Shared Operator >(one As RatingInformation, other As RatingInformation) As Boolean
        If (one Is Nothing) Then Return False
        If (other Is Nothing) Then Return True
        Return (one.Rating > other.Rating)
    End Operator

    Public Overrides Function Equals(obj As Object) As Boolean
        If ReferenceEquals(Me, obj) Then
            Return True
        End If

        If obj Is Nothing Then
            Return False
        End If

        Throw New NotImplementedException()
    End Function

    Public Overrides Function GetHashCode() As Integer
        Throw New NotImplementedException()
    End Function
End Class

The following application code tests the behavior of the IComparable implementation that was shown earlier.

public class TestCompare
{
    public static void Main1036(params string[] args)
    {
        if (args.Length < 2)
        {
            return;
        }
        RatingInformation r1 = new(args[0]);
        RatingInformation r2 = new(args[1]);
        string answer;

        if (r1.CompareTo(r2) > 0)
            answer = "greater than";
        else if (r1.CompareTo(r2) < 0)
            answer = "less than";
        else
            answer = "equal to";

        Console.WriteLine("{0} is {1} {2}", r1.Rating, answer, r2.Rating);
    }
}
Public Class TestCompare
    Public Shared Sub Main1036(ByVal args As String())
        If (args.Length < 2) Then
            Return
        End If
        Dim r1 As New RatingInformation(args(0))
        Dim r2 As New RatingInformation(args(1))
        Dim answer As String

        If (r1.CompareTo(r2) > 0) Then
            answer = "greater than"
        ElseIf (r1.CompareTo(r2) < 0) Then
            answer = "less than"
        Else
            answer = "equal to"
        End If

        Console.WriteLine("{0} is {1} {2}", r1.Rating, answer, r2.Rating)
    End Sub
End Class

See also