为只读结构实现相等性的最佳做法是什么?

What is the best practice to implement equality for readonly structs?

我去年才开始用 C# 编程,我仍在学习这门语言。我有一个关于 readonly struct 类型和相等比较方法的问题。

在 C# 中创建结构时,我知道实现 IEquatable as the default reflection-based comparison is very slow. I also learned that in C# 7.2 and later we can define readonly structs and for these types, we can also use in parameter 以避免不必要的复制通常被认为是最佳实践。

由于结构通常被定义为不可变的只读类型,我想为只读结构定义 Equals 方法并不罕见。

然而,鉴于上述事实,我想知道是否有一种有效的方法可以为它们实现相等比较方法。我的观点是 none 这些相等方法和运算符实际上需要修改参数,所以我想以某种方式利用 in 参数来节省不必要的复制。

以下是我的尝试:

public readonly struct Point : IEquatable<Point>
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    // Explicitly implementing IEquatable<Point> and delegating to an Equals method taking in param.
    bool IEquatable<Point>.Equals(Point other) => Equals(other);

    public bool Equals(in Point other) => X == other.X && Y == other.Y;

    public override bool Equals(object? obj) => obj is Point other && Equals(other);

    public static bool operator ==(in Point left, in Point right) => left.Equals(right);

    public static bool operator !=(in Point left, in Point right) => !left.Equals(right);

    public override int GetHashCode() => HashCode.Combine(X, Y);

    public override string ToString() => $"Point({X}, {Y})";
}

以上确实有效,但我认为这不是完美的解决方案,因为如果通过 IEquatable 接口调用它,仍然需要复制。请注意,我不能仅使用 in 参数隐式实现 IEquatable,因为采用修饰符的 Equal 方法被认为具有不同的签名并被视为重载。

是否有已知的最佳实践来正确实施? 我真正想知道的是,是否有已知的最佳实践和模式来有效地实现此类只读结构的相等性。我尤其对正确利用 in 用于实现相等比较方法的参数修饰符。

目前在网上还没有找到满意的答案,也查了一些核心库的源码。例如,System.DateTime 现在被定义为只读结构,它相当大,但这里没有使用 in 参数。 (现有类型可能需要保持兼容性,但我知道他们经常需要妥协。)

请注意,上面定义的 Point 结构很小,仅包含两个 32 位槽,因此复制在这里实际上可能不是一个大问题,但这只是一个简单的说明性示例。

.NET6(C# 10)更新

现在 C#10 已经正式发布,原来的问题几乎已经过时了。我们现在可以创建一个 readonly record struct 这样的只读结构。当然根据你的型号,也可以定义为普通的引用类型记录(class).

https://devblogs.microsoft.com/dotnet/welcome-to-csharp-10/

重点是为记录类型自动生成优化的相等比较方法和运算符。

    public readonly record struct Person
    {
        public string FirstName { get; init; }
        public string LastName { get; init; }
    }

上面的内容很好,但是:请注意,in 在只读值类型上的优势仅适用于大小非常重要的类型;对于两个整数,您可能想多了。

您无法消除在 IEquatable<T> 场景中使用按值传递的需要,因为那是 API 的定义方式。

可能还值得注意的是,in 用法可能会导致很难从 C# 以外的语言使用此 API;例如,VB 对此的支持很差。这是否重要取决于您的目标受众。