¿Cuál es la forma más elegante de escribir un método de "Prueba" en C # 7?

21

Estoy escribiendo un tipo de implementación de cola que tiene un TryDequeuemétodo que usa un patrón similar a varios TryParsemétodos .NET , donde devuelvo un valor booleano si la acción tuvo éxito, y uso un outparámetro para devolver el valor en cola real.

public bool TryDequeue(out Message message) => _innerQueue.TryDequeue(out message);

Ahora, me gusta evitar los outparams siempre que puedo. C # 7 nos brinda delicaciones variables para facilitar el trabajo con ellas, pero sigo considerando los parámetros más un mal necesario que una herramienta útil.

El comportamiento que quiero de este método es el siguiente:

  • Si hay un artículo que quitar, devuélvalo.
  • Si no hay elementos que retirar (la cola está vacía), proporcione a la persona que llama la información suficiente para actuar de manera adecuada.
  • No solo devuelva un elemento nulo si no quedan elementos.
  • No arroje una excepción si intenta salir de una cola vacía.

En este momento, una persona que llama de este método casi siempre usaría un patrón como el siguiente (usando la sintaxis variable C # 7):

if (myMessageQueue.TryDequeue(out Message dequeued))
    MyMessagingClass.SendMessage(dequeued)
else
    Console.WriteLine("No messages!"); // do other stuff

Lo cual no es lo peor, todo dicho. Pero no puedo evitar sentir que podría haber mejores formas de hacer esto (estoy totalmente dispuesto a admitir que tal vez no exista). Odio cómo la persona que llama tiene que romper su flujo con un condicional cuando todo lo que quiere es obtener un valor si existe.

¿Cuáles son algunos otros patrones que existen para lograr este mismo comportamiento de "intento"?

Para el contexto, este método puede llamarse potencialmente en proyectos VB, por lo tanto, puntos de bonificación por algo que funciona bien en ambos. Sin embargo, este hecho debería tener muy poco peso.

Eric Sondergard
fuente
99
Defina una Option<T>estructura y devuélvala. Esas bool Try(..., out data)funciones son una abominación.
CodesInChaos
2
Estaba pensando similar ... Tal vez <T>, Opción <T> si uno es adverso a los parámetros OUT.
Jon Raynor
1
@FrustratedWithFormsDesigner bien, no he "intentado" otras cosas (los juegos de palabras son bienvenidos), por mucho que haya pensado en ellos. Tuve la idea de devolver una ValueTuple pero, en el mejor de los casos, no creo que eso ofreciera muchas mejoras.
Eric Sondergard
@CodesInChaos Estamos en la misma página, ¡por eso estoy aquí! Y me gusta esta idea. Si tiene tiempo, ¿le gustaría dar más detalles en una respuesta para que pueda aceptarlo?
Eric Sondergard

Respuestas:

24

Utilice un tipo de Opción, es decir, un objeto de un tipo que tiene dos versiones, generalmente llamado "Algunos" (cuando hay un valor) o "Ninguno" (cuando no hay un valor) ... O de vez en cuando Se llaman Just and Nothing. Luego, hay funciones en estos tipos que le permiten acceder al valor si está presente, probar la presencia y, lo más importante, un método que le permite aplicar una función que devuelve una Opción adicional al valor si está presente (generalmente en C # esto debería se llamará FlatMap, aunque en otros idiomas a menudo se llama Bind en su lugar ... El nombre es crítico en C # porque tener un método s de este nombre y tipo le permite usar sus objetos Option en las declaraciones LINQ).

Las características adicionales pueden incluir métodos como IfPresent e IfNotPresent para invocar acciones en las condiciones relevantes, y OrElse (que sustituye un valor predeterminado cuando no hay ningún valor pero no es una opción), y así sucesivamente.

Su ejemplo podría verse así:

myMessageQueue.TryDeque()
    .IfPresent( dequeued => MyMessagingClass.SendMessage(dequeued))
    .IfNotPresent (() =>  Console.WriteLine("No messages!")

Este es el patrón de mónada Opción (o Quizás), y es extremadamente útil. Existen implementaciones existentes (por ejemplo, https://github.com/nlkl/Optional/blob/master/README.md ), pero tampoco es difícil para usted.

(Es posible que desee extender este patrón para que devuelva una descripción de la causa del error en lugar de nada cuando el método falla ... Esto es totalmente factible y a menudo se llama la mónada Either; como su nombre lo indica, puede usar el mismo FlatMap patrón para facilitar el trabajo en ese caso también)

Jules
fuente
Esta es definitivamente una buena opción, gracias por la sugerencia y sus pensamientos. Realmente estaba buscando explorar más ideas aquí.
Eric Sondergard
2
Lo bueno de Option/ Maybees que es una mónada (si se implementa como una, por supuesto), lo que significa que se puede encadenar, envolver, desenvolver y procesar de forma segura sin tener que manejar los diferentes casos directamente, y este encadenamiento y el procesamiento se puede hacer con expresiones de consulta LINQ.
Jörg W Mittag
3
Por cierto, flatMap/ bindse llama SelectManyen .NET.
Jörg W Mittag
55
Me pregunto si sería una mejor idea elegir un nombre diferente, ya que al hacer esto estás rompiendo Try...las convenciones de nomenclatura en .NET (y mantenedores potencialmente confusos).
Bob
@Bob Ese es un gran punto. Estoy de acuerdo.
Eric Sondergard
12

Las respuestas dadas son buenas y yo iría con una de ellas. Considere que esta respuesta es solo completar algunas esquinas con algunas ideas alternativas:

  • Hacer subclases de Messagellamadas SomeMessagey NoMessage. Dequeueahora puede regresar NoMessagesi no hay mensaje y SomeMessagesi hay un mensaje. Si la persona que llama se preocupa por detectar en qué caso se encuentra, puede hacerlo fácilmente mediante inspección de tipo. Si no lo hacen, bueno, simplemente NoMessageno hagan nada cada vez que se llame a alguno de sus métodos, y oigan, obtengan lo que pidieron.

  • Lo mismo que lo anterior, pero hace Messageque sea implícitamente convertible bool(o implemente operator true / operator false). Ahora puede decir if (message)y hacer que sea verdadero si el mensaje es bueno y falso si es malo. (¡Esta es casi una mala idea, pero la incluyo para completarla!)

  • C # 7 tiene tuplas. Devuelve una (bool, Message)tupla.

  • Haga Messageun tipo de estructura y regrese Message?.

Eric Lippert
fuente
Hola, gracias por tu respuesta. Con esta pregunta, intentaba exponerme a ideas diferentes tanto como intentaba encontrar una solución a mi problema específico. ¡Aclamaciones!
Eric Sondergard
Quiero decir, si Unity usa tu "mala idea", ¿por qué no podemos usarla?
Arturo Torres Sánchez
@ ArturoTorresSánchez: Su pregunta es "alguien más usó una práctica de programación realmente mala, entonces ¿por qué no puedo?" La pregunta es incoherente. Nadie te impide usar las mismas malas prácticas de programación que otra persona. Sigue adelante. Eso no lo convertirá en una buena práctica en C #.
Eric Lippert
Era la lengua en la mejilla. Supongo que necesito usar "/ s" para marcarlo.
Arturo Torres Sánchez
@ ArturoTorresSánchez: Este es un sitio de preguntas y respuestas; Considero que las preguntas son preguntas que buscan respuestas .
Eric Lippert
10

En C # 7, puede usar la coincidencia de patrones para lograr lo mismo de una manera más elegante:

if (myMessageQueue.TryDequeue() is Message dequeued) 
{
     MyMessagingClass.SendMessage(dequeued)
} 
else 
{
    Console.WriteLine("No messages!"); // do other stuff
}

Sin embargo, en este caso particular, probablemente usaría eventos en lugar de sondear la cola repetidamente.

JacquesB
fuente
3
Sin embargo, ¿eso no requiere TryDequeuedevolver alguna interfaz de marcador con una Messageimplementación y alguna implementación de "nada"? Claro, es más elegante en el sitio de llamadas, pero está atascado implementando 3 implementaciones en el código real, lo que en sí mismo puede ser imposible si Messagees un tipo existente.
Telastyn
1
@Telastyn: también funciona si solo devuelve nulo . También podría usar un tipo de opción.
JacquesB
@JacquesB: pero ¿y si nulo es un elemento válido que se puede poner en cola?
JBSnorro
5

No hay nada de malo con un método Try ... (que tiene un parámetro out) para un escenario en el que la falla (sin valor) es tan común como el éxito.

Pero, si insiste en hacer las cosas de manera diferente, es posible que desee devolver un mensaje vacío y enviarlo (ya no es su problema) o posponer la declaración if hasta que realmente deba saber si tiene un mensaje con contenido o no.

Tenga en cuenta que alguien tiene que hacer la prueba en algún momento de todos modos. Yo diría que la prueba debe hacerse cuando y donde la pregunta es actual. Esto es en el momento de la extracción.

Martin Maat
fuente
Usted hace un buen punto. Todavía tienes que probar, no importa lo que hagas. Honestamente, todavía puedo estar sin parámetros en este punto (de hecho, actualmente estoy exponiendo múltiples implementaciones de "TryDequeue" en este momento solo para jugar con cada una de ellas). Realmente solo quería discutir otras opciones que podrían estar disponibles.
Eric Sondergard