Quelle est la différence entre System.ValueTuple et System.Tuple?

139

J'ai décompilé certaines bibliothèques C # 7 et vu des ValueTuplegénériques utilisés. Que sont ValueTupleset pourquoi pas à la Tupleplace?

Steve Fan
la source
Je pense que cela fait référence à la classe Dot NEt Tuple. Pourriez-vous s'il vous plaît partager un exemple de code. Pour que ce soit facile à comprendre.
Ranadip Dutta
14
@Ranadip Dutta: Si vous savez ce qu'est un tuple, vous n'avez pas besoin d'exemple de code pour comprendre la question. La question elle-même est simple: qu'est-ce que ValueTuple et en quoi diffère-t-il de Tuple?
BoltClock
1
@BoltClock: C'est la raison pour laquelle je n'ai rien répondu sur ce contexte. Je sais qu'en c #, il y a une classe Tuple que j'utilise assez fréquemment et la même classe que je l'appelle parfois aussi dans PowerShell. C'est un type de référence. Voyant maintenant les autres réponses, j'ai compris qu'il existe également un type de valeur appelé Valuetuple. S'il y a un échantillon, j'aimerais connaître son utilisation.
Ranadip Dutta
2
Pourquoi les décompilez-vous lorsque le code source de Roslyn est disponible sur github?
Zein Makki du
@ user3185569 probablement parce que F12 décompile automatiquement les choses et c'est plus facile que de sauter sur GitHub
John Zabroski

Réponses:

203

Que sont ValueTupleset pourquoi pas à la Tupleplace?

A ValueTupleest une structure qui reflète un tuple, identique à la System.Tupleclasse d' origine .

La principale différence entre Tupleet ValueTuplesont:

  • System.ValueTupleest un type valeur (struct), tandis que System.Tupleest un type référence ( class). C'est significatif quand on parle d'allocations et de pression du GC.
  • System.ValueTuplen'est pas seulement un struct, c'est un mutable , et il faut être prudent lorsqu'on les utilise comme tels. Pensez à ce qui se passe lorsqu'une classe détient un System.ValueTuplecomme champ.
  • System.ValueTuple expose ses éléments via des champs au lieu de propriétés.

Jusqu'à C # 7, l'utilisation de tuples n'était pas très pratique. Leurs noms de champs sont Item1, Item2, etc., et la langue n'a pas fourni de sucre de syntaxe pour eux comme la plupart d' autres langages (Python, Scala).

Lorsque l'équipe de conception du langage .NET a décidé d'incorporer des tuples et de leur ajouter du sucre de syntaxe au niveau du langage, un facteur important était la performance. En ValueTupleétant un type valeur, vous pouvez éviter la pression du GC lors de leur utilisation car (en tant que détail d'implémentation) ils seront alloués sur la pile.

De plus, a structobtient une sémantique d'égalité automatique (superficielle) par le runtime, alors que a classne le fait pas. Bien que l'équipe de conception se soit assurée qu'il y aurait une égalité encore plus optimisée pour les tuples, elle a donc implémenté une égalité personnalisée.

Voici un paragraphe des notes de conception deTuples :

Struct ou Classe:

Comme mentionné, je propose de créer des types de tuple structsplutôt que classes, de sorte qu'aucune pénalité d'allocation ne leur soit associée. Ils doivent être aussi légers que possible.

On structspeut soutenir que cela peut finir par être plus coûteux, car l'affectation copie une plus grande valeur. Donc, si on leur attribue beaucoup plus qu'ils ne sont créés, ce structsserait un mauvais choix.

Dans leur motivation même, les tuples sont éphémères. Vous les utiliseriez lorsque les parties sont plus importantes que le tout. Le modèle commun serait donc de les construire, de les restituer et de les déconstruire immédiatement. Dans cette situation, les structures sont clairement préférables.

Les structures présentent également un certain nombre d'autres avantages, qui deviendront évidents par la suite.

Exemples:

Vous pouvez facilement voir que travailler avec System.Tupledevient très rapidement ambigu. Par exemple, disons que nous avons une méthode qui calcule une somme et un compte de a 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);
}

Du côté de la réception, nous nous retrouvons avec:

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

La façon dont vous pouvez déconstruire les tuples de valeur en arguments nommés est la vraie puissance de la fonctionnalité:

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

Et à la réception:

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

Ou:

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

Goodies du compilateur:

Si nous regardons sous le couvert de notre exemple précédent, nous pouvons voir exactement comment le compilateur interprète ValueTuplelorsque nous lui demandons de déconstruire:

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

En interne, le code compilé utilise Item1et Item2, mais tout cela nous est abstrait puisque nous travaillons avec un tuple décomposé. Un tuple avec des arguments nommés est annoté avec le TupleElementNamesAttribute. Si nous utilisons une seule variable fraîche au lieu de la décomposer, nous obtenons:

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

