Dieser Artikel wurde maschinell übersetzt.

Programmiererpraxis

Multiparadigmatic.NET, Teil 7: Parametrische Metaprogramming

Ted Neward

image: Ted NewardWenn ich einen Studenten University in einer Vorlesung Microeconomics Kalkül gefüllt war freigegebene der Professor einige Wörter, die von wis-Dom und Geräusche zu erzeugen für mich zu diesem Tag:

“Wenn während der mühsamen mühsam Details unserer gewählten Gegenstand, Sie selbst nicht den Grund warum wir über all diese Details mühsam slogging sind sichtbar finden, liegt in Ihrer Verantwortung, unterbrechen mir und sagen"Professor Anderson, was der Punkt ist?’ Und wir werden einige Zeit in Anspruch zurückgehen und erläutern, wie wir hier gekommen ist."

Leser, die durch alle sitzen wurde haben, die die Artikel dieser Reihe gut möglicherweise haben die Wand drücken wir zurückgehen und überprüfen, wie hier haben wir ein paar Augenblicke dauern.

Kurze Zusammenfassung

Im wesentlichen wie in seinem Buch "Multi-Paradigm Design für C++" (Addison-Wesley, 1998), von James Coplien beschrieben, die großen Teil das Schreiben in dieser Artikelreihe inspiriert, alle Programmierung ist eine Übung in der Erfassung von Gemeinsamkeiten – Code schreiben, der die Groß-und Kleinschreibung "Time" – und dann mit die Variabilität innerhalb der Sprache, um das Verhalten ermöglichen konstruiert oder Anders als unter bestimmten Umständen strukturiert sein.

Object-oriented programming, z. B. erfasst Gemeinsamkeiten in Klassen und Variabilität durch Vererbung, das Erstellen von Unterklassen, die diese Gemeinsamkeit ändern lässt. In der Regel wird dies durch das Verhalten bestimmter Teile der Klasse mithilfe der Methoden oder Meldungen, abhängig von der betreffenden Sprache ändern. Die Gemeinsamkeiten und der Variabilität des Projekts nicht immer Bedürfnissen in der objektorientierten Paradigma so ordentlich jedoch oder eine beliebige andere bestimmten des einzelnen Paradigma, darum – objektorientierte Programmierung wuchs der prozeduralen Programmierung wie beim Versuch, die Variabilität der prozeduralen Programmierung bieten ganz einfach sammeln konnte nicht.

Zum Glück sind die Sprachen, die von Microsoft Visual Studio 2010 Angeboten für Leser dieses Magazins, multiparadigmatic Sprachen, was bedeutet, dass sie mehrere unterschiedliche Programmierung Paradigmen zusammen in einer einzigen Sprache zeichnen. Coplien, da es drei wesentliche Paradigmen zusammenzufassen C++ als eine multiparadigmatic Sprache, zunächst identifiziert: verfahrensrechtlichen Objekt und metaprogrammatic (manchmal auch genauer genannt Metaobject). C++ wurde auch stark criticized als eine komplizierte Sprache, für den durchschnittlichen Entwickler zu Master-Shape, zu schwierig, vor allem, da war es schwierig, um festzustellen, wann die verschiedenen Funktionen der Sprache zu verwenden, um bestimmte Probleme zu lösen.

Moderne Sprachen entwickeln sich häufig in sehr multiparadigmatic Sprachen. F#-, c# und Visual Basic, als von Visual Studio 2010 fünf solcher Paradigmen direkte Unterstützung: prozeduralen, objektorientierte, dynamische und funktionale Metaobject. Alle drei Sprachen – vier, wenn Sie C + + / CLI in der Mischung – aus diesem Grund laufen Gefahr, die dasselbe Schicksal erleiden wie C++ fallen.

Ohne ein klares Verständnis der einzelnen der Paradigmen, die in diesen Sprachen gemischt, Entwickler können problemlos ausführen schädigt bevorzugten Feature-Trap, der, in denen Entwickler verlassen sich zu stark auf eine Funktion oder Paradigma zum Ausschluß von den anderen und landen zu kompliziert, Code, der schließlich umgeschrieben und ruft unmöglich zu erstellen. Zu oft dazu, und die Sprache der Brunt Developer Frustration, im Allgemeinen irgendwann in Aufrufe für eine neue Sprache oder nur Ruhestand führende tragen beginnt.

