Tôi đã dịch ngược một số thư viện C # 7 và thấy ValueTuple
thuốc generic đang được sử dụng. Thay vào đó là gì ValueTuples
và tại sao không Tuple
?
Tôi đã dịch ngược một số thư viện C # 7 và thấy ValueTuple
thuốc generic đang được sử dụng. Thay vào đó là gì ValueTuples
và tại sao không Tuple
?
Câu trả lời:
Thay vào đó là gì
ValueTuples
và tại sao khôngTuple
?
A ValueTuple
là một cấu trúc phản ánh một tuple, giống như System.Tuple
lớp gốc .
Sự khác biệt chính giữa Tuple
và ValueTuple
là:
System.ValueTuple
là một loại giá trị (struct), trong khi System.Tuple
là một loại tham chiếu ( class
). Điều này có ý nghĩa khi nói về phân bổ và áp lực GC.System.ValueTuple
không chỉ là một struct
, nó là một thứ có thể thay đổi và người ta phải cẩn thận khi sử dụng chúng như vậy. Hãy nghĩ điều gì xảy ra khi một lớp giữ System.ValueTuple
một trường.System.ValueTuple
trưng bày các mục của nó thông qua các trường thay vì các thuộc tính.Cho đến C # 7, sử dụng bộ dữ liệu không thuận tiện. Tên trường của họ là Item1
, Item2
v.v., và ngôn ngữ đã không cung cấp cú pháp đường cho họ giống như hầu hết các ngôn ngữ khác làm (Python, Scala).
Khi nhóm thiết kế ngôn ngữ .NET quyết định kết hợp các bộ dữ liệu và thêm đường cú pháp cho chúng ở cấp độ ngôn ngữ, một yếu tố quan trọng là hiệu suất. Với ValueTuple
loại giá trị, bạn có thể tránh áp lực GC khi sử dụng chúng vì (như một chi tiết triển khai) chúng sẽ được phân bổ trên ngăn xếp.
Ngoài ra, một struct
ngữ nghĩa bình đẳng tự động (nông) được thực hiện bởi thời gian chạy, trong class
đó không. Mặc dù nhóm thiết kế đã đảm bảo rằng sẽ có một đẳng thức được tối ưu hóa hơn nữa cho các bộ dữ liệu, do đó đã thực hiện một đẳng thức tùy chỉnh cho nó.
Đây là một đoạn từ các ghi chú thiết kế củaTuples
:
Cấu trúc hoặc lớp:
Như đã đề cập, tôi đề xuất thực hiện các loại tuple
structs
hơn làclasses
, để không có hình phạt phân bổ nào được liên kết với chúng. Họ nên càng nhẹ càng tốt.Có thể cho rằng,
structs
cuối cùng có thể tốn kém hơn, bởi vì bài tập sao chép một giá trị lớn hơn. Vì vậy, nếu chúng được gán nhiều hơn so với chúng được tạo ra, thì đóstructs
sẽ là một lựa chọn tồi.Trong động lực rất lớn của họ, mặc dù, tuples là phù du. Bạn sẽ sử dụng chúng khi các bộ phận quan trọng hơn toàn bộ. Vì vậy, mô hình phổ biến sẽ là xây dựng, trả lại và ngay lập tức giải mã chúng. Trong tình huống này, cấu trúc rõ ràng là thích hợp hơn.
Structs cũng có một số lợi ích khác, sẽ trở nên rõ ràng sau đây.
Bạn có thể dễ dàng thấy rằng làm việc với System.Tuple
trở nên mơ hồ rất nhanh. Ví dụ: giả sử chúng ta có một phương pháp tính tổng và tổng số a List<Int>
:
public Tuple<int, int> DoStuff(IEnumerable<int> values)
{
var sum = 0;
var count = 0;
foreach (var value in values) { sum += value; count++; }
return new Tuple(sum, count);
}
Vào cuối nhận, chúng tôi kết thúc với:
Tuple<int, int> result = DoStuff(Enumerable.Range(0, 10));
// What is Item1 and what is Item2?
// Which one is the sum and which is the count?
Console.WriteLine(result.Item1);
Console.WriteLine(result.Item2);
Cách bạn có thể giải cấu trúc các bộ giá trị thành các đối số được đặt tên là sức mạnh thực sự của tính năng:
public (int sum, int count) DoStuff(IEnumerable<int> values)
{
var res = (sum: 0, count: 0);
foreach (var value in values) { res.sum += value; res.count++; }
return res;
}
Và vào cuối nhận:
var result = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {result.Sum}, Count: {result.Count}");
Hoặc là:
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {sum}, Count: {count}");
Nếu chúng ta nhìn vào trang bìa của ví dụ trước, chúng ta có thể thấy chính xác trình biên dịch đang diễn giải như thế nào ValueTuple
khi chúng ta yêu cầu nó giải cấu trúc:
[return: TupleElementNames(new string[] {
"sum",
"count"
})]
public ValueTuple<int, int> DoStuff(IEnumerable<int> values)
{
ValueTuple<int, int> result;
result..ctor(0, 0);
foreach (int current in values)
{
result.Item1 += current;
result.Item2++;
}
return result;
}
public void Foo()
{
ValueTuple<int, int> expr_0E = this.DoStuff(Enumerable.Range(0, 10));
int item = expr_0E.Item1;
int arg_1A_0 = expr_0E.Item2;
}
Trong nội bộ, mã được biên dịch sử dụng Item1
và Item2
, nhưng tất cả những thứ này được trừu tượng hóa khỏi chúng ta vì chúng ta làm việc với một bộ dữ liệu bị phân tách. Một tuple với các đối số được đặt tên được chú thích với TupleElementNamesAttribute
. Nếu chúng ta sử dụng một biến mới duy nhất thay vì phân tách, chúng ta sẽ nhận được:
public void Foo()
{
ValueTuple<int, int> valueTuple = this.DoStuff(Enumerable.Range(0, 10));
Console.WriteLine(string.Format("Sum: {0}, Count: {1})", valueTuple.Item1, valueTuple.Item2));
}
Lưu ý rằng trình biên dịch vẫn phải thực hiện một số phép thuật xảy ra (thông qua thuộc tính) khi chúng tôi gỡ lỗi ứng dụng của chúng tôi, vì nó sẽ là số lẻ để xem Item1
, Item2
.
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Sự khác biệt giữa Tuple
và ValueTuple
đó Tuple
là một loại tham chiếu và ValueTuple
là một loại giá trị. Điều thứ hai là mong muốn vì những thay đổi về ngôn ngữ trong C # 7 có các bộ dữ liệu được sử dụng thường xuyên hơn, nhưng việc phân bổ một đối tượng mới trên heap cho mỗi bộ là một mối quan tâm về hiệu suất, đặc biệt là khi không cần thiết.
Tuy nhiên, trong C # 7, ý tưởng là bạn không bao giờ phải sử dụng một cách rõ ràng loại nào vì đường cú pháp được thêm vào để sử dụng tuple. Ví dụ: trong C # 6, nếu bạn muốn sử dụng một tuple để trả về một giá trị, bạn sẽ phải làm như sau:
public Tuple<string, int> GetValues()
{
// ...
return new Tuple(stringVal, intVal);
}
var value = GetValues();
string s = value.Item1;
Tuy nhiên, trong C # 7, bạn có thể sử dụng điều này:
public (string, int) GetValues()
{
// ...
return (stringVal, intVal);
}
var value = GetValues();
string s = value.Item1;
Bạn thậm chí có thể tiến thêm một bước và đặt tên giá trị:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var value = GetValues();
string s = value.S;
... Hoặc giải mã hoàn toàn bộ dữ liệu:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var (S, I) = GetValues();
string s = S;
Tuples thường không được sử dụng trong C # trước 7 vì chúng cồng kềnh và dài dòng và chỉ thực sự được sử dụng trong trường hợp xây dựng lớp / cấu trúc dữ liệu cho một trường hợp công việc duy nhất sẽ rắc rối hơn giá trị. Nhưng trong C # 7, các bộ dữ liệu hiện có hỗ trợ cấp độ ngôn ngữ, vì vậy sử dụng chúng sẽ sạch hơn và hữu ích hơn nhiều.
Tôi nhìn vào nguồn cho cả hai Tuple
và ValueTuple
. Sự khác biệt là Tuple
một class
và ValueTuple
là một struct
thực hiện IEquatable
.
Điều đó có nghĩa là Tuple == Tuple
sẽ trả về false
nếu chúng không cùng thể hiện, nhưng ValueTuple == ValueTuple
sẽ trả về true
nếu chúng cùng loại và Equals
trả về true
cho mỗi giá trị mà chúng chứa.
Các câu trả lời khác đã quên đề cập đến các điểm quan trọng. Thay vào đó, tôi sẽ tham khảo tài liệu XML từ mã nguồn :
Các loại ValueTuple (từ arity 0 đến 8) bao gồm việc thực hiện thời gian chạy làm cơ sở cho các bộ dữ liệu trong C # và các bộ cấu trúc trong F #.
Ngoài việc được tạo thông qua cú pháp ngôn ngữ , chúng dễ dàng được tạo nhất thông qua các
ValueTuple.Create
phương thức xuất xưởng. Các System.ValueTuple
loại khác với các System.Tuple
loại trong đó:
Với việc giới thiệu loại này và trình biên dịch C # 7.0, bạn có thể dễ dàng viết
(int, string) idAndName = (1, "John");
Và trả về hai giá trị từ một phương thức:
private (int, string) GetIdAndName()
{
//.....
return (id, name);
}
Trái với System.Tuple
bạn có thể cập nhật các thành viên của nó (Mutable) vì chúng là các trường đọc-ghi công khai có thể được đặt tên có ý nghĩa:
(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
class MyNonGenericType : MyGenericType<string, ValueTuple, int>
v.v.
Ngoài các ý kiến ở trên, một điều đáng tiếc của ValueTuple là, như một loại giá trị, các đối số được đặt tên sẽ bị xóa khi được biên dịch sang IL, vì vậy chúng không có sẵn để tuần tự hóa trong thời gian chạy.
tức là các đối số có tên ngọt ngào của bạn sẽ vẫn kết thúc là "Item1", "Item2", v.v. khi được tuần tự hóa qua ví dụ Json.NET.
Tham gia muộn để thêm một sự làm rõ nhanh chóng về hai thực tế này:
Mọi người sẽ nghĩ rằng việc thay đổi giá trị tuples en-masse sẽ đơn giản:
foreach (var x in listOfValueTuples) { x.Foo = 103; } // wont even compile because x is a value (struct) not a variable
var d = listOfValueTuples[0].Foo;
Ai đó có thể cố gắng giải quyết vấn đề này như vậy:
// initially *.Foo = 10 for all items
listOfValueTuples.Select(x => x.Foo = 103);
var d = listOfValueTuples[0].Foo; // 'd' should be 103 right? wrong! it is '10'
Lý do cho hành vi kỳ quặc này là các bộ giá trị hoàn toàn dựa trên giá trị (cấu trúc) và do đó, lệnh gọi .Select (...) hoạt động trên các bản sao nhân bản thay vì trên bản gốc. Để giải quyết vấn đề này, chúng tôi phải sử dụng:
// initially *.Foo = 10 for all items
listOfValueTuples = listOfValueTuples
.Select(x => {
x.Foo = 103;
return x;
})
.ToList();
var d = listOfValueTuples[0].Foo; // 'd' is now 103 indeed
Hoặc tất nhiên người ta có thể thử cách tiếp cận đơn giản:
for (var i = 0; i < listOfValueTuples.Length; i++) {
listOfValueTuples[i].Foo = 103; //this works just fine
// another alternative approach:
//
// var x = listOfValueTuples[i];
// x.Foo = 103;
// listOfValueTuples[i] = x; //<-- vital for this alternative approach to work if you omit this changes wont be saved to the original list
}
var d = listOfValueTuples[0].Foo; // 'd' is now 103 indeed
Hy vọng điều này sẽ giúp ai đó đấu tranh để tạo ra những cái đuôi ra khỏi bộ giá trị được lưu trữ trong danh sách.