Notez que le compilateur doit encore faire quelque chose de magique se produit (via l'attribut) lorsque nous déboguer notre application, car il serait étrange de voir Item1, Item2.

Yuval Itzchakov
la source
1
Notez que vous pouvez également utiliser la syntaxe plus simple (et à mon avis préférable)var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47
@ Abion47 Que se passerait-il si les deux types différaient?
Yuval Itzchakov du
Aussi comment vos points "c'est une structure mutable " et "il expose des champs en lecture seule " sont d'accord?
CodesInChaos du
@CodesInChaos Ce n'est pas le cas. J'ai vu [ceci] ( github.com/dotnet/corefx/blob/master/src/Common/src/System/… ), mais je ne pense pas que ce soit ce qui finit par être émis par le compilateur, car les champs locaux ne peuvent pas être en lecture seule de toute façon. Je pense que la proposition signifiait "vous pouvez les faire en lecture seule si vous le souhaitez, mais cela dépend de vous" , ce que j'ai mal interprété.
Yuval Itzchakov du
1
Quelques nits: "ils seront alloués sur la pile" - vrai uniquement pour les variables locales. Vous le savez sans doute, mais malheureusement, la façon dont vous l'avez formulé est susceptible de perpétuer le mythe selon lequel les types de valeur vivent toujours dans la pile.
Peter Duniho
26

La différence entre Tupleet ValueTupleest qu'il Tuples'agit d'un type de référence et d' ValueTupleun type valeur. Cette dernière option est souhaitable car les modifications apportées au langage en C # 7 font que les tuples sont utilisés beaucoup plus fréquemment, mais l'allocation d'un nouvel objet sur le tas pour chaque tuple est un problème de performances, en particulier lorsqu'il n'est pas nécessaire.

Cependant, en C # 7, l'idée est que vous ne devez utiliser explicitement ou l' autre type à cause du sucre de syntaxe étant ajoutée pour une utilisation tuple. Par exemple, en C # 6, si vous souhaitez utiliser un tuple pour renvoyer une valeur, vous devez effectuer les opérations suivantes:

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

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

Cependant, en C # 7, vous pouvez utiliser ceci:

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

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

Vous pouvez même aller plus loin et donner des noms aux valeurs:

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

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

... Ou déconstruisez entièrement le tuple:

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

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

Les tuples n'étaient pas souvent utilisés dans C # pré-7 car ils étaient encombrants et détaillés, et seulement vraiment utilisés dans les cas où la construction d'une classe / structure de données pour une seule instance de travail serait plus problématique qu'elle n'en valait la peine. Mais en C # 7, les tuples ont maintenant un support au niveau du langage, donc leur utilisation est beaucoup plus propre et plus utile.

Abion47
la source
10

J'ai regardé la source pour les deux Tupleet ValueTuple. La différence est que Tuplec'est un classet ValueTupleest un structqui implémente IEquatable.

Cela signifie que Tuple == Tuplecela retournera falses'ils ne sont pas la même instance, mais ValueTuple == ValueTupleretournera trues'ils sont du même type et Equalsretournera truepour chacune des valeurs qu'ils contiennent.

Peter Morris
la source
Mais c'est plus que ça.
BoltClock
2
@BoltClock Votre commentaire serait constructif si vous deviez élaborer
Peter Morris
3
De plus, les types valeur ne vont pas nécessairement dans la pile. La différence est que le représente sémantiquement la valeur, plutôt qu'une référence, chaque fois que cette variable est stockée, qui peut ou non être la pile.
Servy
6

D'autres réponses ont oublié de mentionner des points importants.Au lieu de reformuler, je vais faire référence à la documentation XML à partir du code source :

Les types ValueTuple (d'arity 0 à 8) comprennent l'implémentation d'exécution qui sous-tend les tuples en C # et les tuples de structure en F #.

En plus d'être créés via la syntaxe du langage , ils sont plus facilement créés via les ValueTuple.Createméthodes d'usine. Les System.ValueTupletypes diffèrent des System.Tupletypes en ce que:

  • ce sont des structures plutôt que des classes,
  • ils sont modifiables plutôt qu'en lecture seule , et
  • leurs membres (tels que Item1, Item2, etc.) sont des champs plutôt que des propriétés.

Avec l'introduction de ce type et du compilateur C # 7.0, vous pouvez facilement écrire

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

Et renvoyez deux valeurs d'une méthode:

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

Contrairement à System.Tuplevous, vous pouvez mettre à jour ses membres (Mutable) car ce sont des champs publics en lecture-écriture qui peuvent recevoir des noms significatifs:

(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
Zein Makki
la source
"Arity 0 à 8". Ah, j'aime le fait qu'ils incluent un 0-tuple. Il pourrait être utilisé comme une sorte de type vide, et sera autorisé dans les génériques lorsqu'un paramètre de type n'est pas nécessaire, comme dans class MyNonGenericType : MyGenericType<string, ValueTuple, int>etc.
Jeppe Stig Nielsen
6

En plus des commentaires ci-dessus, un inconvénient malheureux de ValueTuple est que, en tant que type de valeur, les arguments nommés sont effacés lors de la compilation en IL, ils ne sont donc pas disponibles pour la sérialisation au moment de l'exécution.

ie Vos arguments nommés doux finiront toujours par "Item1", "Item2", etc. lorsqu'ils seront sérialisés via par exemple Json.NET.

ZenÉcureuil
la source
2
Donc, techniquement, c'est une similitude et pas une différence;)
JAD
2

Participation tardive pour ajouter une clarification rapide sur ces deux faits:

  • ce sont des structures plutôt que des classes
  • ils sont modifiables plutôt qu'en lecture seule

On pourrait penser que changer les tuples de valeur en masse serait simple:

 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;

Quelqu'un pourrait essayer de contourner ce problème comme suit:

 // 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 raison de ce comportement bizarre est que les tuples de valeur sont exactement basés sur la valeur (structs) et donc l'appel .Select (...) fonctionne sur les structures clonées plutôt que sur les originaux. Pour résoudre ce problème, nous devons recourir à:

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

Alternativement, bien sûr, on pourrait essayer l'approche simple:

   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

J'espère que cela aide quelqu'un qui a du mal à faire la queue avec des tuples de valeur hébergés sur une liste.

XDS
la source