Apa perbedaan antara System.ValueTuple dan System.Tuple?


144

Saya mendekompilasi beberapa perpustakaan C # 7 dan melihat ValueTupleobat generik digunakan. Apa ValueTuplesdan mengapa tidak Tuple?


Saya pikir ini mengacu pada kelas Dot NEt Tuple. Bisakah Anda membagikan kode contoh. Agar mudah dimengerti.
Ranadip Dutta

15
@Ranadip Dutta: Jika Anda tahu apa itu tuple, Anda tidak memerlukan kode contoh untuk memahami pertanyaannya. Pertanyaannya sendiri langsung: apa itu ValueTuple dan apa bedanya dengan Tuple?
BoltClock

1
@ BoltClock: Itulah alasan saya tidak menjawab apa pun tentang konteks itu. Saya tahu di c #, ada kelas Tuple yang saya gunakan cukup sering dan kelas yang sama beberapa kali saya menyebutnya di PowerShell juga. Ini adalah tipe referensi. Sekarang melihat jawaban lain saya mengerti bahwa ada jenis nilai juga yang dikenal sebagai Valuetuple. Jika ada sampel saya ingin tahu kegunaannya sama.
Ranadip Dutta

2
Mengapa Anda mendekompilasi ini ketika kode sumber untuk Roslyn tersedia di github?
Zein Makki

1
@ user3185569 mungkin karena F12 secara otomatis mendekompilasi sesuatu dan lebih mudah daripada melompat ke GitHub
John Zabroski

Jawaban:


212

Apa ValueTuplesdan mengapa tidak Tuple?

A ValueTupleadalah struct yang mencerminkan tupel, sama dengan System.Tuplekelas aslinya .

Perbedaan utama antara Tupledan ValueTupleadalah:

  • System.ValueTupleadalah tipe nilai (struct), sedangkan System.Tupleadalah tipe referensi ( class). Ini berarti ketika berbicara tentang alokasi dan tekanan GC.
  • System.ValueTuplebukan hanya a struct, ini juga bisa berubah , dan seseorang harus berhati-hati saat menggunakannya. Pikirkan apa yang terjadi ketika kelas memegang System.ValueTuplesebagai bidang.
  • System.ValueTuple mengekspos itemnya melalui bidang, bukan properti.

Sampai C # 7, menggunakan tupel sangat tidak nyaman. Nama bidang mereka adalah Item1,, Item2dll, dan bahasa tersebut belum menyediakan gula sintaks untuk mereka seperti kebanyakan bahasa lain (Python, Scala).

Ketika tim desain bahasa .NET memutuskan untuk menggabungkan tupel dan menambahkan gula sintaks ke mereka di tingkat bahasa, faktor penting adalah kinerja. Dengan ValueTuplemenjadi tipe nilai, Anda dapat menghindari tekanan GC saat menggunakannya karena (sebagai detail implementasi) mereka akan dialokasikan di stack.

Selain itu, structsemantik persamaan otomatis mendapatkan (dangkal) oleh runtime, di mana a classtidak. Meskipun tim desain memastikan akan ada kesetaraan yang lebih optimal untuk tupel, oleh karena itu menerapkan persamaan khusus untuk itu.

Berikut adalah paragraf dari catatan desainTuples :

Struct atau Class:

Seperti yang disebutkan, saya mengusulkan untuk membuat jenis tupel structsdaripada classes, sehingga tidak ada penalti alokasi yang terkait dengannya. Mereka harus seringan mungkin.

Bisa dibilang, structsbisa jadi lebih mahal, karena nilai salinan tugas lebih besar. Jadi jika mereka ditugaskan lebih banyak daripada yang mereka buat, maka structsakan menjadi pilihan yang buruk.

Namun, dalam motivasi mereka, tupel bersifat sementara. Anda akan menggunakannya saat bagian-bagiannya lebih penting daripada keseluruhan. Jadi pola yang umum adalah membangun, mengembalikan dan segera mendekonstruksinya. Dalam situasi ini, struct jelas lebih disukai.

Struktur juga memiliki sejumlah manfaat lain, yang akan dijelaskan berikut ini.

Contoh:

