December 6, 2016

Tuples with named members in C# 7 – Smoke and mirrors I tell ya’

So I’ve been looking at the new syntax that is introduced with C# 7 and tuples with named members caught my eye, specifically I wonder how they had solved it without introducing type explosion.

Consider the following code that doesn’t use named members:

public (string, int) GetMyIdentity() => ("Lasse", 45);

When using the resulting tuple in code, you have access to two fields, Item1 and Item2, like this:

var tuple = GetMyIdentity();
Console.WriteLine($"Name={tuple.Item1}, Age={tuple.Item2}");

However, you can also name the members, like this:

public (string name, int age) GetMyIdentity() => ("Lasse", 45);

And now both Item1/Item2 and name/age are legal members, the following code will compile and run just fine:

var tuple = GetMyIdentity();
Console.WriteLine($"Name={tuple.<strong>Item1</strong>}, Age={tuple.Item2}");
Console.WriteLine($"Name={tuple.<strong>name</strong>}, Age={tuple.age}");

So how did they solve this? Did they create extra properties that simply redirect to Item1/Item2?

No, this is entirely smoke and mirrors. It is a compiler trick.

Let’s take a look at the declaration again and show a hidden attribute:

[TupleElementNames(new[] { "name", "age" })]
public (string, int) GetMyIdentity() => ("Lasse", 45);

The names are injected by an attribute and then the compiler fakes the entire thing at the call site. Basically, every access to the named members name and age are substituted with access to Item1 and Item2 respectively.

We can use LINQPad to compile a very simple program and look at its IL:


This is the resulting IL:

IL_0000: ldarg.0 
IL_0001: call UserQuery.GetMyIdentity
IL_0006: ldfld System.ValueTuple<System.String,System.Int32>.Item1
IL_000B: call System.Console.WriteLine
IL_0010: ret

Notice there is no trace of the name member in the compiled code.

The attribute is used in conjunction with these types of declarations:

  • Method return types
  • Properties
  • Fields
  • Method parameters

The attribute is not, however, used for local variables because they’re not needed. The compiler is holding the method in memory while compiling it so it knows exactly what those members should be named, let’s fire up LINQPad again:

var tuple = (name: "Lasse", age: 45);

Is compiled into this:

IL_0000: ldstr "Lasse"
IL_0005: ldc.i4.s 2D 
IL_0007: newobj System.ValueTuple<System.String,System.Int32>..ctor
IL_000C: ldfld System.ValueTuple<System.String,System.Int32>.Item1
IL_0011: call System.Console.WriteLine
IL_0016: ret</pre>

Notice that there is no trace of the name member.