¿Especificar nombres de parámetros opcionales aunque no sean obligatorios?

25

Considere el siguiente método:

public List<Guid> ReturnEmployeeIds(bool includeManagement = false)
{

}

Y la siguiente llamada:

var ids = ReturnEmployeeIds(true);

Para un desarrollador nuevo en el sistema, sería bastante difícil adivinar lo que truehizo. Lo primero que debe hacer es pasar el mouse sobre el nombre del método o ir a la definición (ninguna de las cuales es una gran tarea en lo más mínimo). Pero, por razones de legibilidad, ¿tiene sentido escribir:

var ids = ReturnEmployeeIds(includeManagement: true);

¿Hay algún lugar que, formalmente, discuta si especificar explícitamente parámetros opcionales cuando el compilador no lo necesita?

El siguiente artículo analiza ciertas convenciones de codificación: https://msdn.microsoft.com/en-gb/library/ff926074.aspx

Algo similar al artículo anterior sería genial.

JᴀʏMᴇᴇ
fuente
2
Estas son dos preguntas no relacionadas: (1) ¿Debería escribir argumentos opcionales para mayor claridad? (2) ¿Debería uno usar booleanargumentos de método, y cómo?
Kilian Foth
55
Todo esto se reduce a preferencias personales / estilo / opinión. Personalmente, me gustan los nombres de parámetros cuando el valor pasado es literal (una variable ya puede transmitir suficiente información a través de su nombre). Pero esa es mi opinión personal.
MetaFight
1
¿quizás incluir ese enlace como fuente de ejemplo en su pregunta?
MetaFight
44
Si agrega algo, lo ejecuté a través de StyleCop y ReSharper, ninguno de los cuales tenía una visión clara. ReSharper simplemente dice que el parámetro con nombre podría eliminarse y luego continúa diciendo que podría agregarse un parámetro con nombre . El asesoramiento es gratuito por una razón, supongo ...: - /
Robbie Dee

Respuestas:

28

Yo diría que en el mundo de C #, una enumeración sería una de las mejores opciones aquí.

Con eso, se vería obligado a explicar lo que está haciendo y cualquier usuario vería lo que está sucediendo. También es seguro para futuras extensiones;

public enum WhatToInclude
{
    IncludeAll,
    ExcludeManagement
}

var ids = ReturnEmployeeIds(WhatToInclude.ExcludeManagement);

Entonces, en mi opinión aquí:

enum> opcional enum> opcional bool

Editar: debido a una discusión con LeopardSkinPillBoxHat a continuación con respecto a una [Flags]enumeración que en este caso específico podría ser una buena opción (ya que estamos hablando específicamente de incluir / excluir cosas), en su lugar propongo usarlo ISet<WhatToInclude>como parámetro.

Es un concepto más "moderno" con varias ventajas, principalmente debido al hecho de que encaja en la familia de colecciones LINQ, pero que también [Flags]tiene un máximo de 32 grupos. La desventaja principal de usar ISet<WhatToInclude>es cuán mal se admiten los conjuntos en la sintaxis de C #:

