Détection des «machines d'état» IEnumerable

17

Je viens de lire un article intéressant intitulé Trop mignon avec le rendement c #

Cela m'a fait me demander quelle est la meilleure façon de détecter si un IEnumerable est une collection énumérable réelle, ou s'il s'agit d'une machine d'état générée avec le mot-clé yield.

Par exemple, vous pouvez modifier DoubleXValue (de l'article) en quelque chose comme:

private void DoubleXValue(IEnumerable<Point> points)
{
    if(points is List<Point>)
      foreach (var point in points)
        point.X *= 2;
    else throw YouCantDoThatException();
}

Question 1) Existe-t-il une meilleure façon de procéder?

Question 2) Est-ce quelque chose dont je dois m'inquiéter lors de la création d'une API?

ConditionRacer
la source
1
Vous devriez utiliser une interface plutôt qu'un béton et également coulé plus bas, ce ICollection<T>serait un meilleur choix car toutes les collections ne le sont pas List<T>. Par exemple, les tableaux Point[]implémentent IList<T>mais ne le sont pas List<T>.
Trevor Pilley
3
Bienvenue dans le problème des types mutables. Ienumerable est implicitement supposé être un type de données immuable, donc les implémentations de mutation sont une violation de LSP. Cet article est une explication parfaite des dangers des effets secondaires, alors entraînez-vous à écrire vos types C # pour être aussi libres d'effets secondaires que possible et vous n'aurez jamais à vous en préoccuper.
Jimmy Hoffa
1
@JimmyHoffa IEnumerableest immuable dans le sens où vous ne pouvez pas modifier une collection (ajouter / supprimer) lors de son énumération, mais si les objets de la liste sont modifiables, il est parfaitement raisonnable de les modifier lors de l'énumération de la liste.
Trevor Pilley
@TrevorPilley Je ne suis pas d'accord. Si l'on s'attend à ce que la collection soit immuable, l'attente d'un consommateur est que les membres ne seront pas mutés par des acteurs extérieurs, ce serait appelé un effet secondaire et viole l'attente d'immuabilité. Viole POLA et implicitement LSP.
Jimmy Hoffa
Que diriez-vous d'utiliser ToList? msdn.microsoft.com/en-us/library/bb342261.aspx Il ne détecte pas s'il a été généré par yield, mais à la place, il le rend inutile.
luiscubal

Réponses:

22

Si je comprends bien, votre question semble reposer sur une prémisse incorrecte. Voyons si je peux reconstruire le raisonnement:

  • L'article lié à décrit comment les séquences générées automatiquement présentent un comportement "paresseux" et montre comment cela peut conduire à un résultat contre-intuitif.
  • Par conséquent, je peux détecter si une instance donnée de IEnumerable va présenter ce comportement paresseux en vérifiant si elle est générée automatiquement.
  • Comment je fais ça?

Le problème est que la deuxième prémisse est fausse. Même si vous pouviez détecter si un IEnumerable donné était le résultat d'une transformation de bloc d'itérateur (et oui, il existe des moyens de le faire), cela ne serait pas utile car l'hypothèse est fausse. Illustrons pourquoi.

class M { public int P { get; set; } }
class C
{
  public static IEnumerable<M> S1()
  {
    for (int i = 0; i < 3; ++i) 
      yield return new M { P = i };
  }

