Docs: C# Conceptual: Tuples vs. ValueTuples vs. Anonymous Types vs. Struct vs Class

Created on 19 Feb 2017  路  14Comments  路  Source: dotnet/docs

There are several different syntax elements developers can use to create types. An important question for developers is when to choose which syntax.

This can be especially difficult with syntax elements that create very similar types (like anonymous types and Value Tuples).

See the comment on this topic for a direct customer question.

Area - C# Guide Technology - C# Fundamentals P1 discussion doc-idea

Most helpful comment

Here is a 馃挬 benchmark I threw together in REPL:

{
    var valueTupleQuery = from i in Enumerable.Range(0, 100000)
                          select (a: i, b: i, c: i, d: i, e: i, x: i, y: i + 1, z: i + 2) into x
                          where x.x > 100001
                          select (x: x, _: 0) into t
                          where t.x.x < 0
                          select t.x;

    var anonymousQuery = from i in Enumerable.Range(0, 100000)
                         select new { a = i, b = i, c = i, d = i, e = i, x = i, y = i + 1, z = i + 2 } into x
                         where x.x > 100001
                         select (x: x, _: 0) into t
                         where t.x.x < 0
                         select t.x;

    var stopwatch = new Stopwatch();

    stopwatch.Restart();
    for (var i = 0; i < 1000; i++)
    {
        valueTupleQuery.ToArray();
    }
    stopwatch.Stop();

    WriteLine(stopwatch.ElapsedMilliseconds);

    stopwatch.Restart();
    for (var i = 0; i < 1000; i++)
    {
        anonymousQuery.ToArray();
    }
    stopwatch.Stop();

    WriteLine(stopwatch.ElapsedMilliseconds);

}

Here, the anonymous class version considerably outperforms the value tuple version. What is interesting is that I made sure that both the anonymous class and the value tuple each had eight members or elements, which is the magic number to cause the tuple literal to produce a nested value tuple. If you remove the eighth member and element from the anonymous type and value tuple, the value tuple version outperforms the anonymous type version.

It is also important to note that the query expression syntax in LINQ will in most cases result in the production of anonymous types with transparent identifier fields, and considering that all IQueryProvider implementations to date have had to know about and explicitly rely on that behavior to get their job done, I don't think it will be changing.

All 14 comments

It is unclear to me if, in the light of the new tuple feature, anonymous types have any value at all. From what I can tell, tuples are superior in every way; no new type, can be returned from methods, etc.

So, are anonymous types preferred over C#7 tuples in any scenario? If so, what scenarios and why?

F# has a similar relationship w.r.t tuples, struct tuples, and records. From the F# Component Design Guidelines:

For return types containing many components, or where the components are related to a single identifiable entity, consider using a named type instead of a tuple.

That roughly breaks down to:

  • Tuples (or struct tuples) for an implementation detail/ad-hoc grouping of values, or something small and only intended for consumption in F#
  • Anything else for data intended to be consumed publicly

I think this makes sense for C# as well, and is also going to be valid if C# ends up adding Records or Discriminated Unions as well.

Structs/Classes is already documented. I'm not sure where/when that is going to be merged over, but I imagine the content could be copied over.

Anonymous Types is interesting. They were the answer for ad-hoc grouping of values prior to C# tuples, but with pattern matching (cough, sorry, C# calls it deconstruction) to decompose data into constituent parts, @somewhatabstract abstract is sort of right in that it's probably better to use tuples in those kinds of situations. I imagine we'll have to consult others for official guidance on this one, but I can see Anonymous Types being relegated a niche language feature.

I'm not sure if anonymous types are going to be relegated. I've been thinking along those lines as well, and trying to come up with use cases where they may be a better choice than Tuples.

The only one I've come up with is if a compound type is used as a key in some hash based container. Would the immutability of anonymous types mean they would be a better choice than Tuples?

That content will be brought over with the Framework docs migration @cartermp. But we can't make updates to that content since it's copyrighted material.

But we can't make updates to that content since it's copyrighted material.

Don't we own the copyright though?...

@cartertmp No, the copyright is owned by Addison-Wesley.

@BillWagner It's a marginally better choice given the immutability, but given it is barely an advantage it still seems like we could manage entirely without anonymous types. I am still struggling to find a scenario where I'd wholeheartedly recommend them over tuples. I mean, in the case of using things in hash-based containers, surely the overwhelmingly better choice than both tuples and anonymous types is to make a named type with an appropriate implementation of GetHashCode.

@somewhatabstract I agree that it's a small difference. Right now, I'm taking notes on any differences I find between ValueTuple and anonymous types. I want to be really sure we've thought of all the ramifications before providing guidance that Anonymous types are not a good idea.

