web-dev-qa-db-fra.com

Pourquoi cette méthode d'extension de chaîne ne lève-t-elle pas une exception?

J'ai une méthode d'extension de chaîne C # qui devrait renvoyer 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 retournés (comme prouvé par l'un de mes tests, mais pas celui ci-dessous), mais un autre test unitaire a découvert un problème avec lui: 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 levé d'exception".

C'est déroutant: j'ai clairement passé null dans la fonction, mais pour une raison quelconque, la comparaison null == null renvoie false. Par conséquent, aucune exception n'est levée et le code continue.

J'ai confirmé que ce n'est pas un bug avec le test: lors de l'exécution de la méthode dans mon projet principal avec un appel à Console.WriteLine dans le bloc null-comparaison if, rien n'est affiché sur la console et aucune exception n'est interceptée par le bloc catch que j'ajoute. De plus, en utilisant string.IsNullOrEmpty au lieu de == null a le même problème.

Pourquoi cette comparaison supposée simple échoue-t-elle?

117
ArtOfCode

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.

D'une manière générale, il réécrit les sections locales dans les champs de cette classe et chaque partie de votre algorithme entre le yield return les instructions deviennent 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 produirait yield return).

Mais la ligne de fond est la suivante: le code de votre méthode ne sera pas exécuté tant que vous n'aurez pas commencé à l'itérer.

La façon habituelle de vérifier les conditions préalables est de diviser votre méthode en deux:

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
    if (str == null)
        throw new ArgumentNullException("str");
    if (searchText == null)
        throw new ArgumentNullException("searchText");

    return AllIndexesOfCore(str, searchText);
}

private static IEnumerable<int> AllIndexesOfCore(string str, string searchText)
{
    for (int index = 0; ; index += searchText.Length)
    {
        index = str.IndexOf(searchText, index);
        if (index == -1)
            break;
        yield return index;
    }
}

Cela fonctionne parce que la première méthode se comportera exactement comme prévu (exécution immédiate) et renverra la machine à états implémentée par la seconde méthode.

Notez que vous devez également vérifier le paramètre str pour null, car les méthodes d'extensions peuvent être appelées sur null valeurs, car ce 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 en utilisant l'option Afficher le code généré par le compilateur .

public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
  Test.<AllIndexesOf>d__0 allIndexesOfD0 = new Test.<AllIndexesOf>d__0(-2);
  allIndexesOfD0.<>3__str = str;
  allIndexesOfD0.<>3__searchText = searchText;
  return (IEnumerable<int>) allIndexesOfD0;
}

[CompilerGenerated]
private sealed class <AllIndexesOf>d__0 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
  private int <>2__current;
  private int <>1__state;
  private int <>l__initialThreadId;
  public string str;
  public string <>3__str;
  public string searchText;
  public string <>3__searchText;
  public int <index>5__1;

  int IEnumerator<int>.Current
  {
    [DebuggerHidden] get
    {
      return this.<>2__current;
    }
  }

  object IEnumerator.Current
  {
    [DebuggerHidden] get
    {
      return (object) this.<>2__current;
    }
  }

  [DebuggerHidden]
  public <AllIndexesOf>d__0(int <>1__state)
  {
    base..ctor();
    this.<>1__state = param0;
    this.<>l__initialThreadId = Environment.CurrentManagedThreadId;
  }

  [DebuggerHidden]
  IEnumerator<int> IEnumerable<int>.GetEnumerator()
  {
    Test.<AllIndexesOf>d__0 allIndexesOfD0;
    if (Environment.CurrentManagedThreadId == this.<>l__initialThreadId && this.<>1__state == -2)
    {
      this.<>1__state = 0;
      allIndexesOfD0 = this;
    }
    else
      allIndexesOfD0 = new Test.<AllIndexesOf>d__0(0);
    allIndexesOfD0.str = this.<>3__str;
    allIndexesOfD0.searchText = this.<>3__searchText;
    return (IEnumerator<int>) allIndexesOfD0;
  }

  [DebuggerHidden]
  IEnumerator IEnumerable.GetEnumerator()
  {
    return (IEnumerator) this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
  }

  bool IEnumerator.MoveNext()
  {
    switch (this.<>1__state)
    {
      case 0:
        this.<>1__state = -1;
        if (this.searchText == null)
          throw new ArgumentNullException("searchText");
        this.<index>5__1 = 0;
        break;
      case 1:
        this.<>1__state = -1;
        this.<index>5__1 += this.searchText.Length;
        break;
      default:
        return false;
    }
    this.<index>5__1 = this.str.IndexOf(this.searchText, this.<index>5__1);
    if (this.<index>5__1 != -1)
    {
      this.<>2__current = this.<index>5__1;
      this.<>1__state = 1;
      return true;
    }
    goto default;
  }

  [DebuggerHidden]
  void IEnumerator.Reset()
  {
    throw new NotSupportedException();
  }

  void IDisposable.Dispose()
  {
  }
}

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, nommer les variables d'une manière que vous ne pourriez pas éviter les collisions de noms.

Mais comme vous pouvez le voir, le AllIndexesOf ne construit et retourne qu'un objet, dont le constructeur initialise seulement un état. GetEnumerator copie uniquement l'objet. Le vrai travail se fait lorsque vous commencez à énumérer (en appelant la méthode MoveNext).

154

Vous avez un bloc itérateur. Aucun du code de cette méthode n'est jamais exécuté en dehors des appels à MoveNext sur l'itérateur renvoyé. Appeler la méthode ne fait pas remarquer mais crée la machine d'état, 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 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 désirent. 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 valider l'argument (afin qu'il puisse être fait avec impatience, plutôt que d'être différé) tout en différant toutes les autres fonctionnalités.

Voici donc le schéma général:

public static IEnumerable<T> Foo<T>(
    this IEnumerable<T> souce, Func<T, bool> anotherArgument)
{
    //note, not an iterator block
    if(anotherArgument == null)
    {
        //TODO make a fuss
    }
    return FooImpl(source, anotherArgument);
}

private static IEnumerable<T> FooImpl<T>(
    IEnumerable<T> souce, Func<T, bool> anotherArgument)
{
    //TODO actual implementation as an iterator block
    yield break;
}
34
Servy

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 le IEnumerable.GetNext est appelée). Ainsi cette

List<int> indexes = "a.b.c.d.e".AllIndexesOf(null).ToList<int>();

n'est évalué que lorsque vous commencez à énumérer, c.-à-d.

foreach(int index in indexes)
{
    // ArgumentNullException
}
0
Jenna