web-dev-qa-db-fra.com

Est-il déraisonnable de s'attendre à ce que Any () * not * lève une exception de référence nulle?

Lorsque vous créez une méthode d'extension, vous pouvez bien sûr l'appeler sur null. Mais, contrairement à un appel de méthode d'instance, l'appel sur null n'a pas pour lancer un NullReferenceException -> vous devez le vérifier et le lancer manuellement.

Pour l'implémentation de la méthode d'extension Linq Any() Microsoft a décidé de lancer un ArgumentNullException ( https://github.com/dotnet/corefx/blob/master/src /System.Linq/src/System/Linq/AnyAll.cs ).

Cela me contrarie d'avoir à écrire if( myCollection != null && myCollection.Any() )

Ai-je tort, en tant que client de ce code, de m'attendre à ce que par exemple ((int[])null).Any() devrait retourner false?

41
thisextendsthat

J'ai un sac avec cinq pommes de terre dedans. Y a-t-il .Any() pommes de terre dans le sac?

"Oui", dites-vous. <= true

Je sors toutes les pommes de terre et je les mange. Y a-t-il .Any() pommes de terre dans le sac?

"Non", dites-vous. <= false

J'incinère complètement le sac dans un feu. Y a-t-il .Any() pommes de terre dans le sac maintenant?

"Il n'y a pas de sac." <= ArgumentNullException

157
Dan Wilson

Tout d'abord, il semble que ce code source jettera ArgumentNullException, pas NullReferenceException.

Cela dit, dans de nombreux cas, vous savez déjà que votre collection n'est pas nulle, car ce code n'est appelé qu'à partir de code qui sait que la collection existe déjà, vous n'aurez donc pas à y mettre très souvent la vérification nulle. Mais si vous ne savez pas qu'elle existe, alors il est logique de vérifier avant d'appeler Any() dessus.

Ai-je tort, en tant que client de ce code, de m'attendre à ce que par exemple ((int[])null).Any() Devrait renvoyer false?

Oui. La question à laquelle Any() répond est "cette collection contient-elle des éléments?" Si cette collection n'existe pas, alors la question elle-même est absurde; il ne peut ni contenir ni ne rien contenir, car il n'existe pas.

53
Mason Wheeler

Null signifie des informations manquantes, pas aucun élément.

Vous pouvez envisager plus largement d'éviter null - par exemple, utilisez l'un des énumérateurs vides intégrés pour représenter une collection sans éléments au lieu de null.

Si vous retournez null dans certaines circonstances, vous pouvez le modifier pour renvoyer la collection vide. (Sinon, si vous trouvez des valeurs nulles retournées par des méthodes de bibliothèque (pas les vôtres), c'est malheureux, et je les envelopperais pour normaliser.)

Voir aussi https://stackoverflow.com/questions/1191919/what-does-linq-return-when-the-results-are-empty

22
Erik Eidt

Mis à part la syntaxe null-conditionnelle , il existe une autre technique pour atténuer ce problème: ne laissez jamais votre variable rester null.

Prenons une fonction qui accepte une collection comme paramètre. Si pour les besoins de la fonction, null et vide sont équivalents, vous pouvez vous assurer qu'elle ne contient jamais null au début:

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

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

Vous pouvez faire de même lorsque vous récupérez des collections à partir d'une autre méthode:

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

(Notez que dans les cas où vous avez le contrôle sur une fonction comme GetWords et null est équivalente à la collection vide, il est préférable de simplement renvoyer la collection vide en premier lieu.)

Vous pouvez maintenant effectuer n'importe quelle opération de votre choix sur la collection. Cela est particulièrement utile si vous devez effectuer de nombreuses opérations qui échoueraient lorsque la collection est null, et dans les cas où vous obtenez le même résultat en bouclant ou en interrogeant un énumérable vide, cela permettra d'éliminer if conditions entièrement.

13
jpmc26

Ai-je tort, en tant que client de ce code, de m'attendre à ce que par exemple ((int []) null) .Any () devrait retourner false?

Oui, tout simplement parce que vous êtes en C # et que ce comportement est bien défini et documenté.

Si vous créez votre propre bibliothèque, ou si vous utilisez une langue différente avec une culture d'exception différente, il serait plus raisonnable de s'attendre à une erreur.

Personnellement, j'ai l'impression que return false est une approche plus sûre qui rend votre programme plus robuste, mais c'est discutable au moins.

12
Telastyn

Si les vérifications nulles répétées vous ennuient, vous pouvez créer votre propre méthode d'extension 'IsNullOrEmpty ()', pour refléter la méthode String de ce nom, et encapsuler à la fois la vérification nulle et l'appel .Any () dans un seul appel.

Sinon, la solution, mentionnée par @ 17 sur 26 dans un commentaire sous votre question, est également plus courte que la méthode `` standard '' et raisonnablement claire pour toute personne familiarisée avec la nouvelle syntaxe à condition nulle.

if(myCollection?.Any() == true)
11
Theo Brinkman

Ai-je tort, en tant que client de ce code, de m'attendre à ce que par exemple ((int []) null) .Any () devrait retourner false?

Si vous vous interrogez sur les attentes, vous devez penser aux intentions.

null signifie quelque chose de très différent de Enumerable.Empty<T>

Comme mentionné dans réponse d'Erik Eidt , il y a une différence de sens entre null et une collection vide.

Voyons d'abord comment ils sont censés être utilisés.

Le livre Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, 2nd Edition écrit par les architectes Microsoft Krzysztof Cwalina = et Brad Abrams indique la meilleure pratique suivante:

X NE retournez PAS de valeurs nulles à partir des propriétés de collection ou des méthodes renvoyant des collections. Renvoyez une collection vide ou un tableau vide à la place.

Considérez votre appel comme une méthode qui obtient finalement des données d'une base de données: Si vous recevez un tableau vide ou Enumerable.Empty<T> cela signifie simplement que votre espace échantillon est vide, c'est-à-dire que votre résultat est un ensemble vide. La réception de null dans ce contexte, cependant, signifierait un état d'erreur .

Dans la même ligne de pensée que analogie de la pomme de terre de Dan Wilson , il est logique de poser des questions sur vos données même s'il s'agit d'un ensemble vide. Mais cela fait beaucoup moins logique, si il n'y a pas de set .

8
Søren D. Ptæus

Il existe de nombreuses réponses expliquant pourquoi null et empty sont différents et suffisamment d'opinions essayant d'expliquer à la fois pourquoi elles doivent être traitées différemment ou non. Cependant, vous demandez:

Ai-je tort, en tant que client de ce code, de m'attendre à ce que par exemple ((int []) null) .Any () devrait retourner false?

C'est une attente parfaitement raisonnable. Vous avez autant raison que quelqu'un d'autre pour défendre le comportement actuel. Je suis d'accord avec la philosophie de mise en œuvre actuelle, mais les facteurs moteurs ne sont pas - uniquement - basés sur des considérations hors contexte.

Étant donné que Any() sans prédicat est essentiellement Count() > 0 alors qu'attendez-vous de cet extrait?

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

Ou un générique:

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

Je suppose que vous vous attendez à NullReferenceException.

  • Any() est une méthode d'extension, elle devrait s'intégrer en douceur avec l'objet étendu puis lancer une exception est la chose la moins surprenante.
  • Tous les langages .NET ne prennent pas en charge les méthodes d'extension.
  • Vous pouvez toujours appeler Enumerable.Any(null) et vous vous attendez certainement ArgumentNullException. C'est la même méthode et elle doit être compatible avec - peut-être - presque TOUT autrement dans le Cadre.
  • L'accès à un objet null est un erreur de programmation, le framework ne doit pas appliquer null comme valeur magique . Si vous l'utilisez de cette façon, il est de votre responsabilité de le gérer.
  • C'est opiniâtre, vous pensez d'une façon et je pense d'une autre façon. Le cadre devrait être le moins d'opinion possible.
  • Si vous avez un cas particulier, vous devez être cohérent: vous devez prendre de plus en plus de décisions très avisées sur toutes les autres méthodes d'extension: si Count() semble une décision facile alors Where() ne l'est pas. Qu'en est-il de Max()? Il lève une exception pour une liste VIDE, ne devrait-il pas lever aussi pour une null une?

Ce que les concepteurs de bibliothèques ont fait avant LINQ était d'introduire des méthodes explicites lorsque null est une valeur valide (par exemple String.IsNullOrEmpty() ) alors ils devaient être conforme à la philosophie de conception existante. Cela dit, même si elle est assez banale à écrire, deux méthodes EmptyIfNull() et EmptyOrNull() pourraient être utiles.

3
Adriano Repetti

Jim est censé laisser des pommes de terre dans chaque sac. Sinon, je vais le tuer.

Jim a un sac avec cinq pommes de terre dedans. Y a-t-il .Any() pommes de terre dans le sac?

"Oui," dites-vous. <= vrai

Ok donc Jim vit cette fois.

Jim sort toutes les pommes de terre et les mange. Y a-t-il des pommes de terre () dans le sac?

"Non," dites-vous. <= faux

Il est temps de tuer Jim.

Jim incinère complètement le sac dans un feu. Y a-t-il maintenant des pommes de terre () dans le sac?

"Il n'y a pas de sac." <= ArgumentNullException

Jim devrait-il vivre ou mourir? Eh bien, nous ne nous attendions pas à cela, j'ai donc besoin d'une décision. Est-ce que laisser Jim s'en tirer avec un bogue ou non?

Vous pouvez tiliser des annotations pour signaler que vous ne supportez pas de manigances nulles de cette façon.

public bool Any( [NotNull] List bag ) 

Mais votre chaîne d'outils doit la supporter. Ce qui signifie que vous finirez probablement par écrire des chèques.

1
candied_orange

Si cela vous dérange tellement, je suggère une méthode d'extension simple.

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

Vous pouvez maintenant le faire:

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

... et l'indicateur sera défini sur false.

0
John Wu

C'est une question sur les méthodes d'extension de C # et leur philosophie de conception, donc je pense que la meilleure façon de répondre à cette question est de citer documentation MSDN sur le but des méthodes d'extension :

Les méthodes d'extension vous permettent "d'ajouter" des méthodes aux types existants sans créer de nouveau type dérivé, sans recompiler ou autrement modifier le type d'origine. Les méthodes d'extension sont un type spécial de méthode statique, mais elles sont appelées comme s'il s'agissait de méthodes d'instance sur le type étendu. Pour le code client écrit en C #, F # et Visual Basic, il n'y a pas de différence apparente entre l'appel d'une méthode d'extension et les méthodes réellement définies dans un type.

En général, nous vous recommandons d'implémenter les méthodes d'extension avec parcimonie et uniquement lorsque cela est nécessaire. Dans la mesure du possible, le code client qui doit étendre un type existant doit le faire en créant un nouveau type dérivé du type existant. Pour plus d'informations, voir Héritage.

Lorsque vous utilisez une méthode d'extension pour étendre un type dont vous ne pouvez pas modifier le code source, vous courez le risque qu'une modification de l'implémentation du type entraîne la rupture de votre méthode d'extension.

Si vous implémentez des méthodes d'extension pour un type donné, n'oubliez pas les points suivants:

  • Une méthode d'extension ne sera jamais appelée si elle a la même signature qu'une méthode définie dans le type.
  • Les méthodes d'extension sont intégrées dans la portée au niveau de l'espace de noms. Par exemple, si vous avez plusieurs classes statiques qui contiennent des méthodes d'extension dans un seul espace de noms nommé Extensions, elles seront toutes mises à portée par le using Extensions; directive.

Pour résumer, les méthodes d'extension sont conçues pour ajouter des méthodes d'instance à un type particulier, même lorsque les développeurs ne peuvent pas le faire directement. Et comme les méthodes d'instance ( remplaceront toujours les méthodes d'extension si elles sont présentes (si elles sont appelées à l'aide de la syntaxe de la méthode d'instance), cela ne devrait que être fait si vous ne pouvez pas ajouter directement une méthode ou étendre la classe. *

En d'autres termes, une méthode d'extension devrait agir comme une méthode d'instance, car elle pourrait finir par devenir une méthode d'instance par un client. Et comme une méthode d'instance doit lancer si l'objet sur lequel elle est appelée est null, la méthode d'extension devrait également l'être.


* En remarque, c'est exactement la situation à laquelle les concepteurs de LINQ ont été confrontés: lorsque C # 3.0 est sorti, il y avait déjà des millions de clients qui utilisaient System.Collections.IEnumerable et System.Collections.Generic.IEnumerable<T>, dans leurs collections et dans les boucles foreach. Ces classes ont renvoyé des objets IEnumerator qui n'avaient que les deux méthodes Current et MoveNext, ajoutant ainsi toutes les méthodes d'instance supplémentaires requises, telles que Count, Any, etc., briserait ces millions de clients. Donc, afin de fournir cette fonctionnalité (d'autant plus qu'elle peut être implémentée en termes de Current et MoveNext avec une relative facilité), ils l'ont libérée en tant que méthodes d'extension, qui peuvent être appliquées à tout actuellement existante IEnumerable et peut également être implémentée par les classes de manière plus efficace. Si les concepteurs de C # avaient décidé de publier LINQ le premier jour, cela aurait été fourni en tant que méthodes d'instance de IEnumerable, et ils auraient probablement conçu une sorte de système pour fournir des implémentations d'interface par défaut de ces méthodes.

0
TheHansinator

Je pense que le point saillant ici est qu'en renvoyant false au lieu de lever une exception, vous masquez des informations qui pourraient être pertinentes pour les futurs lecteurs/modificateurs de votre code.

S'il est possible que la liste soit nulle, je suggérerais d'avoir un chemin logique distinct pour cela, sinon, à l'avenir, quelqu'un peut ajouter une logique basée sur une liste (comme un ajout) à l''autre {} de votre if, ce qui entraîne un exception indésirable qu’ils n’avaient aucune raison de prévoir.

La lisibilité et la maintenabilité l'emportent sur "Je dois écrire une condition supplémentaire" à chaque fois.

0
Callum Bradbury

En règle générale, j'écris la plupart de mon code pour supposer que l'appelant est responsable de ne pas me remettre de données nulles, principalement pour les raisons décrites dans Dites non à Null (et autres messages similaires). Le concept de null est souvent mal compris, et pour cette raison, il est conseillé d'initialiser vos variables à l'avance, autant que possible. En supposant que vous travaillez avec une API raisonnable, vous ne devriez idéalement jamais récupérer une valeur nulle, vous ne devriez donc jamais avoir à vérifier la valeur nulle. Le point de null, comme d'autres réponses l'ont indiqué, est de s'assurer que vous avez un objet valide (par exemple un "sac") avec lequel travailler avant de continuer. Ce n'est pas une fonctionnalité de Any, mais plutôt une fonctionnalité de la langue elle-même. Vous ne pouvez pas effectuer la plupart des opérations sur un objet nul, car il n'existe pas. C'est comme si je vous demandais de vous rendre au magasin dans ma voiture, sauf que je ne possède pas de voiture, vous n'avez donc aucun moyen d'utiliser ma voiture pour vous rendre au magasin. Il est absurde d'effectuer une opération sur quelque chose qui n'existe littéralement pas.

Il existe des cas pratiques d'utilisation de null, par exemple lorsque vous n'avez littéralement pas les informations demandées disponibles (par exemple, si je vous ai demandé quel est mon âge, le choix le plus probable pour vous de répondre à ce stade est "Je ne sais pas ", qui correspond à une valeur nulle). Cependant, dans la plupart des cas, vous devez au moins savoir si vos variables sont initialisées ou non. Et si vous ne le faites pas, je vous recommanderais de resserrer votre code. La toute première chose que je fais dans un programme ou une fonction est d'initialiser une valeur avant de l'utiliser. Il est rare que je doive vérifier les valeurs nulles, car je peux garantir que mes variables ne sont pas nulles. C'est une bonne habitude à prendre, et plus vous vous souvenez d'initialiser vos variables fréquemment, moins vous devez vérifier la valeur null.

0
phyrfox