¿Cuál es la diferencia entre System.ValueTuple y System.Tuple?

139

Descompilé algunas bibliotecas de C # 7 y vi que ValueTuplese usaban genéricos. ¿Qué son ValueTuplesy por qué no en su Tuplelugar?

Steve Fan
fuente
Creo que se refiere a la clase Dot NEt Tuple. ¿Podría por favor compartir un código de muestra? Para que sea fácil de entender.
Ranadip Dutta
14
@Ranadip Dutta: si sabe lo que es una tupla, no necesita un código de muestra para comprender la pregunta. La pregunta en sí es sencilla: ¿qué es ValueTuple y en qué se diferencia de Tuple?
BoltClock
1
@BoltClock: Esa es la razón por la que no respondí nada en ese contexto. Sé que en C #, hay una clase Tuple que uso con bastante frecuencia y la misma clase algunas veces también la llamo en PowerShell. Es un tipo de referencia. Ahora viendo las otras respuestas, entendí que también hay un tipo de valor que se conoce como Valuetuple. Si hay una muestra, me gustaría saber el uso de la misma.
Ranadip Dutta
2
¿Por qué descompila estos cuando el código fuente de Roslyn está disponible en github?
Zein Makki
@ user3185569 probablemente porque F12 descompila automáticamente las cosas y es más fácil que saltar a GitHub
John Zabroski

Respuestas:

203

¿Qué son ValueTuplesy por qué no en su Tuplelugar?

A ValueTuplees una estructura que refleja una tupla, igual que la System.Tupleclase original .

La principal diferencia entre Tupley ValueTupleson:

  • System.ValueTuplees un tipo de valor (estructura), mientras que System.Tuplees 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.ValueTuplecomo 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 ValueTupleser 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 structobtiene 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 structslugar de classes , para que no se asocie una penalización de asignación con ellos. Deben ser lo más livianos posible.

Podría decirse que structspuede 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, entonces structsserí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.

Ejemplos:

Puede ver fácilmente que trabajar con se System.Tuplevuelve 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}");

Compilación de golosinas:

Si miramos debajo de la cubierta de nuestro ejemplo anterior, podemos ver exactamente cómo está interpretando el compilador ValueTuplecuando 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 Item1y 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.

Yuval Itzchakov
fuente
1
Tenga en cuenta que también puede usar la sintaxis más simple (y en mi opinión, preferible)var (sum, count) = DoStuff(Enumerable.Range(0, 10));
Abion47
@ Abion47 ¿Qué pasaría si ambos tipos difieren?
Yuval Itzchakov
Además, ¿cómo concuerdan sus puntos "es una estructura mutable " y "expone campos de solo lectura "?
CodesInChaos
@CodesInChaos No lo hace. Vi [esto] ( github.com/dotnet/corefx/blob/master/src/Common/src/System/… ), pero no creo que eso sea lo que termina siendo emitido por el compilador, ya que los campos locales no pueden ser solo lectura de todos modos. Creo que la propuesta significaba "puedes hacerlos de solo lectura si lo deseas, pero eso depende de ti" , lo cual interpreté mal.
Yuval Itzchakov
1
Algunas liendres: "se asignarán en la pila" - cierto solo para variables locales. Sin duda lo sabe, pero desafortunadamente, la forma en que lo expresó probablemente perpetuará el mito de que los tipos de valor siempre viven en la pila.
Peter Duniho el
26

La diferencia entre Tupley ValueTuplees que Tuplees un tipo de referencia y ValueTuplees 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.

Abion47
fuente
10

Miré la fuente de ambos Tupley ValueTuple. La diferencia es que Tuplees un classy ValueTuplees un structque implementa IEquatable.

Eso significa que Tuple == Tuplese devolverá falsesi no son la misma instancia, pero ValueTuple == ValueTuplese devolverá truesi son del mismo tipo y se Equalsdevuelve truepara cada uno de los valores que contienen.

Peter Morris
fuente
Sin embargo, es más que eso.
BoltClock
2
@BoltClock Su comentario sería constructivo si tuviera que dar más detalles
Peter Morris
3
Además, los tipos de valor no necesariamente van a la pila. La diferencia es que representan semánticamente el valor, en lugar de una referencia, cada vez que se almacena esa variable, que puede o no ser la pila.
Servicio
6

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.Createmétodos de fábrica. Los System.ValueTupletipos difieren de los System.Tupletipos en que:

  • son estructuras en lugar de clases,
  • son mutables en lugar de solo lectura , y
  • sus miembros (como Item1, Item2, etc.) son campos en lugar de propiedades.

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";
Zein Makki
fuente
"Arity 0 a 8". Ah, me gusta el hecho de que incluyen una tupla 0. Podría usarse como una especie de tipo vacío, y se permitirá en genéricos cuando no se necesite algún parámetro de tipo, como en class MyNonGenericType : MyGenericType<string, ValueTuple, int>etc.
Jeppe Stig Nielsen
6

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.

Ardilla zen
fuente
2
Así que técnicamente eso es una similitud y no una diferencia;)
JAD
2

Unión tardía para agregar una aclaración rápida sobre estos dos factores:

  • son estructuras en lugar de clases
  • son mutables en lugar de solo lectura

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.

XDS
fuente