Anda dapat dengan mudah melihat bahwa bekerja dengan System.Tuplemenjadi ambigu dengan sangat cepat. Misalnya, kita memiliki metode yang menghitung jumlah dan jumlah 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);
}

Di sisi penerima, kami berakhir dengan:

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);

Cara Anda mendekonstruksi tupel nilai menjadi argumen bernama adalah kekuatan sebenarnya dari fitur tersebut:

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;
}

Dan di pihak penerima:

var result = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {result.Sum}, Count: {result.Count}");

Atau:

var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {sum}, Count: {count}");

Barang penyusun:

Jika kita melihat di bawah sampul contoh kita sebelumnya, kita dapat melihat dengan tepat bagaimana kompilator menafsirkan ValueTupleketika kita memintanya untuk mendekonstruksi:

[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;
}

Secara internal, kode yang dikompilasi menggunakan Item1dan Item2, tetapi semua ini disarikan dari kita karena kita bekerja dengan tupel yang terdekomposisi. Sebuah tupel dengan argumen bernama mendapat anotasi dengan TupleElementNamesAttribute. Jika kita menggunakan satu variabel segar daripada mendekomposisi, kita mendapatkan:

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));
}

Perhatikan bahwa compiler masih harus membuat beberapa keajaiban terjadi (melalui atribut) ketika kita men-debug aplikasi kita, karena akan menjadi aneh melihat Item1, Item2.


1
Perhatikan bahwa Anda juga dapat menggunakan sintaks yang lebih sederhana (dan menurut saya, lebih disukai)var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47

@ Abion47 Apa yang akan terjadi jika kedua jenis berbeda?
Yuval Itzchakov

Juga bagaimana poin Anda "itu adalah struct yang bisa berubah " dan "itu memaparkan bidang hanya baca " setuju?
CodesInChaos

@CodesInChaos Ini tidak. Saya melihat [ini] ( github.com/dotnet/corefx/blob/master/src/Common/src/System/… ), tetapi saya tidak berpikir itulah yang akhirnya dipancarkan oleh kompilator, karena bidang lokal tidak dapat hanya membaca. Saya pikir proposal itu berarti "Anda dapat membuatnya hanya untuk dibaca jika Anda mau, tetapi itu terserah Anda" , yang salah tafsir.
Yuval Itzchakov

2
Beberapa nits: "mereka akan dialokasikan di stack" - benar hanya untuk variabel lokal. Tidak diragukan lagi Anda tahu ini, tetapi sayangnya, cara Anda mengutarakan hal ini cenderung mengabadikan mitos bahwa tipe nilai selalu hidup di tumpukan.
Peter Duniho

27

Perbedaan antara Tupleand ValueTupleis itu Tupleadalah jenis referensi dan ValueTuplemerupakan jenis nilai. Yang terakhir ini diinginkan karena perubahan pada bahasa di C # 7 membuat tupel lebih sering digunakan, tetapi mengalokasikan objek baru di heap untuk setiap tupel adalah masalah performa, terutama jika tidak diperlukan.

Namun, di C # 7, idenya adalah bahwa Anda tidak perlu secara eksplisit menggunakan kedua jenis karena gula sintaks ditambahkan untuk penggunaan tupel. Misalnya, di C # 6, jika Anda ingin menggunakan tupel untuk mengembalikan nilai, Anda harus melakukan hal berikut:

public Tuple<string, int> GetValues()
{
    // ...
    return new Tuple(stringVal, intVal);
}

var value = GetValues();
string s = value.Item1; 

Namun, di C # 7, Anda dapat menggunakan ini:

public (string, int) GetValues()
{
    // ...
    return (stringVal, intVal);
}

var value = GetValues();
string s = value.Item1; 

Anda bahkan dapat melangkah lebih jauh dan memberikan nama nilai:

public (string S, int I) GetValues()
{
    // ...
    return (stringVal, intVal);
}

var value = GetValues();
string s = value.S; 

... Atau dekonstruksi tupel seluruhnya:

public (string S, int I) GetValues()
{
    // ...
    return (stringVal, intVal);
}

var (S, I) = GetValues();
string s = S;

