valid - summary returns c#
Acepta a los más débiles, devuelve a los más fuertes. ¿Pero por qué? (6)
"Más fuerte" es un término un tanto vago y podría significar al menos dos cosas:
- Devuelva la interfaz "Más derivadas", por ejemplo, devuelva
IList<T>
oList<T>
lugar deICollection<T>
oIEnumerable<T>
- Devuelve el rango de valores "Más restringido". Por ejemplo, devuelva números en el rango 10..20 en lugar de en el rango 5..25
Tomaré el rumbo contrario y sugeriré que quizás no siempre sigan ese consejo, al menos para el primer punto.
Consideremos el caso de Linq. Imagine si la interfaz de Linq siguió ese consejo y devolvió la List<T>
todas partes en lugar de IEnumerable<T>
. Claramente, eso sería inviable.
Para un ejemplo específico, considere Enumerable<T>.Range(int start, int count)
. Esto podría devolver una List<T>
, pero eso sería muy ineficiente en la mayoría de los casos.
También imagine que desea devolver una secuencia de elementos de una clase que no desea que modifique la persona que llama. Debe devolver una ReadOnlyCollection<T>
lugar de una List<T>
. De hecho, en algunos de esos casos, creo que debería devolver un IEnumerable<T>
.
Contratos de código y métodos de reemplazo en clases derivadas
Hay una cosa diferente a considerar cuando se implementan métodos reemplazados en clases derivadas.
Se le permite debilitar una condición previa, ya que esto garantizará que todas las personas que llaman de la versión de clase base continúen funcionando. No se podía tener para que una clase derivada fuera más estricta con respecto a los valores y / o tipos pasados a un método (en este caso, el compilador no permitiría que se pasaran tipos "más débiles" (es decir, menos derivados), pero no tendría control sobre el rango permitido de valores que podrían pasarse en).
Del mismo modo, se le permite fortalecer una condición posterior. Esto puede aplicarse tanto a los tipos como a los rangos de salida. Por ejemplo, si el método de clase base devuelve un IEnumerable<T>
, podría devolver un IList<T>
(aunque, por supuesto, se devolvería como un IEnumerable<T>
).
Y si la clase base dice que los valores devueltos deben estar en el rango 0..100, usted podría devolver valores en el rango 40..60 sin romper el contrato de código.
Preguntándome una y otra vez cuál es el buen código, leí el consejo "Acepte a los más débiles, devuelva los más fuertes".
Para mí es obvio por qué aceptar a los más débiles: el método declara el contrato más débil posible con su cliente. Por lo tanto, los clientes no necesitan estar "especializados" frente a una interfaz muy "fuerte".
"Devolver el más fuerte" no es tan claro para mí. ¿Por qué debería devolver la interfaz más fuerte posible? ¿Cuál es la interfaz más fuerte? ¿Cómo se cuantifica la fuerza?
Supongamos que hay un método que devuelve una secuencia de elementos. La interfaz más débil sería de tipo IEnumerable. Siguiendo las pautas deberíamos devolver algo así como IList. ¿Pero por qué?
Me gustaría pedir una explicación de por qué devolver la interfaz más fuerte.
Bueno, en mi opinión, cuando el valor de retorno está fuertemente definido, es mucho más fácil para el usuario saber qué hacer con él, sin la necesidad de documentación.
Solo observe los ejemplos de java-script donde el valor de retorno es un objeto. NO PUEDES trabajar con él sin usar algún tipo de documentación para el valor. (O correr código para ver lo que significa).
Espero que esto ayude..
Concepto interesante - uno que nunca he escuchado explícitamente antes. Es básicamente la Ley de Postel "sea conservador en lo que envía, liberal en lo que recibe", que generalmente se aplica en las comunicaciones entre sistemas.
IList
es "más fuerte" porque ofrece más garantías que IEnumerable
. Un IList
admite la adición, eliminación, enumeración, etc., mientras que IEnumerable
solo admite la enumeración. Por supuesto, List
es aún más fuerte, siendo un tipo concreto.
En cuanto a la sabiduría de los consejos en sí, creo que generalmente es una buena idea dentro de una aplicación, con algunas advertencias serias para los diseñadores de API.
En el lado positivo, devolver el tipo "más fuerte" permite a la persona que llama realizar todas las operaciones posibles sin romper el contrato (por ejemplo, mediante el lanzamiento) o haciendo trabajo adicional (por ejemplo, copiando el IEnumerable
en una List
). Por lo general, es una ayuda en una aplicación y, dado que está dentro de una sola aplicación, cualquier cambio importante se detectará en el momento de la compilación y se podrá corregir fácilmente.
Sin embargo , para las API que deben preocuparse por la compatibilidad con versiones anteriores (por ejemplo, utilizadas por más de 1 aplicación o implementadas de forma independiente), el tipo de devolución es parte del contrato. En ese caso, realmente debe sopesar la importancia de respaldar para siempre el retorno de IList
lugar de un IEnumerable
más IEnumerable
. Los detalles allí deben ser dictados por la intención de la API, no por adherirse a esta regla.
FWIW, mi declaración personal sería algo así como "devolver lo que es útil para la persona que llama, y no más". Veo esto como un atajo para decidir "lo que es útil para la persona que llama" en el contexto de una sola aplicación, pero de manera activa es perjudicial en una API externa.
Esta regla tiene varias formas, aquí hay otra:
- La entrada debe ser del tipo más general / genérico (no en el modo .NET / C #)
- La salida debe ser el tipo más específico.
El razonamiento detrás de esto es hacerlo:
- Fácil de tomar en todo tipo de tipos que admiten la interfaz general
- Deje en claro qué tipo de estructura de datos se está devolviendo realmente, lo que le da a la persona que llama más flexibilidad y quizás incluso más rendimiento
Tomemos un ejemplo.
Supongamos que desea crear un método que tome una colección de enteros, los procese y luego devuelva una nueva colección de enteros.
Una implementación perfectamente buena de esto podría ser:
public IEnumerable<int> Process(IEnumerable<int> input) { ... }
Tenga en cuenta que no necesariamente he seguido el segundo punto aquí, pero depende de lo que haga el método. Si el método procesa un elemento a la vez, la declaración anterior es bastante buena, indica que no necesariamente está recuperando una lista, o una matriz, o lo que sea, simplemente "una colección".
Sin embargo, supongamos que el método debe reunir todos los valores primero, luego procesarlos y luego devolverlos. Una mejor declaración en este caso podría ser esta:
public List<int> Process(IEnumerable<int> input) { ... }
¿Por qué es esto mejor? Bueno, en primer lugar, muy a menudo, la persona que llama quiere recopilar todos los valores de todos modos, y ahora no tiene que hacerlo, ya vuelve como una colección única que contiene todos los valores. Además, si la persona que llama solo desea continuar utilizando los valores como un IEnumerable<int>
, también es posible.
El propósito de la regla es aumentar la flexibilidad y, en algunos casos, el rendimiento (memoria o velocidad).
Tenga en cuenta que la regla no significa que siempre debe esforzarse por devolver una lista o una matriz o algo así. En su lugar, debe esforzarse por devolver el tipo "más funcional" posible de la estructura de datos que ya ha creado. También tenga en cuenta que, por supuesto, nunca debe devolver referencias a estructuras de datos internas que continuarán viviendo más allá de la llamada al método, en lugar de eso, devuelva una copia.
No creo que devolver el tipo "más fuerte" (o más bien, el más específico o derivado) sea necesariamente una buena idea, porque si su interfaz o contrato especifica que devuelve la List<T>
se quedará atascado si desea cambiar eso para la Collection<T>
en el futuro, porque ambas son implementaciones distintas de IList<T>
sin un ancestro común. Tendrías que modificar la interfaz.
El mantra que has citado parece muy dogmático: siempre hay excepciones y solo digo que deberías seguir con lo que es apropiado para un trabajo determinado. Suena como una bastardización del principio de programación de red / IO "Sé liberal en lo que aceptas como entrada, pero estricto en lo que obtienes", pero la programación de red / IO es muy diferente en comparación con la definición de los tipos de interfaz o implementación de OO porque Una ABI es una bestia muy diferente en comparación con una especificación legible para el ser humano (por ejemplo, el RFC para HTTP).
Supongo que es porque la persona que llama puede querer las características de la interfaz más fuerte (como, por ejemplo, la capacidad de indexación de IList) y devolver las garantías más sólidas. Si el cliente no quiere estas características, no se lo impiden.