Compartir a través de


Este artículo proviene de un motor de traducción automática.

El programador políglota

Multiparadigmatic.NET, parte 7: Paramétrico metaprogramación

Ted Neward

image: Ted NewardCuando era un estudiante de la universidad, en una charla de microeconomics de relleno de cálculo, el profesor compartidas a este día algunas palabras de wis-dom que repercusión conmigo:

«Si, mientras obstinado a través de los tediosos detalles de nuestro tema elegido, te encuentras no se ha podido ver la razón de por qué nos estamos obstinado a través de todos estos detalles tediosos, su responsabilidad es interrumpirme y decir, «Profesor Anderson, ¿de qué sirve?» Y tomaremos unos momentos para volver atrás y explique lo que tenemos aquí."

Los lectores que han sido sentado a través de todos los artículos de esta serie pueden bien han alcanzado ese pared, así que vamos tardar unos minutos para volver atrás y revisar lo que tenemos aquí.

Recapitulación

En su esencia, tal como se describe por James Coplien en su libro "Multi-Paradigm diseño de C++" (Addison-Wesley, 1998), que inspiraron gran parte de la escritura en esta serie de artículos, todos los de la programación es un ejercicio de captura de uniformidad: escribir código que representa el caso de "todo el tiempo", y, a continuación, utilizando la variabilidad construye dentro del lenguaje para permitir que su comportamiento o se estructuran de forma diferente en determinadas circunstancias.

La programación orientada a objetos, por ejemplo, captura la uniformidad en las clases, a continuación, permite la variabilidad a través de herencia, la creación de subclases que alterar esa uniformidad. Normalmente, que se realiza cambiando el comportamiento de determinadas partes de la clase utilizando los métodos o los mensajes, según el idioma en cuestión. Las necesidades de similitudes y variabilidad del proyecto no siempre encajan en el paradigma orientado a objetos tan claramente, sin embargo, o cualquier otro particular único paradigma, de hecho, programación orientada a objetos creció fuera de la programación procedural como un intento de ofrecer la variabilidad que la programación del procedimiento no se pudo capturar fácilmente.

Afortunadamente para los lectores de esta revista, los idiomas ofrecidos por Microsoft en 2010 de Visual Studio son lenguajes de multiparadigmatic, lo que significa que se dibujan varios paradigmas de programación diferentes juntos en un solo idioma. Coplien primero identificado C++ como un lenguaje multiparadigmatic, en que reúnen tres paradigmas principales: el objeto del procedimiento, y metaprogrammatic (a veces también con más precisión denominados metaobject). C++ también ampliamente fue criticada como un lenguaje complicado, demasiado difícil para el desarrollador medio maestro, principalmente porque era difícil ver cuándo se debe utilizar las distintas características del lenguaje para resolver problemas específicos.

Lenguajes modernos desarrollan con frecuencia en idiomas muy multiparadigmatic. F #, C# y Visual Basic, como de Visual Studio de 2010, admiten directamente cinco estos paradigmas: metaobject del procedimiento, orientado a objetos, dinámico y funcional. Los tres lenguajes: cuatro, si incluye C + + / CLI en dicha mezcla, por lo tanto, se corre el riesgo de que se caiga en la misma suerte como C++.

Sin una comprensión clara de cada uno de los paradigmas de mezcla en esos idiomas, los desarrolladores pueden fácilmente ejecutar mantiene de la captura de la característica Favoritos, donde los desarrolladores confían demasiado en una característica o paradigma para la exclusión de los demás y terminan por crear código demasiado complejo que finalmente obtiene generé y volver a escribir. Ello demasiadas veces y se inicia el idioma que soportar el peso de frustración de desarrollador, lleva finalmente a llamadas para un nuevo idioma o la retirada sólo en general.

Paradigmas orientados a objetos y de procedimientos

Hemos visto hasta ahora, el análisis de similitudes y variabilidad aplicado a la programación de procedimiento o estructural, en el que se captura la uniformidad en las estructuras de datos y operar en las estructuras por ellos de alimentación en llamadas a procedimientos diferentes, la creación de variabilidad mediante la creación de nuevas llamadas de procedimiento para operar en las mismas estructuras de datos. También hemos visto similitudes y variabilidad alrededor de objetos, en el que captura la uniformidad en clases y, a continuación, crear variabilidad a partir de esas clases y cambiante bits y partes de ellos a través de propiedades o métodos reemplazo.