Tuple tidak sering digunakan di C # pre-7 karena rumit dan bertele-tele, dan hanya benar-benar digunakan dalam kasus di mana membangun kelas / struct data hanya untuk satu contoh pekerjaan akan lebih merepotkan daripada nilainya. Tetapi di C # 7, tupel memiliki dukungan tingkat bahasa sekarang, jadi menggunakannya jauh lebih bersih dan lebih berguna.


11

Saya melihat sumber untuk Tupledan ValueTuple. Perbedaannya adalah bahwa itu Tupleadalah classdan ValueTuplemerupakan structyang mengimplementasikan IEquatable.

Itu berarti itu Tuple == Tupleakan kembali falsejika mereka bukan contoh yang sama, tetapi ValueTuple == ValueTupleakan kembali truejika mereka memiliki tipe yang sama dan Equalsmengembalikan trueuntuk setiap nilai yang dikandungnya.


Ini lebih dari itu.
BoltClock

3
@BoltClock Komentar Anda akan menjadi konstruktif jika Anda menjelaskan
Peter Morris

3
Selain itu, tipe nilai tidak selalu berada di tumpukan. Perbedaannya adalah semantik mewakili nilai, bukan referensi, setiap kali variabel itu disimpan, yang mungkin atau mungkin bukan tumpukan.
Pelayanan

7

Selain komentar di atas, satu hal yang tidak menguntungkan dari ValueTuple adalah, sebagai tipe nilai, argumen bernama dihapus saat dikompilasi ke IL, sehingga tidak tersedia untuk serialisasi pada waktu proses.

yaitu, argumen manis Anda akan tetap berakhir sebagai "Item1", "Item2", dll. ketika diserialkan melalui mis. Json.NET.


3
Jadi secara teknis itu adalah kesamaan dan bukan perbedaan;)
JAD

6

Jawaban lain lupa menyebutkan poin penting Alih-alih mengulanginya, saya akan mereferensikan dokumentasi XML dari kode sumber :

Jenis ValueTuple (dari arity 0 hingga 8) terdiri dari implementasi runtime yang mendasari tupel di C # dan struct tuple di F #.

Selain dibuat melalui sintaks bahasa , mereka paling mudah dibuat melalui ValueTuple.Createmetode pabrik. The System.ValueTuplejenis berbeda dari System.Tuplejenis di bahwa:

  • mereka adalah struct daripada kelas,
  • mereka bisa berubah daripada hanya membaca , dan
  • anggotanya (seperti Item1, Item2, dll) adalah bidang, bukan properti.

Dengan pengenalan tipe ini dan kompiler C # 7.0, Anda dapat dengan mudah menulis

(int, string) idAndName = (1, "John");

Dan mengembalikan dua nilai dari sebuah metode:

private (int, string) GetIdAndName()
{
   //.....
   return (id, name);
}

Berlawanan dengan System.TupleAnda dapat memperbarui anggotanya (Dapat diubah) karena mereka adalah bidang baca-tulis publik yang dapat diberi nama yang bermakna:

(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";

"Arity 0 sampai 8". Ah, saya suka fakta bahwa mereka menyertakan 0-tuple. Ini dapat digunakan sebagai jenis tipe kosong, dan akan diizinkan dalam generik ketika beberapa parameter tipe tidak diperlukan, seperti di class MyNonGenericType : MyGenericType<string, ValueTuple, int>dll.
Jeppe Stig Nielsen

2

Bergabung terlambat untuk menambahkan klarifikasi cepat pada dua factoids ini:

  • mereka adalah struct daripada kelas
  • mereka bisa berubah daripada hanya membaca

Orang akan berpikir bahwa mengubah nilai-tupel secara massal akan sangat mudah:

 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;

Seseorang mungkin mencoba untuk mengatasinya seperti ini:

 // 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'

Alasan untuk perilaku unik ini adalah karena nilai-tupel persis berbasis nilai (struct) dan dengan demikian panggilan .Select (...) berfungsi pada clone-struct bukan pada aslinya. Untuk mengatasi ini, kita harus menggunakan:

 // 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

Atau, tentu saja, seseorang dapat mencoba pendekatan langsung:

   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

Semoga ini membantu seseorang yang berjuang untuk membuat head of tail out of value-tuple yang dihosting daftar.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.