IEnumerable "State Machines" erkennen

17

Ich habe gerade einen interessanten Artikel mit dem Titel Getting too cute with c # yield return gelesen

Ich habe mich gefragt, wie ich am besten feststellen kann, ob es sich bei IEnumerable um eine tatsächliche Auflistung von Enumerables handelt oder ob es sich um einen mit dem Schlüsselwort yield generierten Zustandsautomaten handelt.

Beispielsweise könnten Sie DoubleXValue (aus dem Artikel) in etwas ändern, wie:

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

Frage 1) Gibt es einen besseren Weg, dies zu tun?

Frage 2) Sollte ich mir darüber Gedanken machen, wenn ich eine API erstelle?

ConditionRacer
quelle
1
Sie sollten eine Benutzeroberfläche anstelle einer konkreten verwenden und auch tiefer legen, ICollection<T>da dies nicht für alle Sammlungen der Fall ist List<T>. Beispielsweise Point[]implementieren Arrays dies IList<T>jedoch nicht List<T>.
Trevor Pilley
3
Willkommen beim Problem mit veränderlichen Typen. Ienumerable soll implizit ein unveränderlicher Datentyp sein, sodass mutierende Implementierungen einen Verstoß gegen LSP darstellen. Dieser Artikel ist eine perfekte Erklärung der Gefahren von Nebenwirkungen. Üben Sie daher, Ihre C # -Typen so zu schreiben, dass sie möglichst nebenwirkungsfrei sind, und Sie brauchen sich darüber keine Sorgen zu machen.
Jimmy Hoffa
1
@JimmyHoffa IEnumerableist in dem Sinne unveränderlich, dass Sie eine Sammlung nicht ändern (hinzufügen / entfernen) können, während Sie sie auflisten. Wenn die Objekte in der Liste jedoch veränderlich sind, ist es durchaus sinnvoll, diese zu ändern, während Sie die Liste auflisten.
Trevor Pilley
@ TrevorPilley Ich bin anderer Meinung. Wenn erwartet wird, dass die Sammlung unveränderlich ist, ist die Erwartung eines Verbrauchers, dass die Mitglieder nicht von externen Akteuren mutiert werden, was als Nebeneffekt bezeichnet wird und die Unveränderlichkeitserwartung verletzt. Verstößt gegen POLA und implizit gegen LSP.
Jimmy Hoffa
Wie wäre es mit ToList? msdn.microsoft.com/en-us/library/bb342261.aspx Es erkennt nicht, ob es durch Yield generiert wurde, sondern macht es irrelevant.
Luiscubal

Antworten:

22

Wie ich es verstehe, scheint Ihre Frage auf einer falschen Prämisse zu beruhen. Lassen Sie mich sehen, ob ich die Argumentation rekonstruieren kann:

  • Der verlinkte Artikel beschreibt, wie sich automatisch generierte Sequenzen "träge" verhalten und wie dies zu einem kontraintuitiven Ergebnis führen kann.
  • Daher kann ich feststellen, ob eine bestimmte Instanz von IEnumerable dieses verzögerte Verhalten aufweisen wird, indem ich überprüfe, ob es automatisch generiert wird.
  • Wie mache ich das?

Das Problem ist, dass die zweite Prämisse falsch ist. Selbst wenn Sie feststellen könnten, ob eine bestimmte IEnumerable das Ergebnis einer Iteratorblocktransformation ist (und ja, es gibt Möglichkeiten, dies zu tun), würde dies nicht helfen, da die Annahme falsch ist. Lassen Sie uns veranschaulichen, warum.

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;
  }
}

Okay, wir haben vier Methoden. S1 und S2 sind automatisch generierte Sequenzen; S3 und S4 sind manuell erzeugte Sequenzen. Nehmen wir nun an, wir haben:

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

Das Ergebnis für S1 und S4 ist 0; Jedes Mal, wenn Sie die Sequenz aufzählen, erhalten Sie einen neuen Verweis auf ein erstelltes M. Das Ergebnis für S2 und S3 ist 100; Jedes Mal, wenn Sie die Sequenz aufzählen, erhalten Sie denselben Verweis auf M, den Sie beim letzten Mal erhalten haben. Ob der Sequenzcode automatisch generiert wird oder nicht, ist orthogonal zur Frage, ob die aufgezählten Objekte eine referenzielle Identität haben oder nicht. Diese beiden Eigenschaften - automatische Generierung und referenzielle Identität - haben eigentlich nichts miteinander zu tun. Der Artikel, den Sie verlinkt haben, widerspricht ihnen etwas.

