¿Usar .NET 4.0 Tuples en mi código C # es una mala decisión de diseño?

166

Con la adición de la clase Tuple en .net 4, he estado tratando de decidir si usarlos en mi diseño es una mala elección o no. A mi modo de ver, una Tupla puede ser un atajo para escribir una clase de resultado (estoy seguro de que también hay otros usos).

Así que esto:

public class ResultType
{
    public string StringValue { get; set; }
    public int IntValue { get; set; }
}

public ResultType GetAClassedValue()
{
    //..Do Some Stuff
    ResultType result = new ResultType { StringValue = "A String", IntValue = 2 };
    return result;
}

Es equivalente a esto:

public Tuple<string, int> GetATupledValue()
{
    //...Do Some stuff
    Tuple<string, int> result = new Tuple<string, int>("A String", 2);
    return result;
}

Entonces, dejando a un lado la posibilidad de que me esté perdiendo el punto de Tuples, ¿es el ejemplo con una Tuple una mala elección de diseño? Para mí, parece menos desorden, pero no tan autodocumentado y limpio. Lo que significa que con el tipo ResultType, queda muy claro más adelante qué significa cada parte de la clase, pero tiene que mantener un código adicional. Con el Tuple<string, int>tendrá que buscar y descubrir qué Itemrepresenta cada uno , pero escribe y mantiene menos código.

Cualquier experiencia que haya tenido con esta elección sería muy apreciada.

Jason Webb
fuente
16
@Boltbait: porque la tupla es de la teoría de conjuntos en.wikipedia.org/wiki/Tuple
Matthew Whited
16
@BoltBait: Una "tupla" es un conjunto ordenado de valores, y no necesariamente tiene nada que ver con las filas de una base de datos.
FrustratedWithFormsDesigner
19
Las tuplas en C # serían aún mejores si hubiera una buena acción de desempaquetado de tuplas :)
Justin
44
@ John Saunders Mi pregunta es específicamente sobre las decisiones de diseño en c #. No uso ningún otro lenguaje .net, así que no estoy seguro de cómo los impactan las tuplas. Entonces lo marqué c #. Me pareció razonable, pero supongo que el cambio a .net también está bien.
Jason Webb
13
@ John Saunders: Estoy bastante seguro de que está preguntando sobre las decisiones de diseño relacionadas con el uso o no del uso de Tuples en una aplicación que está escrita exclusivamente en C #. No creo que esté preguntando sobre las decisiones de diseño que se utilizaron para crear el lenguaje C #.
Brett Widmeier

Respuestas:

163

Las tuplas son geniales si controlas tanto la creación como el uso de ellas; puedes mantener el contexto, que es esencial para comprenderlas.

En una API pública, sin embargo, son menos efectivos. El consumidor (no usted) tiene que adivinar o buscar documentación, especialmente para cosas como Tuple<int, int>.

Los usaría para miembros privados / internos, pero usar clases de resultados para miembros públicos / protegidos.

Esta respuesta también tiene algo de información.

Bryan Watts
fuente
19
La diferencia entre público / privado es realmente importante, gracias por mencionarlo. Devolver tuplas en su API pública es una muy mala idea, pero internamente donde las cosas pueden estar estrechamente unidas (pero está bien ) está bien.
Clinton Pierce
36
¿Estrechamente acoplado o fuertemente tuplado? ;)
contactmatt
¿No podría ser útil la documentación adecuada aquí? Por ejemplo, Scala StringOps(básicamente una clase de "métodos de extensión" para Java String) tiene un método parition(Char => Boolean): (String, String)(toma en predicado, devuelve una tupla de 2 cadenas). Es obvio cuál es el resultado (obviamente, uno será los caracteres para los que el predicado fue verdadero y el otro para los que fue falso, pero no está claro cuál es cuál). Es la documentación lo que aclara esto (y la coincidencia de patrones se puede usar para aclarar en el uso cuál es cuál).
Kat
@ Mike: Eso hace que sea difícil acertar y equivocarse, el sello distintivo de una API que causará dolor a alguien en algún lugar :-)
Bryan Watts
79

