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

对于这个提议,让我们假设我正在编写一个名为Int256的虚构 struct ,该 struct 具有相等和可比较的值语义;例如:

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

我们还假设我要创建一些具有相同语义的其他虚构的 struct ;例如,UInt256Decimal256.虽然这些运算符很简单,但对于每个值类型对象来说,实现这些运算符变得单调乏味.

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

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

一百零二

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

我的建议是,是否可以将接口设计为与IEquatable<T>IComparable<T>相关的典型样板代码的auto-implement some,特别是运算符:==!=>>=<<=;例如:

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),但是,鉴于操作符是在接口上实现的,他们免费获得操作符!

给出我的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);
    }
}

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

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中实现它们.

Questions

  1. 为什么接口中的virtual个默认实现仍然需要实现?也就是说,为什么Int256不直接使用默认实现?
  2. future 的C#版本是否有可能解决这个问题,这样我们就可以使用它来减少编写样板代码的需要?

在这里与C#语言设计团队一起长大:https://github.com/dotnet/csharplang/discussions/7032

推荐答案

答案似乎是"既是又不是".我不能确切地解释原因,但似乎没有办法真正做到这一点.

您可以通过以下方式在接口中"自动"实现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());
}

如果最后一个问题我可以试着用类不从其接口(as explained for example in the default interface members proposal spec)继承成员这一事实来解释,但对于以下几个问题,我的解释更少:

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

因此,就我个人而言,我建议 Select partial门和source generators门.

Csharp相关问答推荐

当Visual Studio处于升级管理模式时,无法安装Torch运行时

无法解析数据库上下文的服务

如何使用XmlSerializer反序列化字符串数组?

.NET 8 Web-API返回空列表

mocking对象的引发事件并作为用于调用方法的参数对象传递到那里

如何使用C#中的主构造函数功能使用多个构造函数?

尽管保证密钥不同,但已添加相同密钥的项(&Q;)

C#和ASP.NET核心标识:提高GetUserAsync调用的性能

C#-VS2022:全局使用和保存时的代码清理

EFR32BG22 BLE在SPP模式下与PC(Windows 10)不连接

Lambda表达式如何与隐式强制转换一起工作?

C#多键字典和TryGetValue

将操作从编辑页重定向到带参数的索引页

使用动态键从请求体反序列化JSON

如何在Polly重试策略成功之前将HttpClient请求排队?

为什么Swashbakle/Swagger在参数中包含变量名?

为什么我的自定义Json.net转换器不工作?

WPF:如何从DatagridHeader的内容模板绑定到词典项

最小API定义的Swagger标头参数

现在是否有一个.NET SDK等效于AsyncEx的AsyncLock?