Tenga en cuenta, también a los que otro problema surge en que la herencia (la mayor parte) sólo permite la variabilidad positivo, no podemos eliminar algo de una clase base, como, por ejemplo, el método de miembro o campo. En el CLR, es posible ocultar la implementación de un miembro accesible derivada sombreándola (mediante la palabra clave virtual en lugar de la palabra clave override en C#, por ejemplo). Sin embargo, eso significa reemplazar su comportamiento con algo más, no lo quita completamente. Campos permanezcan presentes en cualquier caso.

Esta observación conduce a una revelación molesta para algunos: objetos no pueden hacer todo lo que necesitamos: puros al menos no objetos. Por ejemplo, objetos no pueden capturar la variabilidad a lo largo de líneas estructurales utilizando la herencia: una colección ese comportamiento de la pila como capturas de clase, pero que no se puede capturar esa diferencia estructural para una variedad de diferentes tipos de datos (enteros, dobles, cadenas y así sucesivamente). Efectivamente, podemos utilizar el sistema de tipos unificado en CLR. Podemos almacenar las instancias de System.Object y conversión de referencia según sea necesario, pero que no es igual a la posibilidad de crear un tipo que se almacena sólo un tipo.

Esta deducción nos pone al asunto de la programación metaobject, ya que estamos buscando maneras de captar cosas fuera de los ejes del objeto tradicional.

El primero dicho enfoque de meta era generativas, en el que se generó el código fuente basado en algún tipo de plantilla. Este enfoque permite algunos variabilidad en una variedad de diferentes ejes, pero está limitado (la mayor parte) a un modelo de origen de tiempo de ejecución. También se empieza a fallar, ya que la plantilla de origen debe variar de alguna manera el código generado (normalmente con instrucciones de toma de decisiones ocultar dentro el idioma de plantilla) se eleva el número de variaciones en tiempo de generación de código, y esto puede crear las complejidades de las plantillas.

El segundo tal enfoque de meta era de programación reflectante o attributive. En tiempo de ejecución, el código utiliza las instalaciones de metadatos de total fidelidad de la plataforma (reflejo) para inspeccionar el código y se comportan de manera diferente basándose en lo que ve no existe.

Esto permite flexibilidad de implementación o el comportamiento de la toma de decisiones en tiempo de ejecución, pero introdujo sus propias limitaciones: no tiene relaciones de tipo están presentes dentro de un diseño reflectante/attributive, lo que significa que no hay ninguna forma de asegurarse de que se pueden pasar a un método sólo los tipos con persistencia de base de datos mediante programación, por ejemplo, en contraposición a los tipos con persistencia XML. La falta de otras relaciones o una herencia significa que un cierto grado de seguridad de tipos (y, por tanto, una importante capacidad para evitar errores), por tanto, se pierde.

Llegamos a la instalación tercera metaobjeto dentro de la.Entorno de red: polimorfismo paramétrico. Esto significa que la capacidad para definir tipos que tienen tipos como parámetros. O, sencillamente, lo que Microsoft.NET Framework hace referencia a como los genéricos.

Genéricos

En su forma más sencilla, los componentes genéricos permiten la creación de tiempo de compilación de tipos que tienen partes de su estructura suministrada en tiempo de compilación desde el código de cliente. En otras palabras, el desarrollador de una colección de comportamiento de la pila no tiene que conocer en el momento de su biblioteca está compilado qué clases de tipos de los clientes de his podrían desear almacenar en diferentes instancias, proporcionan dicha información cuando creen instancias de la colección.

Por ejemplo, en un artículo anterior, vimos que la definición de un tipo de punto cartesianas requiere una decisión anticipada de tiempo (en la parte de que el desarrollador de punto) en la representación de los valores del eje (X e Y). ¿Debe ser valores integrales? ¿Deben que podían ser negativo?

Un punto cartesiano (Cartesian) que se utiliza en matemáticas muy bien podría debe ser un punto flotante y negativo. Un punto cartesiano (Cartesian) utilizado para representar un píxel en un equipo pantalla de gráficos debe ser positivo, integral y probable de un determinado intervalo numérico, como muestra de 4 mil millones por 4 mil millones de equipo no está aún muy comunes.

Por lo tanto, en la superficie del mismo, una biblioteca de punto cartesianas well-diseño necesitará varios tipos diferentes de punto: uno con bytes sin signo como x y y campos, uno con se dobla como x y y los campos y así sucesivamente. El comportamiento, la mayor parte, va a ser idéntico en todos estos tipos, resaltar claramente una infracción al deseo de capturar la uniformidad (conocen coloquialmente como el principio seco: "Don't repitas").

Con polimorfismo paramétrico, podemos capturar esa uniformidad bastante perfectamente:

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
}

