2010-05-30 7 views
11

nicht allzu lange Zeit, bevor ich entdeckt habe, dass neues dynamic Schlüsselwort nicht gut mit dem C# 's foreach Anweisung funktioniert:C# 4.0 ‚dynamisch‘ und foreach-Anweisung

using System; 

sealed class Foo { 
    public struct FooEnumerator { 
     int value; 
     public bool MoveNext() { return true; } 
     public int Current { get { return value++; } } 
    } 

    public FooEnumerator GetEnumerator() { 
     return new FooEnumerator(); 
    } 

    static void Main() { 
     foreach (int x in new Foo()) { 
      Console.WriteLine(x); 
      if (x >= 100) break; 
     } 

     foreach (int x in (dynamic)new Foo()) { // :) 
      Console.WriteLine(x); 
      if (x >= 100) break; 
     } 
    } 
} 

Ich habe, dass über die iteriert erwartet dynamic Variable sollte vollständig funktionieren, als ob der Typ der Auflistungsvariablen zur Kompilierzeit bekannt ist. Ich habe entdeckt, dass die zweite Schleife tatsächlich so ausgesehen, wenn kompiliert wird:

foreach (object x in (IEnumerable) /* dynamic cast */ (object) new Foo()) { 
    ... 
} 

und jeder Zugriff auf die x-Variable Ergebnisse mit der dynamischen Lookup/cast so C# ignoriert, dass ich angeben habe die richtigen x ' s Typ in der Foreach-Anweisung - das war ein bisschen überraschend für mich ... Und auch, C# -Compiler vollständig ignoriert diese Sammlung von dynamisch typisierte Variable Mai implementiert IEnumerable<T> Schnittstelle!

Die vollständige foreach Anweisung Verhalten ist in der C# 4.0-Spezifikation beschrieben 8.8.4 Die Foreach-Anweisung Artikel.

Aber ... Es ist durchaus möglich, das gleiche Verhalten zur Laufzeit zu implementieren! Es ist möglich, eine zusätzliche CSharpBinderFlags.ForEachCast Flagge, korrigieren Sie den ausgestrahlten Code sieht aus wie hinzuzufügen:

foreach (int x in (IEnumerable<int>) /* dynamic cast with the CSharpBinderFlags.ForEachCast flag */ (object) new Foo()) { 
    ... 
} 

Und fügen Sie einige zusätzliche Logik zu CSharpConvertBinder:

  • Wrap IEnumerable Sammlungen und IEnumerator ‚s zu IEnumerable<T>/IEnumerator<T>.
  • Wrap Collections implementiert Ienumerable<T>/IEnumerator<T> nicht, um diese Schnittstellen zu implementieren.

So heute foreach Anweisung iteriert über dynamic völlig verschieden von Iterieren statisch bekannter Sammlung Variable über und ignoriert vollständig die Typinformationen, vom Benutzer festgelegt. All das führt zu dem unterschiedlichen Iterationsverhalten (IEnumarble<T> -implementierende Sammlungen wird nur als IEnumerable -implementierend iteriert) und mehr als 150x Verlangsamung beim Iterieren über dynamic. Einfache Lösung führt zu einer viel besseren Leistung:

