Pourquoi la boucle .NET foreach lève-t-elle NullRefException lorsque la collection est nulle?

231

Donc je rencontre souvent cette situation ... où Do.Something(...) renvoie une collection nulle, comme ceci:

int[] returnArray = Do.Something(...);

Ensuite, j'essaie d'utiliser cette collection comme ceci:

foreach (int i in returnArray)
{
    // do some more stuff
}

Je suis juste curieux, pourquoi une boucle foreach ne peut-elle pas fonctionner sur une collection nulle? Il me semble logique que 0 itérations soient exécutées avec une collection nulle ... au lieu de cela, elle lance unNullReferenceException . Quelqu'un sait pourquoi cela pourrait être?

C'est ennuyeux car je travaille avec des API qui ne savent pas exactement ce qu'elles renvoient, donc je me retrouve avec if (someCollection != null)partout ...

Edit: Merci à tous d'avoir expliqué que les foreachutilisations GetEnumeratoret s'il n'y a pas d'énumérateur à obtenir, le foreach échouerait. Je suppose que je demande pourquoi le langage / runtime ne peut pas ou ne fera pas de vérification nulle avant de saisir l'énumérateur. Il me semble que le comportement serait encore bien défini.

Polaris878
la source
1
Quelque chose ne va pas quand on appelle un tableau une collection. Mais peut-être que je suis juste de la vieille école.
Robaticus
Oui, je suis d'accord ... Je ne sais même pas pourquoi tant de méthodes dans cette base de code retournent des tableaux x_x
Polaris878
4
Je suppose que, par le même raisonnement, il serait bien défini que toutes les instructions en C # deviennent des non-opérations lorsqu'elles reçoivent une nullvaleur. Suggérez-vous cela uniquement pour les foreachboucles ou autres instructions?
Ken
7
@ Ken ... Je pense juste aux boucles foreach, car il me semble évident pour le programmeur que rien ne se passerait si la collection était vide ou inexistante
Polaris878

Réponses:

251

Eh bien, la réponse courte est "parce que c'est la façon dont les concepteurs de compilateurs l'ont conçue". En réalité, cependant, votre objet de collection est nul, il n'y a donc aucun moyen pour le compilateur d'obtenir l'énumérateur pour parcourir la collection.

Si vous avez vraiment besoin de faire quelque chose comme ça, essayez l'opérateur de coalescence nulle:

int[] array = null;

foreach (int i in array ?? Enumerable.Empty<int>())
{
   System.Console.WriteLine(string.Format("{0}", i));
}
Robaticus
la source
3
Veuillez excuser mon ignorance, mais est-ce efficace? Cela ne donne-t-il pas lieu à une comparaison à chaque itération?
user919426
20
Je ne le crois pas. En regardant l'IL généré, la boucle est après la comparaison est nulle.
Robaticus
10
Holy necro ... Parfois, vous devez regarder l'IL pour voir ce que le compilateur fait pour déterminer s'il y a des hits d'efficacité. L'utilisateur 919426 avait demandé s'il avait effectué la vérification pour chaque itération. Bien que la réponse puisse être évidente pour certaines personnes, elle n'est pas évidente pour tout le monde, et fournir l'indication que la lecture de l'IL vous dira ce que fait le compilateur, aide les gens à pêcher par eux-mêmes à l'avenir.
Robaticus
2
@Robaticus (même pourquoi plus tard) l'IL semble que pourquoi parce que la spécification le dit. L'expansion du sucre syntaxique (aka foreach) consiste à évaluer l'expression sur le côté droit de "in" et à invoquer GetEnumeratorle résultat
Rune FS
2
@RuneFS - exactement. Comprendre la spécification ou regarder l'IL est un moyen de comprendre le «pourquoi». Ou pour évaluer si deux approches C # différentes se résument à la même IL. C'était, essentiellement, mon point à Shimmy ci-dessus.
Robaticus
148

Une foreachboucle appelle la GetEnumeratorméthode.
Si la collection l'est null, cet appel de méthode aboutit à a NullReferenceException.

Il est inapproprié de renvoyer une nullcollection; vos méthodes devraient renvoyer une collection vide à la place.

SLaks
la source
7
Je suis d'accord, les collections vides doivent toujours être retournées ... mais je n'ai pas écrit ces méthodes :)
Polaris878
19
@Polaris, opérateur coalescent nul à la rescousse! int[] returnArray = Do.Something() ?? new int[] {};
JSB
2
Ou: ... ?? new int[0].
Ken
3
+1 Comme le conseil de retourner des collections vides au lieu de null. Merci.
Galilyou
1
Je ne suis pas d'accord sur une mauvaise pratique: voir ⇒ si une fonction échoue, elle peut retourner une collection vide - c'est un appel au constructeur, l'allocation de mémoire et peut-être un tas de code à exécuter. Soit vous pouvez simplement retourner «null» → évidemment il n'y a qu'un code à retourner et un code très court pour vérifier que l'argument est «null». C'est juste une performance.
Hi-Angel
47

Il existe une grande différence entre une collection vide et une référence nulle à une collection.