A mi modo de ver, una Tupla es un atajo para escribir una clase de resultado (estoy seguro de que también hay otros usos).

De hecho, existen otros usos valiososTuple<> : la mayoría de ellos implican abstraer la semántica de un grupo particular de tipos que comparten una estructura similar y tratarlos simplemente como un conjunto ordenado de valores. En todos los casos, un beneficio de las tuplas es que evitan abarrotar su espacio de nombres con clases de solo datos que exponen propiedades pero no métodos.

Aquí hay un ejemplo de uso razonable para Tuple<>:

var opponents = new Tuple<Player,Player>( playerBob, playerSam );

En el ejemplo anterior, queremos representar un par de oponentes, una tupla es una forma conveniente de emparejar estas instancias sin tener que crear una nueva clase. Aquí hay otro ejemplo:

var pokerHand = Tuple.Create( card1, card2, card3, card4, card5 );

Una mano de póker puede considerarse como un juego de cartas, y la tupla (puede ser) una forma razonable de expresar ese concepto.

Dejando a un lado la posibilidad de que me estoy perdiendo el punto de Tuples, ¿es el ejemplo con una Tuple una mala elección de diseño?

Devolver Tuple<>instancias fuertemente tipadas como parte de una API pública para un tipo público rara vez es una buena idea. Como usted mismo reconoce, las tuplas requieren que las partes involucradas (autor de la biblioteca, usuario de la biblioteca) acuerden de antemano el propósito y la interpretación de los tipos de tuplas que se utilizan. Es lo suficientemente desafiante crear API que sean intuitivas y claras, su uso Tuple<>público solo oscurece la intención y el comportamiento de la API.

Los tipos anónimos también son una especie de tupla ; sin embargo, están fuertemente tipados y le permiten especificar nombres claros e informativos para las propiedades que pertenecen al tipo. Pero los tipos anónimos son difíciles de usar en diferentes métodos: se agregaron principalmente para admitir tecnologías como LINQ, donde las proyecciones producirían tipos a los que normalmente no quisiéramos asignar nombres. (Sí, sé que el compilador consolida los tipos anónimos con los mismos tipos y propiedades con nombre).

Mi regla general es: si lo devolverá desde su interfaz pública, hágalo como un tipo con nombre .

Mi otra regla general para usar tuplas es: nombrar los argumentos del método y las variables de tipo localc con la Tuple<>mayor claridad posible: hacer que el nombre represente el significado de las relaciones entre los elementos de la tupla. Piensa en mi var opponents = ...ejemplo.

Aquí hay un ejemplo de un caso del mundo real en el que solía Tuple<>evitar declarar un tipo de solo datos para usar solo dentro de mi propio ensamblado . La situación implica el hecho de que cuando se usan diccionarios genéricos que contienen tipos anónimos, se hace difícil usar el TryGetValue()método para encontrar elementos en el diccionario porque el método requiere un outparámetro que no se puede nombrar:

public static class DictionaryExt 
{
    // helper method that allows compiler to provide type inference
    // when attempting to locate optionally existent items in a dictionary
    public static Tuple<TValue,bool> Find<TKey,TValue>( 
        this IDictionary<TKey,TValue> dict, TKey keyToFind ) 
    {
        TValue foundValue = default(TValue);
        bool wasFound = dict.TryGetValue( keyToFind, out foundValue );
        return Tuple.Create( foundValue, wasFound );
    }
}

public class Program
{
    public static void Main()
    {
        var people = new[] { new { LastName = "Smith", FirstName = "Joe" },
                             new { LastName = "Sanders", FirstName = "Bob" } };

        var peopleDict = people.ToDictionary( d => d.LastName );

        // ??? foundItem <= what type would you put here?
        // peopleDict.TryGetValue( "Smith", out ??? );

        // so instead, we use our Find() extension:
        var result = peopleDict.Find( "Smith" );
        if( result.First )
        {
            Console.WriteLine( result.Second );
        }
    }
}

PD: Hay otra forma (más simple) de solucionar los problemas que surgen de los tipos anónimos en los diccionarios, y es usar la varpalabra clave para permitir que el compilador 'infiera' el tipo por usted. Aquí está esa versión:

