Was ist der Unterschied zwischen System.ValueTuple und System.Tuple?

138

Ich habe einige C # 7-Bibliotheken dekompiliert und gesehen, dass ValueTupleGenerika verwendet werden. Was sind ValueTuplesund warum nicht Tuplestattdessen?

Steve Fan
quelle
Ich denke, es bezieht sich auf die Dot NEt Tuple-Klasse. Könnten Sie bitte einen Beispielcode teilen. Damit es leicht zu verstehen wäre.
Ranadip Dutta
14
@Ranadip Dutta: Wenn Sie wissen, was ein Tupel ist, benötigen Sie keinen Beispielcode, um die Frage zu verstehen. Die Frage selbst ist einfach: Was ist ValueTuple und wie unterscheidet es sich von Tuple?
BoltClock
1
@ BoltClock: Aus diesem Grund habe ich in diesem Zusammenhang nichts beantwortet. Ich weiß, dass es in c # eine Tupel-Klasse gibt, die ich ziemlich häufig benutze, und dieselbe Klasse nenne ich sie manchmal auch in Powershell. Es ist ein Referenztyp. Als ich nun die anderen Antworten sah, verstand ich, dass es einen Werttyp gibt, der auch als Wertetuple bekannt ist. Wenn es ein Beispiel gibt, würde ich gerne die Verwendung für das gleiche wissen.
Ranadip Dutta
2
Warum dekompilieren Sie diese, wenn der Quellcode für Roslyn auf github verfügbar ist?
Zein Makki
@ user3185569 wahrscheinlich, weil F12 Dinge automatisch dekompiliert und es einfacher ist, als zu GitHub zu springen
John Zabroski

Antworten:

202

Was sind ValueTuplesund warum nicht Tuplestattdessen?

A ValueTupleist eine Struktur, die ein Tupel widerspiegelt, genau wie die ursprüngliche System.TupleKlasse.

Der Hauptunterschied zwischen Tupleund ValueTuplesind:

  • System.ValueTupleist ein Werttyp (struct), während System.Tuplees sich um einen Referenztyp ( class) handelt. Dies ist sinnvoll, wenn es um Zuweisungen und GC-Druck geht.
  • System.ValueTupleist nicht nur eine struct, es ist eine veränderliche , und man muss vorsichtig sein, wenn man sie als solche verwendet. Überlegen Sie, was passiert, wenn eine Klasse System.ValueTupleein Feld enthält.
  • System.ValueTuple macht seine Elemente über Felder anstelle von Eigenschaften verfügbar.

Bis C # 7 war die Verwendung von Tupeln nicht sehr praktisch. Ihre Feldnamen sind Item1, Item2usw., und die Sprache noch nicht geliefert Syntax Zucker für sie wie die meisten anderen Sprachen tun (Python, Scala).

Als das .NET-Sprachdesign-Team beschloss, Tupel zu integrieren und ihnen auf Sprachebene Syntaxzucker hinzuzufügen, war die Leistung ein wichtiger Faktor. Da ValueTuplees sich um einen Wertetyp handelt, können Sie GC-Druck vermeiden, wenn Sie sie verwenden, da sie (als Implementierungsdetail) auf dem Stapel zugewiesen werden.

Zusätzlich structerhält a zur Laufzeit eine automatische (flache) Gleichheitssemantik, wo a classdies nicht tut. Obwohl das Designteam dafür gesorgt hat, dass es eine noch optimierte Gleichheit für Tupel gibt, wurde daher eine benutzerdefinierte Gleichheit für Tupel implementiert.

Hier ist ein Absatz aus den Design Notes vonTuples :

Struktur oder Klasse:

Wie bereits erwähnt, schlage ich vor, Tupeltypen structsanstelle von zu erstellen classes, damit mit ihnen keine Zuordnungsstrafe verbunden ist. Sie sollten so leicht wie möglich sein.

structsKann möglicherweise teurer werden, da die Zuweisung einen größeren Wert kopiert. Wenn ihnen also viel mehr zugewiesen wird als sie erstellt werden, structswäre dies eine schlechte Wahl.

In ihrer Motivation sind Tupel jedoch vergänglich. Sie würden sie verwenden, wenn die Teile wichtiger sind als das Ganze. Das übliche Muster wäre also, sie zu konstruieren, zurückzugeben und sofort zu dekonstruieren. In dieser Situation sind Strukturen eindeutig vorzuziehen.

