C# 9 记录的自定义相等检查 [英] Custom Equality check for C# 9 records

查看:52
本文介绍了C# 9 记录的自定义相等检查的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

据我所知,记录实际上是一些类,它们以您的对象是值驱动而非引用驱动的方式实现自己的相等性检查.

简而言之,对于 record Foo 是这样实现的: var foo = new Foo { Value = "foo";}var bar = new Foo { Value = "foo";}foo == bar 表达式将导致 True,即使它们有不同的引用 (ReferenceEquals(foo, bar)//错误).

现在有了记录,即使在 这篇文章发表在 .Net 博客上,它说:

<块引用>

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

当我尝试放置 public override bool Equals、或 public override int GetHashCode、或 public static bool operator == 等时. 我收到 Member with the same signature is already created 错误,所以我认为这是一种受限行为,struct 对象不是这种情况.>

失败示例:

公开密封记录 SimpleVo:IEquatable{public bool Equals(SimpleVo other) =>抛出新的 System.NotImplementedException();public override bool Equals(object obj) =>obj 是 SimpleVo 其他 &&等于(其他);公共覆盖 int GetHashCode() =>抛出新的 System.NotImplementedException();public static bool operator ==(SimpleVo left, SimpleVo right) =>左.等于(右);公共静态布尔运算符!=(SimpleVo 左,SimpleVo 右)=>!left.Equals(right);}

编译结果:

SimpleVo.cs(11,30): error CS0111: Type 'SimpleVo' 已经定义了一个名为 'Equals' 的成员,具有相同的参数类型SimpleVo.cs(17,37):错误 CS0111:类型SimpleVo"已经定义了一个名为op_Equality"的具有相同参数类型的成员SimpleVo.cs(20,37):错误 CS0111:类型SimpleVo"已经定义了一个名为op_Inequality"的成员,具有相同的参数类型

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

有人想要覆盖记录相等性的一个用例是因为您可能有一个 attribute 将从等式检查中排除属性.例如this ValueObject 实现.

那么如果你像这样扩展这个 ValueObject 抽象类:

公共密封类 FullNameVo : ValueObject{public FullNameVo(字符串名称,字符串姓氏){姓名 = 姓名;姓氏 = 姓氏;}[忽略成员]公共字符串名称{获取;}公共字符串姓氏{得到;}[忽略成员]公共字符串全名 =>$"{Name} {Surname}";}

然后你会得到以下结果:

var user1 = new FullNameVo("John", "Doe");var user2 = new FullNameVo(John", Doe");var user3 = new FullNameVo("Jane", "Doe");Console.WriteLine(user1 == user2);//真的Console.WriteLine(ReferenceEquals(user1, user2));//错误的Console.WriteLine(user1 == user3);//真的Console.WriteLine(user1.Equals(user3));//真的

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

公共密封记录 FullNameVo : ValueObject{[忽略成员]公共字符串名称;公共字符串姓氏;[忽略成员]公共字符串全名 =>$"{Name} {Surname}";}

结果如下:

var user1 = new FullNameVo{姓名 = 约翰",姓氏 = 母鹿"};var user2 = 新的 FullNameVo{姓名 = 约翰",姓氏 = 母鹿"};var user3 = user1 with { Name = "Jane";};Console.WriteLine(user1 == user2);//真的Console.WriteLine(ReferenceEquals(user1, user2));//错误的Console.WriteLine(user1 == user3);//错误的Console.WriteLine(user1.Equals(user3));//错误的Console.WriteLine(ValueObject.EqualityComparer.Equals(user1, user3));//真的

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

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

解决方案

Per C#9 记录提案,以下应该编译,即使没有实际实现也不是很有用..

//没有明确的 IEquatable——这是合成的!公开密封记录 SimpleVo{//不是虚拟的,因为 SimpleVo (R) 是密封的.//接受 SimpleVo?(R?),而不是 SimpleVo (R),作为参数.public bool Equals(SimpleVo?other) =>抛出新的 System.NotImplementedException();//可选:当用户定义 Equals(R?) 时,如果未提供,则生成警告.public int GetHashCode() =>抛出新的 System.NotImplementedException();//没有其他标准"相等成员!}

由于大部分代码是合成的,因此对与等式相关的成员有限制.该提案包括预期合成基础类型的示例.

也就是说,给定 just 一个 Equals(R?) 编译器会创建一个 ==, !=> 和 Equals(object).可以定义的方法可以通过在提案中搜索user-defined"找到.

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

<块引用>

如果显式声明覆盖是错误的.

行为在平等成员"中讨论并在段落中进行了总结:

<块引用>

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

From what I understand, records are actually classes that implement their own equality check in a way that your object is value-driven and not reference driven.

In short, for the record Foo that is implemented like so: var foo = new Foo { Value = "foo" } and var bar = new Foo { Value = "foo" }, the foo == bar expression will result in True, even though they have a different reference (ReferenceEquals(foo, bar) // False).

Now with records, even though that in the article posted in .Net Blog, it says:

If you don’t like the default field-by-field comparison behaviour of the generated Equals override, you can write your own instead.

When I tried to place public override bool Equals, or public override int GetHashCode, or public static bool operator ==, and etc. I was getting Member with the same signature is already declared error, so I think that it is a restricted behaviour, which isn't the case with struct objects.

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

Compiler result:

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

My main question here is what if we want to customise the way the equality checker works? I mean, I do understand that this beats the whole purpose of records, but on the other hand, equality checker is not the only feature that makes records cool to use.

One use case where someone would like to override the equality of records is because you could have an attribute that would exclude a property from equality check. Take for example this ValueObject implementation.

Then if you extend this ValueObject abstract class like so:

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

then you would get the following 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

So far, in order to achieve somehow the above use case, I have implemented an abstract record object and utilise it like so:

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

    public string Surname;

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

and the results look like this:

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

To conclude, I'm a bit puzzled, is restricting the override of equality methods of record objects an expected behaviour or is it because it is still in preview stage? If it is by design, would you implement the above behaviour in a different (better) way or you would just continue using classes?

dotnet --version output: 5.0.100-rc.1.20452.10

解决方案

Per the C#9 record proposal, the following should compile, even if not very useful without actual implementations..

// 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!
}

There are restrictions on the equality-related members as most of the code is synthesized. The proposal includes examples of the expected synthesized underlying type.

That is, given just a Equals(R?) the compiler creates a ==, !=, and Equals(object). The methods that can be defined can be found by searching for "user-defined" in the proposal.

Attempting to override/define other equality methods or operators is expected to fail:

It is an error if the override is declared explicitly.

The behavior is discussed in ‘Equality members’ and is summarized in the paragraph:

The record type implements System.IEquatable<R> and includes a synthesized strongly-typed overload of book Equals(R? other) where R is the record type. The method is public, and the method is virtual unless the record type is sealed. The [Equals(R?)] method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility, or the explicit declaration doesn't allow overriding it in a derived type and the record type is not sealed. If Equals(R? other) is user-defined (not synthesized) but GetHashCode is not [user-defined], a warning is produced.

这篇关于C# 9 记录的自定义相等检查的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