在 C# 11 中是否有可能自动实现 IEquatable<T> 和 IComparable<T> 接口?

5

在编写 C# 代码时,我倾向于编写许多实现了 IEquatable<T>IComparable<T> 或两者都实现的值类型对象。

为了这个提案,假设我正在编写一个名为 Int256 的虚构结构体,它具有可比较和可相等的价值语义,例如:

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;
    }
}

假设我创建了一些其他具有相同语义的虚构结构体,例如UInt256Decimal256。虽然微不足道,但这些运算符对于每个值类型对象都变得繁琐。

最近,我一直在研究C# 11的新语言特性,特别是静态接口方法,我认为这在很大程度上使新的通用数学接口成为可能。考虑到这一点,我可以向我的实现添加一些附加接口,特别是IEqualityOperators<TSelf, TOther, TResult>IComparisonOperators<TSelf, TOther, TResult>,例如:

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

为了完整起见,在这里没有必要实现IEqualityOperators<TSelf, TOther, TResult>,因为它们已经被IComparisonOperators<TSelf, TOther, TResult>扩展了。

归根结底,这并不能真正解决问题。所有这些接口所做的只是确保操作符被实现。

我的建议是是否可以设计出一些接口,能够自动实现IEquatable<T>IComparable<T>相关的一些典型样板代码,特别是操作符:==!=>>=<<=;例如:

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;
    }
}

这里的意图是实现者只需要实现bool Equals(T other)int CompareTo(T other),但由于运算符已经在接口上实现,所以它们自动获得了运算符的实现。
以我的示例为例,代码可能如下所示:
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);
    }
}

但我仍然可以使用运算符; 例如:

Int256 a = 123;
Int256 b = 456;

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

然而,有一个问题。

虽然这些接口IAutoEquatable<T>IAutoComparable<T>包含了运算符的实现,但我仍然需要在Int256中实现它们。

问题

  1. 为什么接口中的虚拟默认实现仍然需要实现?即为什么Int256不能使用默认实现?
  2. 未来的C#版本是否可能解决这个问题,以便我们可以使用它来减轻编写样板代码的需求?

在此与C#语言设计团队讨论:https://github.com/dotnet/csharplang/discussions/7032

1个回答

1
答案似乎是“是和不是”。我无法精确解释为什么,但似乎没有有效的方法来做到这一点。
您可以通过以下方式在接口中“自动”实现IEqualityOperators<T, T, bool>
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;
    }
}

问题在于它不会被调用,即使 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());
}

如果最后一个问题,我可以尝试解释一下,因为类不会从其接口继承成员(正如例如在默认接口成员提案规范中所解释的那样),但对于以下问题,我甚至没有更少的解释:

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

所以个人建议使用partial类和源代码生成器


1
非常感谢您提供如此详细的答案。我已经向C#语言设计团队提出了这个问题。如果您感兴趣,可以在问题中查看链接。 - Matthew Layton
运算符是静态的,因此不能被继承。 - Olivier Jacot-Descombes
1
@OlivierJacot-Descombes 是的,很有道理。尽管在这种情况下,这将是一个不错的功能。 - Guru Stron
该功能可能可以使用提议的角色和扩展来实现。 - Olivier Jacot-Descombes
1
运算符仅在通用上下文中由编译器使用。当类型不是通用参数时,编译器会解析为 Foo.op_Equality(如果找到),否则解析为 Object.ReferenceEquals。这是一种非常反直觉的行为。 - JL0PD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接