Lorsque vous utilisez foreach, en interne, cela appelle la méthode GetEnumerator () de IEnumerable . Lorsque la référence est nulle, cela déclenche cette exception.

Cependant, il est parfaitement valable d'avoir un IEnumerableou vide IEnumerable<T>. Dans ce cas, foreach "n'itérera" rien (puisque la collection est vide), mais il ne lancera pas non plus, car c'est un scénario parfaitement valide.


Éditer:

Personnellement, si vous devez contourner ce problème, je recommanderais une méthode d'extension:

public static IEnumerable<T> AsNotNull<T>(this IEnumerable<T> original)
{
     return original ?? Enumerable.Empty<T>();
}

Vous pouvez alors simplement appeler:

foreach (int i in returnArray.AsNotNull())
{
    // do some more stuff
}
Reed Copsey
la source
3
Oui, mais POURQUOI n'effectue-t-il pas une vérification nulle avant d'obtenir l'énumérateur?
Polaris878
12
@ Polaris878: Parce qu'il n'a jamais été conçu pour être utilisé avec une collection nulle. C'est, l'OMI, une bonne chose - car une référence nulle et une collection vide doivent être traitées séparément. Si vous voulez contourner cela, il existe des moyens ... Je vais modifier pour afficher une autre option ...
Reed Copsey
1
@ Polaris878: Je suggérerais de reformuler votre question: "Pourquoi le runtime DEVRAIT-il effectuer une vérification nulle avant d'obtenir l'énumérateur?"
Reed Copsey
Je suppose que je demande "pourquoi pas?" lol il semble que le comportement serait toujours bien défini
Polaris878
2
@ Polaris878: Je suppose que la façon dont j'y pense, renvoyer null pour une collection est une erreur. Dans l'état actuel des choses, le runtime vous donne une exception significative dans ce cas, mais il est facile de contourner (ie: ci-dessus) si vous n'aimez pas ce comportement. Si le compilateur vous cachait cela, vous perdriez la vérification des erreurs au moment de l'exécution, mais il n'y aurait aucun moyen de "l'éteindre" ...
Reed Copsey
12

Il s'agit d'une réponse depuis longtemps, mais j'ai essayé de le faire de la manière suivante pour éviter simplement l'exception de pointeur nul et peut être utile pour quelqu'un qui utilise l'opérateur de vérification null C #?.

     //fragments is a list which can be null
     fragments?.ForEach((obj) =>
        {
            //do something with obj
        });
Devesh
la source
@kjbartel vous a dépassé de plus d'un an (à " stackoverflow.com/a/32134295/401246 "). ;) Il s'agit de la meilleure solution, car elle n'implique pas: a) une dégradation des performances de (même si ce n'est pas le cas null) la généralisation de l'ensemble de la boucle à l'écran LCD de Enumerable(comme on le ??ferait), b) la nécessité d'ajouter une méthode d'extension à chaque projet, et c) il faut éviter null IEnumerables (Pffft! Puh-LEAZE! SMH.) pour commencer.
Tom
10

Une autre méthode d'extension pour contourner ce problème:

public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
{
    if(items == null) return;
    foreach (var item in items) action(item);
}

Consommer de plusieurs façons:

(1) avec une méthode qui accepte T:

returnArray.ForEach(Console.WriteLine);

(2) avec une expression:

returnArray.ForEach(i => UpdateStatus(string.Format("{0}% complete", i)));

(3) avec une méthode anonyme multiligne

int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});
Geai
la source
Il manque juste une parenthèse fermante dans le 3ème exemple. Sinon, beau code qui peut être étendu de manière intéressante (pour les boucles, les inversions, les sauts, etc.). Merci d'avoir partagé.
Lara
Merci pour un code si merveilleux, mais je n'ai pas compris les premières méthodes, pourquoi vous passez console.writeline comme paramètre, bien que son impression des éléments du tableau, mais ne comprenait pas
Ajay Singh
@AjaySingh Console.WriteLinen'est qu'un exemple d'une méthode qui prend un argument (an Action<T>). Les éléments 1, 2 et 3 montrent des exemples de passage de fonctions à la .ForEachméthode d'extension.
Jay
@ La réponse de kjbartel (à « stackoverflow.com/a/32134295/401246 » est la meilleure solution, car il ne fonctionne pas: a) impliquent une dégradation des performances de (même pas null) généralisant toute la boucle à l'écran LCD de Enumerable(comme l' utilisation ??serait ), b) nécessitent d'ajouter une méthode d'extension à chaque projet, ou c) nécessitent d'éviter null IEnumerables (Pffft! Puh-LEAZE! SMH.) pour commencer (cuz, nullsignifie N / A, tandis que la liste vide signifie, c'est appl. mais est actuellement, eh bien, vide !, c'est-à-dire qu'un employeur pourrait avoir des commissions N / A pour les non-ventes ou vide pour les ventes).
Tom
5

Écrivez simplement une méthode d'extension pour vous aider:

