J'ai une méthode d'extension de chaîne C # qui devrait retourner un IEnumerable<int>
de tous les index d'une sous-chaîne dans une chaîne. Il fonctionne parfaitement pour son objectif et les résultats attendus sont renvoyés (comme le prouve l'un de mes tests, mais pas celui ci-dessous), mais un autre test unitaire a découvert un problème: il ne peut pas gérer les arguments nuls.
Voici la méthode d'extension que je teste:
public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
if (searchText == null)
{
throw new ArgumentNullException("searchText");
}
for (int index = 0; ; index += searchText.Length)
{
index = str.IndexOf(searchText, index);
if (index == -1)
break;
yield return index;
}
}
Voici le test qui a signalé le problème:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
string test = "a.b.c.d.e";
test.AllIndexesOf(null);
}
Lorsque le test s'exécute sur ma méthode d'extension, il échoue, avec le message d'erreur standard que la méthode "n'a pas lancé d'exception".
C'est déroutant: je suis clairement passé null
dans la fonction, mais pour une raison quelconque, la comparaison null == null
revient false
. Par conséquent, aucune exception n'est levée et le code continue.
J'ai confirmé que ce n'était pas un bogue avec le test: lors de l'exécution de la méthode dans mon projet principal avec un appel à Console.WriteLine
dans le if
bloc de comparaison nul , rien n'est affiché sur la console et aucune exception n'est interceptée par un catch
bloc que j'ajoute. De plus, utiliser string.IsNullOrEmpty
au lieu de == null
pose le même problème.
Pourquoi cette comparaison supposée simple échoue-t-elle?
la source
Réponses:
Vous utilisez
yield return
. Ce faisant, le compilateur réécrira votre méthode dans une fonction qui renvoie une classe générée qui implémente une machine à états.En gros, il réécrit les sections locales dans les champs de cette classe et chaque partie de votre algorithme entre les
yield return
instructions devient un état. Vous pouvez vérifier avec un décompilateur ce que devient cette méthode après la compilation (assurez-vous de désactiver la décompilation intelligente qui produiraityield return
).Mais l'essentiel est que le code de votre méthode ne sera pas exécuté tant que vous ne commencerez pas l'itération.
La manière habituelle de vérifier les conditions préalables est de diviser votre méthode en deux:
Cela fonctionne car la première méthode se comportera exactement comme vous le souhaitez (exécution immédiate) et renverra la machine à états implémentée par la deuxième méthode.
Notez que vous devez également vérifier le
str
paramètrenull
, car les méthodes d'extensions peuvent être appelées sur desnull
valeurs, car elles ne sont que du sucre syntaxique.Si vous êtes curieux de savoir ce que le compilateur fait à votre code, voici votre méthode, décompilée avec dotPeek à l'aide de l' option Afficher le code généré par le compilateur .
Ce code C # n'est pas valide, car le compilateur est autorisé à faire des choses que le langage n'autorise pas, mais qui sont légales en IL - par exemple en nommant les variables d'une manière que vous ne pourriez pas éviter les collisions de noms.
Mais comme vous pouvez le voir, le
AllIndexesOf
seul construit et renvoie un objet, dont le constructeur n'initialise que certains états.GetEnumerator
copie uniquement l'objet. Le vrai travail est fait lorsque vous commencez à énumérer (en appelant laMoveNext
méthode).la source
str
paramètrenull
, car les méthodes d'extensions peuvent être appelées sur desnull
valeurs, car elles ne sont que du sucre syntaxique.yield return
est une bonne idée en principe, mais il y a tellement de pièges bizarres. Merci d'avoir mis celui-ci en lumière!MoveNext
est appelé sous le capot par laforeach
construction. J'ai écrit une explication de ce queforeach
fait dans ma réponse expliquant la sémantique de la collection si vous souhaitez voir le modèle exact.Vous avez un bloc d'itérateur. Aucun code de cette méthode n'est jamais exécuté en dehors des appels à
MoveNext
sur l'itérateur retourné. L'appel de la méthode ne note mais crée la machine à états, et cela n'échouera jamais (en dehors des extrêmes tels que les erreurs de mémoire insuffisante, les débordements de pile ou les exceptions d'abandon de thread).Lorsque vous essayez réellement d'itérer la séquence, vous obtenez les exceptions.
C'est pourquoi les méthodes LINQ ont en fait besoin de deux méthodes pour avoir la sémantique de gestion des erreurs qu'elles souhaitent. Ils ont une méthode privée qui est un bloc d'itérateur, puis une méthode de bloc non-itérateur qui ne fait que faire la validation des arguments (afin que cela puisse être fait avec empressement, plutôt que d'être différé) tout en reportant toutes les autres fonctionnalités.
Voici donc le schéma général:
la source
Les énumérateurs, comme les autres l'ont dit, ne sont évalués qu'au moment où ils commencent à être énumérés (c'est-à-dire que la
IEnumerable.GetNext
méthode est appelée). Ainsi cen'est pas évalué tant que vous ne commencez pas à énumérer, c.-à-d.
la source