Qual è la differenza tra System.ValueTuple e System.Tuple?


139

Ho decompilato alcune librerie C # 7 e ho visto i ValueTuplegenerici in uso. Cosa sono ValueTuplese perché no Tupleinvece?


Penso che si riferisca alla classe Dot NEt Tuple. Potresti condividere un codice di esempio. In modo che sarebbe facile da capire.
Ranadip Dutta,

14
@Ranadip Dutta: se sai cos'è una tupla, non hai bisogno di un codice di esempio per capire la domanda. La domanda in sé è semplice: che cos'è ValueTuple e in cosa differisce da Tuple?
BoltClock

1
@BoltClock: questa è la ragione per cui non ho risposto a nulla in quel contesto. So che in c #, esiste una classe Tuple che uso abbastanza frequentemente e la stessa classe alcune volte la chiamo anche in PowerShell. È un tipo di riferimento. Ora vedendo le altre risposte ho capito che esiste anche un tipo di valore che è noto come Valuetuple. Se c'è un campione, vorrei sapere l'utilizzo per lo stesso.
Ranadip Dutta,

2
Perché decompilare questi quando il codice sorgente per Roslyn è disponibile su Github?
Zein Makki,

@ user3185569 probabilmente perché F12 auto-decompila le cose ed è più facile che saltare a GitHub
John Zabroski,

Risposte:


203

Cosa sono ValueTuplese perché no Tupleinvece?

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 che classes, 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.

Esempi:

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

Chicche del compilatore:

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.


1
Nota che puoi anche usare la sintassi più semplice (e secondo me, preferibile)var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47

@ Abion47 Cosa accadrebbe se entrambi i tipi differissero?
Yuval Itzchakov,

Inoltre, in che modo i tuoi punti "è una struttura mutabile " e "espone i campi di sola lettura " concordano?
Codici A Caos l'

@CodesInChaos Non è così. Ho visto [questo] ( github.com/dotnet/corefx/blob/master/src/Common/src/System/… ), ma non credo sia quello che finisce per essere emesso dal compilatore, poiché i campi locali non possono essere di sola lettura comunque. Penso che la proposta significasse "puoi renderli di sola lettura se lo desideri, ma dipende da te" , che ho frainteso.
Yuval Itzchakov,

1
Alcuni nits: "saranno allocati nello stack" - vero solo per le variabili locali. Senza dubbio lo sai, ma sfortunatamente, il modo in cui lo hai definito probabilmente perpetuerà il mito secondo cui i tipi di valore vivono sempre nello stack.
Peter Duniho,

26

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.


10

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.


È più di questo però.
BoltClock

2
@BoltClock Il tuo commento sarebbe costruttivo se dovessi elaborare
Peter Morris l'

3
Inoltre, i tipi di valore non vanno necessariamente in pila. La differenza è che rappresentano semanticamente il valore, anziché un riferimento, ogni volta che viene memorizzata quella variabile, che può essere o meno lo stack.
Servito il

6

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:

  • sono strutture piuttosto che classi,
  • sono mutabili piuttosto che di sola lettura , e
  • i loro membri (come Item1, Item2, ecc.) sono campi anziché proprietà.

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

"Arity da 0 a 8". Ah, mi piace il fatto che includano una tupla 0. Potrebbe essere usato come un tipo di tipo vuoto, e sarà consentito in generici quando non è necessario alcun parametro di tipo, come in class MyNonGenericType : MyGenericType<string, ValueTuple, int>ecc.
Jeppe Stig Nielsen,

6

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.


2
Quindi tecnicamente questa è una somiglianza e non una differenza;)
JAD

2

Partecipazione tardiva per aggiungere un rapido chiarimento su questi due factoidi:

  • sono strutture piuttosto che classi
  • sono mutabili piuttosto che di sola lettura

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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.