Verfahrens- und objektorientierter Paradigmen

Bisher haben wir gesehen, die Gemeinsamkeiten/Variabilität Analyse Verfahrens- oder strukturelle Programmierung, in dem wir erfassen Angleichung der Datenstrukturen und zum Betrieb auf diese Strukturen werden in verschiedenen Prozeduraufrufe, Fütterung Variabilität erstellen, indem Sie neue Prozeduraufrufe ausgeführt werden auf die gleichen Datenstrukturen erstellen angewendet. Wir haben die Gemeinsamkeit/Variabilität, um Objekte, auch gesehen, in denen wir Erfassen von Gemeinsamkeiten in Klassen und erstellen Sie Variabilität von Unterklassen von diesen Klassen und veränderbaren Bits und Teile davon über überschreibende Methoden oder Eigenschaften.

Bären, beachten Sie, das ein weiteres Problem ergibt sich insofern Vererbung (zum größten Teil) nur für positive Werte zur Variabilität ermöglicht – wir etwas von einer Basisklasse, wie z. B. Membermethode oder Feld kann nicht entfernt werden. In der CLR können wir abgeleitete zugängliche Memberimplementierung ausblenden, indem Sie shadowing (verwenden Sie das virtual-Schlüsselwort anstelle von das Override-Schlüsselwort in c#, z. B..). Allerdings bedeutet, dass Ihr Verhalten durch etwas anderes, nicht entfernen der sofortiges ersetzen. Felder bleiben unabhängig davon vorhanden.

Diese Beobachtung führt zu eine beunruhigende Offenbarung für einige: Objekte können nicht alles, was wir tun – zumindest nicht reine Objekte. Objekte können nicht z. B. Variabilität entlang strukturelle Vererbung Linien erfassen: eine Auflistung dieser Aufzeichnungen Stapel Verhalten der Klasse, aber für eine Vielzahl unterschiedlicher Datentypen (Ganzzahlen, Doubles, Zeichenfolgen usw.) kann nicht der strukturelle Unterschied zu erfassen. Erteilt, können wir innerhalb der CLR Einheitliches Typensystem verwenden. Referenz-Instanzen von System.Object und Downcasting speichern wir je nach Bedarf, aber, ist nicht dasselbe wie die Fähigkeit zum Erstellen eines Typs, das nur einen Typ speichert.

Diese Erkenntnis geschaltet uns Gegenstand Metaobject zu programmieren, wie wir nach Möglichkeiten suchen, um die Dinge außerhalb des traditionellen Objektachsen erfassen.

Die zuerst solche Meta-Ansatz werden in generative wurde, in dem Quellcode generiert wurde basierend auf einer Art von Vorlage. Dieser Ansatz ermöglicht es, einige Variabilität auf einer Vielzahl von verschiedenen Achsen, aber ist begrenzt (zum größten Teil) zu einem Ausführungsmodell Quellzeit. Es beginnt auch zerlegen, wie die Anzahl der Variierbarkeiten steigt da die Quelle Vorlage (in der Regel mit Entscheidungsfindung Anweisungen innerhalb der Vorlage-Sprache ausblenden) generierten Code irgendwie variieren zum Zeitpunkt der Generierung von Code und kann diese Komplexität in den Vorlagen erstellen.

Der zweite solche Meta-Ansatz war, Reflektierend oder attributive-Programmierung. Zur Laufzeit verwendet den Code die originalgetreue Metadaten-Einrichtungen der Plattform (Reflektion), um Code zu untersuchen, und je nach, was es sieht unterschiedlich Verhalten.

Diese Flexibilität der Implementierung oder Verhalten im Zusammenhang mit Common Language Runtime Entscheidungsfindung zulässig, aber Ihre eigenen Grenzen eingeführt: keine Typbeziehungen sind in einem reflektierenden/Attributive Design, d. h. es gibt keine Möglichkeit, um programmgesteuert sicherzustellen, dass nur Datenbank dauerhafte Typen in eine Methode übergeben werden können, z. B. im Gegensatz zu XML-dauerhafte Typen. Das Fehlen einer Vererbung oder anderen Beziehung bedeutet, dass ein gewisses Maß an Typsicherheit (und daher eine wichtige Fähigkeit, Fehler zu vermeiden) daher verloren gegangen ist.

Das bringt uns zu der dritten Metaobject Einrichtung innerhalb der.NET-Umgebung: parametrische Polymorphie. Dies bedeutet, dass die Möglichkeit zum Definieren von Typen, die Typen als Parameter haben. Oder setzen Sie ganz einfach nur, welche die Microsoft.NET Framework bezeichnet als Generika.

Generika

In seiner einfachsten Form zulassen Generika kompilieren Erstellen von Typen, die Teile ihrer Struktur, die zum Zeitpunkt der Kompilierung von Clientcode angegeben haben. Mit anderen Worten, muss der Entwickler eine Stapelauflistung verhaltensbasierte keinen am wissen die Zeit seiner Bibliothek ist kompiliert, welche Arten von Typen seiner Clients sinnvoll, in verschiedenen Instanzen zu speichern – sie diese Informationen liefern, beim Erstellen von Instanzen dieser Sammlung.

In einem früheren Artikel erkannten wir beispielsweise, dass die Definition eines Punkttyps kartesischen eine Entscheidung ahead of Time (seitens des Entwicklers Point) auf die Darstellung der Achsenwerte (X und Y) erfordert. Sollten sie ganzzahlige Werte sein? Sie dürfen negativ sein?

Ein kartesisches Punkt in Mathematik verwendet konnte sehr gut eine Gleitkommazahl und negativ sein müssen. Ein kartesisches Punkt zum Darstellen eines Pixels auf einem Computer, auf dem Bildschirm muss positiv, Integrale und wahrscheinlich innerhalb eines bestimmten numerischen Bereichs sein, da Computerbildschirmen von 4 Milliarden von 4 Mrd. nicht sind noch alltäglich verwendet.

Folglich auf der Oberfläche von es ein Well-Design kartesischen Punkt Bibliothek müssen mehrere unterschiedliche Punkttypen: mit Bytes ohne Vorzeichen als x und Y-Felder mit einem verdoppelt sich als x und Y Felder usw.. Das Verhalten in den meisten Fällen ist identisch für alle diese Typen, die einen Verstoß gegen die Bestrebung, Gemeinsamkeit erfassen deutlich hervorheben, (Umgangssprachlich als TROCKENEN Prinzip bekannt: "Don't wiederholen sich").

Verwenden parametrische Polymorphie, können wir diese Gemeinsamkeit ziemlich ordentlich erfassen:

class Point2D<T> {
  public Point2D(T x, T y) { this.X = x; this.Y = y; }

  public T X { get; private set; }
  public T Y { get; private set; }
  // Other methods left to the reader's imagination
}

Entwickler kann nun genau die Bereichs- und Eigenschaften des kartesischen Punkt angeben, die er verwenden möchte. Bei der Arbeit in einer mathematischen Domäne erstellt er Instanzen Point2D <double> Werte, und bei der Arbeit mit diesen Werten auf dem Bildschirm anzuzeigen, erstellt er Instanzen von Point2D <sbyte> oder Point2D <ushort>. Jeweils eine eigene Distinkter Datentyp ist, so versucht, vergleichen oder Point2D zuweisen <sbyte> Um Point2D <double> Auswahlsystem fehl zum Zeitpunkt der Kompilierung, wie eine stark typisierte Sprache bevorzugen würden.

Wie bereits beschrieben, hat jedoch der Point2D Typ noch einige Nachteile. Wir haben die Angleichung der kartesischen Punkt erfasst sicherlich, aber wir haben im wesentlichen darf für beliebige Werte für die X- und Y-Werte verwendet werden. Während dies praktisch in bestimmten Szenarios nützlich sein könnte ("In diesem Diagramm sind wir die Bewertungen charting jede Person hat einen Film"), als in der Regel versuchen, eine Point2D erstellen <DateTime> ist potenziell unklare und versuchen, eine Point2D erstellen <System.Windows.Forms.Form> fast immer ist. Wir müssen eine Art von negativen Variabilität hier einführen (oder auf Wunsch drosseln wieder das Maß an Variabilität der positiven), beschränken die Arten von Typen, die Achsenwerte in einem Point2D sein können.

Viele.NET-Sprachen erfassen diese negative Streuung über Parametrisierung Einschränkungen – manchmal auch als Typeinschränkungen bezeichnet – explizit Bedingungen beschrieben, muss der Typparameter verfügen:

class Point2D<T> where T : struct {
  public Point2D(T x, T y) { this.X = x; this.Y = y; }

  public T X { get; private set; }
  public T Y { get; private set; }
  // Other methods left to the reader's imagination
}

Dies bedeutet, dass der Compiler nichts für t nicht akzeptiert, der ein Werttyp ist.

Um ehrlich zu sein, dies nicht genau eine negative Variabilität per se, aber es dient als eine im Vergleich zu das Problem der Versuch, bestimmte Funktionen zu entfernen, die ein gewisses Maß an, welche eine true negative Variabilität würde annähert.

Unterschiedliche Verhalten

Parametrische Polymorphie wird in der Regel verwendet, um die Variabilität der strukturellen Achse bieten, aber als die Entwickler der C++-Boost-Bibliotheken nachgewiesen, es ist nicht der einzige Achse entlang, die er arbeiten kann. Mit intelligenten Einsatz von Typeinschränkungen können wir Generika auch einen Mechanismus für die Richtlinie bereitstellen, in dem Clients einen verhaltensbasierten Mechanismus für Objekte, die erstellt wird angeben können.

Berücksichtigen das traditionelle Problem der Diagnoseprotokollierung für einen Moment: damit die diagnose von Problemen mit Code ausgeführt wird, auf einem Server (oder sogar auf Client-Rechnern), wir die Ausführung von Code über die Codebasis verfolgen möchten. Dies bedeutet in der Regel Nachrichten in Datei schreiben. Aber manchmal wir die Meldungen auf der Konsole, zumindest für bestimmte Szenarien angezeigt werden soll, und manchmal möchten wir die weggeworfen Meldungen. Behandeln von Diagnoseprotokollierung wurde ein schwieriges Problem im Laufe der Jahre, und eine Reihe von Lösungen vorgeschlagen. Die Lektionen der Verstärkung bieten auch einen neuen Ansatz.

Wir starten, indem er eine Schnittstelle definiert:

interface ILoggerPolicy {
  void Log(string msg);
}

Es ist eine unkomplizierte Schnittstelle mit einer oder mehreren Methoden definieren des Verhaltens, die wir variieren möchten, was wir über eine Reihe von Untertypen dieser Schnittstelle:

class ConsoleLogger : ILoggerPolicy {
  public void Log(string msg) { Console.WriteLine(msg); }
}

class NullLogger : ILoggerPolicy {
  public void Log(string msg) { }
}

Hier haben wir zwei mögliche Implementierungen, von denen die Protokollmeldung in die Konsole, geschrieben während der andere Weg, löst.

Verwenden dies erfordert Clients zu nutzen, indem Sie die Protokollierung als einen typisierten Parameter zu deklarieren und erstellen eine Instanz von Aktionen, die tatsächliche Protokollierung:

class Person<A> where A : ILoggerPolicy, new() {
  public Person(string fn, string ln, int a) {
    this.FirstName = fn; this.LastName = ln; this.Age = a;
    logger.Log("Constructing Person instance");
  }

  public string FirstName { get; private set; }
  public string LastName { get; private set; }
  public int Age { get; private set; }

  private A logger = new A();
}

Beschreiben, welcher Typ des Protokollierungsmoduls verwendet, dann lediglich einen Konstruktor-Time-Parameter übergeben wird, wird wie folgt:

Person<ConsoleLogger> ted = 
  new Person<ConsoleLogger>("Ted", "Neward", 40);
var anotherTed  = 
  new Person<NullLogger>("Ted", "Neward", 40);

Dieser Mechanismus ermöglicht es Entwicklern, erstellen eigene benutzerdefinierte Protokollierung-Implementierungen und einfach Einstecken von Person < > verwendet werden Instanzen, ohne die < Person > Entwickler müssen wissen, alle Einzelheiten der Implementierung Protokollierung verwendet. Aber zahlreiche andere Ansätze auch dazu, wie z. B. dass Logger-Feld oder Eigenschaft, die eine Protokollierung übergibt die Instanz von außerhalb (oder erhalten eine über einen Dependency Injection-Ansatz). Der Generika-basierter Ansatz hat den Vorteil, der der Feld basierenden Ansatz nicht, jedoch während der Kompilierung Unterscheidung ist: eine Person <ConsoleLogger> ist eine eindeutige und separate von Personen <NullLogger>.

Money, Money, Money

Ein Problem, das Entwickler Übel ist, dass Mengen nutzlos sind, ohne die Einheiten quantifiziert werden. Tausend verhindern ist eindeutig nicht dasselbe wie 1.000 Pferde oder 1.000 Mitarbeiter oder 1.000 Pizzas;. Noch, sind nur so klar 1.000 verhindern und 10 Dollar, in der Tat den gleichen Wert.

Dies wird sogar noch wichtiger in mathematischen Berechnungen, in denen die Notwendigkeit, die Einheiten (Grad/Rad, Meter, Fahrenheit/Grad Celsius) zu erfassen umso wichtiger ist, insbesondere dann, wenn Sie Software für die Anleitung für einen sehr großen Rocket schreiben. Berücksichtigen Sie die Ariane 5, dessen maiden Flight self-destructed aufgrund von Fehler bei der Konvertierung werden musste. Oder der NASA-Sonde, Mars, von denen in der Martian slammed Landschaft mit voller Geschwindigkeit aufgrund von einem Konvertierungsfehler.

Vor kurzem haben entschieden, neue Sprachen wie F#-Einheiten als direkte Sprache-Funktion acclimate, aber auch c# und Visual Basic können ähnliche Arten von Inhalten, dank zu Generika tun.

Kanäle von unserer inneren Martin Fowler, zunächst mit einer einfachen Money-Klasse, die den Betrag (Menge) und Währung (Typ), der einen bestimmten Geldbetrag kennt:

class Money {
  public float Quantity { get; set; }
  public string Currency { get; set; }
}

Auf der Oberfläche scheint dies funktionsfähig, aber vor lange Wir planten, Wert-ähnliche Dinge mit dieser Option starten möchten z. B. hinzufügen Money Instanzen zusammen (eine ziemlich gemeinsame Sache mit Geld zu tun, wenn Sie darüber nachdenken):

class Money {
  public float Quantity { get; set; }
  public string Currency { get; set; }

  public static Money operator +(Money lhs, Money rhs) {
    return new Money() { 
      Quantity = lhs.Quantity + rhs.Quantity, Currency = lhs.Currency };
  }
}

Natürlich ist das Problem auftreten, wenn wir versuchen, US hinzuzufügen wechseln Dollar (USD) und europäische Euro (EUR) zusammen, z. B., wenn wir zum Mittagessen out gehen (schließlich jeder kennt Europäer brew beste Bier, aber Amerikaner stellen die beste Pizza):

var pizza = new Money() { 
  Quantity = 4.99f, Currency = "USD" };
var beer = new Money() { 
  Quantity = 3.5f, Currency = "EUR" };
var lunch = pizza + beer;

Jeder Benutzer, der einen kurzer Blick auf die finanziellen Dashboards nimmt geht zu erkennen, dass jemand abgerissen erste ist – der Euro werden in Euro im Verhältnis 1: 1 umgewandelt wird. Um unbeabsichtigte Betrug zu verhindern, wir möchten wahrscheinlich stellen Sie sicher, dass der Compiler weiß nicht, um USD EUR zu konvertieren, ohne den Umweg über einen zugelassenen Konvertierungsvorgang, der den aktuellen Umrechnungskurs sucht (finden Sie unter Abbildung 1).

Abbildung 1 Konvertierung in Reihenfolge ist

class USD { }
class EUR { }
class Money<C> {
  public float Quantity { get; set; }
  public C Currency { get; set; }

  public static Money<C> operator +(
    Money<C> lhs, Money<C> rhs) {
    return new Money<C>() { 
      Quantity = lhs.Quantity + rhs.Quantity, 
      Currency = lhs.Currency };
  }
}
...
var pizza = new Money<USD>() { 
  Quantity = 4.99f, Currency = new USD() };
var beer = new Money<EUR>() { 
  Quantity = 3.5f, Currency = new EUR() };
var lunch = pizza + beer;    // ERROR

Beachten Sie, wie USD und EUR im Grunde nur als Platzhalter, dem Compiler bietet etwas, verglichen werden. Wenn die beiden C-Typparameter nicht überein, ist es ein Problem.

Natürlich, wir haben auch die Möglichkeit, die beiden kombinieren verloren, und manchmal, wenn wir genau dies möchten. Dies erfordert etwas mehr parametrische Syntax (finden Sie unter Abbildung 2).

Abbildung 2 Arten absichtlich kombinieren

class USD { }
class EUR { }
class Money<C> {
  public float Quantity { get; set; }
  public C Currency { get; set; }

  public static Money<C> operator +(
    Money<C> lhs, Money<C> rhs) {
    return new Money<C>() { 
      Quantity = lhs.Quantity + rhs.Quantity, 
      Currency = lhs.Currency };
  }

  public Money<C2> Convert<C2>() where C2 : new() {
    return new Money<C2>() { Quantity = 
      this.Quantity, Currency = new C2() };
  }
}

Dies ist eine spezialisierte generische Methode innerhalb einer generischen Klasse und die < > Syntax, die nach den Namen der Methode der Anwendungsbereich des Verfahrens weitere Typparameter hinzufügt – in diesem Fall die zweite Währung, in zu konvertieren. Daher wird das kaufen jetzt eine Pizza und Bier etwa wie folgt:

var pizza = new Money<USD>() { 
  Quantity = 4.99f, Currency = new USD() };
var beer = new Money<EUR>() { 
  Quantity = 3.5f, Currency = new EUR() };
var lunch = pizza + beer.Convert<USD>();

Falls gewünscht, könnten wir sogar den Konvertierungsoperator (in c#) für die Konvertierung automatisch verwenden, aber möglicherweise könnten eher verwirrend als hilfreich für die Leser des Zollkodex Hauptnavigationsregisterkarten Ästhetik.

Zusammenfassung

Was fehlt das Geld < > Beispiel ist offensichtlich: klar muss es eine Möglichkeit zum Konvertieren in Euro-Dollar und Euro, Dollar. Aber gehört das Ziel bei der Designs, wie dies zur Vermeidung von einem geschlossenen System – d. h. Bedarf neue Währungen (Rubel, Rupie, Pfund, Lira oder andere Ihrer monetären Boat gleitet), es wäre schön Wenn wir die ursprünglichen Entwickler von der Money < > Geben Sie, brauchen Sie aufgerufen werden, um sie hinzuzufügen. Im Idealfall können in einem offenen System andere Entwickler anschließen müssen, und alles, was "funktioniert problemlos.

Optimieren Sie nicht nur noch, aber sicherlich starten Sie nicht und Versand des Codes ist. Es gibt noch ein paar Anpassungen vornehmen, die Money < > Geben Sie leistungsfähige, sichere und erweiterbare vornehmen. Dabei haben wir einen Blick auf dynamische und funktionalen Programmierung.

Aber jetzt viel Spaß beim Codieren!

Ted Neward ist ein Geschäftsführer von Neward & Associates, einer unabhängigen Firma, die sich auf .NET Framework- und Java-Plattformsysteme für Unternehmen spezialisiert hat. Er hat mehr als 100 Artikel geschrieben, ist ein C#-MVP und INETA-Sprecher und hat verfasst oder Mitverfasser von einem Dutzend Bücher, einschließlich "Professionelle F#-2.0" (Wrox, 2010). Er berät und regelmäßig mentors – Sie erreichen ihn unter ted@tedneward.com Wenn Sie interessiert, dass ihm kommen zusammen mit Ihrem Team oder seinen Blog unter blogs.tedneward.com.

Dank an den folgenden technischen Experten für die Überprüfung dieses Artikels: Krzysztof Cwalina