var ids = ReturnEmployeeIds(
    new HashSet<WhatToInclude>(new []{ 
        WhatToInclude.Cleaners, 
        WhatToInclude.Managers});

Parte de esto podría ser mitigado por funciones auxiliares, tanto para generar el conjunto como para crear "conjuntos de atajos" como

var ids = ReturnEmployeeIds(WhatToIncludeHelper.IncludeAll)
NiklasJ
fuente
Tiendo a estar de acuerdo; y generalmente he usado el uso de enumeraciones en mi nuevo código si hay alguna posibilidad vaga de que las condiciones se expandan en el futuro. Reemplazar un bool con una enumeración cuando algo se convierte en un tri-estado termina creando diferencias realmente ruidosas y haciendo que culpar al código sea mucho más tedioso; cuando terminas teniendo que actualizar tu código nuevamente un año más adelante para una tercera opción.
Dan Neely
3
Me gusta el enumenfoque, pero no soy un gran fanático de la mezcla Includey, Excludeal mismo enumtiempo, es más difícil comprender lo que está sucediendo. Si desea que esto sea realmente extensible, puede convertirlo en un [Flags] enum, hacerlos todos IncludeXxxy proporcionar un valor IncludeAllestablecido en Int32.MaxValue. Luego puede proporcionar 2 ReturnEmployeeIdssobrecargas, una que devuelve a todos los empleados y otra que toma un WhatToIncludeparámetro de filtro que puede ser una combinación de indicadores de enumeración.
LeopardSkinPillBoxHat
@LeopardSkinPillBoxHat Ok, estoy de acuerdo con la exclusión / inclusión. Así que este es un poco de ejemplo artificial, pero podría haberlo llamado IncludeAllButManagement. En su otro punto, no estoy de acuerdo: creo que [Flags]es una reliquia y solo debe usarse por motivos heredados o de rendimiento. Creo que a ISet<WhatToInclude>es un concepto mucho mejor (desafortunadamente C # carece de algo de azúcar sintáctico para que sea agradable de usar).
NiklasJ
@NiklasJ: me gusta el Flagsenfoque porque permite que la persona que llama pase WhatToInclude.Managers | WhatToInclude.Cleanersy el bit a bit o expresa exactamente cómo la persona que llama lo procesará (es decir, gerentes o limpiadores). Azúcar sintáctico intuitivo :-)
LeopardSkinPillBoxHat
@LeopardSkinPillBoxHat Exactamente, pero esa es la única ventaja de ese método. Las desventajas incluyen, por ejemplo, un número limitado de opciones y no ser compatible con otros conceptos similares a linq.
NiklasJ
20

¿Tiene sentido escribir:

 var ids=ReturnEmployeeIds(includeManagement: true);

Es discutible si este es un "buen estilo", pero en mi humilde opinión, este no es un mal estilo y es útil, siempre y cuando la línea de código no sea "demasiado larga". Sin parámetros con nombre, también es un estilo común introducir una variable explicativa:

 bool includeManagement=true;
 var ids=ReturnEmployeeIds(includeManagement);

Ese estilo es probablemente más común entre los programadores de C # que la variante de parámetro con nombre, ya que los parámetros con nombre no formaban parte del lenguaje anterior a la Versión 4.0. El efecto sobre la legibilidad es casi el mismo, solo necesita una línea de código adicional.

Entonces, mi recomendación: si cree que usar una enumeración o dos funciones con nombres diferentes es "excesivo", o si no desea o no puede cambiar la firma por un motivo diferente, continúe y use el parámetro con nombre.

Doc Brown
fuente
Es posible que desee tener en cuenta que está utilizando memoria adicional en el segundo ejemplo
Alvaro
10
@Alvaro Es muy poco probable que sea cierto en un caso tan trivial (donde la variable tiene un valor constante). Incluso si lo fuera, apenas vale la pena mencionarlo. La mayoría de nosotros no corremos con 4KB de RAM en estos días.
Chris Hayes
3
Como se trata de C #, puede marcar includeManagementcomo local consty evitar el uso de memoria adicional.
Jacob Krall
8
Chicos, no voy a agregar nada a mi respuesta que solo pueda distraerme de lo que creo que es importante aquí.
Doc Brown
17

Si encuentra código como:

public List<Guid> ReturnEmployeeIds(bool includeManagement = false)
{

}

puedes garantizar que lo que hay dentro {}será algo así como:

public List<Guid> ReturnEmployeeIds(bool includeManagement = false)
{
    if (includeManagement)
        return something
    else
        return something else
}

En otras palabras, el booleano se usa para elegir entre dos cursos de acción: el método tiene dos responsabilidades. Esto está prácticamente garantizado un olor a código . Siempre debemos esforzarnos por garantizar que los métodos solo hagan una cosa; solo tienen una responsabilidad. Por lo tanto, argumentaría que ambas soluciones son el enfoque equivocado. El uso de parámetros opcionales es una señal de que el método está haciendo más de una cosa. Los parámetros booleanos también son un signo de esto. Tener ambos definitivamente debería hacer sonar las alarmas. Por lo tanto, lo que debe hacer es refactorizar los dos conjuntos diferentes de funcionalidad en dos métodos separados. Dé nombres a los métodos que aclaren lo que hacen, por ejemplo:

public List<Guid> GetNonManagementEmployeeIds()
{

}

public List<Guid> GetAllEmployeeIds()
{

}

Esto mejora la legibilidad del código y garantiza que siga mejor los principios de SOLID.

EDITAR Como se ha señalado en los comentarios, el caso aquí es que estos dos métodos probablemente tendrán una funcionalidad compartida, y esa funcionalidad compartida probablemente estará mejor envuelta en una función privada que necesitará un parámetro booleano para controlar algún aspecto de lo que hace. .

En este caso, ¿se debe proporcionar el nombre del parámetro, aunque el compilador no lo necesite? Sugeriría que no hay una respuesta simple a eso y que se necesita un juicio caso por caso:

  • El argumento para especificar el nombre es que otros desarrolladores (incluido usted dentro de seis meses) deberían ser la audiencia principal de su código. El compilador es definitivamente una audiencia secundaria. Así que siempre escriba código para que sea más fácil de leer para otras personas; en lugar de simplemente proporcionar lo que necesita el compilador. Un ejemplo de cómo usamos esta regla todos los días es que no llenamos nuestro código con las variables _1, _2, etc., usamos nombres significativos (que no significan nada para el compilador).
  • El argumento contrario es que los métodos privados son detalles de implementación. Uno podría esperar razonablemente que cualquiera que mire un método como el siguiente, rastree el código para comprender el propósito del parámetro booleano, ya que están dentro de las partes internas del código:
public List<Guid> GetNonManagementEmployeeIds()
{
    return ReturnEmployeeIds(true);
}

¿El archivo fuente y los métodos son pequeños y fáciles de entender? En caso afirmativo, entonces el parámetro nombrado probablemente equivale a ruido. Si no, puede ser muy útil. Entonces aplique la regla de acuerdo a las circunstancias.

David Arno
fuente
12
Escucho lo que dices, pero has perdido el punto de lo que te estoy preguntando. Suponiendo un escenario en el que es más apropiado usar parámetros opcionales (estos escenarios existen), ¿está bien nombrar los parámetros aunque sea innecesario?
JᴀʏMᴇᴇ
44
Todo es cierto, pero la pregunta era sobre una llamada a tal método. Además, un parámetro de bandera no garantiza una rama dentro del método en absoluto. Simplemente podría pasar a otra capa.
Robbie Dee
Esto no está mal, pero simplifica demasiado. Encontrará public List<Guid> ReturnEmployeeIds(bool includeManagement) { calculateSomethingHereForBothBranches; if (includeManagement) return something else return something else }que en el código real, dividirlo simplemente en dos métodos probablemente significará que esos dos métodos necesitarán el resultado del primer cálculo como parámetro.
Doc Brown
44
Creo que lo que todos decimos es que la cara pública de su clase probablemente debería exponer dos métodos (cada uno con una responsabilidad) pero, internamente, cada uno de esos métodos puede reenviar razonablemente la solicitud a un único método privado con un parámetro bool de ramificación .
MetaFight el
1
Puedo imaginar 3 casos para el comportamiento que difiere entre las dos funciones. 1. Hay un preprocesamiento que es diferente. Haga que las funciones públicas procesen previamente los argumentos en la función privada. 2. Hay postprocesamiento que es diferente. Haga que las funciones públicas procesen el resultado de la función privada. 3. Hay un comportamiento intermedio que difiere. Esto se puede pasar como lambda a la función privada, suponiendo que su idioma lo admita. Muchas veces, esto conduce a una nueva abstracción reutilizable que realmente no se te había ocurrido antes.
jpmc26
1

Para un desarrollador nuevo en el sistema, sería bastante difícil adivinar lo que hizo. Lo primero que debe hacer es pasar el mouse sobre el nombre del método o ir a la definición (ninguna de las cuales es una gran tarea en lo más mínimo)

Si verdad. El código autodocumentado es algo hermoso. Como han señalado otras respuestas, hay formas de eliminar la ambigüedad de la llamada al ReturnEmployeeIdsusar una enumeración, diferentes nombres de métodos, etc. Sin embargo, a veces no se puede evitar el caso, pero no desea ir y usar ellos en todas partes (a menos que te guste la verbosidad de visual basic).

Por ejemplo, puede ayudar a aclarar una llamada pero no necesariamente otra.

Un argumento con nombre puede ser útil aquí:

var ids = ReturnEmployeeIds(includeManagement: true);

No agrega mucha claridad adicional (voy a adivinar que esto está analizando una enumeración):

Enum.Parse(typeof(StringComparison), "Ordinal", ignoreCase: true);

En realidad, puede reducir la claridad (si una persona no entiende qué capacidad tiene una lista):

var employeeIds = new List<int>(capacity: 24);

O donde no importa porque estás usando buenos nombres de variables:

bool includeManagement = true;
var ids = ReturnEmployeeIds(includeManagement);

¿Hay algún lugar que, formalmente, discuta si especificar explícitamente parámetros opcionales cuando el compilador no lo necesita?

No tengo miedo. Sin embargo, abordemos ambas partes: la única vez que necesita usar explícitamente parámetros con nombre es porque el compilador requiere que lo haga (generalmente es para eliminar la ambigüedad de la declaración). Aparte de eso, puede usarlos cuando lo desee. Este artículo de MS tiene algunas sugerencias sobre cuándo puede usarlas, pero no es particularmente definitivo https://msdn.microsoft.com/en-us/library/dd264739.aspx .

Personalmente, creo que los uso con mayor frecuencia cuando estoy creando atributos personalizados o creando un nuevo método donde mis parámetros pueden cambiar o reordenarse (los atributos con nombre b / c se pueden enumerar en cualquier orden). Además, solo los uso cuando proporciono un valor estático en línea, de lo contrario, si paso una variable, trato de usar buenos nombres de variables.

TL; DR

En última instancia, se trata de preferencias personales. Por supuesto, hay algunos casos de uso en los que tiene que usar parámetros con nombre, pero después de eso debe hacer una llamada de juicio. OMI: utilícelo en cualquier lugar que crea que ayudará a documentar su código, reducirá la ambigüedad o le permitirá proteger las firmas de métodos.


fuente
0

Si el parámetro es obvio por el nombre (completo) del método y su existencia es natural para lo que se supone que el método para hacerlo, debe no utiliza la sintaxis de parámetro con nombre. Por ejemplo, ReturnEmployeeName(57)es bastante obvio que 57es la identificación del empleado, por lo que es redundante anotarlo con la sintaxis del parámetro con nombre.

Con ReturnEmployeeIds(true), como dijiste, a menos que mires la declaración / documentación de la función, es imposible saber qué truesignifica, por lo que debes usar la sintaxis del parámetro con nombre.

Ahora, considere este tercer caso:

void PrintEmployeeNames(bool includeManagement) {
    foreach (id; ReturnEmployeeIds(includeManagement)) {
        Console.WriteLine(ReturnEmployeeName(id));
    }
}

La sintaxis del parámetro con nombre no se usa aquí, pero dado que le estamos pasando una variable ReturnEmployeeIds, y esa variable tiene un nombre, y ese nombre significa lo mismo que el parámetro al que se lo estamos pasando (a menudo es el caso, pero ¡no siempre!): no necesitamos la sintaxis del parámetro con nombre para comprender su significado del código.

Por lo tanto, la regla es simple: si puede comprender fácilmente con solo la llamada al método lo que se supone que significa el parámetro, no use el parámetro con nombre. Si no puede, eso es una deficiencia en la legibilidad del código, y probablemente quiera arreglarlos (no a cualquier precio, por supuesto, pero generalmente quiere que el código sea legible). La corrección no tiene que nombrarse parámetros: también puede poner el valor en una variable primero, o usar los métodos sugeridos en las otras respuestas, siempre que el resultado final facilite la comprensión de lo que hace el parámetro.

Idan Arye
fuente
77
No estoy de acuerdo, eso ReturnEmployeeName(57)hace que sea ​​inmediatamente obvio que esa 57es la identificación. GetEmployeeById(57)por otro lado ...
Hugo Zink
@HugoZink Inicialmente quería usar algo así para el ejemplo, pero lo cambié intencionalmente ReturnEmployeeNamepara demostrar casos en los que, incluso sin mencionar explícitamente el parámetro en el nombre del método, es bastante razonable esperar que la gente se dé cuenta de lo que es. En cualquier caso, es digno de mención que ReturnEmployeeName, a diferencia , no se puede renombrar razonablemente ReturnEmployeeIdsalgo que indique el significado detrás de los parámetros.
Idan Arye
1
En cualquier caso, es bastante improbable que escribamos ReturnEmployeeName (57) en un programa real. Es mucho más probable que escribamos ReturnEmployeeName (employeeId). ¿Por qué codificaríamos una identificación de empleado? A menos que sea la identificación del programador y haya algún tipo de fraude, como agregar un poco al sueldo de este empleado. :-)
Jay
0

