Devolviendo dos valores, Tuple vs 'out' vs 'struct'

86

Considere una función que devuelve dos valores. Podemos escribir:

// Using out:
string MyFunction(string input, out int count)

// Using Tuple class:
Tuple<string, int> MyFunction(string input)

// Using struct:
MyStruct MyFunction(string input)

¿Cuál es la mejor práctica y por qué?

Xaqron
fuente
La cadena no es un tipo de valor. Creo que quiso decir "considere una función que devuelve dos valores".
Eric Lippert
@Eric: Tienes razón. Me refiero a tipos inmutables.
Xaqron
y que pasa con una clase?
Lukasz Madon
1
@lukas: Nada, pero seguro que no está en las mejores prácticas. Este es un valor ligero (<16 KB) y si voy a agregar un código personalizado, lo seguiré structcomo se Ericmencionó.
Xaqron
1
Yo diría que solo se usa cuando necesita el valor de retorno para decidir si debe procesar los datos de retorno, como en TryParse; de ​​lo contrario, siempre debe devolver un objeto estructurado, como si el objeto estructurado debería ser un tipo de valor o una referencia el tipo depende del uso adicional que haga de los datos
MikeT

Respuestas:

93

Cada uno tiene sus pros y sus contras.

Los parámetros de salida son rápidos y económicos, pero requieren que pase una variable y confíe en la mutación. Es casi imposible utilizar correctamente un parámetro de salida con LINQ.

Las tuplas ejercen presión sobre la recolección de basura y no se documentan por sí mismas. "Item1" no es muy descriptivo.

Las estructuras personalizadas pueden ser lentas de copiar si son grandes, pero se autodocumentan y son eficientes si son pequeñas. Sin embargo, también es complicado definir un montón de estructuras personalizadas para usos triviales.

Me inclinaría a la solución de estructura personalizada si todas las demás cosas son iguales. Aún mejor, es hacer una función que solo devuelva un valor . ¿Por qué devuelve dos valores en primer lugar?

ACTUALIZACIÓN: Tenga en cuenta que las tuplas en C # 7, que se enviaron seis años después de que se escribió este artículo, son tipos de valor y, por lo tanto, es menos probable que creen presión de recolección.

Eric Lippert
fuente
2
Devolver dos valores a menudo es un sustituto de no tener tipos de opciones o ADT.
Anton Tykhyy
2
Por mi experiencia con otros lenguajes, diría que generalmente las tuplas se utilizan para agrupar elementos de forma rápida y sucia. Por lo general, es mejor crear una clase o estructura simplemente porque le permite nombrar cada elemento. Cuando se utilizan tuplas, el significado de cada valor puede ser difícil de determinar. Pero le evita tomarse el tiempo para crear la clase / estructura que puede ser excesiva si dicha clase / estructura no se usa en otro lugar.
Kevin Cathcart
23
@Xaqron: Si encuentra que la idea de "datos con un tiempo de espera" es común en su programa, entonces podría considerar hacer un tipo genérico "TimeLimited <T>" para que su método devuelva un TimeLimited <string> o TimeLimited <Uri> o lo que sea. La clase TimeLimited <T> puede tener métodos auxiliares que le digan "¿cuánto tiempo nos queda?" o "¿está caducado?" o lo que sea. Intente capturar una semántica interesante como esta en el sistema de tipos.
Eric Lippert
3
Absolutamente, nunca usaría Tuple como parte de la interfaz pública. Pero incluso para el código 'privado', obtengo una legibilidad enorme de un tipo adecuado en lugar de usar Tuple (especialmente lo fácil que es crear un tipo interno privado con Propiedades automáticas).
SolutionYogi
2
¿Qué significa presión de recogida?
lanza el
25

Además de las respuestas anteriores, C # 7 trae tuplas de tipo de valor, a diferencia de System.Tupleque es un tipo de referencia y también ofrece una semántica mejorada.

Aún puede dejarlos sin nombre y usar la .Item*sintaxis:

(string, string, int) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.Item1; //John
person.Item2; //Doe
person.Item3;   //42

Pero lo realmente poderoso de esta nueva característica es la capacidad de tener tuplas con nombre. Entonces podríamos reescribir lo anterior así:

(string FirstName, string LastName, int Age) getPerson()
{
    return ("John", "Doe", 42);
}

var person = getPerson();
person.FirstName; //John
person.LastName; //Doe
person.Age;   //42

También se admite la desestructuración:

(string firstName, string lastName, int age) = getPerson()

dimlucas
fuente
2
¿Estoy en lo cierto al pensar que esto devuelve básicamente una estructura con referencias como miembros bajo el capó?
Austin_Anderson
4
¿Sabemos cómo se compara el rendimiento de eso con el uso de parámetros?
SpaceMonkey
20

Creo que la respuesta depende de la semántica de lo que está haciendo la función y la relación entre los dos valores.

Por ejemplo, los TryParsemétodos toman un outparámetro para aceptar el valor analizado y devuelven un boolpara indicar si el análisis se realizó correctamente o no. Los dos valores realmente no van juntos, por lo que, semánticamente, tiene más sentido y la intención del código es más fácil de leer para usar el outparámetro.

