Search code examples
c#.net-7.0default-interface-memberc#-11.0

Is it possible to auto-implement IEquatable<T> and IComparable<T> in C# 11?


When writing C#, I tend to write a lot of value type objects that implement IEquatable<T>, IComparable<T>, or both.

For the sake of this proposal, let's assume that I'm writing a fictitious struct called Int256 with equatable and comparable value semantics; for example:

public readonly struct Int256 : IEquatable<Int256>, IComparable<Int256>, IComparable
{
    public bool Equals(Int256 other)
    {
        // TODO : is this equal to other?
    }

    public int CompareTo(Int256 other)
    {
        // TODO : how does this compare to other?
    }

    public int CompareTo(object? obj)
    {
        if (obj is null) return 1;
        if (obj is not Int256 int256) throw new ArgumentException("Obj must be of type Int256.");
        return CompareTo(int256);
    }

    public static bool operator ==(Int256 left, Int256 right)
    {
        return Equals(left, right);
    }
    
    public static bool operator !=(Int256 left, Int256 right)
    {
        return !Equals(left, right);
    }
    
    public static bool operator >(Int256 left, Int256 right)
    {
        return left.CompareTo(right) is 1;
    }
    
    public static bool operator >=(Int256 left, Int256 right)
    {
        return left.CompareTo(right) is 1 or 0;
    }
    
    public static bool operator <(Int256 left, Int256 right)
    {
        return left.CompareTo(right) is -1;
    }
    
    public static bool operator <=(Int256 left, Int256 right)
    {
        return left.CompareTo(right) is -1 or 0;
    }
}

Let's also assume that I were to create some other fictitious structs with the same semantics; for example UInt256 and Decimal256. While trivial, those operators become tedious to implement for every value type object.

Recently, I've been looking at C# 11's new language features, specifically static interface methods, which I believe is largely what makes the new generic math interfaces possible. With that in mind, I could add some additional interfaces to my implementation, specifically IEqualityOperators<TSelf, TOther, TResult> and IComparisonOperators<TSelf, TOther, TResult>; for example:

public readonly struct Int256 : IEquatable<Int256>, IComparable<Int256>, IComparable, IComparisonOperators<Int256, Int256, bool>
{
  ...
}

For completeness, there is no need to implement IEqualityOperators<TSelf, TOther, TResult> here, as they are extended by IComparisonOperators<TSelf, TOther, TResult> anyway.

Ultimately, this doesn't really solve the problem. All these interfaces do is ensure that the operators are implemented.

My proposal is whether interfaces could be designed that auto-implement some of the typical boilerplate code associated with IEquatable<T> and IComparable<T>, specifically the operators: ==, !=, >, >=, <, <=; for example:

IAutoEquatable<T>

public interface IAutoEquatable<T> : IEquatable<T>, IEqualityOperators<T, T, bool> where T : IAutoEquatable<T>
{
    // Auto-implemented boilerplate.
    static virtual bool operator ==(T? left, T? right)
    {
        return Equals(left, right);
    }
    
    // Auto-implemented boilerplate.
    static virtual bool operator !=(T? left, T? right)
    {
        return !Equals(left, right);
    }
}

IAutoComparable<T>

public interface IAutoComparable<T> : IComparable<T>, IComparable, IComparisonOperators<T, T, bool> where T : IAutoComparable<T>
{
    // Auto-implemented boilerplate.
    static virtual bool operator ==(T? left, T? right)
    {
        return Equals(left, right);
    }

    // Auto-implemented boilerplate.
    static virtual bool operator !=(T? left, T? right)
    {
        return !Equals(left, right);
    }

    // Auto-implemented boilerplate.
    static virtual bool operator >(T left, T right)
    {
        return left.CompareTo(right) is 1;
    }

    // Auto-implemented boilerplate.
    static virtual bool operator >=(T left, T right)
    {
        return left.CompareTo(right) is 1 or 0;
    }

    // Auto-implemented boilerplate.
    static virtual bool operator <(T left, T right)
    {
        return left.CompareTo(right) is -1;
    }

    // Auto-implemented boilerplate.
    static virtual bool operator <=(T left, T right)
    {
        return left.CompareTo(right) is -1 or 0;
    }
}

The intention here is that the implementor would only require implementation of bool Equals(T other) and int CompareTo(T other) respectively, but, given that the operators are implemented on the interface, they get the operators for free!

Given my Int256 example, it might look something like this:

public readonly struct Int256 : IEquatable<Int256>, IComparable<Int256>, IComparable
{
    public bool Equals(Int256 other)
    {
        // TODO : is this equal to other?
    }

    public int CompareTo(Int256 other)
    {
        // TODO : how does this compare to other?
    }

    public int CompareTo(object? obj)
    {
        if (obj is null) return 1;
        if (obj is not Int256 int256) throw new ArgumentException("Obj must be of type Int256.");
        return CompareTo(int256);
    }
}

But I would still be able to use the operators with it; for example:

Int256 a = 123;
Int256 b = 456;

a == b; // False
a != b; // True
a > b;  // False
a >= b; // False
a < b;  // True
a <= b; // True

There is a problem, however.

Whilst those interfaces IAutoEquatable<T> and IAutoComparable<T> contain implementations for the operators, I'm still expected to implement them in Int256.

Questions

  1. Why do virtual default implementations in interfaces still require implementation? i.e. why doesn't Int256 just use the default implementation?
  2. Might it be possible for a future version of C# to address this issue, such that we can use it to alleviate the need to write boilerplate code?

Raised here with the C# language design team: https://github.com/dotnet/csharplang/discussions/7032


Solution

  • Answer seems to be "yes and no". I can't explain exactly why, but it seems that there is no way to meaningfully do that.

    You can "auto" implement the IEqualityOperators<T, T, bool> in the interface the following way:

    public interface IAutoEquatable<T> : IEquatable<T>, IEqualityOperators<T, T, bool> where T : IAutoEquatable<T>
    {
        static bool IEqualityOperators<T, T, bool>.operator ==(T? left, T? right)
        {
            Console.Write("ieop.== ");
            if (ReferenceEquals(left, null) && ReferenceEquals(right, null))
            {
                return true;
            }
    
            if (ReferenceEquals(left, null) || ReferenceEquals(right, null))
            {
                return false;
            }
    
            return left.Equals(right);
        }
    
        static bool IEqualityOperators<T, T, bool>.operator !=(T? left, T? right)
        {
            return !(left == right);
        }
    }
    
    class Foo : IAutoEquatable<Foo>
    {
        public bool Equals(Foo? other)
        {
            Console.Write("eq ");
            return true;
        }
    }
    

    The problem is that it will not be called for Foo == Foo:

    Do<Foo>(); // prints "ieop.== eq True"
    Do1<Foo>(); // prints "ieop.== eq True"
    Console.WriteLine(new Foo() == new Foo()); // prints "False"
    
    void Do<T>() where T : IAutoEquatable<T>, new()
    {
        Console.WriteLine(new T() == new T());
    }
    
    void Do1<T>() where T : IEqualityOperators<T, T, bool>, new()
    {
        Console.WriteLine(new T() == new T());
    }
    

    And if the last one can be (somewhat) explained by the fact that a class does not inherit members from its interfaces (as explained for example in the default interface members proposal spec), but for the following I have even less explanations:

    IAutoEquatable<Foo> foo1 = new Foo(); // or IEqualityOperators<Foo, Foo, bool> for both
    IAutoEquatable<Foo> foo2 = new Foo();
    Console.WriteLine(foo1 == foo2); // prints "False"
    

    So personally I would suggest to go with partial classes and source generators.