Sofern ein Sequenzanbieter nicht dokumentiert ist, dass er Objekte mit referentieller Identität immer anbietet , ist es nicht ratsam , dies anzunehmen.

Eric Lippert
quelle
2
Wir alle wissen, dass Sie hier die richtige Antwort haben werden, das ist selbstverständlich. Aber wenn Sie den Begriff "referentielle Identität" etwas genauer definieren könnten, könnte er gut sein. Ich lese ihn als "unveränderlich", aber das liegt daran, dass ich unveränderlich in C # verschmelze und jedes get- oder value-Objekt einen neuen zurückgebe, was ich denke ist das Gleiche, worauf Sie sich beziehen. Obwohl die gewährte Unveränderlichkeit auf verschiedene andere Arten erreicht werden kann, ist dies die einzige vernünftige Methode in C # (die ich kenne, Sie kennen möglicherweise Methoden, die mir nicht bekannt sind).
Jimmy Hoffa
2
@JimmyHoffa: Zwei Objektreferenzen x und y haben "referenzielle Identität", wenn Object.ReferenceEquals (x, y) true zurückgibt.
Eric Lippert
Immer schön eine Antwort direkt von der Quelle zu bekommen. Danke Eric.
ConditionRacer
10

Ich denke, das Schlüsselkonzept ist, dass Sie jede IEnumerable<T>Sammlung als unveränderlich behandeln sollten : Sie sollten die Objekte daraus nicht ändern, Sie sollten eine neue Sammlung mit neuen Objekten erstellen:

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

(Oder schreiben Sie dasselbe mit LINQ genauer Select().)

Wenn Sie die Elemente in der Auflistung tatsächlich ändern möchten, verwenden Sie nicht IEnumerable<T>, verwenden Sie IList<T>(oder möglicherweise, IReadOnlyList<T>wenn Sie die Auflistung selbst nicht ändern möchten, nur die Elemente in der Auflistung, und Sie sind auf .Net 4.5):

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

Auf diese Weise IEnumerable<T>schlägt die Kompilierung fehl , wenn jemand versucht, Ihre Methode mit zu verwenden .

svick
quelle
1
Der wahre Schlüssel ist, wie Sie sagten, eine neue Zahl mit den Änderungen zurückzugeben, wenn Sie etwas ändern möchten. Ienumerable als ein Typ ist vollkommen brauchbar und hat keinen Grund, geändert zu werden, es ist nur die Technik für die Verwendung, die verstanden werden muss, wie Sie sagten. +1, um zu erwähnen, wie mit unveränderlichen Typen gearbeitet wird.
Jimmy Hoffa
1
Etwas verwandt - Sie sollten auch jeden IEnumerable so behandeln, als ob er über eine verzögerte Ausführung erfolgt. Was möglicherweise zum Zeitpunkt des Abrufs Ihrer IEnumerable-Referenz zutrifft, trifft möglicherweise nicht zu, wenn Sie tatsächlich eine Aufzählung durchführen. Wenn Sie beispielsweise eine LINQ-Anweisung innerhalb einer Sperre zurückgeben, kann dies zu interessanten und unerwarteten Ergebnissen führen.
Dan Lyons
@ JimmyHoffa: Ich stimme zu. Ich gehe noch einen Schritt weiter und versuche zu vermeiden, IEnumerablemehr als einmal zu wiederholen. Die Notwendigkeit, dies mehr als einmal zu tun, kann vermieden werden, indem ToList()die Liste aufgerufen und durchlaufen wird , obwohl ich in dem speziellen Fall, der durch das OP gezeigt wird, Svicks Lösung bevorzuge, dass DoubleXValue auch eine IEnumerable zurückgibt. Natürlich würde ich in echtem Code wahrscheinlich verwenden LINQ, um diese Art von Code zu generieren IEnumerable. Die Wahl von svick yieldist jedoch im Zusammenhang mit der Frage des OP sinnvoll.
Brian
@ Brian Ja, ich wollte den Code nicht zu sehr ändern, um meine Meinung zu sagen. In echtem Code hätte ich verwendet Select(). Und in Bezug auf mehrere Iterationen von IEnumerable: ReSharper ist dabei hilfreich, da es Sie warnen kann, wenn Sie dies tun.
Svick
5