Sí, creo que es buen estilo.

Esto tiene más sentido para las constantes booleanas y enteras.

Si está pasando una variable, el nombre de la variable debe aclarar el significado. Si no es así, debe darle un mejor nombre a la variable.

Si está pasando una cadena, el significado a menudo es claro. Al igual que si veo una llamada a GetFooData ("Oregon"), creo que un lector puede adivinar que el parámetro es un nombre de estado.

No todas las cadenas son obvias, por supuesto. Si veo GetFooData ("M"), a menos que conozca la función, no tengo idea de lo que significa "M". Si se trata de algún tipo de código, como M = "empleados de nivel gerencial", entonces probablemente deberíamos tener una enumeración en lugar de un literal, y el nombre de la enumeración debería estar claro.

Y supongo que una cadena podría ser engañosa. Tal vez "Oregon" en mi ejemplo anterior se refiere, no al estado, sino a un producto o un cliente o lo que sea.

Pero GetFooData (verdadero) o GetFooData (42) ... eso podría significar cualquier cosa. Los parámetros con nombre son una forma maravillosa de hacer que se documente automáticamente. Los he usado exactamente para ese propósito en varias ocasiones.

Arrendajo
fuente
0

No hay razones súper claras de por qué usar este tipo de sintaxis en primer lugar.

