Jaka jest różnica między System.ValueTuple i System.Tuple?

139

Zdekompilowałem niektóre biblioteki C # 7 i zobaczyłem ValueTupleużywane typy generyczne. Czym są ValueTuplesi dlaczego nie Tuple?

Steve Fan
źródło
Myślę, że odnosi się to do klasy Dot NEt Tuple. Czy mógłbyś udostępnić przykładowy kod. Aby było to łatwe do zrozumienia.
Ranadip Dutta,
14
@Ranadip Dutta: Jeśli wiesz, co to jest krotka, nie potrzebujesz przykładowego kodu, aby zrozumieć pytanie. Samo pytanie jest proste: czym jest ValueTuple i czym różni się od Tuple?
BoltClock
1
@BoltClock: To jest powód, dla którego nie odpowiedziałem na nic w tym kontekście. Wiem, że w języku C # jest klasa Tuple, której używam dość często, a czasami nazywam ją również w PowerShell. To typ referencyjny. Widząc inne odpowiedzi, zrozumiałem, że istnieje również typ wartości, który jest znany jako Valuetuple. Jeśli istnieje próbka, chciałbym poznać jej zastosowanie.
Ranadip Dutta,
2
Dlaczego dekompilujesz je, skoro kod źródłowy Roslyna jest dostępny na github?
Zein Makki,
@ user3185569 prawdopodobnie dlatego, że F12 automatycznie dekompiluje rzeczy i jest łatwiejsze niż przeskakiwanie do GitHub
John Zabroski

Odpowiedzi:

203

Czym są ValueTuplesi dlaczego nie Tuple?

A ValueTuplejest strukturą, która odzwierciedla krotkę, taką samą jak oryginałSystem.Tuple klasa.

Główne różnice między Tuplei ValueTupleto:

  • System.ValueTuplejest typem wartości (struct), podczas gdy System.Tuplejest typem referencyjnym (class ). Ma to znaczenie, gdy mówimy o alokacjach i presji na GC.
  • System.ValueTupleto nie tylko a struct, jest zmienna i należy zachować ostrożność, używając ich jako takich. Pomyśl, co się dzieje, gdy klasa ma System.ValueTuplejako pole.
  • System.ValueTuple ujawnia swoje elementy za pośrednictwem pól zamiast właściwości.

Aż do C # 7 używanie krotek nie było zbyt wygodne. Ich nazwy pól to Item1, Item2itd., A język nie dostarczył im cukru składniowego, jak robi to większość innych języków (Python, Scala).

Kiedy zespół projektantów języka .NET zdecydował się włączyć krotki i dodać do nich cukier składniowy na poziomie języka, ważnym czynnikiem była wydajność. ZValueTuple bycia typ wartości, można uniknąć presji GC podczas korzystania z nich, ponieważ (jak szczegółowo wdrożenia) będą przydzielane na stosie.

Ponadto a structotrzymuje automatyczną (płytką) semantykę równości przez środowisko wykonawcze, gdzie a classnie. Chociaż zespół projektowy upewnił się, że będzie jeszcze bardziej zoptymalizowana równość dla krotek, dlatego zaimplementował dla niej niestandardową równość.

Oto akapit z uwag projektowychTuples :

Struktura lub klasa:

Jak wspomniano, proponuję structsraczej tworzyć typy krotek niż classes, aby nie wiązała się z nimi żadna kara za alokację. Powinny być tak lekkie, jak to tylko możliwe.

Prawdopodobnie structsmoże się to okazać bardziej kosztowne, ponieważ przypisanie kopiuje większą wartość. Więc jeśli przypisuje się im znacznie więcej, niż są tworzone, structsbyłby to zły wybór.

Jednak w samej ich motywacji krotki są efemeryczne. Używałbyś ich, gdy części są ważniejsze niż całość. Tak więc powszechnym schematem byłoby ich skonstruowanie, zwrócenie i natychmiastowa dekonstrukcja. W tej sytuacji struktury są zdecydowanie lepsze.