var foundItem = peopleDict.FirstOrDefault().Value;
if( peopleDict.TryGetValue( "Smith", out foundItem ) )
{
   // use foundItem...
}
LBushkin
fuente
55
@stakx: Sí, estoy de acuerdo. Pero tenga en cuenta que el ejemplo pretende ilustrar casos en los que el uso de a Tuple<> podría tener sentido. Siempre hay alternativas ...
LBushkin
1
Creo que reemplazar los parámetros con una Tuple, como en TryGetValue o TryParse, es uno de los pocos casos en los que podría tener sentido que una API pública devuelva una Tuple. De hecho, al menos un lenguaje .NET realiza automáticamente este cambio de API por usted.
Joel Mueller
1
@stakx: las tuplas también son inmutables, mientras que las matrices no lo son.
Robert Paulson
2
Comprendí que las Tuplas son útiles solo como objetos de jugador de póker inmutables.
Gennady Vanin Геннадий Ванин
2
+1 Gran respuesta: me encantan tus dos ejemplos iniciales que realmente me hicieron cambiar de opinión. Nunca antes había visto ejemplos en los que una tupla fuera al menos tan apropiada como una estructura o tipo personalizado. Sin embargo, su último "ejemplo de la vida real" para mí ya no parece agregar mucho valor. Los dos primeros ejemplos son perfectos y simples. El último es un poco difícil de entender y su "PS" lo hace inútil ya que su enfoque alternativo es mucho más simple y no requiere tuplas.
chiccodoro
18

Las tuplas pueden ser útiles ... pero también pueden ser un dolor más tarde. Si tiene un método que devuelve, Tuple<int,string,string,int>¿cómo sabe cuáles son esos valores más adelante? ¿Eran ID, FirstName, LastName, Ageo eran ellos UnitNumber, Street, City, ZipCode?

Matthew Whited
fuente
9

Las tuplas son una adición bastante decepcionante al CLR desde la perspectiva de un programador de C #. Si tiene una colección de elementos que varía en longitud, no necesita que tengan nombres estáticos únicos en el momento de la compilación.

Pero si tiene una colección de longitud constante, esto implica que las ubicaciones fijas de la colección tienen un significado predefinido específico. Y es siempre mejor darles nombres estáticos apropiados en ese caso, en lugar de tener que recordar el significado de Item1, Item2, etc.

Las clases anónimas en C # ya proporcionan una excelente solución para el uso privado más común de las tuplas, y dan nombres significativos a los elementos, por lo que en realidad son superiores en ese sentido. El único problema es que no pueden filtrarse de los métodos con nombre. Prefiero ver que se levante esa restricción (quizás solo para métodos privados) que tener soporte específico para tuplas en C #:

private var GetDesserts()
{
    return _icecreams.Select(
        i => new { icecream = i, topping = new Topping(i) }
    );
}