  private static M[] ems = new M[] 
  { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  public static IEnumerable<M> S2()
  {
    for (int i = 0; i < 3; ++i)
      yield return ems[i];
  }

  public static IEnumerable<M> S3()
  {
    return new M[] 
    { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  }

  private class X : IEnumerable<M>
  {
    public IEnumerator<X> GetEnumerator()
    {
      return new XEnum();
    }
    // Omitted: non generic version
    private class XEnum : IEnumerator<X>
    {
      int i = 0;
      M current;
      public bool MoveNext()
      {
        current = new M() { P = i; }
        i += 1;
        return true;
      }
      public M Current { get { return current; } }
      // Omitted: other stuff.
    }
  }

  public static IEnumerable<M> S4()
  {
    return new X();
  }

  public static void Add100(IEnumerable<M> items)
  {
    foreach(M item in items) item.P += 100;
  }
}

D'accord, nous avons quatre méthodes. S1 et S2 sont des séquences générées automatiquement; S3 et S4 sont des séquences générées manuellement. Supposons maintenant que nous ayons:

var items = C.Sn(); // S1, S2, S3, S4
S.Add100(items);
Console.WriteLine(items.First().P);

Le résultat pour S1 et S4 sera 0; chaque fois que vous énumérez la séquence, vous obtenez une nouvelle référence à un M créé. Le résultat pour S2 et S3 sera 100; chaque fois que vous énumérez la séquence, vous obtenez la même référence à M que vous avez obtenue la dernière fois. Que le code de séquence soit généré automatiquement ou non est orthogonal à la question de savoir si les objets énumérés ont une identité référentielle ou non. Ces deux propriétés - génération automatique et identité référentielle - n'ont en fait rien à voir l'une avec l'autre. L'article auquel vous avez lié les confond un peu.

À moins qu'un fournisseur de séquence ne soit documenté comme offrant toujours des objets ayant une identité référentielle , il est imprudent de supposer qu'il le fait.

Eric Lippert
la source
2
Nous savons tous que vous aurez la bonne réponse ici, c'est une donnée; mais si vous pouviez mettre un peu plus de définition dans le terme "identité référentielle" cela pourrait être bien, je le lis comme "immuable" mais c'est parce que je confond immuable en C # avec un nouvel objet retourné chaque type get ou value, ce que je pense c'est la même chose dont vous parlez. Bien que l'immuabilité accordée puisse être obtenue de diverses autres manières, c'est la seule manière rationnelle en C # (à ma connaissance, vous connaissez peut-être des façons que je ne connais pas).
Jimmy Hoffa
2
@JimmyHoffa: Deux références d'objet x et y ont une "identité référentielle" si Object.ReferenceEquals (x, y) renvoie true.
Eric Lippert
Toujours agréable d'obtenir une réponse directement de la source. Merci Eric.
ConditionRacer
10

Je pense que le concept clé est que vous devez traiter toute IEnumerable<T>collection comme immuable : vous ne devez pas en modifier les objets, vous devez créer une nouvelle collection avec de nouveaux objets:

private IEnumerable<Point> DoubleXValue(IEnumerable<Point> points)
{
    foreach (var point in points)
        yield return new Point(point.X * 2, point.Y);
}

(Ou écrivez le même plus succinctement en utilisant LINQ Select() .)

Si vous voulez vraiment modifier les éléments de la collection, ne pas utiliser IEnumerable<T>, l' utilisation IList<T>(ou peut - être IReadOnlyList<T>si vous ne voulez pas modifier la collection elle - même, seuls les éléments qu'il contient , et vous êtes sur .Net 4.5):

private void DoubleXValue(IList<Point> points)
{
    foreach (var point in points)
        point.X *= 2;
}

De cette façon, si quelqu'un tente d'utiliser votre méthode avec IEnumerable<T>, elle échouera au moment de la compilation.

svick
la source
1
La vraie clé est comme vous l'avez dit, retournez un nouvel ienumerable avec les modifications quand vous voulez changer quelque chose. Iénombrable en tant que type est parfaitement viable et n'a aucune raison d'être changé, c'est juste la technique d'utilisation qui doit être comprise comme vous l'avez dit. +1 pour avoir mentionné comment travailler avec des types immuables.
Jimmy Hoffa
1
Assez lié - vous devez également traiter chaque IEnumerable comme s'il se faisait via une exécution différée. Ce qui peut être vrai au moment où vous récupérez votre référence IEnumerable peut ne pas être vrai lorsque vous l'énumérez réellement. Par exemple, le renvoi d'une instruction LINQ à l'intérieur d'un verrou peut entraîner des résultats intéressants et inattendus.
Dan Lyons
@JimmyHoffa: Je suis d'accord. Je vais plus loin et j'essaie d'éviter de répéter plus d'une IEnumerablefois. La nécessité de le faire plus d'une fois peut être évitée en appelant ToList()et en itérant sur la liste, bien que dans le cas spécifique indiqué par l'OP, je préfère la solution de svick consistant à faire en sorte que DoubleXValue renvoie également un IEnumerable. Bien sûr, dans du vrai code, j'utiliserais probablement LINQpour générer ce type de IEnumerable. Cependant, le choix de svick est yieldlogique dans le contexte de la question du PO.
Brian
@Brian Ouais, je ne voulais pas trop changer le code pour faire valoir mon point de vue. En vrai code, j'aurais utilisé Select(). Et en ce qui concerne plusieurs itérations de IEnumerable: ReSharper est utile, car il peut vous avertir lorsque vous faites cela.
svick
5

Personnellement, je pense que le problème mis en évidence dans cet article provient de la surutilisation de l' IEnumerableinterface par de nombreux développeurs C # ces jours-ci. Il semble être devenu le type par défaut lorsque vous avez besoin d'une collection d'objets - mais il y a beaucoup d'informations qui IEnumerablene vous disent tout simplement pas ... de manière cruciale: si oui ou non il a été réifié *.

Si vous trouvez que vous devez détecter si un IEnumerableest vraiment un IEnumerableou quelque chose d'autre, l'abstraction a fui et vous êtes probablement dans une situation où votre type de paramètre est un peu trop lâche. Si vous avez besoin d'informations supplémentaires qui IEnumerableseules ne fournissent pas, expliquez cette exigence en choisissant l'une des autres interfaces telles que ICollectionou IList.

Modifier pour les downvoters Veuillez revenir en arrière et lire attentivement la question. L'OP demande un moyen de s'assurer que les IEnumerableinstances ont été matérialisées avant d'être transmises à sa méthode API. Ma réponse répond à cette question. Il y a un problème plus large concernant la bonne façon d'utiliserIEnumerable , et certainement les attentes du code que l'OP a collé sont basées sur une mauvaise compréhension de ce problème plus large, mais l'OP n'a pas demandé comment utiliser correctement IEnumerable, ou comment travailler avec des types immuables . Ce n'est pas juste de me déprécier de ne pas avoir répondu à une question qui n'a pas été posée.

* J'utilise le terme reify, d'autres utilisent stabilizeou materialize. Je ne pense pas qu'une convention commune ait été adoptée par la communauté pour l'instant.

MattDavey
la source
2
Un problème avec ICollectionet IListest qu'ils sont mutables (ou du moins ressemblent à ça). IReadOnlyCollectionet à IReadOnlyListpartir de .Net 4.5 résoudre certains de ces problèmes.
svick
Veuillez expliquer le downvote?
MattDavey
1
Je ne suis pas d'accord pour que vous changiez les types que vous utilisez. Ienumerable est un excellent type et il peut souvent bénéficier de l'immuabilité. Je pense que le vrai problème est que les gens ne comprennent pas comment travailler avec des types immuables en C #. Pas étonnant, c'est un langage extrêmement dynamique, c'est donc un nouveau concept pour de nombreux développeurs C #.
Jimmy Hoffa
Seul ienumerable permet une exécution différée, donc le rejeter en tant que paramètre supprime toute une fonctionnalité de C #
Jimmy Hoffa
2
J'ai voté contre parce que je pense que c'est faux. Je pense que c'est dans l'esprit de SE, pour s'assurer que les gens ne stockent pas les informations incorrectes, c'est à nous tous de voter contre ces réponses. Peut-être que je me trompe, totalement possible que je me trompe et vous avez raison. C'est à la communauté au sens large de décider en votant. Désolé que vous le preniez personnellement, si c'est une consolation, j'ai eu de nombreuses réponses négatives que j'ai écrites et sommairement supprimées parce que j'accepte que la communauté pense que c'est faux, donc j'ai peu de chances d'avoir raison.
Jimmy Hoffa