Ahora, el desarrollador puede especificar con exactitud las propiedades de intervalo y tipo del punto cartesiano (Cartesian) que pretenda utilizar. Cuando se trabaja en un dominio matemático, crea instancias de Point2D <double> valores, y cuando se trabaja para mostrar esos valores en la pantalla, el administrador crea instancias de Point2D <sbyte> o Point2D <ushort>. Cada uno de ellos es su propio tipo distinto, por lo que intenta comparar o asignar Point2D <sbyte> para Point2D <double> se producirá un error equivocó rotundamente en tiempo de compilación, tal y como se prefiere un lenguaje con establecimiento inflexible de tipos.

Como está escrito, sin embargo, el tipo de Point2D aún tiene algunos inconvenientes. Sin duda, nos hemos capturado la uniformidad de los puntos cartesiano (Cartesian), pero básicamente nos hemos permitido para cualquier tipo de valores que se utilizará para los valores de x e Y. Aunque cabe la posibilidad podría ser útil en ciertos escenarios ("en este gráfico, nos estamos gráficos la clasificación de cada persona dio una película particular"), como norma general, intente crear un Point2D <DateTime> es potencialmente confuso y tratando de crear un Point2D <System.Windows.Forms.Form> es casi con seguridad. Es necesario introducir algún tipo de la variabilidad negativa aquí (o, si lo prefiere, moderar el grado de variabilidad positivo), la restricción de las clases de tipos que pueden ser el eje de valores en un Point2D.

Muchos.NET pueden captura este variabilidad negativa a través de las restricciones de la parametrización, también conocido como restricciones de tipo, describiendo las condiciones de explícitamente el parámetro de tipo debe tener:

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
}

Esto significa que el compilador no aceptaría nada para t que no es un tipo de valor.

Para ser sinceros, esto no es exactamente una variabilidad negativo, sí, pero sirve como uno en comparación con el problema de intentar quitar cierta funcionalidad, que se aproxima bastante sería qué una variabilidad negativo es true.

Comportamiento diversos

Polimorfismo paramétrico se utiliza normalmente para proporcionar la variabilidad en el eje estructural pero, como los desarrolladores de las bibliotecas de Boost de C++ demostrados, no es el eje sólo por las que puede funcionar. Con un uso juicioso de las restricciones de tipo, también podemos utilizar genéricos para proporcionar un mecanismo de directiva en el que los clientes pueden especificar para los objetos que se está construyendo un mecanismo de comportamiento.

Considere, por un momento, el problema tradicional de registro de diagnóstico: para ayudar a diagnosticar problemas con el código que se ejecuta en un servidor (o incluso en los equipos cliente), deseamos hacer un seguimiento de la ejecución de código a través del código base. Esto suele significar escribir mensajes en el archivo. Pero a veces, queremos que los mensajes aparezcan en la consola de, al menos para determinados escenarios, y algunas veces deseamos que los mensajes desechados. Tratamiento de mensajes de registro de diagnóstico ha sido un problema complejo en los años y han propuesto una variedad de soluciones. Las lecciones de aumento ofrecen un nuevo enfoque.

Comenzaremos por definir una interfaz:

interface ILoggerPolicy {
  void Log(string msg);
}

Es una interfaz sencilla, con uno o varios métodos de definir el comportamiento que desea variar, que hacemos a través de una serie de subtipos de esa interfaz:

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

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

Aquí tenemos dos implementaciones posibles, uno de los cuales escribe el mensaje de registro en la consola, mientras que el otro descarta.

Mediante este requiere que los clientes participar al declarar el registrador como un parámetro con tipo y la creación de una instancia de ella para realizar el registro real:

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

Describe qué tipo de registrador para utilizar, a continuación, es sólo cuestión de pasar un parámetro de tiempo de constructor, de este modo:

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

Este mecanismo permite a los desarrolladores crear sus propias implementaciones de registro personalizados y conectarlos a ser utilizados por < > de la persona instancias sin < > de la persona necesidad de conocer los detalles de la implementación de registro que se utiliza el programador. Pero muchos otros enfoques también hacerlo, como tener un campo de registrador o la propiedad que se pasa en un registrador instancia desde fuera (o cómo obtener uno mediante un enfoque de inserción de dependencia). El enfoque basado en los componentes genéricos tiene una ventaja que el enfoque basado en el campo no existe, sin embargo, y que se encuentra la distinción de tiempo de compilación: una persona <ConsoleLogger> es un tipo distinto e independiente de una persona <NullLogger>.

Money, Money, Money

