¿Es irrazonable esperar que Any () * no * arroje una excepción de referencia nula?

41

Cuando creas un método de extensión, puedes, por supuesto, llamarlo a null . Pero, a diferencia de una llamada al método de instancia, llamarlo a null no have para lanzar un NullReferenceException - > Tienes que comprobarlo y tirarlo manualmente.

Para la implementación del método de extensión Linq Any() , Microsoft decidió que deberían lanzar un ArgumentNullException ( enlace ).

Me molesta tener que escribir if( myCollection != null && myCollection.Any() )

Estoy equivocado, como cliente de este código, al esperar que, por ejemplo, ((int[])null).Any() debería devolver false ?

    
pregunta thisextendsthat 18.09.2018 - 17:09

13 respuestas

152

Tengo una bolsa con cinco papas en ella. ¿Hay .Any() papas en la bolsa?

"", dices. <= true

Saco todas las papas y las como. ¿Hay .Any() papas en la bolsa?

" No ", dices. <= false

Incinero completamente la bolsa en un incendio. ¿Hay .Any() papas en la bolsa ahora?

" No hay bolsa ." <= ArgumentNullException

    
respondido por el Dan Wilson 18.09.2018 - 18:39
52

En primer lugar, parece que el código fuente arrojará ArgumentNullException , no NullReferenceException .

Habiendo dicho eso, en muchos casos ya sabes que tu colección no es nula, porque a este código solo se llama desde el código que sabe que la colección ya existe, por lo que no tendrás que poner el cheque nulo muy a menudo. Pero si no sabe que existe, entonces tiene sentido verificar antes de llamar a Any() .

  

Estoy equivocado, como cliente de este código, al esperar que, por ejemplo, ((int[])null).Any() debería devolver false ?

Sí. La pregunta que responde Any() es "¿contiene esta colección algún elemento?" Si esta colección no existe, entonces la pregunta en sí no tiene sentido; no puede contener ni no contener nada, porque no existe.

    
respondido por el Mason Wheeler 18.09.2018 - 17:23
23

Nulo significa que falta información, no elementos.

Podría considerar evitar en general un valor nulo; por ejemplo, use uno de los enumerados vacíos incorporados para representar una colección sin elementos en lugar de nulos.

Si devuelve un valor nulo en algunas circunstancias, puede cambiarlo para devolver la colección vacía. (De lo contrario, si encuentra null devuelto por los métodos de la biblioteca (no los suyos), es desafortunado, y los ajustaría para normalizarlos).

Ver también enlace

    
respondido por el Erik Eidt 18.09.2018 - 17:59
13

Aparte de la sintaxis condicional nula , existe otra técnica para solucionar este problema: no permita tu variable siempre permanecerá como null .

Considere una función que acepte una colección como parámetro. Si a los efectos de la función, null y vacío son equivalentes, puede asegurarse de que nunca contenga null al principio:

