5 Stimmen

Beschränkungen des dynamischen Typs in C#

Könnten Sie mir einige Gründe für die Einschränkungen des dynamischen Typs in C# geben? Ich habe darüber in "Pro C# 2010 and the .NET 4 platform" gelesen. Hier ist ein Auszug (falls es hier illegal ist, Bücher zu zitieren, sagen Sie es mir und ich werde den Auszug entfernen):

Obwohl viele Dinge mit dem dynamischen Schlüsselwort definiert werden können, gibt es einige Einschränkungen hinsichtlich seiner Verwendung. Auch wenn sie keine Showstopper sind, beachten Sie, dass ein dynamisches Datenobjekt keine Verwendung von Lambda-Ausdrücken oder C#-anonymen Methoden machen kann, wenn es eine Methode aufruft. Zum Beispiel wird der folgende Code immer zu Fehlern führen, auch wenn die Zielmethode tatsächlich einen Delegatenparameter akzeptiert, der einen String-Wert annimmt und void zurückgibt.

dynamic a = GetDynamicObject(); 
// Fehler! Methoden auf dynamischen Daten können keine Lambdas verwenden! 
a.Method(arg => Console.WriteLine(arg));

Um diese Einschränkung zu umgehen, müssen Sie direkt mit dem zugrunde liegenden Delegaten arbeiten, unter Verwendung der Techniken, die in Kapitel 11 beschrieben sind (anonyme Methoden und Lambda-Ausdrücke, usw.). Eine weitere Einschränkung ist, dass ein dynamischer Datenpunkt keine Verwendung von Erweiterungsmethoden (siehe Kapitel 12) verstehen kann. Leider würden dazu auch alle Erweiterungsmethoden gehören, die aus den LINQ-APIs stammen. Daher hat eine mit dem Schlüsselwort dynamic deklarierte Variable sehr begrenzte Verwendung in LINQ to Objects und anderen LINQ-Technologien:

dynamic a = GetDynamicObject(); 
// Fehler! Dynamische Daten finden die Select() Erweiterungsmethode nicht! 
var data = from d in a select d;

Vielen Dank im Voraus.

15voto

Eric Lippert Punkte 628543

Die Vermutungen von Tomas sind ziemlich gut. Sein Denken über Erweiterungsmethoden ist genau richtig. Grundsätzlich benötigen wir, um Erweiterungsmethoden zu verwenden, dass die Aufrufstelle zur Laufzeit auf irgend eine Weise wissen, welche using-Direktiven zum Kompilierzeitpunkt in Kraft waren. Wir hatten einfach nicht genug Zeit oder Budget, um ein System zu entwickeln, mit dem diese Informationen in die Aufrufstelle persistiert werden könnten.

Bei Lambdas ist die Situation tatsächlich komplexer als das einfache Problem festzustellen, ob die Lambda in einen Ausdrucksbaum oder einen Delegaten umgewandelt wird. Betrachten Sie das Folgende:

d.M(123)

wo d ein Ausdruck vom Typ dynamic ist. *Welches Objekt sollte zur Laufzeit als Argument an die Aufrufstelle "M" übergeben werden? Offensichtlich verpacken wir 123 und übergeben das. Dann betrachtet der Überlastungsauflösungsalgorithmus im Laufzeitbinder den Laufzeittyp von d und den Kompilierzeittyp der Ganzzahl 123 und arbeitet damit.

Was ist, wenn es jetzt

d.M(x=>x.Foo())

Was sollen wir jetzt als Argument übergeben? Wir haben keine Möglichkeit, "Lambda-Methode einer Variablen, die eine unbekannte Funktion namens Foo aufruft, in welchem auch immer der Typ von x herausstellt" zu repräsentieren.

Angenommen, wir wollten dieses Feature implementieren: Was müssten wir implementieren? Zuerst bräuchten wir eine Möglichkeit, eine ungebundene Lambda zu repräsentieren. Ausdrucksbäume sind grundsätzlich nur für die Darstellung von Lambdas, in denen alle Typen und Methoden bekannt sind. Wir müssten eine neue Art von "untypisiertem" Ausdrucksbaum erfinden. Und dann müssten wir alle Regeln für die Lambda-Bindung im Laufzeitbinder implementieren.

Betrachten Sie diesen letzten Punkt. Lambdas können Anweisungen enthalten. Die Implementierung dieses Features erfordert, dass der Laufzeitbinder den gesamten semantischen Analyser für jede mögliche Anweisung in C# enthalten muss.

Das hätte weit über unser Budget hinausgehen würde. Wir würden immer noch an C# 4 arbeiten, wenn wir dieses Feature implementieren wollten.

Leider bedeutet dies, dass LINQ nicht sehr gut mit dynamic funktioniert, denn LINQ verwendet natürlich untypisierte Lambdas an vielen Stellen. Hoffentlich werden wir in einer hypothetischen zukünftigen Version von C# einen besser ausgestatteten Laufzeitbinder haben und die Fähigkeit haben, homoikonische Darstellungen von ungebundenen Lambdas zu machen. Aber ich würde nicht darauf warten, wenn ich du wäre.

UPDATE: Ein Kommentar fragt nach Klärung des Punktes zum semantischen Analyser.

Betrachten Sie die folgenden Überladungen:

class C {
  public void M(Func f) { ... }
  public void M(Func f) { ... }
  ...
}

und ein Aufruf

d.M(x=> { using(x) { return 123; } });

