Note: phiên bản Tiếng Việt của bài này ở link dưới.

https://duongnt.com/valuetuple-name-vie

All about ValueTuple field names in C#

From C# 7.0 onwards, the ValueTuple type supports naming its members. This makes code using ValueTuple much more readable. But how does this feature work? Let’s find out in today’s article.

A primer on ValueTuple field names

Python is one of my favourite languages. In Python, it’s simple to return multiple values from a method. We can just use a tuple.

def get_info():
    return "John Doe", 30

name, age = get_info()
# name is "John Doe"
# age is 30

Unfortunately, if I want to do the same thing in C#, I either have to define my own struct, or use System.Tuple (which is a reference type). And if I use System.Tuple, I will have to read its members with generic names like Item1/Item2/....

Starting from C# 7.0, we can use the System.ValueTuple type instead. As its name suggests, it is a value type. And it supports naming its members.

(string Name, int Age) GetInfo() => (Name: "John Doe", Age: 30);
// Or even shorter: (string Name, int Age) GetInfo() => ("John Doe", 30);

var info = GetInfo();
var name = info.Name; // John Doe
var age = info.Age; // 30

Moreover, Visual Studio can display hints about the two fields Name and Age.

ValueTuple field name hints

It’s as if we have added two new members into the ValueTuple type. But that shouldn’t be possible, should it?

The ValueTuple definition does not include field names

We can assign the return value of the GetInfo method to a ValueTuple with default field names.

ValueTuple<string, int> info2 = GetInfo();
Console.WriteLine($"{info.Item1} is {info.Item2} years old");

In this case, we must access the name and age as Item1 and Item2. Visual Studio also can’t display hints for Name and Age anymore.

ValueTuple field name hints

In the other direction, we can assign a ValueTuple without field names to a ValueTuple that has field names. Then the field names will magically reappear.

ValueTuple<string, int> info3 = ("Jane Doe", 31);
(string name, int age) = info3;

Console.WriteLine(info3.name) # "Jane Doe"
Console.WriteLine(info3.age) # 31

We can even change the field names on the fly.

(string full_name, int age) info4 = GetInfo();
// This will throw exception: Console.WriteLine($"{info4.Name} is {info4.Age} years old")
// This is correct: Console.WriteLine($"{info4.full_name} is {info4.age} years old")

If we check the type’s name, we can see that all the ValueTuple objects above have the same type.

Console.WriteLine(info.GetType().FullName);
Console.WriteLine(info2.GetType().FullName);
Console.WriteLine(info3.GetType().FullName);
Console.WriteLine(info4.GetType().FullName);

All prints.

System.ValueTuple`2[[System.String, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]

Dive into the IL code

If you want to know how to get the generated IL code, please see this article.

IL code to create ValueTuple objects

Let’s say we have the following code.

var info = (Name: "John Doe", Age: 30);
ValueTuple<string, int> info3 = ("Jane Doe", 31);
(string full_name, int age) info4 = ("John Doe", 30);

Below is their IL code.

.maxstack  3
.locals init (valuetype [System.Runtime]System.ValueTuple`2<string,int32> V_0,
       valuetype [System.Runtime]System.ValueTuple`2<string,int32> V_1,
       valuetype [System.Runtime]System.ValueTuple`2<string,int32> V_2)
IL_0000:  nop
IL_0001:  ldloca.s   V_0
IL_0003:  ldstr      "John Doe"
IL_0008:  ldc.i4.s   30
IL_000a:  call       instance void valuetype [System.Runtime]System.ValueTuple`2<string,int32>::.ctor(!0,
                                                                                                    !1)
IL_000f:  ldloca.s   V_1
IL_0011:  ldstr      "Jane Doe"
IL_0016:  ldc.i4.s   31
IL_0018:  call       instance void valuetype [System.Runtime]System.ValueTuple`2<string,int32>::.ctor(!0,
                                                                                                    !1)
