J'ai décompilé certaines bibliothèques C # 7 et vu des ValueTuple
génériques utilisés. Que sont ValueTuples
et pourquoi pas à la Tuple
place?
J'ai décompilé certaines bibliothèques C # 7 et vu des ValueTuple
génériques utilisés. Que sont ValueTuples
et pourquoi pas à la Tuple
place?
Réponses:
Que sont
ValueTuples
et pourquoi pas à laTuple
place?
A ValueTuple
est une structure qui reflète un tuple, identique à la System.Tuple
classe d' origine .
La principale différence entre Tuple
et ValueTuple
sont:
System.ValueTuple
est un type valeur (struct), tandis que System.Tuple
est un type référence ( class
). C'est significatif quand on parle d'allocations et de pression du GC.System.ValueTuple
n'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.ValueTuple
comme 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 struct
obtient une sémantique d'égalité automatique (superficielle) par le runtime, alors que a class
ne 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
structs
plutôt queclasses
, de sorte qu'aucune pénalité d'allocation ne leur soit associée. Ils doivent être aussi légers que possible.On
structs
peut 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, cestructs
serait 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.
Vous pouvez facilement voir que travailler avec System.Tuple
devient 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}");
Si nous regardons sous le couvert 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 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
.
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
La différence entre Tuple
et ValueTuple
est qu'il Tuple
s'agit d'un type de référence et d' ValueTuple
un 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.
J'ai regardé la source pour les deux Tuple
et ValueTuple
. La différence est que Tuple
c'est un class
et ValueTuple
est un struct
qui implémente IEquatable
.
Cela signifie que Tuple == Tuple
cela retournera false
s'ils ne sont pas la même instance, mais ValueTuple == ValueTuple
retournera true
s'ils sont du même type et Equals
retournera true
pour chacune des valeurs qu'ils 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 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.Create
méthodes d'usine. Les System.ValueTuple
types diffèrent des System.Tuple
types en ce que:
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.Tuple
vous, 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";
class MyNonGenericType : MyGenericType<string, ValueTuple, int>
etc.
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.
Participation tardive pour ajouter une clarification rapide sur ces deux faits:
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.