public void Eat()
{
    foreach (var dessert in GetDesserts())
    {
        dessert.icecream.AddTopping(dessert.topping);
        dessert.Eat();
    }
}
Daniel Earwicker
fuente
Como característica general, lo que me gustaría ver sería que .net defina un estándar para algunos tipos especiales de tipos anónimos, de modo que, por ejemplo struct var SimpleStruct {int x, int y;}, defina una estructura con un nombre que comience con un guid asociado con estructuras simples y tiene algo como {System.Int16 x}{System.Int16 y}adjunto; se requeriría cualquier nombre que comience con ese GUID para representar una estructura que contenga solo esos elementos y contenidos específicos particulares; Si varias definiciones para el mismo nombre están dentro del alcance y coinciden, todas se considerarían del mismo tipo.
supercat
Tal cosa sería útil para algunos tipos de tipos, incluidas las estructuras de clase y estructura mutables e inmutables, así como las interfaces y delegados para su uso en situaciones en las que se debe considerar que la estructura coincidente implica una semántica coincidente. Si bien existen razones por las cuales puede ser útil tener dos tipos de delegados que tienen los mismos parámetros que no se consideran intercambiables, también sería útil si uno pudiera especificar que un método desea un delegado que tome alguna combinación de parámetros pero más allá de eso no no importa qué tipo de delegado sea.
supercat
Es desafortunado que si dos personas diferentes desean escribir funciones que toman como delegados de entrada que necesitan un solo refparámetro, la única forma en que será posible tener un delegado que se pueda pasar a ambas funciones será si la persona produce y compila un delega el tipo y se lo da al otro para construir. No hay forma de que ambas personas puedan indicar que los dos tipos deberían considerarse sinónimos.
supercat
¿Tiene más ejemplos concretos de dónde usaría clases anónimas en lugar de Tuple? Acabo de pasar por 50 lugares en mi base de código donde estoy usando tuplas, y todos son valores de retorno (típicamente yield return) o son elementos de colecciones que se usan en otros lugares, así que no busques clases anónimas.
2
@JonofAllTrades: las opiniones probablemente varían en eso, pero trato de diseñar los métodos públicos como si fueran a ser utilizados por otra persona, incluso si esa persona fuera yo, ¡así que me estoy dando un buen servicio al cliente! Tiende a llamar a una función más veces de lo que la escribe. :)
Daniel Earwicker
8

Similar a la palabra clave var, pretende ser una conveniencia, pero se abusa con tanta facilidad.

En mi opinión más humilde, no exponer Tuplecomo una clase de retorno. Úselo en privado, si la estructura de datos de un servicio o componente lo requiere, pero devuelva clases bien formadas y conocidas de métodos públicos.

// one possible use of tuple within a private context. would never
// return an opaque non-descript instance as a result, but useful
// when scope is known [ie private] and implementation intimacy is
// expected
public class WorkflowHost
{
    // a map of uri's to a workflow service definition 
    // and workflow service instance. By convention, first
    // element of tuple is definition, second element is
    // instance
    private Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> _map = 
        new Dictionary<Uri, Tuple<WorkflowService, WorkflowServiceHost>> ();
}
johnny g
fuente
2
Los lenguajes "RAD" convencionales (Java es el mejor ejemplo) siempre han elegido la seguridad y evitan dispararse en los escenarios de tipo pie. Me alegra que Microsoft esté tomando algunas oportunidades con C # y dándole al lenguaje un buen poder, incluso si puede ser mal utilizado.
Matt Greer
44
La palabra clave var no es posible abusar. ¿Quizás quiso decir dinámico?
Chris Marisic
@ Chris Marisic, solo para aclarar que no quise decir en el mismo escenario: tiene toda la razón, varno puede ser devuelto o existir fuera de su alcance declarado. sin embargo, todavía es posible usarlo en exceso de su propósito previsto y ser "abusado"
johnny g
55
Todavía estoy de pie en que var no se puede abusar del período. Es simplemente una palabra clave para hacer una definición variable más corta. Su argumento tal vez se trata de tipos anónimos, pero incluso esos realmente no se pueden abusar porque todavía son tipos estáticamente vinculados, ahora dinámicos es una historia bastante diferente.
Chris Marisic
44
Se puede abusar de var en el sentido de que si se usa en exceso, a veces puede causar problemas para la persona que lee el código. Especialmente si no estás en Visual Studio y te falta inteligencia.
Matt Greer
5

Usar una clase como ResultTypees más claro. Puede dar nombres significativos a los campos de la clase (mientras que con una tupla se llamarían Item1y Item2). Esto es aún más importante si los tipos de los dos campos son iguales: el nombre distingue claramente entre ellos.

Richard Fearn
fuente
Una pequeña ventaja adicional: puede reordenar de forma segura los parámetros para una clase. Hacerlo para una tupla romperá el código, y si los campos tienen tipos similares, esto puede no ser detectable en el momento de la compilación.
Estoy de acuerdo, las Tuplas se usan cuando un desarrollador carece de energía para pensar en un nombre de clase significativo que agrupe nombres de propiedad significativos. La programación es acerca de la comunicación. Las tuplas son un antipatrón para la comunicación. Hubiera preferido que Microsoft lo dejara fuera del lenguaje para obligar a los desarrolladores a tomar buenas decisiones de diseño.
Mike Gold
5