public static class Extensions
{
   public static void ForEachWithNull<T>(this IEnumerable<T> source, Action<T> action)
   {
      if(source == null)
      {
         return;
      }

      foreach(var item in source)
      {
         action(item);
      }
   }
}
BFree
la source
5

Parce qu'une collection null n'est pas la même chose qu'une collection vide. Une collection vide est un objet de collection sans éléments; une collection null est un objet inexistant.

Voici quelque chose à essayer: Déclarez deux collections de toute sorte. Initialisez l'un normalement pour qu'il soit vide et affectez à l'autre la valeur null. Essayez ensuite d'ajouter un objet aux deux collections et voyez ce qui se passe.

COUP
la source
3

C'est la faute de Do.Something(). La meilleure pratique ici serait de renvoyer un tableau de taille 0 (c'est possible) au lieu d'un null.

Henk Holterman
la source
2

Parce que dans les coulisses l' foreachacquiert un énumérateur, équivalent à ceci:

using (IEnumerator<int> enumerator = returnArray.getEnumerator()) {
    while (enumerator.MoveNext()) {
        int i = enumerator.Current;
        // do some more stuff
    }
}
Lucero
la source
2
alors? Pourquoi ne peut-il pas simplement vérifier s'il est nul en premier et sauter la boucle? AKA, exactement ce qui est montré dans les méthodes d'extension? La question est, est-il préférable de sauter par défaut la boucle si null ou de lever une exception? Je pense qu'il vaut mieux sauter! Il semble probable que les conteneurs nuls sont censés être ignorés plutôt que bouclés car les boucles sont censées faire quelque chose SI le conteneur n'est pas nul.
AbstractDissonance
@AbstractDissonance Vous pouvez discuter de la même chose avec toutes les nullréférences, par exemple lors de l'accès aux membres. Il s'agit généralement d'une erreur, et si ce n'est pas le cas, il est assez simple de gérer cela, par exemple avec la méthode d'extension qu'un autre utilisateur a fournie comme réponse.
Lucero
1
Je ne pense pas. Le foreach est censé fonctionner sur la collection et est différent de référencer directement un objet nul. Bien que l'on puisse argumenter la même chose, je parie que si vous avez analysé tout le code dans le monde, vous auriez la plupart des boucles foreach avec des vérifications nulles devant elles uniquement pour contourner la boucle lorsque la collection est "null" (ce qui est donc traité de la même manière que vide). Je ne pense pas que quiconque considère le bouclage sur une collection nulle comme quelque chose qu'il souhaite et préfère simplement ignorer la boucle si la collection est nulle. Peut-être, plutôt, un foreach? (Var x en C) pourrait être utilisé.
AbstractDissonance
Le point que j'essaye principalement de faire est que cela crée un peu de litière dans le code car il faut vérifier à chaque fois sans raison valable. Les extensions, bien sûr, fonctionnent mais une fonctionnalité de langue pourrait être ajoutée pour éviter ces choses sans trop de problèmes. (principalement, je pense que la méthode actuelle produit des bogues cachés car le programmeur peut oublier de mettre la vérification et donc une exception ... parce qu'il s'attend à ce que la vérification se produise ailleurs avant la boucle ou pense qu'elle a été pré-initialisée (ce qui il peut ou peut avoir changé) .Mais dans les deux cas, le comportement serait le même que s'il était vide
AbstractDissonance
@AbstractDissonance Eh bien, avec une analyse statique appropriée, vous savez où vous pourriez avoir des valeurs nulles et où non. Si vous obtenez un nul là où vous ne vous attendez pas à un, il vaut mieux échouer au lieu d'ignorer silencieusement les problèmes à mon humble avis (dans un esprit d' échec rapide ). Par conséquent, je pense que c'est le bon comportement.
Lucero
1

Je pense que l'explication de la raison pour laquelle l'exception est levée est très claire avec les réponses fournies ici. Je souhaite simplement compléter la façon dont je travaille habituellement avec ces collections. Parce que, parfois, j'utilise la collection plus d'une fois et je dois tester si elle est nulle à chaque fois. Pour éviter cela, je fais ce qui suit:

    var returnArray = DoSomething() ?? Enumerable.Empty<int>();

    foreach (int i in returnArray)
    {
        // do some more stuff
    }

De cette façon, nous pouvons utiliser la collection autant que nous le voulons sans craindre l'exception et nous ne polluons pas le code avec des instructions conditionnelles excessives.

L'utilisation de l'opérateur de vérification nulle ?.est également une excellente approche. Mais, dans le cas de tableaux (comme l'exemple dans la question), il doit être transformé en List avant:

    int[] returnArray = DoSomething();

    returnArray?.ToList().ForEach((i) =>
    {
        // do some more stuff
    });
Alielson Piffer
la source
2
La conversion en liste juste pour avoir accès à la ForEachméthode est l'une des choses que je déteste dans une base de code.
huysentruitw
Je suis d'accord ... j'évite cela autant que possible. :(
Alielson Piffer
-2
SPListItem item;
DataRow dr = datatable.NewRow();

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
Naveen Baabu K
la source