Descompilé algunas bibliotecas de C # 7 y vi que ValueTuple
se usaban genéricos. ¿Qué son ValueTuples
y por qué no en su Tuple
lugar?
Descompilé algunas bibliotecas de C # 7 y vi que ValueTuple
se usaban genéricos. ¿Qué son ValueTuples
y por qué no en su Tuple
lugar?
Respuestas:
¿Qué son
ValueTuples
y por qué no en suTuple
lugar?
A ValueTuple
es una estructura que refleja una tupla, igual que la System.Tuple
clase original .
La principal diferencia entre Tuple
y ValueTuple
son:
System.ValueTuple
es un tipo de valor (estructura), mientras que System.Tuple
es un tipo de referencia ( class
). Esto es significativo cuando se habla de asignaciones y presión de GC.System.ValueTuple
no es solo un struct
, es mutable , y hay que tener cuidado al usarlos como tales. Piensa en lo que sucede cuando una clase tiene un System.ValueTuple
como un campo.System.ValueTuple
expone sus elementos a través de campos en lugar de propiedades.Hasta C # 7, usar tuplas no era muy conveniente. Sus nombres de campo son Item1
, Item2
, azúcar sintaxis, etc, y el idioma no se había suministrado para ellos como la mayoría de otros lenguajes (Python, Scala).
Cuando el equipo de diseño de lenguaje .NET decidió incorporar tuplas y agregarles azúcar de sintaxis a nivel de lenguaje, un factor importante fue el rendimiento. Al ValueTuple
ser un tipo de valor, puede evitar la presión del GC al usarlos porque (como detalle de implementación) se asignarán en la pila.
Además, a struct
obtiene semántica de igualdad automática (superficial) por el tiempo de ejecución, donde unclass
no. Aunque el equipo de diseño se aseguró de que hubiera una igualdad aún más optimizada para las tuplas, por lo tanto, implementó una igualdad personalizada para ello.
Aquí hay un párrafo de las notas de diseño deTuples
:
Estructura o clase:
Como mencioné, propongo hacer tipos de tuplas en
structs
lugar declasses
, para que no se asocie una penalización de asignación con ellos. Deben ser lo más livianos posible.Podría decirse que
structs
puede terminar siendo más costoso, porque la asignación copia un valor mayor. Entonces, si se les asigna mucho más de lo que se crean, entoncesstructs
sería una mala elección.Sin embargo, en su propia motivación, las tuplas son efímeras. Los usarías cuando las partes son más importantes que el todo. Entonces, el patrón común sería construirlos, devolverlos e inmediatamente deconstruirlos. En esta situación, las estructuras son claramente preferibles.
Las estructuras también tienen una serie de otros beneficios, que serán obvios a continuación.
Puede ver fácilmente que trabajar con se System.Tuple
vuelve ambiguo muy rápidamente. Por ejemplo, supongamos que tenemos un método que calcula una suma y un recuento 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);
}
En el extremo receptor, terminamos 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);
La forma en que puede deconstruir tuplas de valor en argumentos con nombre es el poder real de la característica:
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;
}
Y en el extremo receptor:
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}");
Si miramos debajo de la cubierta de nuestro ejemplo anterior, podemos ver exactamente cómo está interpretando el compilador ValueTuple
cuando le pedimos que deconstruya:
[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, el código compilado utiliza Item1
y Item2
, pero todo esto se abstrae de nosotros ya que trabajamos con una tupla descompuesta. Una tupla con argumentos nombrados se anota con el TupleElementNamesAttribute
. Si usamos una única variable nueva en lugar de descomponernos, obtenemos:
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));
}
Tenga en cuenta que el compilador todavía tiene que hacer un poco de magia suceda (a través del atributo) cuando nuestra aplicación de depuración, ya que sería extraño ver Item1
, Item2
.
var (sum, count) = DoStuff(Enumerable.Range(0, 10));
La diferencia entre Tuple
y ValueTuple
es que Tuple
es un tipo de referencia y ValueTuple
es un tipo de valor. Esto último es deseable porque los cambios en el lenguaje en C # 7 hacen que las tuplas se usen con mucha más frecuencia, pero la asignación de un nuevo objeto en el montón para cada tupla es un problema de rendimiento, especialmente cuando no es necesario.
Sin embargo, en C # 7, la idea es que nunca tenga que usar explícitamente ninguno de los tipos debido a que se agrega azúcar de sintaxis para el uso de tuplas. Por ejemplo, en C # 6, si quisiera usar una tupla para devolver un valor, tendría que hacer lo siguiente:
public Tuple<string, int> GetValues()
{
// ...
return new Tuple(stringVal, intVal);
}
var value = GetValues();
string s = value.Item1;
Sin embargo, en C # 7, puede usar esto:
public (string, int) GetValues()
{
// ...
return (stringVal, intVal);
}
var value = GetValues();
string s = value.Item1;
Incluso puede ir un paso más allá y dar nombres a los valores:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var value = GetValues();
string s = value.S;
... o deconstruir la tupla por completo:
public (string S, int I) GetValues()
{
// ...
return (stringVal, intVal);
}
var (S, I) = GetValues();
string s = S;
Las tuplas no se usaban con frecuencia en C # pre-7 porque eran engorrosas y detalladas, y solo se usaban realmente en casos en los que construir una clase / estructura de datos para una sola instancia de trabajo sería más problemático de lo que valía la pena. Pero en C # 7, las tuplas tienen soporte de nivel de lenguaje ahora, por lo que usarlas es mucho más limpio y más útil.
Miré la fuente de ambos Tuple
y ValueTuple
. La diferencia es que Tuple
es un class
y ValueTuple
es un struct
que implementa IEquatable
.
Eso significa que Tuple == Tuple
se devolverá false
si no son la misma instancia, pero ValueTuple == ValueTuple
se devolverá true
si son del mismo tipo y se Equals
devuelve true
para cada uno de los valores que contienen.
Otras respuestas olvidaron mencionar puntos importantes. En lugar de reformular, haré referencia a la documentación XML del código fuente :
Los tipos ValueTuple (de arity 0 a 8) comprenden la implementación en tiempo de ejecución que subyace a las tuplas en C # y las tuplas de estructura en F #.
Además de crearse mediante la sintaxis del lenguaje , se crean más fácilmente a través de los
ValueTuple.Create
métodos de fábrica. Los System.ValueTuple
tipos difieren de los System.Tuple
tipos en que:
Con la introducción de este tipo y el compilador de C # 7.0, puede escribir fácilmente
(int, string) idAndName = (1, "John");
Y devuelve dos valores de un método:
private (int, string) GetIdAndName()
{
//.....
return (id, name);
}
Al contrario System.Tuple
, puede actualizar sus miembros (Mutable) porque son campos públicos de lectura y escritura a los que se les puede dar nombres significativos:
(int id, string name) idAndName = (1, "John");
idAndName.name = "New Name";
class MyNonGenericType : MyGenericType<string, ValueTuple, int>
etc.
Además de los comentarios anteriores, un inconveniente desafortunado de ValueTuple es que, como tipo de valor, los argumentos nombrados se borran cuando se compilan en IL, por lo que no están disponibles para la serialización en tiempo de ejecución.
es decir, sus argumentos con nombre dulce terminarán siendo "Item1", "Item2", etc. cuando se serialicen a través de, por ejemplo, Json.NET.
Unión tardía para agregar una aclaración rápida sobre estos dos factores:
Uno pensaría que cambiar las tuplas de valor en masa sería sencillo:
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;
Alguien podría intentar solucionar esto así:
// 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 razón de este comportamiento peculiar es que las tuplas de valor se basan exactamente en el valor (estructuras) y, por lo tanto, la llamada .Select (...) funciona en estructuras clonadas en lugar de en los originales. Para resolver esto debemos recurrir 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
Alternativamente, por supuesto, uno podría intentar el enfoque directo:
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
Espero que esto ayude a alguien que lucha por hacer cabezas de colas a partir de tuplas de valor alojadas en listas.