80

I'm just wondering why designers of the language decided to implement Equals on anonymous types similarly to Equals on value types. Isn't it misleading?

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
}

At first glance, all printed boolean values should be false. But lines with Equals calls return different values when Person type is used, and anonymous type is used.

Gilad Green
  • 36,708
  • 7
  • 61
  • 95
dragonfly
  • 17,407
  • 30
  • 110
  • 219
  • 11
    This is one of the few posts on SO having 3 or more answers where the average rep. of answering guys is over **95k**, as of May-23-2017. – dotNET May 22 '17 at 19:39

4 Answers4

83

Anonymous type instances are immutable data values without behavior or identity. It doesn't make much sense to reference-compare them. In that context I think it is entirely reasonable to generate structural equality comparisons for them.

If you want to switch the comparison behavior to something custom (reference comparison or case-insensitivity) you can use Resharper to convert the anonymous type to a named class. Resharper can also generate equality members.

There is also a very practical reason to do this: Anonymous types are convenient to use as hash keys in LINQ joins and groupings. For that reason they require semantically correct Equals and GetHashCode implementations.

usr
  • 168,620
  • 35
  • 240
  • 369
  • 1
    I'm still confused why for anonymous types == doesn't compare values like .Equals? – Brad Rem Aug 25 '12 at 16:20
  • 12
    Because == does not invoke Equals in C# (never!). It invokes `operator ==` and anonymous types don't have that one. So C# uses reference equality.; Is that good or bad? Who knows because it is a trade-off. After all, I have never felt the need for comparing two anonymous type instances. Never happens for some reason. – usr Aug 25 '12 at 16:23
  • 1
    I agree it's smart from a convenience perspective (GroupBy() etc), but a bit deceptive from a technical perspective. MSDN states "Anonymous types are class types that derive directly from object...". However object does not implement IStructuralEquatable, so that interface is somehow injected behind the curtains, making things a bit confusing (although useful)... – Anders Sep 28 '15 at 15:57
  • 6
    Having operator == return something different from Equals is just asking for trouble. – Triynko Oct 07 '15 at 20:58
  • Sorry for bringing this topic back from the grave, but in C# at least the equality operators (`==` and `!=`) should only be doing referential equality on reference types, with the exception that the type has value semantics (such as `string`). In OP's example they have a `Person` class, typically a person wouldn't be considered a value type. – Matthew Nov 23 '18 at 18:05
40

For the why part you should ask the language designers...

But I found this in Eric Lippert’s article about Anonymous Types Unify Within An Assembly, Part Two

An anonymous type gives you a convenient place to store a small immutable set of name/value pairs, but it gives you more than that. It also gives you an implementation of Equals, GetHashCode and, most germane to this discussion, ToString. (*)

Where the why part comes in the note:

(*) We give you Equals and GetHashCode so that you can use instances of anonymous types in LINQ queries as keys upon which to perform joins. LINQ to Objects implements joins using a hash table for performance reasons, and therefore we need correct implementations of Equals and GetHashCode.

nemesv
  • 138,284
  • 16
  • 416
  • 359
25

The official answer from the C# Language Specification (obtainable here):

The Equals and GetHashcode methods on anonymous types override the methods inherited from object, and are defined in terms of the Equals and GetHashcode of the properties, so that two instances of the same anonymous type are equal if and only if all their properties are equal.

(My emphasis)

The other answers explain why this is done.

It's worth noting that in VB.Net the implementation is different:

An instance of an anonymous types that has no key properties is equal only to itself.

The key properties must be indicated explicitly when creating an anonymous type object. The default is: no key, which can be very confusing for C# users!

These objects aren't equal in VB, but would be in C#-equivalent code:

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

These objects evaluate to "equal":

Dim prod3 = New With {Key .Name = "paperclips", .Price = 1.29}
Dim prod4 = New With {Key .Name = "paperclips", .Price = 2.00}
Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
10

Because it gives us something that's useful. Consider the following:

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

The works because the implementation of Equals() and GetHashCode() for anonymous types works on the basis of field-by-field equality.

  1. This means the above will be closer to the same query when run against at PersonInfoStore that isn't linq-to-objects. (Still not the same, it'll match what an XML source will do, but not what most databases' collations would result in).
  2. It means we don't have to define an IEqualityComparer for every call to GroupBy which would make group by really hard with anonymous objects - it's possible but not easy to define an IEqualityComparer for anonymous objects - and far from the most natural meaning.
  3. Above all, it doesn't cause problems with most cases.

The third point is worth examining.

When we define a value type, we naturally want a value-based concept of equality. While we may have a different idea of that value-based equality than the default, such as matching a given field case-insensitively, the default is naturally sensible (if poor in performance and buggy in one case*). (Also, reference equality is meaningless in this case).

When we define a reference type, we may or may not want a value-based concept of equality. The default gives us reference equality, but we can easily change that. If we do change it, we can change it for just Equals and GetHashCode or for them and also ==.

When we define an anonymous type, oh wait, we didn't define it, that's what anonymous means! Most of the scenarios in which we care about reference equality aren't there any more. If we're going to be holding an object around for long enough to later wonder if it's the same as another one, we're probably not dealing with an anonymous object. The cases where we care about value-based equality come up a lot. Very often with Linq (GroupBy as we saw above, but also Distinct, Union, GroupJoin, Intersect, SequenceEqual, ToDictionary and ToLookup) and often with other uses (it's not like we weren't doing the things Linq does for us with enumerables in 2.0 and to some extent before then, anyone coding in 2.0 would have written half the methods in Enumerable themselves).

In all, we gain a lot from the way equality works with anonymous classes.

In the off-chance that someone really wants reference equality, == using reference equality means they still have that, so we don't lose anything. It's the way to go.

*The default implementation of Equals() and GetHashCode() has an optimisation that let's it use a binary match in cases where it's safe to do so. Unfortunately there's a bug that makes it sometimes mis-identify some cases as safe for this faster approach when they aren't (or at least it used to, maybe it was fixed). A common case is if you have a decimal field, in a struct, then it'll consider some instances with equivalent fields as unequal.

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251