Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/valuetuple-name-vie
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
.
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.
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.
One Thought on “All about ValueTuple field names in C#”