Sin embargo, si su función devuelve coordenadas X / Y de algún objeto en la pantalla, entonces los dos valores pertenecen semánticamente juntos y sería mejor usar a struct.

Personalmente, evitaría usar un tuplepara cualquier cosa que sea visible para el código externo debido a la sintaxis incómoda para recuperar los miembros.

Andrew Cooper
fuente
+1 para semántica. Su respuesta es más apropiada con tipos de referencia cuando podemos dejar el outparámetro null. Hay algunos tipos inmutables que aceptan valores NULL.
Xaqron
3
En realidad, los dos valores en TryParse pertenecen en gran medida juntos, mucho más de lo que implica tener uno como valor de retorno y el otro como parámetro ByRef. En muchos sentidos, lo lógico para devolver sería un tipo anulable. Hay algunos casos en los que el patrón TryParse funciona bien, y otros en los que es un fastidio (es bueno que se pueda usar en una declaración "if", pero hay muchos casos en los que se devuelve un valor anulable o se puede especificar un valor predeterminado sería más conveniente).
supercat
@supercat Estoy de acuerdo con Andrew, no van de la mano. aunque están relacionados, la devolución le indica si necesita preocuparse por el valor, no es algo que deba procesarse en conjunto. por lo tanto, después de haber procesado la devolución, ya no es necesario para ningún otro procesamiento relacionado con el valor de salida, esto es diferente a devolver un KeyValuePair desde un diccionario donde hay un vínculo claro y continuo entre la clave y el valor. aunque estoy de acuerdo si los tipos que aceptan valores NULL han estado en .Net 1.1, probablemente los habrían usado ya que nulos sería una forma adecuada de marcar sin valor
MikeT
@MikeT: Considero excepcionalmente desafortunado que Microsoft haya sugerido que las estructuras solo se deben usar para cosas que representan un valor único, cuando de hecho una estructura de campo expuesto es el medio ideal para pasar juntas un grupo de variables independientes unidas con cinta adhesiva . El indicador de éxito y el valor son significativos juntos en el momento en que se devuelven , incluso si después de eso serían más útiles como variables independientes. Los campos de una estructura de campo expuesto almacenados en una variable se pueden utilizar ellos mismos como variables independientes. En cualquier caso ...
supercat
@MikeT: Debido a las formas en que la covarianza es y no es compatible dentro del marco, el único trypatrón que funciona con interfaces covariantes es T TryGetValue(whatever, out bool success); ese enfoque habría permitido interfaces IReadableMap<in TKey, out TValue> : IReadableMap<out TValue>y permitiría que el código que quiere mapear instancias de Animala instancias de Caracepte un Dictionary<Cat, ToyotaCar>[using TryGetValue<TKey>(TKey key, out bool success). No es posible tal variación si TValuese utiliza como refparámetro.
supercat
2

Seguiré con el enfoque de usar el parámetro Out porque en el segundo enfoque necesitaría crear un objeto de la clase Tuple y luego agregarle valor, lo que creo que es una operación costosa en comparación con devolver el valor en el parámetro out. Aunque si desea devolver varios valores en Tuple Class (que de hecho no se puede lograr simplemente devolviendo un parámetro), optaré por un segundo enfoque.

Sumit
fuente
Estoy de acuerdo con out. Además, hay una paramspalabra clave que no mencioné para aclarar la pregunta.
Xaqron
2

No mencionaste una opción más, que es tener una clase personalizada en lugar de una estructura. Si los datos tienen una semántica asociada que puede ser operada por funciones, o el tamaño de la instancia es lo suficientemente grande (> 16 bytes como regla general), se puede preferir una clase personalizada. No se recomienda el uso de "out" en la API pública debido a su asociación con punteros y requiere comprender cómo funcionan los tipos de referencia.

https://msdn.microsoft.com/en-us/library/ms182131.aspx

Tuple es bueno para uso interno, pero su uso es incómodo en API públicas. Entonces, mi voto es entre estructura y clase para API pública.

JeanP
fuente
1
Si un tipo existe únicamente con el propósito de devolver una agregación de valores, yo diría que un tipo de valor de campo expuesto simple es el ajuste más claro para esa semántica. Si no hay nada en el tipo que no sean sus campos, no habrá ninguna duda sobre qué tipo de validación de datos realiza (ninguna, obviamente), si representa una vista capturada o en vivo (una estructura de campo abierto no puede actuar como una vista en vivo), etc. Las clases inmutables son menos convenientes para trabajar y solo ofrecen un beneficio de rendimiento si las instancias se pueden pasar varias veces.
supercat
0

No existe una "mejor práctica". Es con lo que se siente cómodo y lo que funciona mejor en su situación. Siempre que sea coherente con esto, no hay ningún problema con ninguna de las soluciones que ha publicado.

astuto
fuente
3
Por supuesto que todos funcionan. En caso de que no haya una ventaja técnica, todavía tengo curiosidad por saber qué es lo que más utilizan los expertos.
Xaqron