Ho decompilato alcune librerie C # 7 e ho visto i ValueTuple
generici in uso. Cosa sono ValueTuples
e perché no Tuple
invece?
Ho decompilato alcune librerie C # 7 e ho visto i ValueTuple
generici in uso. Cosa sono ValueTuples
e perché no Tuple
invece?
Risposte:
Cosa sono
ValueTuples
e perché noTuple
invece?
A ValueTuple
è una struttura che riflette una tupla, uguale alla System.Tuple
classe originale .
Le principali differenze tra Tuple
e ValueTuple
sono:
System.ValueTuple
è un tipo di valore (struct), mentre System.Tuple
è un tipo di riferimento ( class
). Ciò è significativo quando si parla di allocazioni e pressione GC.System.ValueTuple
non è solo un struct
, è mutevole e bisogna stare attenti quando li si utilizza come tale. Pensa cosa succede quando una classe contiene System.ValueTuple
un campo.System.ValueTuple
espone i suoi elementi tramite campi anziché proprietà.Fino a C # 7, usare le tuple non era molto conveniente. I loro nomi dei campi sono Item1
, Item2
ecc., E la lingua non aveva fornito zucchero di sintassi per loro come la maggior parte delle altre lingue (Python, Scala).
Quando il team di progettazione del linguaggio .NET ha deciso di incorporare le tuple e aggiungere zucchero di sintassi a livello linguistico, un fattore importante sono state le prestazioni. Con ValueTuple
essendo un tipo di valore, si può evitare la pressione GC quando li utilizzano perché (come un dettaglio di implementazione) essi saranno attribuiti sulla pila.
Inoltre, a struct
ottiene la semantica di uguaglianza automatica (superficiale) dal runtime, dove a class
no. Sebbene il team di progettazione si sia assicurato che ci sarà un'uguaglianza ancora più ottimizzata per le tuple, quindi ha implementato un'uguaglianza personalizzata per essa.
Ecco un paragrafo dalle note di progettazione diTuples
:
Struct o Class:
Come accennato, propongo di creare tipi di tupla
structs
piuttosto checlasses
, in modo che non sia associata alcuna penalità di allocazione. Dovrebbero essere il più leggeri possibile.Probabilmente,
structs
può finire per essere più costoso, perché l'assegnazione copia un valore maggiore. Quindi, se vengono assegnati molto più di quanto vengano creati,structs
sarebbe una cattiva scelta.Nella loro stessa motivazione, tuttavia, le tuple sono effimere. Li useresti quando le parti sono più importanti del tutto. Quindi lo schema comune sarebbe quello di costruirli, restituirli e decostruirli immediatamente. In questa situazione le strutture sono chiaramente preferibili.
Le strutture hanno anche una serie di altri vantaggi, che diventeranno evidenti di seguito.
Si può facilmente vedere che lavorare con System.Tuple
diventa ambiguo molto rapidamente. Ad esempio, supponiamo di avere un metodo che calcola una somma e un conteggio di un 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);
}
Alla fine della ricezione, finiamo con:
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);
Il modo in cui puoi decostruire le tuple di valore in argomenti con nome è il vero potere della funzione:
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;
}
E alla fine di ricezione:
var result = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {result.Sum}, Count: {result.Count}");
O:
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Console.WriteLine($"Sum: {sum}, Count: {count}");
Se guardiamo sotto la copertina del nostro esempio precedente, possiamo vedere esattamente come il compilatore sta interpretando ValueTuple
quando gli chiediamo di decostruire:
[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;
}
Internamente, il codice compilato utilizza Item1
e Item2
, ma tutto questo viene sottratto a noi poiché lavoriamo con una tupla decomposta. Una tupla con argomenti denominati viene annotata con TupleElementNamesAttribute
. Se utilizziamo una singola variabile nuova invece di decomporre, otteniamo:
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));
}
Si noti che il compilatore deve ancora fare accadere qualche magia (tramite l'attributo) quando si esegue il debug la nostra applicazione, in quanto sarebbe strano vedere Item1
, Item2
.
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
La differenza tra Tuple
e ValueTuple
è che Tuple
è un tipo di riferimento ed ValueTuple
è un tipo di valore. Quest'ultimo è desiderabile perché le modifiche al linguaggio in C # 7 hanno tuple utilizzate molto più frequentemente, ma l'allocazione di un nuovo oggetto sull'heap per ogni tupla è un problema di prestazioni, in particolare quando non è necessario.
Tuttavia, in C # 7, l'idea è che non si deve utilizzare in modo esplicito entrambi i tipi a causa dello zucchero sintassi che viene aggiunto per l'uso tuple. Ad esempio, in C # 6, se si desidera utilizzare una tupla per restituire un valore, è necessario effettuare le seguenti operazioni:
public Tuple<string, int> GetValues()
{
// ...
return new Tuple(stringVal, intVal);
}
var value = GetValues();
string s = value.Item1;
Tuttavia, in C # 7, puoi usare questo:
public (string, int) GetValues()
{
// ...
return (stringVal, intVal);
}
var value = GetValues();
string s = value.Item1;
Puoi anche fare un passo ulteriore e dare i nomi ai valori:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var value = GetValues();
string s = value.S;
... O decostruisci completamente la tupla:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var (S, I) = GetValues();
string s = S;
Le tuple non venivano spesso utilizzate in C # pre-7 perché erano ingombranti e dettagliate, e venivano utilizzate solo nei casi in cui la creazione di una classe di dati / struttura per una sola istanza di lavoro avrebbe comportato più problemi di quanto ne valesse la pena. Ma in C # 7, le tuple hanno ora il supporto a livello di lingua, quindi usarle è molto più pulito e utile.
Ho guardato la fonte per entrambi Tuple
e ValueTuple
. La differenza è che Tuple
è una class
ed ValueTuple
è una struct
che implementa IEquatable
.
Ciò significa che Tuple == Tuple
restituirà false
se non sono la stessa istanza, ma ValueTuple == ValueTuple
restituirà true
se sono dello stesso tipo e Equals
restituisce true
per ciascuno dei valori che contengono.
Altre risposte hanno dimenticato di menzionare punti importanti. Invece di riformulare, farò riferimento alla documentazione XML dal codice sorgente :
I tipi ValueTuple (da arity 0 a 8) comprendono l'implementazione di runtime che sta alla base delle tuple in C # e le tuple di struttura in F #.
Oltre a essere creati tramite la sintassi del linguaggio , sono più facilmente creati tramite i
ValueTuple.Create
metodi di fabbrica. I System.ValueTuple
tipi differiscono dai System.Tuple
tipi in quanto:
Con l'introduzione di questo tipo e il compilatore C # 7.0, puoi scrivere facilmente
(int, string) idAndName = (1, "John");
E restituisce due valori da un metodo:
private (int, string) GetIdAndName()
{
//.....
return (id, name);
}
Contrariamente a System.Tuple
te puoi aggiornare i suoi membri (mutabili) perché sono campi di lettura / scrittura pubblici a cui possono essere dati nomi significativi:
(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
class MyNonGenericType : MyGenericType<string, ValueTuple, int>
ecc.
Oltre ai commenti sopra, uno sfortunato gotcha di ValueTuple è che, come tipo di valore, gli argomenti nominati vengono cancellati quando compilati in IL, quindi non sono disponibili per la serializzazione in fase di runtime.
vale a dire che i tuoi argomenti con nome dolce finiranno comunque come "Item1", "Item2", ecc. se serializzati tramite ad esempio Json.NET.
Partecipazione tardiva per aggiungere un rapido chiarimento su questi due factoidi:
Si potrebbe pensare che cambiare in massa le tuple di valore sarebbe semplice:
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;
Qualcuno potrebbe provare a risolvere il problema in questo modo:
// 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'
La ragione di questo strano comportamento è che le tuple dei valori sono esattamente basate sul valore (strutture) e quindi la chiamata .Select (...) funziona su strutture clonate piuttosto che sugli originali. Per risolvere questo dobbiamo ricorrere a:
// 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
In alternativa, ovviamente, si potrebbe provare l'approccio diretto:
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
Spero che questo aiuti qualcuno che lotta per ricavare la testa dalle code delle tuple ospitate in una lista.