【问题标题】:Custom Equality check for C# 9 recordsC# 9 记录的自定义相等检查
【发布时间】:2021-01-27 06:40:14
【问题描述】:

据我了解,记录实际上是实现自己的相等检查的类,其方式是您的对象是值驱动而不是引用驱动。

简而言之,对于像这样实现的record Foovar foo = new Foo { Value = "foo" }var bar = new Foo { Value = "foo" }foo == bar 表达式将导致True,即使它们具有不同的引用 (ReferenceEquals(foo, bar) // False)。

现在有了记录,尽管the article 发布在 .Net 博客中,它说:

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

当我尝试放置 public override bool Equalspublic override int GetHashCodepublic static bool operator == 等时,我收到了 Member with the same signature is already declared 错误,所以我认为这是一种受限制的行为,但情况并非如此struct 对象。

Failing example:

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

我的主要问题是,如果我们想自定义相等检查器的工作方式怎么办?我的意思是,我明白这超出了记录的全部目的,但另一方面,平等检查器并不是让记录使用起来很酷的唯一功能。

有人想要覆盖记录的相等性的一个用例是因为您可能有一个 attribute 会从相等性检查中排除一个属性。以 this 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}";
}

那么你会得到以下results:

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

到目前为止,为了以某种方式实现上述用例,我已经实现了an abstract record object并像这样使用它:

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

【问题讨论】:

  • 您使用的是哪个版本的 C# 9 编译器?我确实注意到 C# 9.0 仍处于预览阶段(据我所知),因此某些功能可能尚不可用。
  • @Dai 你是对的朋友!我错过了提及该信息!我现在将更新我的问题。仅供参考:5.0.100-rc.1.20452.10
  • @Dai,补充一下,是的,我知道它仍在开发中,如果它不在 RC1 中,我不会问这个问题,所以作为发布候选版本,我很友善感到困惑的是这是设计使然还是尚未实施。 :)
  • 粘贴您的实际 Equals 方法。我刚试过,它奏效了。
  • @JeremyThompson 是的,您可以毫无问题地使用类和结构来做到这一点:dotnetfiddle.net/Widget/apnl6x 到目前为止,我只能用记录来做到这一点(这可能是我的错,因为记录可能需要一种不同的方法).

标签: c# record equality c#-9.0


【解决方案1】:

根据the C#9 record proposal,以下应该编译,即使在没有实际实现的情况下不是很有用..

// 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)。 可以定义的方法可以在提案中搜索“user-defined”找到。

尝试覆盖/定义其他相等方法或运算符预计会失败:

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

该行为在“平等成员”中进行了讨论,并在以下段落中进行了总结:

记录类型实现System.IEquatable&lt;R&gt; 并包括book Equals(R? other) 的综合强类型重载,其中R 是记录类型。该方法是公共的,并且该方法是虚拟的,除非记录类型是密封的。 [Equals(R?)] 方法可以显式声明。 如果显式声明与预期的签名或可访问性不匹配,或者显式声明不允许在派生类型中覆盖它,则会出错并且记录类型没有密封。 如果Equals(R? other) 是用户定义的(未合成)但GetHashCode 不是[用户定义的],则会产生警告。

【讨论】:

  • 我会将您的答案标记为正确,因为确实这样我设法实现了我想要的。我唯一剩下的问题是,如果您查看我的回购:FullNameVo 记录中的github.com/panosru/JustDemo/tree/master/ValueObjects/… 此处:github.com/panosru/JustDemo/blob/master/ValueObjects/… 我必须输入public bool Equals(FullNameVo? other) =&gt; base.Equals(other); 才能使其正常工作,有没有办法避免这样做,只是从public abstract record ValueObject 继承Equals 方法?谢谢!
  • 我认为我在上一条评论中的问题与此有关:stackoverflow.com/questions/64094373/…
  • 看起来不像:“[默认合成的 Equals 如果 .. 并且] 有一个基本记录类型,则base.Equals(other) 的值(对 public virtual bool Equals(Base? other) 的非虚拟调用); ..” — 请注意,如果基础 基础记录类型,例如手动创建的类型:record Foo : record ValueObjectShim : ValueObject
  • 你能解释得简单一点吗?既然我已经创建了一个abstract record ValueObject,在其中定义了Equals 方法,然后我创建了sealed record FullNameVo : ValueObject,那么FullNameVo 记录不应该继承Equals 抽象记录的Equals 方法吗?相反,如果我没有将public bool Equals(FullNameVo? other) =&gt; base.Equals(other); 放在FullNameVo 中,那么它会忽略在ValueObject 抽象记录中实现的那个,FullNameVo 是从该抽象记录中派生出来的。
  • 是的,我添加了一些示例......但基于逻辑,我同意你的观点,这就是我要争论的问题,尽管基于这里的答案:stackoverflow.com/a/64094532/395187 它应该' t 使用基本的 Equals ......这很烦人......我应该就这件事开一个新问题吗?
【解决方案2】:

不,这不是因为记录处于预览状态(当我写这篇文章时它们不在)。这是因为自定义Equals 的记录不是record 的正确使用方式。这是滥用记录的一个例子。

仅仅因为记录存在,您就应该不要在任何地方使用它们。

记录的美妙之处在于事半功倍,因此由编译器生成的样板代码,而不是由您生成。

如果你坚持使用记录,我会走这条路:

using System;

namespace ConsoleApp1;

class Program
{
    static void Main()
    {
        var foo = new Test("ShouldBeSame");
        var bar = new Test("shouldbesame");
        Console.WriteLine(foo.CustomEquals(bar));
    }
}

public record Test(string ToCompare);

public static class TestExtensions
{
    // You can do whatever you want here...
    public static bool CustomEquals(this Test foo, Test other) => string.Equals(foo.ToCompare.ToLowerInvariant(), other.ToCompare.ToLowerInvariant());
}

【讨论】:

  • “这是因为带有自定义 Equals 的记录不是如何使用记录的正确方式” 并且该声明的证据是?
  • 记录应该用在类似值的语义中。很明显,自定义Equals 偏离了这个想法,但如果你想这样使用它,请成为我的客人。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-05-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多