foreach (int x in (IEnumerable<int>) dynamicVariable) { 

Aber warum sollte ich Code so schreiben?

Es ist sehr schön zu sehen, dass manchmal C# 4.0 dynamic Werke vollständig gleich, wenn der Typ zum Zeitpunkt der Kompilierung bekannt sein wird, aber es ist leider sehr zu sehen, dass dynamic funktioniert völlig anders, wo es kann das gleiche wie statisch typisierte Code funktioniert .

Also meine Frage ist: Warum funktioniert foreach über dynamic funktioniert anders als foreach über etwas anderes?

+0

Ich interessiere mich sehr für das Verhalten, das ist völlig anders als statische 'foreach'! Das Leistungsproblem ist nur das Ergebnis von falschem Verhalten. Warum "IEnumerable" + dynamische Konvertierung verwenden, wobei statisch bekannt ist, dass "forech" über die Ganzzahl-Sequenz iteriert? – ControlFlow

Antwort

23

Zunächst einmal, um Lesern, die durch die Frage verwirrt sind, etwas Hintergrund zu erklären: die C# -Sprache erfordert tatsächlich nicht, dass die Sammlung eines "foreach" implementieren IEnumerable.Stattdessen muss entweder IEnumerable implementiert werden oder IEnumerable<T>, oder einfach eine GetEnumerator-Methode implementiert werden (und die GetEnumerator-Methode gibt etwas mit Current und MoveNext zurück, das mit dem erwarteten Muster übereinstimmt, usw.).

Das mag wie ein merkwürdiges Feature für eine statisch typisierte Sprache wie C# scheinen. Warum sollten wir "dem Muster entsprechen"? Warum nicht erfordern, dass Sammlungen IEnumerable implementieren?

Denken Sie an die Welt vor Generika. Wenn Sie eine Sammlung von Ints erstellen möchten, müssen Sie IEnumerable verwenden. Und daher würde jeder Aufruf von Current ein int enthalten, und dann würde der Anrufer ihn sofort wieder auf int zurücksetzen. Was langsam ist und Druck auf den GC erzeugt. Mit einem Muster-basierten Ansatz können Sie stark typisierte Sammlungen in C# 1.0 erstellen!

Heutzutage implementiert natürlich niemand dieses Muster; Wenn Sie eine stark typisierte Sammlung möchten, implementieren Sie IEnumerable<T> und Sie sind fertig. Wäre ein generisches Typsystem für C# 1.0 verfügbar gewesen, wäre es unwahrscheinlich, dass das Feature "Mit dem Muster übereinstimmen" überhaupt implementiert worden wäre.

Wie Sie bemerkt haben, anstatt für das Muster von suchen, erzeugt der Code für eine dynamische Sammlung in einer foreach sucht eine dynamische Umwandlung in IEnumerable (und führt dann eine Umwandlung von der von Strom an das zurückgegebene Objekt Typ der Schleifenvariable natürlich.) Ihre Frage ist also im Grunde: "Warum kann der Code, der durch die Verwendung des dynamischen Typs als Sammlungstyp von foreach erzeugt wird, nicht zur Laufzeit nach dem Muster suchen?"

Da es nicht mehr 1999 ist und selbst wenn es in den C# 1.0 Tagen zurück war, implementierten Sammlungen, die das Muster verwendeten, fast immer auch IEnumerable. Die Wahrscheinlichkeit, dass ein echter Benutzer C# 4.0-Code in Produktionsqualität schreibt, der eine foreach über eine Sammlung ausführt, die das Muster aber nicht IEnumerable implementiert, ist äußerst gering. Nun, wenn Sie in dieser Situation sind, ist das unerwartet, und es tut mir leid, dass unser Design Ihre Anforderungen nicht vorausgesehen hat. Wenn Sie der Meinung sind, dass Ihr Szenario tatsächlich häufig vorkommt und wir uns darüber im Klaren waren, wie selten es ist, veröffentlichen Sie bitte weitere Details zu Ihrem Szenario, und wir überlegen, dies für zukünftige hypothetische Versionen zu ändern.

Beachten Sie, dass die Konvertierung, die wir zu IEnumerable generieren, eine dynamische Konvertierung ist, nicht einfach ein Typprüfung. Auf diese Weise kann das dynamische Objekt teilnehmen; Wenn IEnumerable nicht implementiert wird, aber ein Proxy-Objekt bereitgestellt werden soll, kann dies geschehen.

Kurz gesagt, ist das Design von "dynamic foreach" "dynamisch das Objekt für eine IEnumerable-Sequenz zu fragen", anstatt "dynamisch jede Typprüfung durchzuführen, die wir zur Kompilierzeit durchgeführt hätten". Dies verletzt in der Theorie subtil das Designprinzip, dass die dynamische Analyse das gleiche Ergebnis liefert wie die statische Analyse, aber in der Praxis erwarten wir, dass die große Mehrheit der dynamisch zugänglichen Sammlungen funktioniert.

+0

1. Vielen Dank für Ihre Antwort, Eric! :) 2. Die "Match das Muster" -Feature ist nicht nur für "Welt vor Generika", es wird auch heutzutage verwendet, zum Beispiel durch so grundlegende ** generische ** Typ als "Liste " Typ (und die Gründe für Das ist das Gleiche - Leistungsvorteil. 3. Ich verstehe, dass "Match the Pattern" -Feature heutzutage sehr selten ist und eigentlich nicht wollen, dass C# dies überhaupt unterstützt, es ist nur die Frage über das Matching des dynamischen und statischen Verhaltens. – ControlFlow

+0

4. Es ist mir wichtig, das IEnumarable heutzutage vollständig zu ignorieren! Ich bevorzuge das Design von "dynamic foreach" wird "dynamisch das Objekt für eine IEnumerable Sequenz fragen, wenn foreach-Anweisung den T-Typ der IEnumerable-Sequenz sonst fragen". Dies wird dem statischen Verhalten viel näher kommen und mit der besseren Leistung resultieren. Das einzige Problem sind die Sammlungen, die nur 'IEnumerable' implementieren, was heutzutage selten ist und von 'CSharpConvertBinder' gehandhabt werden kann. – ControlFlow

+0

@ControlFlow Liste nicht verwenden "um das Muster passen", wie es IEnumerable direkt implementieren: public class Liste : IList , ICollection , IEnumerable , IList, ICollection, IEnumerable –

2

Aber warum sollte ich Code so schreiben?

In der Tat. Und warum würde der Compiler Code so schreiben? Sie haben jede Chance aus dem Weg geräumt, dass die Schleife optimiert werden konnte. Btw, Sie scheinen die IL falsch zu interpretieren, es ist wieder bindenden IEnumerable.Current, der MoveNext() Aufruf ist direkt und GetEnumerator() wird nur einmal aufgerufen. Was ich für angemessen halte, könnte das nächste Element ohne Probleme auf einen int werfen. Es könnte eine Sammlung verschiedener Typen sein, jeder mit seinem eigenen Bindemittel.

Verwandte Themen