J'ai décompilé des bibliothèques C # 7 et vu les génériques ValueTuple
utilisés. Que sont ValueTuples
et pourquoi pas Tuple
à la place?
Que sont
ValuedTuples
et pourquoi pasTuple
à la place?
Un ValueTuple
est une structure qui reflète un tuple, identique à la classe System.Tuple
d'origine.
La principale différence entre Tuple
et ValueTuple
est la suivante:
System.ValueTuple
est un type de valeur (struct), alors que System.Tuple
est un type de référence (class
). Ceci est significatif quand on parle d'allocations et de pression du GC.System.ValueTuple
n'est pas seulement une struct
, c'est un mutable, et il faut faire attention en les utilisant comme tels. Pensez à ce qui se passe lorsqu'une classe contient un System.ValueTuple
en tant que champ.System.ValueTuple
expose ses éléments via des champs plutôt que des propriétés.Jusqu'au C 7, utiliser des n-uplets n'était pas très pratique. Leurs noms de champs sont Item1
, Item2
, etc., et la langue ne leur avait pas fourni de sucre de syntaxe, contrairement à la plupart des autres langues (Python, Scala).
Lorsque l'équipe de conception du langage .NET a décidé d'incorporer des n-uplets et de leur ajouter du sucre de syntaxe au niveau de la langue, un facteur important était la performance. ValueTuple
étant un type de valeur, vous pouvez éviter la pression du GC lorsque vous les utilisez car (en tant que détail d'implémentation), elles seront allouées sur la pile.
De plus, une struct
obtient une sémantique d'égalité automatique (peu profonde) par le temps d'exécution, contrairement à class
. Bien que l'équipe de conception se soit assurée que l'égalité pour les n-uplets soit encore plus optimisée, une égalité personnalisée a donc été mise en œuvre.
Voici un paragraphe tiré des notes de conception de Tuples
:
Struct ou classe:
Comme mentionné, je propose de rendre les types de tuples
structs
plutôt queclasses
, de sorte qu'aucune pénalité d'allocation ne leur soit associée. Ils devrait être aussi léger que possible.On peut soutenir que
structs
peut finir par être plus coûteux, car l’affectation copie une plus grande valeur. Donc, si on leur attribue beaucoup plus qu'eux sont créés, alorsstructs
serait un mauvais choix.Dans leur motivation même, les n-uplets sont éphémères. Vous utiliseriez les quand les parties sont plus importantes que le tout. Donc le commun le modèle serait de construire, de retourner et de déconstruire immédiatement leur. Dans cette situation, les structures sont clairement préférables.
Les structures ont également un certain nombre d'autres avantages, qui deviendront évident dans ce qui suit.
Vous pouvez facilement voir que travailler avec System.Tuple
devient très ambigu. Par exemple, supposons que nous ayons une méthode qui calcule une somme et un nombre de List<Int>
:
public Tuple<int, int> DoStuff(IEnumerable<int> ints)
{
var sum = 0;
var count = 0;
foreach (var value in values) { sum += value; count++; }
return new Tuple(sum, count);
}
A la réception, on se retrouve 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écomposer les nuplets de valeur en arguments nommés est le vrai pouvoir 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}");
Si nous regardons sous la couverture de notre exemple précédent, nous pouvons voir exactement comment le compilateur interprète ValueTuple
lorsque 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 Item1
et Item2
, mais tout cela est extrait de nous depuis que 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 de la magie (via l'attribut) lors du débogage de notre application, car il serait étrange de voir Item1
, Item2
.
La différence entre Tuple
et ValueTuple
est que Tuple
est un type de référence et ValueTuple
est un type de valeur. Ce dernier est souhaitable car les modifications apportées au langage dans C # 7 font que les nuplets sont utilisés beaucoup plus fréquemment, mais allouer un nouvel objet sur le tas pour chaque nuplet est un problème de performances, en particulier lorsqu'il est inutile.
Cependant, en C # 7, l’idée est que vous n'avez jamais devez explicitement utiliser l’un ou l’autre type à cause du sucre de syntaxe ajouté pour l’utilisation de Tuple. Par exemple, en C # 6, si vous souhaitez utiliser un tuple pour renvoyer une valeur, vous devez procéder comme suit:
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 les noms de valeurs:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var value = GetValues();
string s = value.S;
... ou déconstruire 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 # pre-7 car ils étaient lourds et verbeux, et uniquement utilisés dans les cas où la construction d'une classe/structure de données pour une seule instance de travail causerait plus de problèmes qu'elle n'en valait la peine. Mais en C # 7, les n-uplets ont maintenant une prise en charge au niveau de la langue, donc leur utilisation est beaucoup plus propre et plus utile.
J'ai regardé la source pour Tuple
et ValueTuple
. La différence est que Tuple
est une class
et ValueTuple
est une struct
qui implémente IEquatable
.
Cela signifie que Tuple == Tuple
retournera false
s'il ne s'agit pas de la même instance, mais ValueTuple == ValueTuple
retournera true
si elles sont du même type et Equals
renvoie true
pour chacune des valeurs qu'elles contiennent.
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 de code source :
Les types ValueTuple (de 0 à 8) comprennent l’implémentation d’exécution qui sous-tend Tuples en C # et struct tuples en F #.
Mis à part la création via la syntaxe de langue, elles sont plus facilement créées via les méthodes d'usine ValueTuple.Create
. Les types System.ValueTuple
diffèrent des types System.Tuple
par le fait que:
Avec l'introduction de ce type et du compilateur C # 7.0, vous pouvez facilement écrire
(int, string) idAndName = (1, "John");
Et retourne deux valeurs d'une méthode:
private (int, string) GetIdAndName()
{
//.....
return (id, name);
}
Contrairement à System.Tuple
, vous pouvez mettre à jour ses membres (Mutable) car il s’agit de zones de lecture/écriture publiques. Vous pouvez attribuer des noms significatifs aux champs:
(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
En plus des commentaires ci-dessus, ValueTuple regrette que, comme type de valeur, les arguments nommés sont effacés lors de la compilation en IL, de sorte qu'ils ne sont pas disponibles pour la sérialisation à l'exécution.
c'est-à-dire que vos arguments nommés seront toujours "Item1", "Item2", etc. lorsqu'ils seront sérialisés via e.g. Json.NET.
Adhésion tardive pour ajouter une clarification rapide sur ces deux faits:
On pourrait penser que changer en masse les nuées de valeur 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 ceci:
// 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 étrange est que les tuples de valeur sont exactement basés sur la valeur (structs) et donc, l'appel .Select (...) fonctionne sur des 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 aidera quelqu'un qui a du mal à se faire une belle tête de liste à partir de tuples de valeur hébergés sur des listes.
C’est un très bon résumé de ce qu’il est et de ses limites.
https://josephwoodward.co.uk/2017/04/csharp-7-valuetuple-types-and-the-limitations