Strukturen haben auch eine Reihe anderer Vorteile, die im Folgenden deutlich werden.

Beispiele:

Sie können leicht erkennen, dass das Arbeiten mit System.Tuplesehr schnell mehrdeutig wird. Angenommen, wir haben eine Methode, die eine Summe und eine Anzahl von a berechnet 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);
}

Am empfangenden Ende erhalten wir:

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

Die Art und Weise, wie Sie Wertetupel in benannte Argumente zerlegen können, ist die eigentliche Stärke der Funktion:

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

Und auf der Empfangsseite:

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

Oder:

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

Compiler-Goodies:

Wenn wir uns das Cover unseres vorherigen Beispiels ansehen, können wir genau sehen, wie der Compiler interpretiert, ValueTuplewenn wir ihn zum Dekonstruieren auffordern:

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

Intern verwendet der kompilierte Code Item1und Item2, aber all dies wird von uns abstrahiert, da wir mit einem zerlegten Tupel arbeiten. Ein Tupel mit benannten Argumenten wird mit dem versehen TupleElementNamesAttribute. Wenn wir eine einzelne neue Variable verwenden, anstatt sie zu zerlegen, erhalten wir:

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

Beachten Sie, dass der Compiler muss noch etwas Magie (über das Attribut) geschehen , wenn wir unsere Anwendung debuggen, wie es seltsam wäre , um zu sehen Item1, Item2.