IL_001d:  ldstr      "John Doe"
IL_0022:  ldc.i4.s   30
IL_0024:  newobj     instance void valuetype [System.Runtime]System.ValueTuple`2<string,int32>::.ctor(!0,
                                                                                                    !1)
IL_0029:  stloc.2
IL_002a:  ret

The names of ValueTuple fields are meaningless to the compiler. It generates the same IL code in all cases. On the other hand, the number and order of generic types are important.

valuetype [System.Runtime]System.ValueTuple`2<string,int32>

This explains why we can assign info/info2/info3/info4 to each other without error. Although they have different field names, they all have two generic types in the same order: string and int.

IL code to access fields in ValueTuple objects

Below is the code to access the name and age of each ValueTuple object created above.

Console.WriteLine($"{info.Name}{info.Age}");
Console.WriteLine($"{info3.Item1}{info3.Item2}");
Console.WriteLine($"{info4.full_name}{info4.age}");

The generated IL code is here.

IL_0029:  stloc.2
IL_002a:  ldloc.0
IL_002b:  ldfld      !0 valuetype [System.Runtime]System.ValueTuple`2<string,int32>::Item1
IL_0030:  stloc.3
IL_0031:  ldloc.0
IL_0032:  ldfld      !1 valuetype [System.Runtime]System.ValueTuple`2<string,int32>::Item2
IL_0037:  stloc.s    V_4
IL_0039:  ldloc.1
IL_003a:  ldfld      !0 valuetype [System.Runtime]System.ValueTuple`2<string,int32>::Item1
IL_003f:  stloc.s    V_5
IL_0041:  ldloc.1
IL_0042:  ldfld      !1 valuetype [System.Runtime]System.ValueTuple`2<string,int32>::Item2
IL_0047:  stloc.s    V_6
IL_0049:  ldloc.2
IL_004a:  ldfld      !0 valuetype [System.Runtime]System.ValueTuple`2<string,int32>::Item1
IL_004f:  stloc.s    V_7
IL_0051:  ldloc.2
IL_0052:  ldfld      !1 valuetype [System.Runtime]System.ValueTuple`2<string,int32>::Item2
  IL_0057:  stloc.s    V_8

We can see that in the IL code, all fields are accessed via the default names Item1 and Item2. Visual Studio can display hints for field names and it allows us to use them. But when it generates the IL code, it reverts back to the default names.

How can Visual Studio know ValueTuple field names?

If the generated code above does not have any information about field names, then how can Visual Studio display hints and let us use them? For ValueTuple defined within the same file, the answer is simple. It saw that we gave names to those fields.

But what if the ValueTuple object is created by a method defined in a different file or project? In an earlier section, we used this method to create a ValueTuple.

public (string Name, int Age) GetInfo() => (Name: "John Doe", Age: 30);

And in a different file or project, we can assign its return value to an implicitly typed local variable. Yet Visual Studio can still display hints and use those field names.

var info = GetInfo();
// Can display hints and use info.Name and info.Age

Let’s see the generated IL for GetInfo().

.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
.param [0]
.custom instance void [System.Runtime]System.Runtime.CompilerServices.TupleElementNamesAttribute::.ctor(string[]) = ( 01 00 02 00 00 00 04 4E 61 6D 65 03 41 67 65 00   // .......Name.Age.
                                                                                                                    00 ) 
.custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = ( 01 00 02 00 00 00 00 01 00 00 ) 
// Code size       16 (0x10)
.maxstack  8
IL_0000:  ldstr      "John Doe"
IL_0005:  ldc.i4     0x01e
IL_000a:  newobj     instance void valuetype [System.Runtime]System.ValueTuple`2<string,int32>::.ctor(!0,
                                                                                                    !1)
IL_000f:  ret

Notice in the generated IL, we have a TupleElementNamesAttribute that stores the field names. That attribute by itself does not affect the execution of the GetInfo method at all. But Visual Studio can read the names from that attribute and let us use them in our code.

Conclusion

At first glance, it feels like we can add arbitrary members to the ValueTuple type on the fly. But at the end of the day, they are just syntactic sugar. Despite that, I still think the way the .NET Core team implemented this feature is very interesting.

A software developer from Vietnam and is currently living in Japan.

One Thought on “All about ValueTuple field names in C#”

Leave a Reply