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ị.