Sự khác biệt giữa System.ValueTuple và System.Tuple là gì?


139

Tôi đã dịch ngược một số thư viện C # 7 và thấy ValueTuplethuốc generic đang được sử dụng. Thay vào đó là gì ValueTuplesvà tại sao không Tuple?


Tôi nghĩ rằng nó đề cập đến lớp Dot NEt Tuple. Bạn có thể vui lòng chia sẻ một mã mẫu. Vì vậy, nó sẽ dễ hiểu.
Ranadip Dutta

14
@Ranadip Dutta: Nếu bạn biết tuple là gì, bạn không cần mã mẫu để hiểu câu hỏi. Câu hỏi tự nó rất đơn giản: ValueTuple là gì và nó khác với Tuple như thế nào?
BoltClock

1
@BoltClock: Đó là lý do tôi không trả lời bất cứ điều gì trong bối cảnh đó. Tôi biết trong c #, có một lớp Tuple mà tôi sử dụng khá thường xuyên và cùng một số lớp tôi cũng gọi nó trong powershell. Đây là một loại tài liệu tham khảo. Bây giờ nhìn thấy các câu trả lời khác, tôi hiểu rằng có một loại giá trị còn được gọi là Valuetuple. Nếu có một mẫu tôi muốn biết cách sử dụng cho cùng.
Ranadip Dutta

2
Tại sao bạn dịch ngược chúng khi mã nguồn cho Roslyn có sẵn trên github?
Zein Makki

@ user3185569 có lẽ vì F12 tự động dịch ngược mọi thứ và dễ dàng hơn là nhảy sang GitHub
John Zabroski

Câu trả lời:


203

Thay vào đó là gì ValueTuplesvà tại sao không Tuple?

A ValueTuplelà một cấu trúc phản ánh một tuple, giống như System.Tuplelớp gốc .

Sự khác biệt chính giữa TupleValueTuplelà:

  • System.ValueTuplelà một loại giá trị (struct), trong khi System.Tuplelà 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.ValueTuplekhô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.ValueTuplemộ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, Item2v.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 ValueTupleloạ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 structngữ 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 structshơ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, structscuố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ì đó structssẽ 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.

Ví dụ:

Bạn có thể dễ dàng thấy rằng làm việc với System.Tupletrở 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}");

Trình biên dịch goodies:

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 ValueTuplekhi 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 Item1Item2, 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.


1
Lưu ý rằng bạn cũng có thể sử dụng cú pháp đơn giản hơn (và theo ý kiến ​​của tôi, tốt hơn)var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47

@ Abion47 Điều gì sẽ xảy ra nếu cả hai loại khác nhau?
Yuval Itzchakov

Ngoài ra, làm thế nào để các điểm của bạn "đó là một cấu trúc có thể thay đổi " và "nó phơi bày các trường chỉ đọc " đồng ý?
CodeInChaos

@CodesInChaos Nó không. Tôi đã thấy [cái này] ( github.com/dotnet/corefx/blob/master/src/Common/src/System/ trộm ), nhưng tôi không nghĩ đó là thứ mà trình biên dịch phát ra, vì các trường cục bộ không thể dù sao cũng chỉ đọc. Tôi nghĩ đề xuất này có nghĩa là "bạn có thể làm cho họ sẵn sàng đọc nếu bạn muốn, nhưng điều đó tùy thuộc vào bạn" , mà tôi đã giải thích sai.
Yuval Itzchakov

1
Một số nits: "chúng sẽ được phân bổ trên ngăn xếp" - chỉ đúng với các biến cục bộ. Không nghi ngờ gì bạn biết điều này, nhưng thật không may, cách bạn thực hiện điều này có khả năng tiếp tục huyền thoại rằng các loại giá trị luôn sống trong ngăn xếp.
Peter Duniho

26

Sự khác biệt giữa TupleValueTupleđó Tuplelà một loại tham chiếu và ValueTuplelà 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.


10

Tôi nhìn vào nguồn cho cả hai TupleValueTuple. Sự khác biệt là Tuplemột classValueTuplelà một structthực hiện IEquatable.

Điều đó có nghĩa là Tuple == Tuplesẽ trả về falsenếu chúng không cùng thể hiện, nhưng ValueTuple == ValueTuplesẽ trả về truenếu chúng cùng loại và Equalstrả về truecho mỗi giá trị mà chúng chứa.


Nó còn hơn thế nữa.
BoltClock

2
@BoltClock Nhận xét của bạn sẽ mang tính xây dựng nếu bạn muốn giải thích
Peter Morris

3
Ngoài ra, các loại giá trị không nhất thiết phải đi vào ngăn xếp. Sự khác biệt là về mặt ngữ nghĩa đại diện cho giá trị, thay vì tham chiếu, bất cứ khi nào biến đó được lưu trữ, có thể hoặc không thể là ngăn xếp.
Phục vụ

6

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.Createphương thức xuất xưởng. Các System.ValueTupleloại khác với các System.Tupleloại trong đó:

  • chúng là các cấu trúc chứ không phải là các lớp,
  • chúng có thể thay đổi thay vì chỉ đọc
  • các thành viên của họ (như Item1, Item2, v.v.) là các trường chứ không phải là thuộc tính.

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.Tuplebạ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";

"Arity 0 đến 8". Ah, tôi thích thực tế là chúng bao gồm 0-tuple. Nó có thể được sử dụng như một loại trống và sẽ được phép sử dụng trong tổng quát khi không cần một số tham số loại, như trong class MyNonGenericType : MyGenericType<string, ValueTuple, int>v.v.
Jeppe Stig Nielsen

6

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.


2
Vì vậy, về mặt kỹ thuật đó là một sự tương đồng và không phải là một sự khác biệt;)
JAD

2

Tham gia muộn để thêm một sự làm rõ nhanh chóng về hai thực tế này:

  • chúng là các cấu trúc chứ không phải là các lớp
  • chúng có thể thay đổi thay vì chỉ đọc

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.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.