Condividi tramite


Il presente articolo è stato tradotto automaticamente.

Il programmatore al lavoro

Piattaforma.NET multiparadigmatica, Parte 7: metaprogrammazione parametrica

Ted Neward

image: Ted NewardQuando ero uno studente universitario, in una lezione microeconomics riempiti di calcolo, il professor comunicate alcune parole del dom di elementi di lavoro utilizzare me al giorno:

"Se, mentre a percussione attraverso i dettagli noiosi del nostro soggetto scelto, ti ritrovi incapaci di vedere il motivo dietro perché noi stiamo a percussione attraverso tutti questi dettagli noiosi, vostra responsabilità è di interrompermi e dire,"Professor Anderson, qual è il punto?" Ed effettueremo tra qualche minuto per tornare indietro e spiegare il modo in cui abbiamo qui."

I lettori che sono stati quando ci si trova in tutti gli articoli contenuti in questa serie possono anche sia stato raggiunto tale parete viene riportato di seguito alcuni istanti per tornare indietro e rivedere il modo in cui abbiamo qui.

Ricapitolando

Essenzialmente, come descritto da James Coplien nel suo libro "Multi-Paradigm Design per C++" (Addison-Wesley, 1998), che ha ispirato gran parte dalla redazione di questa serie di articoli, tutta la programmazione è un esercizio nell'acquisizione di compatibilità, la scrittura di codice che rappresenta il caso "sempre", e quindi utilizzando la variabilità all'interno del linguaggio per consentirne il funzionamento o essere strutturato in modo diverso in determinate circostanze.

Programmazione orientata ad esempio, acquisisce la compatibilità in classi, quindi consente la variabilità tramite l'ereditarietà, la creazione di sottoclassi che modificano tale compatibilità. In genere questa operazione modificando il comportamento di alcune parti della classe con metodi o i messaggi in base alla lingua in questione. Le esigenze di uniformità e la variabilità del progetto non sempre rientrano nel paradigma orientato in modo da inserire, tuttavia, o qualsiasi altro particolare unico paradigma, eventualmente, è aumentata di programmazione a oggetti di programmazione procedurale come un tentativo di offrire la variabilità che la programmazione procedura Impossibile acquisire facilmente.

Fortunatamente per i lettori di questa rivista, le lingue offerte da Microsoft in Visual Studio 2010 sono lingue multiparadigmatic, vale a dire che essi disegnare diversi paradigmi di programmazione diversi tra loro in un'unica lingua. Coplien first identified C++ as such a multiparadigmatic language, in that it brought three major paradigms together: procedural, object and metaprogrammatic (sometimes also more accurately referred to as metaobject). C++ è stato inoltre ampiamente criticati come un linguaggio complesso, troppo difficile per lo sviluppatore medio allo schema, principalmente perché era difficile vedere quando utilizzare le diverse funzionalità del linguaggio per la risoluzione dei problemi specifici.

Lingue moderne spesso trasformarsi in linguaggi altamente multiparadigmatic. F#, C# and Visual Basic, as of Visual Studio 2010, support five such paradigms directly: procedural, object-oriented, metaobject, dynamic and functional. Tutte le lingue di tre, quattro, se si include C + + / CLI in tale combinazione, pertanto corre il rischio di caduta nel destino stesso come C++.

Senza comprendere chiaramente di ciascuno dei paradigmi mixati in tali lingue, gli sviluppatori possono facilmente eseguire afoul delle trap la funzione preferita, in cui gli sviluppatori si basano eccessivamente su una feature o paradigma per l'esclusione degli altri e di creare codice eccessivamente complesso che alla fine si ottiene tossed e riscritto. Eseguire questa operazione troppe volte e la lingua inizia a recare brunt di frustrazione developer, causando alla fine di inviti a una nuova lingua o anzianità solo in generale.

Paradigmi procedurali e orientate agli oggetti

Finora abbiamo visto l'analisi di compatibilità/variabilità applicato alla programmazione procedurale o strutturali, in quanto è acquisire continuità a strutture di dati e operare su tali strutture da essi confluiscono nelle chiamate di procedure diverse, creazione di variabilità creando nuove chiamate di procedure per operare su queste stesse strutture di dati. Abbiamo inoltre visto uniformità/variabilità attorno agli oggetti, in cui abbiamo uniformità di acquisizione in classi e quindi creare variabilità creando una sottoclasse di tali classi e la modifica bit e parti di esse mediante l'override di metodi o proprietà.