Struktury mają również szereg innych korzyści, które staną się oczywiste w dalszej części.

Przykłady:

Możesz łatwo zauważyć, że praca z System.Tuplebardzo szybko staje się niejednoznaczna. Na przykład, powiedzmy, że mamy metodę, która oblicza sumę i liczbę 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);
}

Po stronie odbiorczej otrzymujemy:

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

Sposób, w jaki można dekonstruować krotki wartości na nazwane argumenty, to prawdziwa siła tej funkcji:

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

A po stronie odbiorczej:

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

Lub:

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

Dodatki kompilatora:

Jeśli spojrzymy pod okładkę naszego poprzedniego przykładu, możemy dokładnie zobaczyć, jak kompilator interpretuje, ValueTuplekiedy poprosimy go o dekonstrukcję:

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

Wewnętrznie skompilowany kod wykorzystuje Item1i Item2, ale wszystko to jest od nas oderwane, ponieważ pracujemy na zdekomponowanej krotce. Krotka z nazwanymi argumentami zostanie oznaczona rozszerzeniem TupleElementNamesAttribute. Jeśli zamiast dekompozycji użyjemy jednej świeżej zmiennej, otrzymamy:

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

Zwróć uwagę, że kompilator nadal musi wykonać jakąś magię (za pośrednictwem atrybutu) podczas debugowania naszej aplikacji, ponieważ byłoby dziwne zobaczyć Item1, że Item2.