En general, trate de evitar tener argumentos booleanos que a distancia puedan parecer arbitrarios. includeManagement(muy probablemente) afectará en gran medida el resultado. Pero el argumento parece que tiene "poco peso".

Se ha discutido el uso de una enumeración, no solo parecerá que el argumento "tiene más peso", sino que también proporcionará escalabilidad al método. Sin embargo, esta podría no ser la mejor solución en todos los casos, ya que su ReturnEmployeeIdsmétodo tendrá que escalar junto al WhatToInclude-enum (vea la respuesta de NiklasJ ). Esto podría darte un dolor de cabeza más tarde.

Considere esto: si escala el WhatToInclude -enum, pero no el ReturnEmployeeIds-metodo. Entonces podría arrojar un ArgumentOutOfRangeException(mejor de los casos) o devolver algo completamente no deseado ( nullo vacío List<Guid>). Lo que en algunos casos puede confundir al programador, especialmente si ReturnEmployeeIdsestá en una biblioteca de clases, donde el código fuente no está disponible.

Se supondrá que WhatToInclude.Traineesfuncionará si lo WhatToInclude.Allhace, ya que los alumnos son un "subconjunto" de todos.

Esto (por supuesto) depende de cómo ReturnEmployeeIds se implemente.


En los casos en que se pueda pasar un argumento booleano, intento dividirlo en dos métodos (o más, si es necesario), en su caso; uno podría extraerReturnAllEmployeeIds , ReturnManagementIdsy ReturnRegularEmployeeIds. Esto cubre todas las bases y se explica por sí mismo a quien lo implemente. Esto tampoco tendrá el problema de "escala" mencionado anteriormente.

Dado que solo hay dos resultados para algo que tiene un argumento booleano. Implementar dos métodos requiere muy poco esfuerzo adicional.

Menos código, rara vez es mejor.


Con esto dicho, no son algunos casos en los que expresamente se declara el argumento mejora la legibilidad. Considerar por ejemplo. GetLatestNews(max: 10). GetLatestNews(10)es todavía bastante auto-explicativo, la declaración explícita de maxayuda aclarará cualquier confusión.

Y si absolutamente debe tener un argumento booleano, qué uso no puede inferirse simplemente leyendo trueo false. Entonces diría que:

var ids = ReturnEmployeeIds(includeManagement: true);

.. es absolutamente mejor y más legible que:

var ids = ReturnEmployeeIds(true);

Como en el segundo ejemplo, truepodría significar absolutamente cualquier cosa . Pero trataría de evitar este argumento.

die maus
fuente