Ho decompilato alcune librerie C # 7 e ho visto i ValueTuplegenerici in uso. Cosa sono ValueTuplese perché no Tupleinvece?
Ho decompilato alcune librerie C # 7 e ho visto i ValueTuplegenerici in uso. Cosa sono ValueTuplese perché no Tupleinvece?
Risposte:
Cosa sono
ValueTuplese perché noTupleinvece?
A ValueTupleè una struttura che riflette una tupla, uguale alla System.Tupleclasse originale .
Le principali differenze tra Tuplee ValueTuplesono:
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.ValueTuplenon è solo un struct, è mutevole e bisogna stare attenti quando li si utilizza come tale. Pensa cosa succede quando una classe contiene System.ValueTupleun 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, Item2ecc., 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 ValueTupleessendo 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 structottiene la semantica di uguaglianza automatica (superficiale) dal runtime, dove a classno. 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
structspiuttosto checlasses, in modo che non sia associata alcuna penalità di allocazione. Dovrebbero essere il più leggeri possibile.Probabilmente,
structspuò finire per essere più costoso, perché l'assegnazione copia un valore maggiore. Quindi, se vengono assegnati molto più di quanto vengano creati,structssarebbe 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.Tuplediventa 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 ValueTuplequando 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 Item1e 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 Tuplee 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 Tuplee ValueTuple. La differenza è che Tupleè una classed ValueTupleè una structche implementa IEquatable.
Ciò significa che Tuple == Tuplerestituirà falsese non sono la stessa istanza, ma ValueTuple == ValueTuplerestituirà truese sono dello stesso tipo e Equalsrestituisce trueper 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.Createmetodi di fabbrica. I System.ValueTupletipi differiscono dai System.Tupletipi 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.Tuplete 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.