Note: see the link below for the English version of this article.
https://duongnt.com/valuetuple-name

Từ bản C# 7.0, chúng ta có thể đặt tên cho trường trong kiểu ValueTuple. Tính năng này giúp code sử dụng ValueTuple trở nên dễ hiểu hơn nhiều. Nhưng nó hoạt động như thế nào? Hãy cùng tìm hiểu trong bài hôm nay.
Nhắc lại về cách đặt tên cho trường trong ValueTuple
Python là một trong những ngôn ngữ tôi ưa thích. Với Python, ta dễ dàng trả về nhiều giá trị từ một hàm thông qua tuple.
def get_info():
return "John Doe", 30
name, age = get_info()
# name là "John Doe"
# age là 30
Tuy nhiên, nếu muốn làm điều tương tự với C#, tôi sẽ phải tự mình tạo một struct mới, hoặc sử dụng System.Tuple (đây là kiểu reference). Và nếu tôi dùng System.Tuple, tôi sẽ phải đọc dữ liệu thông qua những cái tên khó hiểu như là Item1/Item2/....
Từ bản C# 7.0, chúng ta có thể sử dụng System.ValueTuple. Từ cái tên, ta có thể đoán được đây là kiểu value. Và nó còn hỗ trợ đặt tên cho các trường.
(string Name, int Age) GetInfo() => (Name: "John Doe", Age: 30);
// Hoặc ngắn hơn: (string Name, int Age) GetInfo() => ("John Doe", 30);
var info = GetInfo();
var name = info.Name; // John Doe
var age = info.Age; // 30
Hơn thế nữa, Visual Studio có thể gợi ý tên cho các trường Name và Age.

Có vẻ là ta đã thêm được 2 trường mới vào kiểu ValueTuple. Điều này nghe có vẻ vô lý phải không?
Định nghĩa của ValueTuple không bao gồm tên trường
Ta có thể gán giá trị trả về của hàm GetInfo vào một ValueTuple với tên trường mặc định.
ValueTuple<string, int> info2 = GetInfo();
Console.WriteLine($"{info.Item1} is {info.Item2} years old");
Lúc này, ta phải đọc tên và tuổi qua 2 trường Item1 và Item2. Đồng thời Visual Studio cũng không hiển thị gợi ý tên trường Name và Age nữa.

Ở chiều ngược lại, ta có thể gán một ValueTuple với tên trường mặc định vào một ValueTuple có tên trường ta tự đặt. Lúc này, tên trường ta đặt sẽ lại hiện ra.
ValueTuple<string, int> info3 = ("Jane Doe", 31);
(string name, int age) = info3;
Console.WriteLine(info3.name) # "Jane Doe"
Console.WriteLine(info3.age) # 31
Thậm chí ta có thể đổi tên trường.
(string full_name, int age) info4 = GetInfo();
// Dòng này sinh lỗi: Console.WriteLine($"{info4.Name} is {info4.Age} years old")
// Dòng này sẽ chạy: Console.WriteLine($"{info4.full_name} is {info4.age} years old")
Nếu ta xem thử tên kiểu dữ liệu của các biến trên, ta sẽ thấy là tất cả các object ValueTuple đều dùng chung một kiểu.
Console.WriteLine(info.GetType().FullName);
Console.WriteLine(info2.GetType().FullName);
Console.WriteLine(info3.GetType().FullName);
Console.WriteLine(info4.GetType().FullName);
Tất cả đều in ra.
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]]
Thử xem code IL
Nếu muốn biết cách xem code IL, xin hãy tham khảo bài này.
Code IL để tạo object ValueTuple
Ta sẽ nghiên cứu code ví dụ dưới đây.
var info = (Name: "John Doe", Age: 30);
ValueTuple<string, int> info3 = ("Jane Doe", 31);
(string full_name, int age) info4 = ("John Doe", 30);
Code IL tương ứng của nó là như sau.
.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
Có thể thấy tên trường trong ValueTuple không có ý nghĩa gì đối với compiler. Nó sẽ tạo code IL giống hệt nhau trong mọi trường hợp. Thay vào đó, số kiểu generic và thứ tự của chúng lại là quan trọng.
valuetype [System.Runtime]System.ValueTuple`2<string,int32>
Điều này giải thích tại sao ta có thể gán giá trị của info/info2/info3/info4 cho nhau mà không gặp lỗi. Dù chúng có tên trường khác nhau, chúng đều có hai kiểu generic với cùng thứ tự: string và int.
Code IL để đọc giá trị trường từ ValueTuple
Dưới đây là code để đọc tên và tuổi từ ValueTuple ta tạo ở phần trước.
Console.WriteLine($"{info.Name}{info.Age}");
Console.WriteLine($"{info3.Item1}{info3.Item2}");
Console.WriteLine($"{info4.full_name}{info4.age}");
Nó tương ứng với code IL sau.
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
Chúng ta thấy rằng trong code IL, tất cả trường được đọc thông qua tên mặc định là Item1 và Item2. Visual Studio có thể hiện gợi ý tên và cho ta dùng tên trường mình tự đặt. Nhưng khi sinh code IL, nó vẫn sử dụng tên mặc định.
Làm sao Visual Studio biết được tên trường của ValueTuple?
Nếu trong code IL không chứa tên trường thì làm sao Visual Studio hiển thị được tên trường và cho ta sử dụng chúng? Với những ValueTuple định nghĩa trong cùng file thì câu trả lời rất đơn giản. Visual Studio thấy được ta đặt tên nào cho trường nào.
Nhưng nếu object ValueTuple được tạo bởi hàm định nghĩa trong file hay project khác thì sao? Trong phần trước, ta dùng hàm sau đây để tạo ValueTuple.
public (string Name, int Age) GetInfo() => (Name: "John Doe", Age: 30);
Và trong một file hay project khác, ta có thể gán giá trị trả về của nó cho một biến kiểu var. Mặc dù thế, Visual Studio vẫn hiển thị được gợi ý và cho phép ta dùng tên tự đặt.
var info = GetInfo();
// Có thể hiển thị gợi ý và cho phép dùng tên info.Name và info.Age
Hãy cùng xem code IL của 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
Để ý là trong IL ta có một TupleElementNamesAttribute để chứa tên trường. Bản thân attribute này không làm thay đổi cách hàm GetInfo hoạt động. Nhưng Visual Studio có thể đọc tên trường từ attribute đó rồi cho phép ta dùng tên khi viết code.
Kết thúc
Thoạt nhìn, có vẻ là ta có thể thêm trường một các tùy ý vào kiểu ValueTuple. Nhưng thực ra đó chỉ là hỗ trợ của IDE khi ta viết code. Tuy nhiên, tôi vẫn nghĩ rằng cách đội phát triển của .NET Core tạo chức năng này là khá thú vị.