¿Qué tal usar Tuples en un patrón decorar-ordenar-decorar? (Transformación Schwartziana para el pueblo Perl). Aquí hay un ejemplo artificial, sin duda, pero las Tuplas parecen ser una buena manera de manejar este tipo de cosas:

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] files = Directory.GetFiles("C:\\Windows")
                    .Select(x => new Tuple<string, string>(x, FirstLine(x)))
                    .OrderBy(x => x.Item2)
                    .Select(x => x.Item1).ToArray();
        }
        static string FirstLine(string path)
        {
            using (TextReader tr = new StreamReader(
                        File.Open(path, FileMode.Open)))
            {
                return tr.ReadLine();
            }
        }
    }
}

Ahora, podría haber usado un objeto [] de dos elementos o en este ejemplo específico una cadena [] de dos elementos. El punto es que podría haber usado cualquier cosa como el segundo elemento en una Tuple que se usa internamente y es bastante fácil de leer.

Clinton Pierce
fuente
3
Puede usar Tuple.Create en lugar de constructor. Es mas corto.
Kugel
un tipo anónimo será mucho más legible; como usaría nombres de variables reales en lugar de 'x'
Dave Cousineau
Lo haría ahora . Ese es un problema SO, las respuestas publicadas hace años y años nunca se limpian o actualizan para la nueva tecnología.
Clinton Pierce el
4

En mi opinión, estas "tuplas" son básicamente todos los structtipos anónimos de acceso público con miembros sin nombre .

El único lugar donde usaría tupla es cuando necesita juntar rápidamente algunos datos, en un alcance muy limitado. La semántica de los datos debe ser obvia , por lo que el código no es difícil de leer. Entonces, usar una tupla ( int, int) para (fila, col) parece razonable. Pero me cuesta encontrar una ventaja sobre los structmiembros nombrados (por lo que no se cometen errores y la fila / columna no se intercambian accidentalmente)

Si está enviando datos de vuelta a la persona que llama, o está aceptando datos de una persona que llama, realmente debería usar un structcon miembros nombrados.

Tome un ejemplo simple:

struct Color{ float r,g,b,a ; }
public void setColor( Color color )
{
}

La versión tupla

public void setColor( Tuple<float,float,float,float> color )
{
  // why?
}

No veo ninguna ventaja en usar tuplas en lugar de una estructura con miembros nombrados. El uso de miembros sin nombre es un paso atrás para la legibilidad y la comprensión de su código.

Tuple me parece una forma perezosa de evitar crear una structcon miembros reales nombrados. El uso excesivo de la tupla, donde realmente sientes que tú / alguien que encuentra tu código necesitaría miembros nombrados es A Bad Thing ™ si alguna vez viera uno.

bobobobo
fuente
3

No me juzguen, no soy un experto, pero con las nuevas Tuplas en C # 7.x ahora, podría devolver algo como:

return (string Name1, int Name2)

Al menos ahora puede nombrarlo y los desarrolladores pueden ver alguna información.

Denis Gordin
fuente
2

Depende, por supuesto! Como dijiste, una tupla puede ahorrarte código y tiempo cuando deseas agrupar algunos artículos para el consumo local. También puede usarlos para crear algoritmos de procesamiento más genéricos que si pasa una clase concreta. No recuerdo cuántas veces he deseado tener algo más allá de KeyValuePair o DataRow para pasar rápidamente alguna fecha de un método a otro.

Por otro lado, es bastante posible exagerar y pasar tuplas donde solo puedes adivinar lo que contienen. Si va a utilizar una tupla en todas las clases, tal vez sería mejor crear una clase concreta.

Utilizado con moderación, por supuesto, las tuplas pueden conducir a un código más conciso y legible. Puede consultar C ++, STL y Boost para ver ejemplos de cómo se usan las tuplas en otros lenguajes, pero al final, todos tendremos que experimentar para encontrar la mejor forma en el entorno .NET.

Panagiotis Kanavos
fuente
1

