为什么匿名类型的Equals实现比较字段?

我只是想知道为什么该语言的设计者决定在匿名类型上实现Equals,类似于Equals的值类型。 这不是误导吗?

 public class Person { public string Name { get; set; } public int Age { get; set; } } public static void ProofThatAnonymousTypesEqualsComparesBackingFields() { var personOne = new { Name = "Paweł", Age = 18 }; var personTwo = new { Name = "Paweł", Age = 18 }; Console.WriteLine(personOne == personTwo); // false Console.WriteLine(personOne.Equals(personTwo)); // true Console.WriteLine(Object.ReferenceEquals(personOne, personTwo)); // false var personaOne = new Person { Name = "Paweł", Age = 11 }; var personaTwo = new Person { Name = "Paweł", Age = 11 }; Console.WriteLine(personaOne == personaTwo); // false Console.WriteLine(personaOne.Equals(personaTwo)); // false Console.WriteLine(Object.ReferenceEquals(personaOne, personaTwo)); // false } 

乍一看,所有打印的布尔值都应该为false。 但是当使用Person类型时,使用Equals调用的行返回不同的值,并使用匿名类型。

匿名类型实例是不具有行为或标识的不可变数据值。 参考比较它们没有多大意义。 在这种情况下,我认为为它们产生结构性平等比较是完全合理的。

如果要将比较行为切换为自定义(引用比较或不区分大小写),可以使用Resharper将匿名类型转换为命名类。 Resharper还可以生成平等成员。

这样做还有一个非常实际的原因:匿名类型可以方便地用作LINQ连接和分组中的哈希键。 出于这个原因,他们需要语义正确的EqualsGetHashCode实现。

为什么你应该问语言设计师…

但我在Eric Lippert的文章中发现了这一点,这篇文章是关于组装中的匿名类型统一,第二部分

匿名类型为您提供了一个存储一组不可变的名称/值对的便利位置,但它为您提供了更多。 它还为您提供了Equals,GetHashCode的实现,以及与此讨论最密切相关的ToString。 (*)

该部分的原因在于:

(*)我们为您提供Equals和GetHashCode,以便您可以在LINQ查询中使用匿名类型的实例作为执行连接的键。 由于性能原因,LINQ to Objects使用哈希表实现连接,因此我们需要正确实现Equals和GetHashCode。

C#语言规范的官方答案(可在此处获得):

匿名类型上的Equals和GetHashcode方法重写从objectinheritance的方法,并根据属性的Equals和GetHashcode定义,因此当且仅当所有属性相等时,同一匿名类型的两个实例才相等

(我的重点)

其他答案解释了为什么这样做。

值得注意的是,在VB.Net中 , 实现方式有所不同 :

没有键属性的匿名类型的实例仅等于它自己。

创建匿名类型对象时,必须明确指示键属性。 默认值为:无密钥,这对C#用户来说可能非常混乱!

这些对象在VB中不相同,但是在C#等价代码中:

 Dim prod1 = New With {.Name = "paperclips", .Price = 1.29} Dim prod2 = New With {.Name = "paperclips", .Price = 1.29} 

这些对象评估为“相等”:

 Dim prod3 = New With {Key .Name = "paperclips", .Price = 1.29} Dim prod4 = New With {Key .Name = "paperclips", .Price = 2.00} 

因为它给了我们一些有用的东西。 考虑以下:

 var countSameName = from p in PersonInfoStore group p.Id by new {p.FirstName, p.SecondName} into grp select new{grp.Key.FirstName, grp.Key.SecondName, grp.Count()}; 

这是因为匿名类型的Equals()GetHashCode()实现是基于逐字段相等的。

  1. 这意味着当在不是linq-to-objects的PersonInfoStore上运行时,上面将更接近相同的查询。 (仍然不一样,它将匹配XML源将执行的操作,但不符合大多数数据库的排序将导致的结果)。
  2. 这意味着我们不必为每次调用GroupBy定义一个IEqualityComparer ,它会使匿名对象变得非常困难 – 为匿名对象定义IEqualityComparer是可能的但不容易 – 并且远非最自然的含义。
  3. 最重要的是,它不会导致大多数情况下的问题。

第三点值得研究。

当我们定义一个值类型时,我们自然想要一个基于价值的平等概念。 虽然我们可能对基于值的相等性的想法与默认值不同,例如匹配给定字段不区分大小写,但默认情况下自然是合理的(如果在一种情况下性能不佳和错误*)。 (此外,在这种情况下,引用相等是没有意义的)。

当我们定义引用类型时,我们可能想要也可能不想要基于值的平等概念。 默认值为我们提供引用相等,但我们可以轻松地改变它。 如果我们确实更改了它,我们可以只为EqualsGetHashCode或它们以及==更改它。

当我们定义一个匿名类型时,哦等等,我们没有定义它,这就是匿名的意思! 我们关心参考平等的大多数情景都不再存在。 如果我们要持有一个物体足够长的时间,以后想知道它是否与另一个物体相同,我们可能不会处理一个匿名物体。 我们关心基于价值的平等的案例出现了很多。 经常使用Linq(我们在上面看到的GroupBy ,但也有DistinctUnionToDictionaryIntersectSequenceEqualToDictionaryToLookup )以及其他用途(这并不像我们没有做Linq为我们做的事情中的枚举在某种程度上,在2.0之前,任何使用2.0编码的人都会在Enumerable编写一半的方法。

总而言之,我们从平等与匿名类一起工作的方式中获益良多。

如果有人真的想要引用相等,那么==使用引用相等意味着他们仍然拥有引用相等,所以我们不会丢失任何东西。 这是要走的路。

* Equals()GetHashCode()的默认实现有一个优化,让它在安全的情况下使用二进制匹配。 不幸的是,有一个错误使得有时错误地识别某些情况对于这种更快的方法是安全的,当它们不是(或者至少习惯了,也许它是固定的)。 一个常见的情况是,如果在结构中有一个decimal字段,那么它会将一些具有等效字段的实例视为不等。