C# 记录类中的自定义 EqualityContract

Custom EqualityContract in a C# record class

提问人:Bartosz 提问时间:7/21/2022 最后编辑:Bartosz 更新时间:7/23/2022 访问量:493

问:

在 C# 记录类中为属性提供自定义实现的正确方法和预期用途是什么?System.Type EqualityContract { get; }

默认(综合)实现返回记录类。因此,只能将 type 的实例与 类型的其他实例进行比较,否则结果始终为 。这在典型情况下非常有意义。但是,允许将该默认实现替换为自定义实现,因此必须有原因。我希望它允许多个记录类共享相等契约,例如,如果它们具有相同的实例属性或它们派生自相同的基本记录类。但是,这需要声明 和 的自定义覆盖,这是被禁止的。typeof(R)RRRfalsebool Equals(object? other)bool Equals(Base? other)

[编辑:请注意,问题是关于定义自定义平等合同的目的和可用性。下面的例子只是为了展示一个潜在的用途,但这个用途是行不通的。我不是在问如何使用不同的机制使示例工作。

例如,假设我希望一个记录类重用其基本记录类的相等协定,以便允许在 的实例之间进行比较,并基于 的实例属性。我需要做这样的事情:DerivedBaseBaseDerivedBase

record Base(int X);

record Derived(int X) : Base(X) {
    protected override System.Type EqualityContract => base.EqualityContract;
    public virtual bool Equals(Derived? other) => base.Equals(other);
    public sealed override bool Equals(Base? other) => base.Equals(other); // forbidden
    public override bool Equals(object? other) => base.Equals(other);      // forbidden
    /* some additional stuff */
}

我不能这样做,因为在 C# 记录中禁止声明自定义重写。如果没有禁止的自定义覆盖,记录类将如下所示(包含用于说明的合成覆盖):EqualsDerivedEquals

record Derived(int X) : Base(X) {
    protected override System.Type EqualityContract => base.EqualityContract;
    public virtual bool Equals(Derived? other) => base.Equals(other);
    public sealed override bool Equals(Base? other) => Equals((object)other); // synthesized
    public override bool Equals(object? other) => Equals(other as Derived);   // synthesized
    /* some additional stuff */
}

然后,相等契约无法正常工作。例如,以下代码生成:True False

Base obj1 = new Base(1);
Derived obj2 = new Derived(1);
System.Console.WriteLine($"{obj1 == obj2} {obj2 == obj1}");

这两种比较都使用 的运算符 ,该运算符调用方法 。对于 ,使用了 from 的实现,它比较了相等契约(在本例中相等)和 的值(在本例中相等)并返回 。对于 ,使用了 from 的实现,这会导致对 的调用,该调用返回 ,因为 是 。==Basebool Equals(Base? other)obj1 == obj2BaseXtrueobj2 == obj1Derivedobj2.Equals(obj1 as Derived)falseobj1 as Derivednull

那么,我应该如何利用自定义平等合同呢?我错过了什么吗?允许自定义覆盖有什么问题?Equals

我搜索了 C# 设计存储库中的讨论以查找一些信息。我发现的仅有的两个相关评论表明,我上面的方案是可行的:https://github.com/dotnet/csharplang/issues/3137#issuecomment-581558013 https://github.com/dotnet/csharplang/discussions/3787#discussioncomment-130523

C 记录 相等 C#-9.0

评论


答:

0赞 tymtam 7/23/2022 #1

据我了解.NET6 无法实现记录的镜像相等性。如您所示,您可以使用,但另一种方式是不可能的。truebase1 == derived1EqualityContract

问题在于,虽然 in 将被调用,但它将被调用 for,正如您正确指出的那样,覆盖另一个是被禁止的。Equals(Derived? other)Derivednullderived1 == base1Equals

在我看来,这不是问题,因为:

  1. 没有额外字段的派生记录是值得怀疑的。
  2. 如果记录具有不同的 flield,则 compare-only-x 行为会令人困惑。

您的问题的解决方案可能是自定义相等比较器。(如果你不了解它们,官方的 EqualityComparer 类有一个很好的例子,其中有两本字典用于相同类型但具有不同的相等比较器。

拥有:

record Base(int X) {}

record Derived(int X, int Y) : Base(X) {}

class CheckOnlyXComparer : EqualityComparer<Base>
{
    public override bool Equals(Base? b1, Base? b2)
    {
        if (b1 == null && b2 == null) return true;
        if (b1 == null || b2 == null) return false;
        return (b1.X == b2.X);
    }

    public override int GetHashCode(Base b) => b.X.GetHashCode();
}

以下代码片段

WriteLine(base1 == derived1);
WriteLine(derived1 == base1);

var comparer = new CheckOnlyXComparer();
WriteLine(comparer.Equals(base1,derived1));
WriteLine(comparer.Equals(derived1,derived1));

指纹

False
False
True
True

List<Base> l1 = new (){base1, derived1};
WriteLine(l1.Count());
var distinctByX = l1.Distinct(new CheckOnlyXComparer());
WriteLine(distinctByX.Count());

我们得到

2
1

评论

0赞 Bartosz 7/23/2022
谢谢。但是,我的问题是要理解定义自定义相等契约的目的,而不是使 Base/Derived 示例使用不同的机制工作。我宁愿寻找一些可以证明定义自定义平等合同使用的东西,而这个例子只是为了展示一种潜在的用途,但这种用途是行不通的。此外,我的问题为什么禁止自定义覆盖 Equals。
0赞 tymtam 7/23/2022
你关于禁止的观点绝对成立。我想说的是,值得就此提出一个单独的问题,问为什么它们被禁止。
1赞 tymtam 7/23/2022
我的回答集中在提供一个解决方案上 - 我允许自己说这是一个优雅的解决方案;),并确认您的调查。
0赞 tymtam 7/23/2022
EqualityContract允许派生类型“禁用”类型检查,恕我直言,它已经提供了很好的价值。base == derived
0赞 Bartosz 7/23/2022
允许派生类型“禁用”类型检查是什么意思?我不明白。EqualityContractbase == derived