To that end, I thought of one other major difference: Reflection and field/property names: Anonymous types have semantic names for the properties. ValueTask has fields named Item1, Item2, etc. The semantic names are a combination of compiler magic and attributes (on publicly accessible Tuples).

Again, I'm not sure this is a common use case. I just want to keep notes...

I don't have cold hard data to back me up on this, but I have a hunch that when it comes to LINQ, anonymous classes could prove to be more performant, especially as more Enumerable operators used and more members are present in the anonymous class. All of that yield return-ing with a bunch of heavy structs could result in a ton of copying.

Here is a 馃挬 benchmark I threw together in REPL:

{
    var valueTupleQuery = from i in Enumerable.Range(0, 100000)
                          select (a: i, b: i, c: i, d: i, e: i, x: i, y: i + 1, z: i + 2) into x
                          where x.x > 100001
                          select (x: x, _: 0) into t
                          where t.x.x < 0
                          select t.x;

    var anonymousQuery = from i in Enumerable.Range(0, 100000)
                         select new { a = i, b = i, c = i, d = i, e = i, x = i, y = i + 1, z = i + 2 } into x
                         where x.x > 100001
                         select (x: x, _: 0) into t
                         where t.x.x < 0
                         select t.x;

    var stopwatch = new Stopwatch();

    stopwatch.Restart();
    for (var i = 0; i < 1000; i++)
    {
        valueTupleQuery.ToArray();
    }
    stopwatch.Stop();

    WriteLine(stopwatch.ElapsedMilliseconds);

    stopwatch.Restart();
    for (var i = 0; i < 1000; i++)
    {
        anonymousQuery.ToArray();
    }
    stopwatch.Stop();

    WriteLine(stopwatch.ElapsedMilliseconds);

}

Here, the anonymous class version considerably outperforms the value tuple version. What is interesting is that I made sure that both the anonymous class and the value tuple each had eight members or elements, which is the magic number to cause the tuple literal to produce a nested value tuple. If you remove the eighth member and element from the anonymous type and value tuple, the value tuple version outperforms the anonymous type version.

It is also important to note that the query expression syntax in LINQ will in most cases result in the production of anonymous types with transparent identifier fields, and considering that all IQueryProvider implementations to date have had to know about and explicitly rely on that behavior to get their job done, I don't think it will be changing.

@tuespetre, I agree with those thoughts on performance; at some point the copying of structs may outweigh the savings from not being allocated on (and GC'ed from) the heap.

Another difference is that tuple names are a compile-time illusion, whereas anonymous object member names are really there at runtime, observable through dynamic and reflection.

Until https://github.com/dotnet/roslyn/issues/12897 is implemented, we're stuck with anonymous types.

We get serialization for free with anonymous types in ASP via the framework. Do we get tuple serialization for free as well?

Pasting this code here, using benchmark .NET

using System;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

public class Program
{
    static void Main()
    {
        _ = BenchmarkRunner.Run<SystemTupleVsValueTupleVsAnonymousType>();
        Console.ReadLine();
    }
}

[
    RPlotExporter, SimpleJob(RuntimeMoniker.NetCoreApp31, baseline: true)
]
public class SystemTupleVsValueTupleVsAnonymousType
{
    const int Start = 0;
    const int End = 10000000;

    [Benchmark]
    public List<long> AnonymousType() =>
        Enumerable.Range(Start, End)
            .Select(i => new { a = i * 2, b = i ^ 7, c = i % 3, d = i / 5, x = i + i, y = i + 1, date = DateTime.Today.AddMinutes(i) })
            .Where(x => x.x <= End)
            .Select(x => x.a + x.b + x.c + x.d - x.x - x.y - x.date.Ticks)
            .ToList();

    [Benchmark]
    public List<long> SystemTuple() =>
        Enumerable.Range(Start, End)
            .Select(i => new Tuple<int, int, int, int, int, int, DateTime>(i * 2, i ^ 7, i % 3, i / 5, i + i, i + 1, DateTime.Today.AddMinutes(i)))
            .Where(x => x.Item6 <= End)
            .Select(x => x.Item1 + x.Item2 + x.Item3 + x.Item4 - x.Item5 - x.Item6 - x.Item7.Ticks)
            .ToList();

    [Benchmark]
    public List<long> ValueTuple() =>
        Enumerable.Range(Start, End)
            .Select(i => (a: i * 2, b: i ^ 7, c: i % 3, d: i / 5, x: i + i, y: i + 1, date: DateTime.Today.AddMinutes(i)))
            .Where(x => x.x <= End)
            .Select(x => x.a + x.b + x.c + x.d - x.x - x.y - x.date.Ticks)
            .ToList();
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

tswett picture tswett  路  3Comments

ygoe picture ygoe  路  3Comments

sime3000 picture sime3000  路  3Comments

Eilon picture Eilon  路  3Comments

gmatv picture gmatv  路  3Comments