C# 9记录类型的自定义相等性检查

53
据我所了解,记录实际上是实现了自己的相等检查的类,以使您的对象是基于值而不是基于引用的。
简而言之,对于像这样实现的 record Foovar foo = new Foo {Value = "foo"}var bar = new Foo {Value = "foo"}foo == bar表达式将导致True,即使它们具有不同的引用 (ReferenceEquals(foo,bar) // False)。
现在,即使在文章中提到:

如果您不喜欢生成的 Equals 覆盖默认的逐字段比较行为,则可以编写自己的覆盖。

但当我尝试放置 public override bool Equalspublic override int GetHashCodepublic static bool operator == 等时,我会得到 Member with the same signature is already declared 的错误,因此我认为这是一种受限制的行为,这在struct 对象中并非如此。 失败的示例
public sealed record SimpleVo
    : IEquatable<SimpleVo>
{
    public bool Equals(SimpleVo other) =>
        throw new System.NotImplementedException();

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

    public override int GetHashCode() =>
        throw new System.NotImplementedException();

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

    public static bool operator !=(SimpleVo left, SimpleVo right) =>
        !left.Equals(right);
}

编译器结果:

SimpleVo.cs(11,30): error CS0111: Type 'SimpleVo' already defines a member called 'Equals' with the same parameter types

SimpleVo.cs(17,37): error CS0111: Type 'SimpleVo' already defines a member called 'op_Equality' with the same parameter types

SimpleVo.cs(20,37): error CS0111: Type 'SimpleVo' already defines a member called 'op_Inequality' with the same parameter types

在这里,我的主要问题是如果我们想要自定义相等性检查器的工作方式怎么办?我的意思是,我确实理解这打破了记录的整个目的,但另一方面,相等性检查器并不是使记录使用起来很酷的唯一功能。

有些情况下,某人希望覆盖记录的相等性,例如可以使用一个属性来排除某个属性进行相等性检查。例如,考虑此实现ValueObject

然后,如果你按如下方法扩展这个ValueObject抽象类:

public sealed class FullNameVo : ValueObject
{
    public FullNameVo(string name, string surname)
    {
        Name    = name;
        Surname = surname;
    }

    [IgnoreMember]
    public string Name { get; }

    public string Surname { get; }

    [IgnoreMember]
    public string FullName => $"{Name} {Surname}";
}

那么您将得到以下结果

var user1 = new FullNameVo("John", "Doe");
var user2 = new FullNameVo("John", "Doe");
var user3 = new FullNameVo("Jane", "Doe");

Console.WriteLine(user1 == user2); // True
Console.WriteLine(ReferenceEquals(user1, user2)); // False
Console.WriteLine(user1 == user3); // True
Console.WriteLine(user1.Equals(user3)); // True

为了以某种方式实现上述用例,到目前为止,我已经实现了一个抽象记录对象并像这样利用它:

public sealed record FullNameVo : ValueObject
{
    [IgnoreMember]
    public string Name;

    public string Surname;

    [IgnoreMember]
    public string FullName => $"{Name} {Surname}";
}

结果如下所示:

var user1 = new FullNameVo
{
    Name    = "John",
    Surname = "Doe"
};

var user2 = new FullNameVo
{
    Name    = "John",
    Surname = "Doe"
};

var user3 = user1 with { Name = "Jane" };

Console.WriteLine(user1 == user2); // True
Console.WriteLine(ReferenceEquals(user1, user2)); // False
Console.WriteLine(user1 == user3); // False
Console.WriteLine(user1.Equals(user3)); // False
Console.WriteLine(ValueObject.EqualityComparer.Equals(user1, user3)); // True

总的来说,我有点困惑,限制记录对象的等式方法覆盖是一种预期行为,还是因为它仍处于预览阶段?如果这是设计之意,您会以不同的方式实现上述行为(更好),还是继续使用类?

dotnet --version 输出:5.0.100-rc.1.20452.10


1
我猜,要么删除virtual,要么也取消FullNamVo类型的封印:“记录类型实现了System.IEquatable<R>,并包括一个合成的强类型重载Equals(R? other),其中R是记录类型。该方法是公共的,如果记录类型未被封印,则该方法是虚拟的。* [bool Equals(R? r)]方法可以被明确声明。如果显式声明与预期签名或可访问性不匹配,或者显式声明不允许在派生类型中覆盖它且记录类型未被封印,则会出现错误。*” - user2864740
基本上,目标是防止编写所有方法,这可能通常使用“生成相等成员”完成,并防止在此过程中出现错误。拥有单个Equals(R?)允许编译器合成的==、!=、Equals(object)形式,并隐式接受Equals(R)。 - user2864740
2
@JeremyThompson,是的,您可以使用类和结构来完成,没有任何问题:https://dotnetfiddle.net/Widget/apnl6x。到目前为止,我只是无法使用记录来完成(可能是我的问题,因为记录可能需要不同的方法)。 - panosru
所以,你解决了如何实现自定义的相等检查吗?我尝试了被接受的答案的解决方案,但后来意识到我不能使用sealed关键字,因为我正在使用一个基础记录并派生它。对我来说,那个解决方案行不通。你现在是使用 Records 还是仍然使用类?你和其他一起工作的人对这个问题有什么看法吗?顺便说一下,我刚刚下载了最新版本的 Visual Studio 2019 Community 16.11.3,你描述的Equals编译器错误仍然存在。 - Bob Bryan
@BobBryan,非常抱歉回复晚了,我出国了。目前我主要使用类来创建值对象,但最终我会转向记录类型。你能否在此处检查工作示例并查看是否符合您的需求?https://github.com/panosru/Playground/tree/master/C%23/ValueObjects/ValueObjectsCS9Working - panosru
显示剩余14条评论
1个回答

27

根据C#9记录提案,以下内容“应该编译”,即使没有实际实现也不是很有用..

// No explicit IEquatable<R> - this is synthesized!
public sealed record SimpleVo
{
    // Not virtual, as SimpleVo (R) is sealed.
    // Accepts SimpleVo? (R?), and not SimpleVo (R), as argument.
    public bool Equals(SimpleVo? other) =>
        throw new System.NotImplementedException();

    // Optional: warning generated if not supplied when Equals(R?) is user-defined.
    public int GetHashCode() =>
        throw new System.NotImplementedException();

    // No other “standard” equality members!
}

由于大部分代码是合成的,因此平等相关成员受到限制。该提案包括预期合成底层类型的示例。

也就是说,给定一个Equals(R?),编译器会创建一个==!=Equals(object)

可以通过搜索提案中的“用户定义”来找到可定义的方法。

试图覆盖/定义其他相等性方法或运算符通常会失败

如果显式声明了覆盖,则会出现错误。

该行为在‘Equality members’中讨论,并在以下段落中概括:

记录类型实现 System.IEquatable<R> 并包括一个合成的强类型重载 book Equals(R? other),其中 R 是记录类型。该方法是公共的,并且该方法是虚拟的,除非记录类型是密封的。[Equals(R?)] 方法可以被显式声明。 如果显式声明不符合预期的签名或可访问性或显式声明不允许在派生类型中重写它而记录类型未被密封,则会出现错误。 如果Equals(R?)是用户定义的(不是合成的),但GetHashCode不是[用户定义的],则会产生一个警告。


我会将您的答案标记为正确,因为确实是这样,我成功地实现了我想要的功能。我唯一剩下的问题是,如果您查看我的存储库:https://github.com/panosru/JustDemo/tree/master/ValueObjects/ValueObjectsCS9Working,在此处的`FullNameVo`记录中:https://github.com/panosru/JustDemo/blob/master/ValueObjects/ValueObjectsCS9Working/FullNameVo.cs,我必须放置`public bool Equals(FullNameVo? other) => base.Equals(other);才能使其工作,有没有办法避免这样做,只需从public abstract record ValueObject继承Equals`方法即可?谢谢! - panosru
看起来不像是这样的:“[默认合成的Equals返回true,如果..并且] 有一个基础记录类型,则base.Equals(other)的值(对公共虚拟bool Equals(Base?other)的非虚拟调用); ..” - 请注意,如果基类基础记录类型,例如手动创建的一个:record Foo : record ValueObjectShim : ValueObject - user2864740
但我并不是从类中继承,而是从记录类型继承:https://github.com/panosru/JustDemo/blob/b67ec8e4768f9d06c3ea874c7e15413762e9644c/ValueObjects/ValueObjectsCS9Working/ValueObject.cs#L7 - panosru
1
是的,我添加了一些例子...但基于逻辑,我同意你的观点,这就是我所争论的,尽管根据这个答案:https://dev59.com/YL3pa4cB1Zd3GeqPeXPH#64094532 它不应该使用基本的Equals...这真的很烦人...我应该为这个问题开一个新的问题吗? - panosru
6
嗯,当我尝试写GetHashCode但没有override关键字时,我的编译器为何会报错呢?奇怪的是,似乎必须继承/重写GetHashCode,而Equals会被合成/调用。 - Bruno Brant
显示剩余4条评论

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