Note: see the link below for the English version of this article.

https://duongnt.com/valuetuple-name

Tên trường của ValueTuple trong C#

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

Gợi ý tên trường của ValueTuple

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 Item1Item2. Đồng thời Visual Studio cũng không hiển thị gợi ý tên trường NameAge nữa.

Gợi ý tên trường của ValueTuple

Ở 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ự: stringint.

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

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

Leave a Reply