Angenommen, dass d zum Kompilierzeitpunkt Typ dynamic und zur Laufzeit Typ C ist. Was muss der Laufzeitbinder tun?

Der Laufzeitbinder muss zur Laufzeit bestimmen, ob der Ausdruck x=>{...} in jeden der Delegattypen in jeder der Überladungen von M umgewandelt werden kann.

Um das zu tun, muss der Laufzeitbinder feststellen können, dass die zweite Überladung nicht anwendbar ist. Wenn sie anwendbar wäre, dann könnte man eine Ganzzahl als Argument für eine using-Anweisung haben, aber das Argument für eine using-Anweisung muss verwertbar sein. Das bedeutet, dass der Laufzeitbinder alle Regeln für die using-Anweisung kennen und korrekt angeben muss, ob jede mögliche Verwendung der using-Anweisung legal oder illegal ist.

Offensichtlich beschränkt sich dies nicht nur auf die using-Anweisung. Der Laufzeitbinder muss alle Regeln für alle C# kennen, um zu bestimmen, ob eine bestimmte Anweisungslambda in einen bestimmten Delegattyp umwandelbar ist.

Wir hatten keine Zeit, einen Laufzeitbinder zu schreiben, der im Wesentlichen einen völlig neuen C#-Compiler darstellt, der DLR-Bäume anstelle von IL generiert. Indem wir keine Lambdas zulassen, müssen wir nur einen Laufzeitbinder schreiben, der weiß, wie er Methodenaufrufe, arithmetische Ausdrücke und einige andere einfache Arten von Aufrufstellen bindet. Das Zulassen von Lambdas macht das Problem der Laufzeitbindung um einiges teurer, um zu implementieren, zu testen und zu warten.

8voto

Tomas Petricek Punkte 233658

Lambdas: Ich denke, dass ein Grund dafür, dass Lambdas nicht als Parameter für dynamische Objekte unterstützt werden, ist, dass der Compiler nicht wissen würde, ob er das Lambda als Delegate oder als Ausdrucksbaum kompilieren soll.

Wenn Sie ein Lambda verwenden, entscheidet der Compiler basierend auf dem Typ des Zielparameters oder der Variablen. Wenn es sich um Func<...> (oder ein anderes Delegate) handelt, wird das Lambda als ausführbares Delegate kompiliert. Wenn das Ziel Expression<...> ist, wird das Lambda in einen Ausdrucksbaum kompiliert.

Wenn Sie jetzt einen dynamic-Typ haben, wissen Sie nicht, ob der Parameter ein Delegate oder ein Ausdruck ist, sodass der Compiler nicht weiß, was zu tun ist!

Erweiterungsmethoden: Ich denke, der Grund hierfür ist, dass es schwierig (und vielleicht auch ineffizient) wäre, Erweiterungsmethoden zur Laufzeit zu finden. Zunächst müsste die Laufzeitumgebung wissen, welche Namespaces mit using referenziert wurden. Dann müsste sie alle Klassen in allen geladenen Assemblys durchsuchen, diejenigen filtern, die zugänglich sind (nach Namespace) und dann nach Erweiterungsmethoden suchen...

2voto

Brian Punkte 115257

Eric (und Tomas) sagen es gut, aber hier ist, wie ich darüber denke.

Diese C# Anweisung

a.Method(arg => Console.WriteLine(arg)); 

hat keine Bedeutung ohne eine Menge von Kontext. Lambda-Ausdrücke haben selbst keine Typen, sondern sie sind konvertierbar zu delegate (oder Expression) Typen. Also der einzige Weg, um die Bedeutung zu erfassen, besteht darin, einen Kontext bereitzustellen, der die Lambda-Funktion dazu zwingt, in einen bestimmten Delegattyp konvertiert zu werden. Dieser Kontext ist in der Regel (wie in diesem Beispiel) Überladenauflösung; gegeben den Typ von a und den verfügbaren Überladungen Method dieses Typs (einschließlich Erweiterungsmitgliedern), können wir möglicherweise einen Kontext platzieren, der der Lambda-Funktion Bedeutung verleiht.

Ohne diesen Kontext, um die Bedeutung zu erzeugen, müssen Sie alle möglichen Informationen über die Lambda-Funktion bündeln in der Hoffnung, die Unbekannten zur Laufzeit zu verknüpfen. (Welchen IL könnten Sie generieren?)

In großem Kontrast dazu wird es viel einfacher, wenn Sie einen bestimmten Delegattyp festlegen,

a.Method(new Action(arg => Console.WriteLine(arg))); 

Plötzlich! Dinge sind viel einfacher. Egal welcher Code sich innerhalb der Lambda-Funktion befindet, wir wissen jetzt genau, welchen Typ sie hat, was bedeutet, dass wir IL genau so kompilieren können wie jede andere Methodenrumpf (wir wissen jetzt beispielsweise, welche der vielen Überladungen von Console.WriteLine wir aufrufen). Und dieser Code hat einen spezifischen Typ (Action), was bedeutet, dass es für den Laufzeitbinder einfach ist zu prüfen, ob a eine Method hat, die dieses Art von Argument akzeptiert.

In C# ist eine nackte Lambda-Funktion fast bedeutungslos. C# Lambdas benötigen einen statischen Kontext, um ihnen Bedeutung zu verleihen und Mehrdeutigkeiten auszuschließen, die sich aus vielen möglichen Umwandlungen und Überladungen ergeben. Ein typisches Programm bietet diesen Kontext mühelos, aber der dynamic Fall mangelt es an diesem wichtigen Kontext.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X