Un problema que asedia a los desarrolladores es que las cantidades son inútiles sin las unidades que se está cuantificadas. Mil céntimos claramente no es lo mismo que 1.000 caballos o 1.000 empleados o 1.000 pizzas. Aún, simplemente como claramente, 1.000 céntimos y 10 dólares son, de hecho, el mismo valor.

Esto resulta aún más importante en cálculos matemáticos, donde la necesidad para capturar las unidades (grados o radianes, metros/pies, grados Celsius o Fahrenheit) es aún más importante, especialmente si va a escribir software de control de orientación para un rocket muy grande. Tenga en cuenta el 5 de Ariane, cuyo flight soltera se tenía que ser self-destructed debido a un error en la conversión. O el sondeo de la NASA Marte, uno de los cuales slammed en el Martian horizontal a toda velocidad debido a un error de conversión.

Recientemente, han decidido aclimatar unidades de medida como una característica del lenguaje directa nuevos idiomas como F #, pero incluso C# y Visual Basic pueden hacer tipos similares de cosas, gracias a los componentes genéricos.

Nuestro Martin Fowler interna de la tecnología, es preferible empezar con una clase simple de dinero, que conoce la cantidad (cantidad) y la moneda (tipo) de un determinado importe monetario:

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

En la superficie, parece factible, pero antes de demasiado tiempo, vamos a iniciar haciendo cosas como valor de esta forma, como agregar Money instancias juntas (bastante habitual algo que ver con money, cuando se piensa):

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

Por supuesto, el problema va a surgir cuando intentamos Añadir U.S. dólares de los EE.UU. y Europea euro (EUR) junto, como cuando salimos a comer (después de todo, todo el mundo sabe la mejor cerveza de producción de los europeos, pero los estadounidenses hacen la mejor pizza):

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

Cualquiera que toma un vistazo rápido a los paneles de mandos financieros se va a darse cuenta de que alguien está obteniendo copiado: los euros se están convirtiendo a dólares en una proporción de 1-1. A fin de evitar el fraude accidental, probablemente desee para asegurarse de que el compilador no sabe para convertir USD euros sin pasar por un proceso de conversión aprobado que busca la actual tasa de conversión (véase figura 1).

Figura 1 conversión es en orden

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

Observe cómo USD y euros son básicamente sólo marcadores de posición, diseñados para ofrecer el compilador algo que se va a comparar. Si los parámetros de tipo c dos no son iguales, es un problema.

Por supuesto, también nos hemos perdido la capacidad de combinar los dos, y a veces, cuando queremos hacer exactamente eso. Hacerlo requiere un poco más sintaxis paramétrica (véase figura 2).

Figura 2 intencionalmente la combinación de tipos

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

Se trata de un método genérico especializado dentro de una clase genérica y el < > Después de que el nombre del método, agrega más parámetros de tipo al ámbito del método de sintaxis: en este caso, el tipo de la segunda moneda para la conversión a. Por lo tanto, al comprar una pizza y cerveza ahora se vuelve algo como:

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

Si lo desea, incluso podríamos usar el operador de conversión (en C#) para realizar la conversión automáticamente, pero que puedan contener más confusas que útiles para los lectores del código, según sus preferencias estéticas.

Conclusión

¿Qué falta en el < > Money ejemplo es obvia: claramente debe haber alguna manera de convertir dólares a euros y en euros en dólares. Pero es parte de la meta en diseños de este evitar un sistema cerrado, es decir, como sean necesarias nuevas monedas (rublos, rupias, libras, lira o cualquier otra cosa que flota el barco monetario), sería estupendo si nosotros, los diseñadores originales de la < > Money Escriba, no tiene que llamarse para agregarlos. Lo ideal es que, en un sistema abierto, otros desarrolladores pueden conectarlos a medida que necesitan y todo lo que "just works".

No ajustar todavía, no obstante y ciertamente no se inician los gastos de envío el código tal cual. Todavía tenemos unos ajustes a realizar en el < > Money tipo para que sea más eficaz, seguro y extensible. Además, tenemos un vistazo a la programación funcional y dinámico.

Pero por ahora, disfrute codificando!

Ted Neward es un director de Neward & Associates, una empresa independiente que se especializa en sistemas empresariales de .NET Framework y plataformas Java. Ha escrito más de 100 artículos, es un altavoz de C# MVP e INETA y ha creado o coautores una decena de libros, incluido "Profesional F # 2.0" (Wrox, 2010). Consulta y mentores regularmente: llegar a él en ted@tedneward.com si está interesado en que le vienen a trabajar con su equipo, o lee su blog en blogs.tedneward.com.

Gracias al siguiente experto técnico para el examen de este artículo: Krzysztof Cwalina