Ich persönlich denke, das in diesem Artikel hervorgehobene Problem rührt von der übermäßigen Nutzung der IEnumerableBenutzeroberfläche durch viele C # -Entwickler in diesen Tagen her. Es scheint der Standardtyp geworden zu sein, wenn Sie eine Sammlung von Objekten benötigen - aber es gibt eine Menge Informationen, IEnumerabledie Ihnen einfach nicht sagen ... Entscheidend ist, ob sie erneut bestätigt wurden oder nicht *.

Wenn Sie feststellen, dass Sie feststellen müssen , ob a IEnumerablewirklich ein IEnumerableoder etwas anderes ist, ist die Abstraktion durchgesickert und Sie befinden sich wahrscheinlich in einer Situation, in der Ihr Parametertyp etwas zu locker ist. Wenn Sie die zusätzlichen Informationen benötigen, die IEnumerableallein nicht zur Verfügung stehen, machen Sie diese Anforderung explizit, indem Sie eine der anderen Schnittstellen wie ICollectionoder auswählen IList.

Edit für die Downvoter Bitte gehen Sie zurück und lesen Sie die Frage sorgfältig. Das OP fragt nach einer Möglichkeit, um sicherzustellen, dass IEnumerableInstanzen materialisiert wurden, bevor sie an seine API-Methode übergeben werden. Meine Antwort beantwortet diese Frage. Es gibt ein größeres Problem in Bezug auf die ordnungsgemäße Verwendung IEnumerable, und die Erwartungen an den vom OP eingefügten Code beruhen sicherlich auf einem Missverständnis dieses umfassenderen Problems, aber das OP fragte nicht, wie es richtig verwendet IEnumerableoder wie mit unveränderlichen Typen gearbeitet werden soll . Es ist nicht fair, mich dafür zu beleidigen, dass ich eine nicht gestellte Frage nicht beantwortet habe.

* Ich verwende den Begriff reify, andere verwenden stabilizeoder materialize. Ich glaube nicht, dass die Community gerade eine gemeinsame Konvention verabschiedet hat.

MattDavey
quelle
2
Ein Problem mit ICollectionund IListist, dass sie veränderlich sind (oder zumindest so aussehen). IReadOnlyCollectionund IReadOnlyListvon .Net 4.5 lösen einige dieser Probleme.
Svick
Bitte erklären Sie die Ablehnung?
MattDavey
1
Ich bin nicht einverstanden, dass Sie die Typen ändern sollten, die Sie verwenden. Ienumerable ist ein großartiger Typ und kann oft den Vorteil der Unveränderlichkeit haben. Ich denke, das eigentliche Problem ist, dass die Leute nicht verstehen, wie man mit unveränderlichen Typen in C # arbeitet. Es ist nicht überraschend, dass es sich um eine äußerst zustandsbehaftete Sprache handelt, weshalb es für viele C # -Entwickler ein neues Konzept ist.
Jimmy Hoffa
Nur ienumerable erlaubt die verzögerte Ausführung. Wenn Sie es als Parameter nicht zulassen, wird eine ganze Funktion von C # entfernt
Jimmy Hoffa
2
Ich habe dafür gestimmt, weil ich denke, dass es falsch ist. Ich denke, das liegt im Sinne von SE. Um sicherzustellen, dass die Leute keine falschen Informationen auf Lager haben, ist es an uns allen, diese Antworten abzustimmen. Vielleicht irre ich mich, vielleicht irre ich mich und du hast recht. Es liegt an der breiteren Community, durch Abstimmung zu entscheiden. Tut mir leid, dass Sie es persönlich nehmen, wenn es ein Trost ist, dass ich viele negative Antworten erhalten habe, die ich geschrieben und zusammenfassend gelöscht habe, weil ich akzeptiere, dass die Community der Meinung ist, dass es falsch ist, und deshalb bin ich unwahrscheinlich, dass ich Recht habe.
Jimmy Hoffa