Orsacchiotto ricordare, inoltre, un altro problema ha iniziato a emergere solo (per la maggior parte) consente di ereditarietà della variabilità positiva-Impossibile rimuovere un elemento da una classe base, ad esempio un campo o un metodo di membro. In CLR, è possibile nascondere implementazione di un membro derivato accessibile da lo shadowing (utilizzando la parola chiave virtual anziché la parola chiave override in c#, ad esempio). Tuttavia, che consiste nella sostituzione il comportamento con un'altra, non rimozione definitiva. I campi rimangono indipendentemente dal presenti.

Questa osservazione conduce a una rivelazione disturbare per alcuni: gli oggetti non è tutto ciò che abbiamo bisogno, ovvero oggetti almeno non puri. Ad esempio, gli oggetti non possono acquisire variabilità lungo le linee strutturale utilizzando l'ereditarietà: un insieme che un funzionamento simile a quello dello stack acquisizioni di classe, ma per una serie di diversi tipi di dati (valori integer, Double, stringhe e così via) non è possibile acquisire tale differenza struttura. All'interno di CLR, possiamo utilizzare il sistema di tipi unificato. È possibile memorizzare le istanze di riferimento di System. Object e downcast, se necessario, ma non è quello di riuscire a creare un tipo che contiene un solo tipo.

Questa scoperta ha al soggetto metaobject di programmazione, come la ricerca di metodi per fissare le cose di fuori degli assi oggetto tradizionale.

Il primo approccio di questo tipo di metadati è stato informazioni teoriche, in cui è stato generato il codice sorgente in base a un tipo di modello. Questo approccio consente di alcuni variabilità a una serie di assi diversi, ma è limitato (per la maggior parte) a un modello di origine in fase di esecuzione. Inizia anche a suddividere il numero di variabilities raggiunge perché il modello di origine deve variare in qualche modo il codice generato (in genere con le istruzioni decisionali nascondendo all'interno del linguaggio del modello) in fase di generazione del codice e ciò può creare complessità dei modelli.

In secondo luogo tale approccio di meta era quello della programmazione riflettenti o attributive. In fase di esecuzione, il codice utilizza le funzioni di metadati completo e accurato della piattaforma (Reflection) per esaminare il codice e si comportano in modo diverso in base a ciò che si vede.

Questo consentita la flessibilità di implementazione o il comportamento all'interno del processo decisionale di runtime, ma ha introdotto le proprie limitazioni: le relazioni di tipo non sono presenti all'interno di una struttura riflettente/attributive, vale a dire non esiste un modo per garantire a livello di codice che possono essere passati solo i tipi di database-persistenti in un metodo, ad esempio, di tipi XML-persistente. La mancanza di eredità o altre relazioni significa che una certa quantità di indipendenza dai tipi (e pertanto un'importante possibilità di evitare errori) pertanto perse.

Porta a esaminare il terzo dell'impianto portuale metaobject all'interno di.Ambiente di rete: polimorfismo dei parametri. Ciò significa la possibilità di definire tipi di tipi come parametri. In alternativa, in termini più semplici, quali Microsoft.NET Framework fa riferimento come generics.

Generics

Nella sua forma più semplice, i generics consentono la creazione della fase di compilazione di tipi che sono parti della loro struttura fornita dal codice client in fase di compilazione. In altre parole, lo sviluppatore di un insieme di comportamentale dello stack non deve necessariamente sapere in tempo sua libreria è compilato gli svariati tipi di client his può essere necessario memorizzare nelle diverse istanze, essi forniscono le informazioni durante la creazione di istanze dell'insieme.

Ad esempio, in un articolo precedente si è visto che la definizione di un tipo di punto Cartesiano (Cartesian) richiede una decisione di ahead del tempo (in parte dello sviluppatore punto) alla rappresentazione dei valori dell'asse (X e Y). Essi devono essere valori integrali? Essi potranno essere negativa?

Molto bene potrebbe necessario un punto Cartesiano (Cartesian) utilizzato in calcoli matematici a virgola mobile e il valore negativo. Un punto Cartesiano (Cartesian) utilizzato per rappresentare un pixel su un computer deve essere positivo, integrali e probabilmente in un determinato intervallo numerico, perché non possono visualizzare computer 4 miliardi per 4 miliardi di area grafica ma comune.

In questo modo, sulla superficie di esso, una libreria punto cartesiano well-progettazione dovranno diversi tipi di punto: tramite byte senza segno x e utilizzando una raddoppia i campi di Y, x, Y i campi e così via. The behavior, for the most part, will be identical across all of these types, clearly highlighting a violation of the desire to capture commonality (known colloquially as the DRY Principle: “Don’t Repeat Yourself”).

Il polimorfismo parametrico possiamo tale compatibilità molto chiaramente:

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
}

A questo punto, lo sviluppatore può specificare con precisione le proprietà di intervallo e il tipo del punto cartesiano che intenda utilizzare. Quando si lavora in un dominio matematico, egli crea istanze di Point2D <double> i valori, e quando si utilizza questa opzione per visualizzare tali valori sullo schermo, crea istanze di Point2D <sbyte> o Point2D <ushort>. Ognuno è un tipo distinto, pertanto tenta di confrontare o assegnare Point2D <sbyte> per Point2D <double> verrà fallirà in fase di compilazione, esattamente come si preferisce un linguaggio fortemente tipizzato.

Tuttavia, durante la scrittura, tipo Point2D presenta comunque alcuni svantaggi. Ovviamente ci abbiamo acquisito l'uniformità dei punti Cartesiano (Cartesian), ma abbiamo essenzialmente abbiamo consentito per qualsiasi tipo di valori da utilizzare per i valori x e Y. Mentre è ovviamente possibile utile in determinate situazioni ("In questo grafico, ci stiamo per grafici la classificazione di ogni persona ha assegnato un determinato filmato"), come regola generale, la creazione di un Point2D <DateTime> è potenzialmente creare confusione e la creazione di un Point2D <System.Windows.Forms.Form> è quasi certamente. È necessario introdurre qualche qui variabilità negativa (o, se si preferisce, limita il grado di variabilità positiva), limitando i tipi di tipi che possono essere i valori degli assi in un Point2D.

Molti.NET supportati acquisire questa variabilità negativa tramite i vincoli di parametrizzazione, anche noto come vincoli di tipo, descrivendo in modo esplicito le condizioni il parametro di tipo deve avere:

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
}

Ciò significa che il compilatore non accetta un valore per t non è un tipo di valore.

Per essere sinceri, questo non è esattamente una variabilità negativa, per sé, ma costituisce uno rispetto al problema del tentativo di rimuovere alcune funzionalità che approssima una quantità considerevole di quali una variabilità negativa true sarebbe.

Variazione di comportamento

Polimorfismo dei parametri in genere viene utilizzato per fornire la variabilità dell'asse strutturali, ma gli sviluppatori delle librerie C++ Boost dimostrate, non è l'unico asse lungo il quale può funzionare. Con un utilizzo appropriato dei vincoli di tipo, è possibile utilizzare anche i generics per fornire un meccanismo di criteri in cui i client possono specificare un meccanismo comportamentale per gli oggetti in fase di costruzione.

Consider, for a moment, the traditional problem of diagnostic logging: To help diagnose problems with code running on a server (or even on client machines), we want to track the execution of code through the codebase. Ciò significa in genere la scrittura dei messaggi di file. Tuttavia, talvolta si desidera che i messaggi vengono visualizzati nella finestra di console, almeno per alcuni scenari, e a volte si desidera che i messaggi scartati. La gestione dei messaggi di registrazione diagnostica è stato un problema complesso nel corso degli anni, e un'ampia gamma di soluzioni sono state proposte. Le lezioni dell'amplificazione offrono un nuovo approccio.

Iniziamo definendo un'interfaccia:

interface ILoggerPolicy {
  void Log(string msg);
}

È un'interfaccia semplice, con uno o più metodi di definire il comportamento da variare, che facciamo tramite una serie di sottotipi di quell'interfaccia:

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

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

Qui ci sono due possibili implementazioni, tra cui scrive il messaggio del registro sulla console mentre l'altro genera immediatamente.

Utilizzo di questo richiede che i client partecipa dichiarando il logger come un parametro tipizzato e creazione di un'istanza per effettuare la registrazione effettiva:

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

Che descrive il tipo di logger da utilizzare, quindi, è sufficiente far passare un parametro in fase di costruttore, come segue:

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

Questo meccanismo consente agli sviluppatori di creare le proprie implementazioni di registrazione personalizzato e collegarle a essere utilizzato da < > persona istanze senza < > la persona sviluppatori che sia necessario conoscere i dettagli dell'implementazione di registrazione utilizzato. Ma eseguire numerosi altri approcci anche questa operazione, ad esempio la necessità di un Logger campo o proprietà che passa in un Logger istanza dall'esterno (o ottenere uno tramite un approccio di inserimento delle dipendenze). The generics-based approach has one advantage that the field-based approach doesn’t, however, and that’s compile-time distinction: a Person<ConsoleLogger> is a distinct and separate type from a Person<NullLogger>.

Money, Money, Money

Un problema che riguarda gli sviluppatori è che quantitativi sono inutili senza le unità da quantificare. Spiccioli 1000 non è chiaramente equivale a 1000 cavalli o 1000 dipendenti o pizze 1000;. Ancora, esattamente come chiaramente spiccioli 1000 e 10 dollari sono, infatti, lo stesso valore.

Questo diventa ancora più importante nei calcoli matematici in cui la necessità di acquisire le unità di misura (gradi/radianti, piedi/metri, / Celsius Fahrenheit) è ancora più importante, in particolare se si sta scrivendo il software di controllo linee guida per un rocket di grandi dimensioni. Prendere in considerazione il 5 Ariane, cui volo conseguito doveva essere self-destructed a causa di un errore nella conversione. O la sonda NASA MARS, uno dei quali slammed in Martian il paesaggio a piena velocità a causa di un errore di conversione.

Recentemente, nuovi linguaggi come F # hanno deciso di acclimatazione prosegue unità di misura come una funzionalità del linguaggio diretto, ma anche il linguaggio c# e Visual Basic può eseguire tipi simili di elementi, grazie ai generics.

Canalizzazioni nostro interna Martin Fowler, iniziamo con una semplice classe di Money, che conosce la quantità (quantità) e la valuta (tipo) di un importo monetario specifico:

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

Sulla superficie, questo sembra gestibile, ma prima troppo tempo ci concentreremo da avviare operazioni simili a valore, ad esempio l'aggiunta Money istanze insieme (una cosa piuttosto comune a che fare con money, quando si pensa):

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

Naturalmente, il problema sta per sorgere quando si tenta di aggiungere U.S. dollari (USD) e dell'euro (EUR) insieme, come ad esempio quando andiamo a pranzo (dopo tutto, tutti sanno gli europei la birra migliore, ma gli americani fanno la migliore pizza):

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

Chiunque ha una rapida occhiata al dashboard finanziario sta per rendersi conto che qualcuno è ottenere copiato, ovvero l'euro da convertire in euro con un rapporto di 1-1. Al fine di prevenire le frodi accidentale, vogliamo probabilmente per assicurarsi che il compilatore sa non convertirsi USD a EUR senza passare attraverso un processo di conversione approvato che cerca l'attuale tasso di conversione (vedere Figura 1).

Figura 1 conversione è in ordine

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

Si noti come USD ed EUR sono fondamentalmente semplicemente segnaposto, progettati per fornire un valore da confrontare con il compilatore. Se i parametri di tipo c due non sono uguali, è un problema.

Naturalmente, ci abbiamo perso anche la possibilità di combinare i due e vi sono situazioni in cui si vogliono esattamente a tale scopo. In questo modo richiede un po' più sintassi parametrica (vedere Figura 2).

Figura 2 la combinazione di tipi intenzionalmente

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

Si tratta di un metodo generico specializzato all'interno di una classe generica e la < > sintassi dopo il nome del metodo consente di aggiungere ulteriori parametri di tipo per l'ambito del metodo, ovvero in questo caso, il tipo della valuta per convertire rispetto al secondo. Acquista ora una pizza e birra vengono in questo modo, diventa anche qualcosa di simile a:

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>();

Se opportuno, è anche possibile utilizzare l'operatore di conversione (in c#) per eseguire automaticamente la conversione, ma che potrebbero contenere più confusione anziché essere di aiuto ai lettori del codice, a seconda delle preferenze estetiche.

Conclusioni

Che cosa manca il <> Money esempio è ovvia: chiaramente deve esistere un modo per convertire dollari in euro e l'euro in dollari. Ma è parte dell'obiettivo in un design simile al seguente per evitare un sistema chiuso, ovvero, sono necessarie nuove valute (rubles, rupees, libbre, lira o qualsiasi altra la barca monetaria è mobile), sarebbe bello se ci, le finestre di progettazione originale di < > il denaro tipo, non devono essere chiamati per aggiungerli. In teoria, in un sistema aperto, gli altri sviluppatori possono collegarle come necessari e tutti gli elementi "appena funziona."

Non regolare ancora, tuttavia e certamente non avviare il codice di spedizione. Sono ancora disponibili alcune modifiche apportare a < > il denaro tipo per renderlo più potente, sicuro ed estensibile. Inoltre, avremo un'occhiata alla programmazione dinamica e funzionale.

Ma per ora, buona codifica!

Ted Neward è un'entità con Neward & Consente di associare, un'impresa indipendente specializzato in azienda.NET Framework e Java sistemi di piattaforma. Egli ha scritto oltre 100 articoli, come relatore c# MVP e INETA e ha creato o coautore di numerosi libri, inclusi "Professionale F # 2. 0" (Wrox 2010). Egli consulta e mentors regolarmente — è possibile contattarlo al ted@tedneward.com se siete interessati ad avere lui venire a lavorare con il vostro team, o leggere il suo blog a indirizzo blogs.tedneward.com.

Grazie all'esperto tecnica seguente per la revisione di questo articolo: Krzysztof Cwalina