Yuval Itzchakov
źródło
1
Zauważ, że możesz również użyć prostszej (i moim zdaniem, preferowanej) składnivar (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47
@ Abion47 Co by się stało, gdyby oba typy się różniły?
Yuval Itzchakov
W jaki sposób zgadzają się Twoje punkty „to zmienna struktura” i „ujawnia pola tylko do odczytu ”?
CodesInChaos
@CodesInChaos To nie jest. Widziałem [to] ( github.com/dotnet/corefx/blob/master/src/Common/src/System/… ), ale nie sądzę, że to jest to, co kończy się emisją przez kompilator, ponieważ pola lokalne nie mogą być i tak tylko do odczytu. Myślę, że propozycja oznaczała „możesz je czytać tylko jeśli chcesz, ale to zależy od ciebie” , co źle zinterpretowałem.
Yuval Itzchakov,
1
Niektóre gnidy: „zostaną przydzielone na stosie” - prawda tylko dla zmiennych lokalnych. Bez wątpienia o tym wiesz, ale niestety sposób, w jaki to sformułowałeś, prawdopodobnie utrwali mit, że typy wartości zawsze żyją na stosie.
Peter Duniho
26

Różnica między Tuplei ValueTuplepolega na tym, że Tuplejest to typ referencyjny i ValueTupletyp wartości. To drugie jest pożądane, ponieważ zmiany języka w C # 7 powodują, że krotki są używane znacznie częściej, ale przydzielanie nowego obiektu na stercie dla każdej krotki jest problemem związanym z wydajnością, szczególnie gdy jest to niepotrzebne.

Jednak w C # 7 chodzi o to, że nigdy nie musisz jawnie używać żadnego typu z powodu dodawania cukru składniowego do użycia krotki. Na przykład w języku C # 6, jeśli chcesz użyć krotki do zwrócenia wartości, musisz wykonać następujące czynności:

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

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

Jednak w C # 7 możesz użyć tego:

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

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

Możesz nawet pójść o krok dalej i nadać wartościom nazwy:

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

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

... Lub całkowicie zdekonstruuj krotkę:

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

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

Krotki nie były często używane w języku C # przed wersją 7, ponieważ były uciążliwe i rozwlekłe, a tak naprawdę używane tylko w przypadkach, gdy tworzenie klasy / struktury danych dla pojedynczego wystąpienia pracy byłoby bardziej kłopotliwe niż było warte. Ale w C # 7 krotki mają teraz obsługę na poziomie języka, więc ich używanie jest znacznie czystsze i bardziej przydatne.

Abion47
źródło
10

Spojrzałem na źródło zarówno Tuplei ValueTuple. Różnica polega na tym, że Tuplejest to a classi ValueTuplejest to structimplementacja IEquatable.

Oznacza to, że Tuple == Tuplezwróci, falsejeśli nie są one tą samą instancją, ale ValueTuple == ValueTuplezwróci, truejeśli są tego samego typu i Equalszwróci truedla każdej z wartości, które zawierają.

Peter Morris
źródło
Ale to coś więcej.
BoltClock
2
@BoltClock Twój komentarz byłby konstruktywny, gdybyś go rozwinął
Peter Morris
3
Ponadto typy wartości niekoniecznie trafiają na stos. Różnica polega na tym, że semantycznie reprezentuje wartość, a nie odniesienie, ilekroć ta zmienna jest przechowywana, która może być stosem lub nie.
Servy
6

W innych odpowiedziach zapomniałem wspomnieć o ważnych kwestiach, zamiast przeformułować odwołam się do dokumentacji XML z kodu źródłowego :

Typy ValueTuple (od arity 0 do 8) obejmują implementację środowiska uruchomieniowego, która opiera się na krotkach w C # i strukturach w języku F #.

Oprócz tworzenia za pomocą składni języka , najłatwiej jest je tworzyć za pomocą ValueTuple.Createmetod fabrycznych. Te System.ValueTupletypy różnią się od System.Tuplerodzaju tym, że

  • są raczej strukturami niż klasami,
  • są zmienne, a nie tylko do odczytu , i
  • ich członkowie (tacy jak Item1, Item2 itd.) są polami, a nie właściwościami.

Dzięki wprowadzeniu tego typu i kompilatorowi C # 7.0 można łatwo pisać

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

I zwróć dwie wartości z metody:

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

W przeciwieństwie do System.Tupleciebie możesz aktualizować jego członków (Mutable), ponieważ są to publiczne pola do odczytu i zapisu, którym można nadać znaczące nazwy:

(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
Zein Makki
źródło
„Arity 0 do 8”. Ach, podoba mi się fakt, że zawierają 0-krotkę. Może być używany jako rodzaj pustego typu i będzie dozwolony w typach generycznych, gdy jakiś parametr typu nie jest potrzebny, np. W class MyNonGenericType : MyGenericType<string, ValueTuple, int>itp.
Jeppe Stig Nielsen
6

Oprócz powyższych komentarzy, jedną niefortunną wadą ValueTuple jest to, że jako typ wartości, nazwane argumenty są usuwane podczas kompilacji do IL, więc nie są dostępne do serializacji w czasie wykonywania.

tzn. Twoje słodkie nazwane argumenty nadal będą kończyć się jako „Pozycja1”, „Pozycja2” itd. po serializacji przez np. Json.NET.

ZenSquirrel
źródło
2
Więc technicznie jest to podobieństwo, a nie różnica;)
JAD
2

Późne dołączanie, aby dodać szybkie wyjaśnienie tych dwóch faktów:

  • są raczej strukturami niż klasami
  • są zmienne, a nie tylko do odczytu

Można by pomyśleć, że masowa zmiana krotek wartości byłaby prosta:

 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;

Ktoś może spróbować obejść to w następujący sposób:

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

Przyczyną tego dziwacznego zachowania jest to, że krotki wartości są dokładnie oparte na wartościach (struktury), a zatem wywołanie .Select (...) działa raczej na sklonowanych strukturach niż na oryginałach. Aby rozwiązać ten problem, musimy skorzystać z:

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

Alternatywnie można oczywiście spróbować prostego podejścia:

   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

Mam nadzieję, że pomoże to komuś, kto zmaga się z krotkami wartości przechowywanymi na liście.

XDS
źródło