Ich habe einige C # 7-Bibliotheken dekompiliert und gesehen, dass ValueTuple
Generika verwendet werden. Was sind ValueTuples
und warum nicht Tuple
stattdessen?
Ich habe einige C # 7-Bibliotheken dekompiliert und gesehen, dass ValueTuple
Generika verwendet werden. Was sind ValueTuples
und warum nicht Tuple
stattdessen?
Antworten:
Was sind
ValueTuples
und warum nichtTuple
stattdessen?
A ValueTuple
ist eine Struktur, die ein Tupel widerspiegelt, genau wie die ursprüngliche System.Tuple
Klasse.
Der Hauptunterschied zwischen Tuple
und ValueTuple
sind:
System.ValueTuple
ist ein Werttyp (struct), während System.Tuple
es sich um einen Referenztyp ( class
) handelt. Dies ist sinnvoll, wenn es um Zuweisungen und GC-Druck geht.System.ValueTuple
ist 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.ValueTuple
ein 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
, Item2
usw., 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 ValueTuple
es 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 struct
erhält a zur Laufzeit eine automatische (flache) Gleichheitssemantik, wo a class
dies 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
structs
anstelle von zu erstellenclasses
, damit mit ihnen keine Zuordnungsstrafe verbunden ist. Sie sollten so leicht wie möglich sein.
structs
Kann möglicherweise teurer werden, da die Zuweisung einen größeren Wert kopiert. Wenn ihnen also viel mehr zugewiesen wird als sie erstellt werden,structs
wä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.
Sie können leicht erkennen, dass das Arbeiten mit System.Tuple
sehr 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}");
Wenn wir uns das Cover unseres vorherigen Beispiels ansehen, können wir genau sehen, wie der Compiler interpretiert, ValueTuple
wenn 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 Item1
und 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
.
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Der Unterschied zwischen Tuple
und ValueTuple
besteht darin, dass Tuple
es sich um einen Referenztyp und ValueTuple
einen 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.
Ich schaute auf die Quelle für beide Tuple
und ValueTuple
. Der Unterschied ist, dass Tuple
es sich um ein class
und ValueTuple
handelt struct
, das implementiert wird IEquatable
.
Das heißt , die Tuple == Tuple
zurückkehren wird , false
wenn sie nicht dieselbe Instanz sind, aber ValueTuple == ValueTuple
wird zurückkehren , true
wenn sie vom gleichen Typ und sind Equals
Erträge true
für jeden der Werte , die sie enthalten.
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.Create
Factory-Methoden erstellt werden. Die System.ValueTuple
Typen unterscheiden sich von den System.Tuple
Typen darin:
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.Tuple
Sie 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";
class MyNonGenericType : MyGenericType<string, ValueTuple, int>
usw.
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.
Später Beitritt, um eine kurze Erläuterung dieser beiden Faktoide hinzuzufügen:
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.