Yuval Itzchakov
quelle
1
Beachten Sie, dass Sie auch die einfachere (und meiner Meinung nach vorzuziehende) Syntax verwenden könnenvar (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47
@ Abion47 Was würde passieren, wenn sich beide Typen unterscheiden?
Yuval Itzchakov
Wie stimmen Ihre Punkte "Es ist eine veränderbare Struktur" und "Es legt schreibgeschützte Felder offen " überein?
CodesInChaos
@CodesInChaos Das tut es nicht. Ich habe [dies] gesehen ( github.com/dotnet/corefx/blob/master/src/Common/src/System/… ), aber ich glaube nicht, dass dies vom Compiler ausgegeben wird, da lokale Felder dies nicht können schreibgeschützt sowieso. Ich denke, der Vorschlag bedeutete "Sie können sie nur lesbar machen, wenn Sie möchten, aber das liegt an Ihnen" , was ich falsch interpretiert habe.
Yuval Itzchakov
1
Einige Nissen: "Sie werden auf dem Stapel zugewiesen" - gilt nur für lokale Variablen. Zweifellos wissen Sie das, aber leider wird die Art und Weise, wie Sie dies formuliert haben, wahrscheinlich den Mythos aufrechterhalten, dass Werttypen immer im Stapel leben.
Peter Duniho
25

Der Unterschied zwischen Tupleund ValueTuplebesteht darin, dass Tuplees sich um einen Referenztyp und ValueTupleeinen Werttyp handelt. Letzteres ist wünschenswert, da bei Änderungen an der Sprache in C # 7 Tupel viel häufiger verwendet werden. Das Zuweisen eines neuen Objekts auf dem Heap für jedes Tupel ist jedoch ein Leistungsproblem, insbesondere wenn es nicht erforderlich ist.

In C # 7 besteht die Idee jedoch darin, dass Sie keinen der beiden Typen explizit verwenden müssen , da der Syntaxzucker für die Tupelverwendung hinzugefügt wird. Wenn Sie beispielsweise in C # 6 ein Tupel verwenden möchten, um einen Wert zurückzugeben, müssen Sie Folgendes tun:

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

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

In C # 7 können Sie jedoch Folgendes verwenden:

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

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

Sie können sogar noch einen Schritt weiter gehen und den Werten Namen geben:

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

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

... oder das Tupel komplett dekonstruieren:

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

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

Tupel wurden in C # vor 7 nicht oft verwendet, da sie umständlich und ausführlich waren und nur dann wirklich verwendet wurden, wenn das Erstellen einer Datenklasse / -struktur für nur eine einzelne Arbeitsinstanz schwieriger war als es wert war. In C # 7 werden Tupel jetzt auf Sprachebene unterstützt, sodass ihre Verwendung viel sauberer und nützlicher ist.

Abion47
quelle
10

Ich schaute auf die Quelle für beide Tupleund ValueTuple. Der Unterschied ist, dass Tuplees sich um ein classund ValueTuplehandelt struct, das implementiert wird IEquatable.

Das heißt , die Tuple == Tuplezurückkehren wird , falsewenn sie nicht dieselbe Instanz sind, aber ValueTuple == ValueTuplewird zurückkehren , truewenn sie vom gleichen Typ und sind EqualsErträge truefür jeden der Werte , die sie enthalten.

Peter Morris
quelle
Es ist jedoch mehr als das.
BoltClock
2
@ BoltClock Ihr Kommentar wäre konstruktiv, wenn Sie näher darauf eingehen würden
Peter Morris
3
Außerdem werden Werttypen nicht unbedingt auf den Stapel gelegt. Der Unterschied besteht darin, dass die semantisch den Wert und nicht eine Referenz darstellen, wenn diese Variable gespeichert wird, die der Stapel sein kann oder nicht.
Servy
6

Andere Antworten haben vergessen, wichtige Punkte zu erwähnen. Anstelle einer Umformulierung werde ich auf die XML-Dokumentation aus dem Quellcode verweisen :

Die ValueTuple-Typen (von Arity 0 bis 8) umfassen die Laufzeitimplementierung, die Tupeln in C # und Strukturtupeln in F # zugrunde liegt.

Abgesehen davon , dass sie über die Sprachsyntax erstellt wurden , können sie am einfachsten über die ValueTuple.CreateFactory-Methoden erstellt werden. Die System.ValueTupleTypen unterscheiden sich von den System.TupleTypen darin:

  • Sie sind eher Strukturen als Klassen.
  • sie sind eher veränderlich als schreibgeschützt , und
  • Ihre Mitglieder (wie Item1, Item2 usw.) sind eher Felder als Eigenschaften.

Mit der Einführung dieses Typs und des C # 7.0-Compilers können Sie problemlos schreiben

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

Und geben Sie zwei Werte von einer Methode zurück:

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

Im Gegensatz dazu können System.TupleSie seine Mitglieder aktualisieren (veränderbar), da es sich um öffentliche Lese- / Schreibfelder handelt, denen aussagekräftige Namen zugewiesen werden können:

(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
Zein Makki
quelle
"Arity 0 bis 8". Ah, ich mag die Tatsache, dass sie ein 0-Tupel enthalten. Es könnte als eine Art leerer Typ verwendet werden und wird in Generika zugelassen, wenn ein Typparameter nicht benötigt wird, wie in class MyNonGenericType : MyGenericType<string, ValueTuple, int>usw.
Jeppe Stig Nielsen
6

Zusätzlich zu den obigen Kommentaren besteht ein unglückliches Problem von ValueTuple darin, dass die benannten Argumente als Werttyp beim Kompilieren in IL gelöscht werden, sodass sie zur Laufzeit nicht für die Serialisierung verfügbar sind.

Das heißt, Ihre süßen Argumente werden weiterhin als "Item1", "Item2" usw. angezeigt, wenn sie über z. B. Json.NET serialisiert werden.

ZenSquirrel
quelle
2
Technisch gesehen ist das eine Ähnlichkeit und kein Unterschied;)
JAD
2

Später Beitritt, um eine kurze Erläuterung dieser beiden Faktoide hinzuzufügen:

  • Sie sind eher Strukturen als Klassen
  • Sie sind eher veränderlich als schreibgeschützt

Man würde denken, dass es einfach wäre, Werttupel in Massen zu ändern:

 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;

Jemand könnte versuchen, dies wie folgt zu umgehen:

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

Der Grund für dieses skurrile Verhalten ist, dass die Wertetupel genau wertebasiert sind (Strukturen) und der Aufruf von .Select (...) daher eher für geklonte Strukturen als für die Originale funktioniert. Um dies zu beheben, müssen wir auf Folgendes zurückgreifen:

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

Alternativ könnte man natürlich auch den einfachen Ansatz ausprobieren:

   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

Ich hoffe, dies hilft jemandem, der Schwierigkeiten hat, aus von Listen gehosteten Wertetupeln Kopf an Schwanz zu machen.

XDS
quelle