0

I believe the following code should generate two instances of the same anonymous type, with properties in the same order, with the same names, types and values.

    static void Main(string[] args)
    {
        var letterFreq1 = CountLetters("aabbbc");  
        var letterFreq2 = CountLetters("aabbbc");  

        if (letterFreq1.Equals(letterFreq2))
            Console.WriteLine("Is anagram");
        else
            Console.WriteLine("Is not an anagram");
        
    }
    public static object CountLetters(string input) => input.ToCharArray()
                                                            .GroupBy(x => x)
                                                            .Select(x => new {Letter = x.Key, Count = x.Count()})
                                                            .OrderBy(x => x.Letter)
                                                            .ToList();

According to MS documentation:

If two or more anonymous object initializers in an assembly specify a sequence of properties that are in the same order and that have the same names and types, the compiler treats the objects as instances of the same type. They share the same compiler-generated type information.

and

Because the Equals and GetHashCode methods on anonymous types are defined in terms of the Equals and GetHashCode methods of the properties, two instances of the same anonymous type are equal only if all their properties are equal.

I interpret this as meaning I should get equality on letterFreq1 and letterFreq2, but this isn't occurring. Can anyone identify why the equality check fails please? I'm trying to avoid a manual approach to comparing property values.

This is a similar question but doesn't seen to help solve my issue.

Many thanks.

Ratty
  • 33
  • 4
  • 2
    You're right in that the anonymous type objects will compare equal, but that's not what you're doing. You're invoking .Equals on a `List`, and it doesn't compare contents, it does reference comparison, so even though the two lists have the same content, they compare different. – Lasse V. Karlsen Jul 31 '20 at 19:37
  • Thanks Lasse, much appreciated. I didn't spot that one. – Ratty Jul 31 '20 at 19:39
  • See my answer for some alternatives. – Lasse V. Karlsen Jul 31 '20 at 19:45
  • The part of the spec you quoted doesn't apply here. It's about two or more *separate expressions*, but you only have one expression (executed however many times). – madreflection Jul 31 '20 at 19:54

1 Answers1

4

I believe the following code should generate two instances of the same anonymous type

No, it generates two instances of List<T>, with the same content.

So when you execute this:

if (letterFreq1.Equals(letterFreq2))

you're invoking the .Equals method on List<T> objects, which does not override the method inherited from System.Object, and thus do reference comparison.

You're right, however, in that the anonymous types would compare equal, and the two lists does in fact have the same content, but the list objects doesn't do content comparison by themselves, so they will compare as different.

If you were to coax the compiler into converting the two into the same type of collection, such as:

var letterFreq1 = CountLetters("aabbbc") as IEnumerable<object>;
var letterFreq2 = CountLetters("aabbbc") as IEnumerable<object>;

then you could compare their contents:

if (letterFreq1.SequenceEqual(letterFreq2))

but then you need to first know that they are collections, so depending on how general/generic your code is supposed to be, this may or may not be a solution for you.


My real advice, however, would be to avoid using anonymous types in this case. They're nice when used with local variables, but as you notice, when they escape the confines of a method they become very cumbersome to work with.

A better replacement would be a tuple:

void Main(string[] args)
{
    var letterFreq1 = CountLetters("aabbbc");  
    var letterFreq2 = CountLetters("aabbbc");  

    if (letterFreq1.SequenceEqual(letterFreq2))
        Console.WriteLine("Is anagram");
    else
        Console.WriteLine("Is not an anagram");
}

public static List<(char Letter, int Count)> CountLetters(string input)
    => input.ToCharArray()
        .GroupBy(x => x)
        .Select(x => (Letter: x.Key, Count : x.Count()))
        .OrderBy(x => x.Letter)
        .ToList();

An even better solution would be to create a named type for this, but again, depending on your situation this may or may not be a good solution for you.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • Why return List? IEnumerable would work just fine here, no call to ToList is needed. Mutating the resulting collection makes no sense as it would no longer correspond to the input string. – asawyer Jul 31 '20 at 19:57
  • It would work just fine. It's just that this is my style of programming. Make output types as open as possible, without making them more difficult to work with. Returning a `List` *in my opinion* signals that the list is yours, do with it what you want. You can easily stuff that into a variable of type `IEnumerable`, but if you intend to stuff it into a list, for you to then have to call `.ToList` on the object, *when it already is a list* is making it more complicated. But that's just me. `IEnumerable` would work as well. – Lasse V. Karlsen Jul 31 '20 at 20:01
  • If you're asking about why there is a `.ToList()` inside the method, then ask that question of the OP. I simply mirrored the entire method, replacing the anonymous object with a tuple. – Lasse V. Karlsen Jul 31 '20 at 20:02
  • I returned List due to force of habit when using EF materialised queries via Linq lambdas. Dropping the ToList() and comparing via SequenceEqual on IEnumerable works great. Thanks for the comprehensive answer and @asawyer for your comment. – Ratty Jul 31 '20 at 20:09
  • @LasseV.Karlsen Interesting. I do my best to do the exact opposite - sealed types by default, use the least derived type or interface as possible ect. See: https://stackoverflow.com/a/8717782/426894 more thoughts. `signals that the list is yours` but that's not what it signals, it signals that the return type is a list, nothing more. The question is, in a given context, what return type is the most appropriate to implement the semantics? If a adding or removing items is expected use ICollection, if they also need by index, IList, if they just need to enumerable a set, IEnumerable ect ect. – asawyer Jul 31 '20 at 20:22
  • Yes, well, my opinion differs, what can I say :) If my code in the method constructs a list, for whatever reason, and it is fine to return that list and relinquish ownership, unless I'm adhering to some API or interface that require a specific type, I'll just return the type I constructed internally. The caller can easily treat this object as whatever type of collection type that is compatible, and does not get any performance downsides of having to construct a list, out of a list, for instance. But again, my opinion, nothing more. – Lasse V. Karlsen Jul 31 '20 at 20:40
  • Basically, my rule for types regarding methods is to be as open as possible for inputs and as specific as possible for outputs. I got it wrong in an earlier comment. But again, this is just my opinion. – Lasse V. Karlsen Jul 31 '20 at 20:41