public MyResult DoSomething(int a, IEnumerable<string> words)
{
    words = words ?? Enumerable.Empty<string>();

    if (!words.Any())
    {
        ...

Puedes hacer lo mismo cuando recuperas colecciones de algún otro método:

var words = GetWords() ?? Enumerable.Empty<string>();

(Tenga en cuenta que en los casos en que tenga control sobre una función como GetWords y null es equivalente a la colección vacía, es preferible devolver la colección vacía en primer lugar).

Ahora puede realizar cualquier operación que desee en la colección. Esto es especialmente útil si necesita realizar muchas operaciones que fallarían cuando la recopilación es null , y en los casos en que obtenga el mismo resultado al realizar un bucle o consultar a un enumerable vacío, permitirá eliminar por completo las condiciones de if .

    
respondido por el jpmc26 19.09.2018 - 05:32
12
  

Estoy equivocado, como cliente de este código, al esperar que, por ejemplo, ((int []) null). ¿Cualquier () debería devolver falso?

Sí, simplemente porque está en C # y ese comportamiento está bien definido y documentado.

Si estuviera creando su propia biblioteca, o si estuviera utilizando un idioma diferente con una cultura de excepción diferente, sería más razonable esperar que fuera falso.

Personalmente, siento que devolver falso es un enfoque más seguro que hace que su programa sea más sólido, pero al menos es discutible.

    
respondido por el Telastyn 18.09.2018 - 18:52
10

Si las repetidas comprobaciones nulas lo molestan, puede crear su propio método de extensión 'IsNullOrEmpty ()', para reflejar el método String con ese nombre, y envolver la comprobación nula y la llamada .Any () en una llamada única.

De lo contrario, la solución, mencionada por @ 17 de 26 en un comentario bajo su pregunta, también es más breve que el método 'estándar' y es bastante clara para cualquiera que esté familiarizado con la nueva sintaxis condicional nula.

if(myCollection?.Any() == true)
    
respondido por el Theo Brinkman 18.09.2018 - 19:59
8
  

Estoy equivocado, como cliente de este código, al esperar que, por ejemplo, ((int []) null). ¿Cualquier () debería devolver falso?

Si te preguntas sobre las expectativas, tienes que pensar en las intenciones.

null significa algo muy diferente de Enumerable.Empty<T>

Como se menciona en la respuesta de Erik Eidt , hay una diferencia de significado entre null y una colección vacía .

Primero echemos un vistazo a cómo se supone que deben usarse.

El libro < Em> Framework Design Guidelines: Convenciones, expresiones idiomáticas y patrones para bibliotecas .NET reutilizables, 2ª edición escrita por los arquitectos de Microsoft Krzysztof Cwalina y Brad Abrams declara las siguientes mejores prácticas:

  

X NO devuelve valores nulos de propiedades de colección o de métodos que devuelven colecciones. Devuelve una colección vacía o una matriz vacía en su lugar.

Considere que está llamando a un método que finalmente obtiene datos de una base de datos: si recibe una matriz vacía o Enumerable.Empty<T> , esto simplemente significa que su espacio de muestra está vacío, es decir, su resultado es un conjunto vacío . Sin embargo, recibir null en este contexto significaría un estado de error .

En la misma línea de pensamiento que analogía de la papa de Dan Wilson , tiene sentido hacer preguntas sobre sus datos incluso si se trata de un conjunto vacío . Pero tiene mucho menos sentido, si no hay ningún conjunto .

    
respondido por el Søren D. Ptæus 19.09.2018 - 10:08
3

Hay muchas respuestas que explican por qué null y vacío son diferentes y suficientes opiniones que intentan explicar por qué deben tratarse de manera diferente o no. Sin embargo, usted está preguntando:

  

Estoy equivocado, como cliente de este código, al esperar que, por ejemplo, ((int []) null). ¿Cualquier () debería devolver falso?

Es una expectativa perfectamente razonable . Tienes tanto derecho como alguien más que defiende el comportamiento actual. Estoy de acuerdo con la filosofía de implementación actual, pero los factores determinantes no se basan solo en consideraciones fuera de contexto.

Dado que Any() sin predicado es esencialmente Count() > 0 , ¿qué esperas de este fragmento?

List<int> list = null;
if (list.Count > 0) {}

O un genérico:

List<int> list = null;
if (list.Foo()) {}

Supongo que esperas NullReferenceException .

  • Any() es un método de extensión, debería integrarse sin problemas con el objeto extendido y luego lanzar una excepción es lo menos sorprendente.
  • No todos los lenguajes .NET admiten métodos de extensión.
  • Siempre puedes llamar a Enumerable.Any(null) y allí definitivamente esperas ArgumentNullException . Es el mismo método y tiene que ser coherente con, posiblemente, casi TODO lo demás en el Marco.
  • El acceso a un objeto null es un error de programación , el marco no debe imponer nulo como valor mágico . Si lo usas de esa manera, entonces es tu responsabilidad lidiar con eso.
  • Es de opinión , piensas de una manera y yo pienso de otra manera. El marco debe estar lo más desopinado posible.
  • Si tiene un caso especial, debe ser consistente : debe tomar decisiones cada vez más altamente calificadas sobre todos los demás métodos de extensión: si Count() parece una decisión fácil, entonces Where() no es. ¿Qué hay de Max() ? Lanza una excepción para una lista VACÍA, ¿no debería lanzarse también para un null uno?

Lo que hicieron los diseñadores de bibliotecas antes de LINQ fue introducir métodos explícitos cuando null es un valor válido (por ejemplo, String.IsNullOrEmpty() ), entonces TENÍAN que ser compatibles con la filosofía de diseño existente . Dicho esto, incluso si es bastante trivial de escribir, dos métodos EmptyIfNull() y EmptyOrNull() podrían ser útiles.

    
respondido por el Adriano Repetti 19.09.2018 - 12:43
1

Se supone que Jim debe dejar las papas en cada bolsa. De lo contrario, lo voy a matar.

Jim tiene una bolsa con cinco papas. ¿Hay .Any() papas en la bolsa?

"Sí", dices. < = true

Ok, Jim vive esta vez.

Jim saca todas las papas y se las come. ¿Hay .Any () papas en la bolsa?

"No", dices. < = falso

Es hora de matar a Jim.

Jim incinera completamente la bolsa en un incendio. ¿Hay .Any () papas en la bolsa ahora?

"No hay bolsa". < = ArgumentNullException

¿Jim debería vivir o morir? Bueno, no esperábamos esto, así que necesito una decisión. ¿Dejar que Jim se salga con la suya es un error o no?

Puede usar anotaciones para indicar que no está tolerando ningún chanchullo nulo de esta manera.

public bool Any( [NotNull] List bag ) 

Pero su cadena de herramientas tiene que soportarlo. Lo que significa que es probable que todavía termines escribiendo cheques.

    
respondido por el candied_orange 21.09.2018 - 01:52
0

Si esto te molesta tanto, sugiero un método de extensión simple.

static public IEnumerable<T> NullToEmpty<T>(this IEnumerable<T> source)
{
    return (source == null) ? Enumerable.Empty<T>() : source;
}

Ahora puedes hacer esto:

List<string> list = null;
var flag = list.NullToEmpty().Any( s => s == "Foo" );

... y la bandera se establecerá en false .

    
respondido por el John Wu 20.09.2018 - 04:52
0

Esta es una pregunta sobre los métodos de extensión de C # y su filosofía de diseño, por lo que creo que la mejor forma de responder esta pregunta es citar a Documentación de MSDN sobre el propósito de los métodos de extensión :

  

Los métodos de extensión le permiten "agregar" métodos a los tipos existentes sin crear un nuevo tipo derivado, recompilar o modificar el tipo original. Los métodos de extensión son un tipo especial de método estático, pero se llaman como si fueran métodos de instancia en el tipo extendido. Para el código de cliente escrito en C #, F # y Visual Basic, no hay una diferencia aparente entre llamar a un método de extensión y los métodos que realmente están definidos en un tipo.

     

...

     

En general, recomendamos que implemente métodos de extensión con moderación y solo cuando sea necesario. Siempre que sea posible, el código de cliente que debe extender un tipo existente debe hacerlo creando un nuevo tipo derivado del tipo existente. Para más información, ver Herencia.

     

Cuando usa un método de extensión para extender un tipo cuyo código fuente no puede cambiar, corre el riesgo de que un cambio en la implementación del tipo provoque que su método de extensión se rompa.

     

Si implementa métodos de extensión para un tipo determinado, recuerde los siguientes puntos:

     
  • Nunca se llamará a un método de extensión si tiene la misma firma que un método definido en el tipo.
  •   
  • Los métodos de extensión están dentro del alcance en el nivel del espacio de nombres. Por ejemplo, si tiene varias clases estáticas que contienen métodos de extensión en un único espacio de nombres llamado Extensions , la directiva using Extensions; las pondrá en alcance.
  •   

Para resumir, los métodos de extensión están diseñados para agregar métodos de instancia a un tipo particular, incluso cuando los desarrolladores no pueden hacerlo directamente. Y debido a que los métodos de instancia siempre anularán los métodos de extensión si están presentes (si se los llama utilizando la sintaxis del método de instancia), esto debería solo si no puede agregar un método directamente o ampliar la clase . *

En otras palabras, un método de extensión debe actuar como lo haría un método de instancia, ya que algún cliente puede convertirlo en un método de instancia. Y debido a que un método de instancia debería lanzarse si el objeto al que se está llamando es null , también debería hacerlo el método de extensión.

* Como nota al margen, esta es exactamente la situación que enfrentaron los diseñadores de LINQ: cuando se lanzó C # 3.0, ya había millones de clientes que usaban System.Collections.IEnumerable y System.Collections.Generic.IEnumerable<T> , tanto en sus colecciones como en foreach loops. Estas clases devolvieron IEnumerator objetos que solo tenían los dos métodos Current y MoveNext , por lo que agregar cualquier método de instancia adicional requerido, como Count , Any , etc., estaría rompiendo estos millones de clientes. Entonces, para proporcionar esta funcionalidad (especialmente porque se puede implementar en términos de Current y MoveNext con relativa facilidad), lo lanzaron como métodos de extensión, que se pueden aplicar a cualquier instancia existente de IEnumerable y También puede ser implementado por clases de manera más eficiente. Si los diseñadores de C # hubieran decidido lanzar LINQ el primer día, se les habría proporcionado como métodos de instancia de IEnumerable , y probablemente habrían diseñado algún tipo de sistema para proporcionar implementaciones de interfaz predeterminada de esos métodos.

    
respondido por el TheHansinator 21.09.2018 - 02:31
0

Creo que el punto más importante aquí es que al devolver falso en lugar de lanzar una excepción, está ocultando información que puede ser relevante para futuros lectores / modificadores de su código.

Si es posible que la lista sea nula, sugeriría tener una ruta lógica separada para eso, ya que de lo contrario, en el futuro, alguien puede agregar alguna lógica basada en listas (como un agregado) al else {} de su if, dando como resultado una excepción no deseada que no tenían motivos para predecir.

La legibilidad y la capacidad de mantenimiento triunfan 'Tengo que escribir una condición adicional' cada vez.

    
respondido por el Callum Bradbury 21.09.2018 - 08:28
0

Como regla general, escribo la mayor parte de mi código para asumir que la persona que llama es responsable de no entregarme datos nulos, principalmente por los motivos descritos en Say No to Null (y otras publicaciones similares). El concepto de nulo a menudo no se comprende bien, y por esa razón, es recomendable inicializar sus variables con anticipación, tanto como sea práctico. Suponiendo que está trabajando con una API razonable, lo ideal es que nunca recupere un valor nulo, por lo que nunca debe verificar si es nulo. El punto de nulo, como han indicado otras respuestas, es asegurarse de tener un objeto válido (por ejemplo, una "bolsa") con el que trabajar antes de continuar. Esta no es una característica de Any , sino una característica del idioma en sí. No puede realizar la mayoría de las operaciones en un objeto nulo, porque no existe. Es como si le pidiera que conduzca hasta la tienda en mi automóvil, excepto que no tengo un automóvil, por lo que no tiene forma de usar mi automóvil para conducir a la tienda. No tiene sentido realizar una operación en algo que literalmente no existe.

Hay casos prácticos para usar null, como cuando literalmente no tiene la información solicitada disponible (por ejemplo, si le pregunté cuál es mi edad, la opción más probable para que responda en este momento es "No "No sé", que es lo que es un valor nulo). Sin embargo, en la mayoría de los casos, al menos debe saber si sus variables están inicializadas o no. Y si no lo hace, le recomendaría que apriete su código. Lo primero que hago en cualquier programa o función es inicializar un valor antes de usarlo. Es raro que tenga que verificar los valores nulos, porque puedo garantizar que mis variables no son nulas. Es un buen hábito el ingresar y cuanto más frecuentemente recuerdes inicializar tus variables, menor es la frecuencia con la que debes verificar si hay un valor nulo.

    
respondido por el phyrfox 22.09.2018 - 23:12

Lea otras preguntas en las etiquetas