Las tuplas son una característica de marco inútil en .NET 4. Creo que se perdió una gran oportunidad con C # 4.0. Me hubiera encantado tener tuplas con miembros nombrados, para que puedas acceder a los diversos campos de una tupla por nombre en lugar de Value1 , Value2 , etc.

Hubiera requerido un cambio de idioma (sintaxis), pero habría sido muy útil.

Philippe Leybaert
fuente
¿Cómo son las tuplas una "característica del lenguaje"? Es una clase disponible para todos los lenguajes .net, ¿no?
Jason Webb
11
Relativamente inútil para C #, tal vez. Pero System.Tuple no se agregó al marco debido a C #. Se agregó para facilitar la interoperabilidad entre F # y otros idiomas.
Joel Mueller
@Jason: No dije que era una característica del lenguaje (lo escribí mal, pero lo corregí antes de que hicieras este comentario).
Philippe Leybaert
2
@Jason: los idiomas con soporte directo para tuplas tienen soporte implícito para deconstruir tuplas en sus valores componentes. El código escrito en esos idiomas generalmente nunca, nunca, accede a las propiedades Item1, Item2. let success, value = MethodThatReturnsATuple()
Joel Mueller
@ Joel: esta pregunta está etiquetada como C #, por lo que de alguna manera se trata de C #. Todavía creo que podrían haberlo convertido en una característica de primera clase en el lenguaje.
Philippe Leybaert
1

Yo personalmente nunca usaría una Tupla como un tipo de retorno porque no hay indicación de lo que representan los valores. Las tuplas tienen algunos usos valiosos porque, a diferencia de los objetos, son tipos de valor y, por lo tanto, comprenden la igualdad. Debido a esto, los usaré como claves de diccionario si necesito una clave multiparte o como clave para una cláusula GroupBy si quiero agrupar por múltiples variables y no quiero agrupaciones anidadas (¿Quién quiere agrupaciones anidadas?). Para superar el problema con extrema verbosidad, puede crearlos con un método auxiliar. Tenga en cuenta que si está accediendo con frecuencia a miembros (a través de Item1, Item2, etc.), probablemente debería usar una construcción diferente, como una estructura o una clase anónima.

Seth Paulson
fuente
0

He usado tuplas, tanto las Tuplenuevas como las nuevas ValueTuple, en varios escenarios diferentes y llegué a la siguiente conclusión: no usar .

Cada vez, me encontré con los siguientes problemas:

  • el código se volvió ilegible debido a la falta de nombres fuertes;
  • incapaz de utilizar características de jerarquía de clases, como DTO de clase base y DTO de clase secundaria, etc .;
  • si se usan en más de un lugar, terminas copiando y pegando estas definiciones feas, en lugar de un nombre de clase limpio.

Mi opinión es que las tuplas son un detrimento, no una característica, de C #.

Tengo una crítica algo similar, pero mucho menos dura, de Func<>y Action<>. Esos son útiles en muchas situaciones, especialmente las simples Actiony las Func<type>variantes, pero cualquier cosa más allá de eso, he descubierto que crear un tipo de delegado es más elegante, legible, mantenible y le brinda más funciones, como ref/ outparámetros.

Sr. TA
fuente
Mencioné tuplas con nombre ValueTuple, y tampoco me gustan. Primero, el nombre no es fuerte, es más una sugerencia, muy fácil de confundir porque se puede asignar implícitamente a cualquiera Tupleo ValueTuplecon el mismo número y tipo de elementos. En segundo lugar, la falta de herencia y la necesidad de copiar y pegar toda la definición de un lugar a otro siguen siendo perjudiciales.
Sr. TA
Estoy de acuerdo con esos puntos. Intento usar Tuples solo cuando controlo al productor y al consumidor, y si no están demasiado "lejos". Significado, productor y consumidor en el mismo método = 'cerrar', mientras que productor y consumidor en diferentes conjuntos = 'años luz de distancia'. Entonces, ¿si creo tuplas al comienzo de un método y las uso al final? Claro, estoy de acuerdo con eso. También documento los términos y tal. Pero sí, estoy de acuerdo en que, en general, no son la elección correcta.
Mike Christiansen