Este artigo foi traduzido por máquina.

O programador

.NET com vários paradigmas, Parte 7: Metaprogramação paramétrica

Ted Neward

image: Ted NewardQuando eu era um aluno university, em uma palestra de Microeconomia preenchido de cálculo, o professor compartilhados algumas palavras do dom wis Repercutir comigo até hoje:

"Se, ao mesmo tempo em que slogging os detalhes tediosos de nosso assunto escolhido, você encontrar-se incapaz de ver a razão por que nós estamos slogging todos esses detalhes tediosos, sua responsabilidade é para interromper a mim e dizer: 'Professor Anderson, o que é o ponto?' E vamos dar alguns instantes para voltar e explicar como chegamos até aqui.

Os leitores que já foi colocada em todos os artigos desta série pode ser bem tem visitas que parede, vamos levar alguns instantes para voltar e revisar como chegamos até aqui.

Recapitulação

Em sua essência, conforme descrito por James Coplien em seu livro "Multi-Paradigm Design para C++" (Addison-Wesley, 1998), que inspirou grande parte escrita nesta série de artigos, programação de todos os é um exercício em capturar semelhança — escrever código que representa o caso de "sempre" — e, em seguida, usando a variabilidade constrói o idioma para permitir que ele funcione ou estruturados de forma diferente em determinadas circunstâncias.

Fase de programação orientada a objeto, por exemplo, captura a semelhança em classes, em seguida, permite a variabilidade através de herança, a criação de subclasses que alteram essa semelhança. Geralmente isso é feito alterando o comportamento de determinadas partes da classe usando métodos ou mensagens, dependendo do idioma em questão. As necessidades de aspectos comuns e variabilidade do projeto não sempre ajustam o paradigma orientado a objeto tão claramente, no entanto, ou qualquer outro determinada única paradigma, no caso — programação orientada a objeto surgiu de programação de procedimento como uma tentativa de oferecer variabilidade que procedimento programação não foi possível capturar facilmente.

Felizmente para leitores desta revista, os idiomas oferecidos pela Microsoft no Visual Studio 2010 são idiomas multiparadigmatic, que significa que eles desenhar vários paradigmas de programação diferentes juntos em um único idioma. Coplien primeiro identificado C++ como tal idioma multiparadigmatic, que reuniu três paradigmas principais: objeto de procedimento, e metaprogrammatic (às vezes também com mais precisão conhecido como meta-objeto). C++ foi também amplamente criticized como uma linguagem complicada, muito difícil para o desenvolvedor médio ao mestre, principalmente porque era difícil ver quando usar os vários recursos da linguagem para resolver problemas específicos.

Idiomas modernos desenvolvem com freqüência em idiomas altamente multiparadigmatic. F #, TRANSLATION FROM VPE FOR CSHARP e Visual Basic, como do Visual Studio 2010, suportam a cinco desses paradigmas diretamente: metaobjeto e orientada a objeto, procedimento, dinâmico e funcional. Todos os três idiomas — quatro, se você incluir C + + / CLI essa combinação de — portanto corre o risco de dividem a mesma colocaria como C++.

Sem uma compreensão clara de cada um dos paradigmas misturados para esses idiomas, os desenvolvedores podem facilmente executar afoul do ajuste de registro de recurso favorito, onde os desenvolvedores dependem muito de um recurso ou paradigma para a exclusão dos outros e acabam criando código excessivamente complexo que eventualmente obtém lancei e regravado. Isso muitas vezes e o idioma começa a bear a brunt de frustração de desenvolvedor, levando eventualmente a chamadas para um novo idioma ou aposentadoria apenas em geral.

Paradigmas de procedimentos e orientada a objeto

Até agora, vimos a análise de variabilidade/aspectos comuns aplicada a programação de procedimento ou estrutural, em que podemos capturar a semelhança em estruturas de dados e operar essas estruturas por alimentam chamadas de procedimento diferente, criando variabilidade, criando novas chamadas de procedimento para operar nessas mesmas estruturas de dados. Também vimos variabilidade/aspectos comuns ao redor de objetos, na qual podemos capturar a semelhança em classes e crie variabilidade criando subclasses dessas classes e mudança bits e partes deles por meio de propriedades ou métodos de sobrescrita.

Lembre-se, também, que outro problema surge em que a herança permite (na maior parte) apenas variabilidade positiva — nós não é possível remover algo de uma classe base, como o campo ou método membro. No CLR, podemos ocultar implementação derivada acessível membro sombreamento (usando a palavra-chave virtual em vez da palavra-chave override no TRANSLATION FROM VPE FOR CSHARP, por exemplo). No entanto, isso significa que a substituição de seu comportamento com algo, não removê-lo imediatamente. Campos permaneçam presentes independentemente.

Esta observação leva a uma revelação perturbadora para alguns: objetos não podem fazer tudo o que precisamos — pelo menos não puros objetos. Por exemplo, objetos não podem capturar a variabilidade ao longo de linhas estruturais usando herança: uma coleção desse comportamento semelhante à pilha de capturas de classe, mas para uma variedade de diferentes tipos de dados (seqüências de números inteiros, duplicatas, e assim por diante) não pode capturar essa diferença estrutural. Certo, dentro do CLR podemos usar o sistema unificado de tipos. Podemos armazenar instâncias de referência de System. Object e lançamento decrescente conforme necessário, mas não é igual a possibilidade de criar um tipo que armazena somente um tipo.

Essa percepção nos trouxe ao assunto do Meta-objeto de programação, como estamos procurando maneiras de capturar as coisas fora dos eixos do objeto tradicional.

O primeiro tal abordagem meta foi produtivas, no qual o código-fonte foi gerado com base em algum tipo de modelo. Essa abordagem permite alguns variabilidade em uma variedade de diferentes eixos, mas é limitada (na maior parte) a um modelo de fonte em tempo de execução. Ele também começa a dividir conforme o número das variabilidades aumenta porque o modelo de origem deve variar de alguma forma o código gerado (normalmente com as instruções de tomada de decisão ocultando dentro do idioma do modelo) em tempo de geração de código, e isso pode criar complexidades nos modelos.

Em segundo lugar, tal abordagem de meta era que programação refletiva ou attributive. Em tempo de execução, o código usa as facilidades de metadados com total fidelidade da plataforma (reflexão) para inspecionar o código e se comportam de forma diferente dependendo o que ele vê lá.

Isso permitido a flexibilidade de implementação ou o comportamento de tomada de decisões em tempo de execução, mas apresentou suas próprias limitações: não há relações de tipo estão presentes em um design Refletivo/attributive, que significa que não é possível por meio de programação, garantir que somente os tipos de banco de dados persistente podem ser passados para um método, por exemplo, em oposição a tipos XML persistente. A falta de uma herança ou outra relação significa que uma certa quantidade de segurança de tipos (e, portanto, uma habilidade importante para evitar erros), portanto, é perdida.

O que nos leva para a terceira facilidade de meta-objeto dentro do.NET environment: polimorfismo paramétrico. Isso significa que a capacidade de definir os tipos que têm tipos como parâmetros. Ou, mais simples e colocar o que a Microsoft.NET Framework se refere como genéricos.

Genéricos

Em sua forma mais simples, os genéricos permitem a criação de tempo de compilação de tipos que possuem as partes de sua estrutura fornecida em tempo de compilação do código de cliente. Em outras palavras, o desenvolvedor de uma coleção de comportamento de pilha não precisa saber em tempo de sua biblioteca é compilado que tipos de tipos a clientes his talvez queira armazenar em instâncias diferentes — eles fornecerá essas informações ao criarem instâncias da coleção.

Por exemplo, em um artigo anterior, vimos que a definição de um tipo de ponto cartesiano requer uma decisão de ahead tempo (por parte do desenvolvedor ponto) sobre a representação dos valores do eixo (X e Y). Eles devem ser valores integrais? Eles devem ter permissão para ser negativo?

Um ponto cartesiano usado em matemática poderia muito bem precisa ser um ponto flutuante e negativo. Um ponto cartesiano usado para representar um pixel em um computador tela gráfica precisa ser positiva, integral e provavelmente em um determinado intervalo numérico, como monitores de computador de 4 bilhões por 4 bilhões não estão ainda LANs.

Assim, a superfície dele, uma biblioteca de ponto Cartesiano de design bem terá vários tipos diferentes de ponto: usando bytes sem sinal como x e campos de y, usando uma dobra como x e y campos e assim por diante. O comportamento, na maior parte, serão idêntico em todos esses tipos de realce claramente uma violação à intenção de capturar a semelhança (conhecido coloquialmente como princípio seco: "Don't Repeat Yourself").

Usando o polimorfismo paramétrico, podemos capturar essa semelhança bem organizada:

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
}

Agora, o desenvolvedor pode especificar com precisão as propriedades de intervalo e tipo do ponto cartesiano que ele deseja usar. Ao trabalhar em um domínio de matemático, ele cria instâncias de Point2D <double> valores, e ao trabalhar para exibir esses valores na tela, ele cria instâncias de Point2D <sbyte> ou Point2D <ushort>. Cada um é o seu próprio tipo distinto, então, tenta comparar ou atribuir Point2D <sbyte> para Point2D <double> falhará irremediáveis em tempo de compilação, exatamente como gostaria que uma linguagem com rigidez de tipos.

No entanto, como escrito, o tipo de Point2D ainda tem algumas desvantagens. Podemos ter capturado a semelhança de pontos cartesianas, certamente, mas basicamente estamos tenha permitido para qualquer tipo de valores a serem usados para os valores de x e Y. Embora isso perfeitamente poderia ser útil em determinados cenários ("neste gráfico, podemos estiver representar graficamente as classificações de cada pessoa deu um determinado filme"), como regra geral, tentando criar um Point2D <DateTime> é potencialmente confusas e tentar criar uma Point2D <System.Windows.Forms.Form> é quase certo. Precisamos apresentar algum tipo de negativo variabilidade aqui (ou, se preferir, reduzir o grau de variabilidade positiva), restringindo os tipos de tipos que podem ser valores de eixo em um Point2D.

Muitos.NET diferentes capturar essa variação negativa por meio de restrições de parametrização — também conhecido como restrições de tipo — explicitamente descrevendo as condições o parâmetro de tipo deve ter:

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
}

Isso significa que o compilador não aceitará qualquer coisa para t não é um tipo de valor.

Para ser honesto, isso não é exatamente uma variação negativa, si, mas serve como um em comparação com o problema de tentar remover certas funções, que se aproxima de uma quantidade razoável de que uma verdadeira variabilidade negativa seria.

Comportamento variado

Polimorfismo paramétrico é normalmente usado para fornecer a variabilidade no eixo estrutural, mas os desenvolvedores de bibliotecas C++ Boost demonstrados, não é o único eixo ao longo do qual ele pode operar. Com o uso moderado de restrições de tipo, podemos também usar genéricos para fornecer um mecanismo de política na qual os clientes podem especificar um mecanismo comportamental para objetos que estão sendo construídos.

Considere, por alguns instantes, o problema tradicional do log de diagnóstico: para ajudar a diagnosticar problemas com o código em execução em um servidor (ou mesmo em máquinas cliente), queremos rastrear a execução de código por meio da Base de código. Isso normalmente significa gravar arquivo de mensagens. Mas, às vezes, queremos que as mensagens sejam exibidas no console do, pelo menos para alguns cenários, e algumas vezes, queremos que as mensagens jogadas fora. Tratamento de mensagens de log de diagnóstico tem sido um problema complicado ao longo dos anos e uma variedade de soluções que foram propostas. As lições de propulsão oferecem também uma nova abordagem.

Começamos definindo uma interface:

interface ILoggerPolicy {
  void Log(string msg);
}

É uma interface simples, com um ou mais métodos definindo o comportamento queremos variam, o que fazemos por meio de uma série de subtipos dessa interface:

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

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

Aqui temos duas implementações possíveis, uma das quais grava a mensagem de log no console enquanto o outro lança fora.

Usar isso requer que os clientes optarem por declarando o agente de log como um parâmetro de tipo, e criando uma instância para fazer o 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();
}

Que descreve que tipo de agente de log para usar, em seguida, é uma simples questão de passar um parâmetro de construtor tempo, da seguinte forma:

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

Esse mecanismo permite que os desenvolvedores a criar suas próprias implementações de log personalizado e conectá-los a ser usado por pessoa < de > instâncias sem a < > pessoa desenvolvedor precisar saber os detalhes da implementação de log usado. Mas numerosas outras abordagens também fazer isso, como, por exemplo, ter um campo de agente de log ou a propriedade passa um agente de log a instância de fora (ou a obtenção de um por meio de uma abordagem de injeção de dependência). A abordagem baseada em classes genéricas tem uma vantagem que não tenha a abordagem baseada em campo, no entanto, e que é a diferença de tempo de compilação: uma pessoa <ConsoleLogger> é um tipo de distinto e separado de uma pessoa <NullLogger>.

Money, Money, Money

Um problema que assombra desenvolvedores é que as quantidades são inúteis sem as unidades sendo quantificou. Mil centavos claramente não é a mesma coisa que 1000 cavalos ou 1000 funcionários ou 1000 pizzas. Ainda assim, apenas tão claramente, centavos 1000 e 10 dólares são, na verdade, o mesmo valor.

Isso se torna ainda mais importante em cálculos matemáticos, onde a necessidade de capturar as unidades (graus/radianos, metros/pés Fahrenheit/Celsius) é ainda mais importante, especialmente se você estiver escrevendo o guidance software de controle para um rocket realmente grande. Considere o Ariane 5, cujo vôo maiden tinha que ser self-destructed devido a um erro na conversão. Ou o teste da NASA para Marte, dentre os quais slammed para o Martian landscape a plena velocidade devido a um erro de conversão.

Recentemente, novas linguagens como F # decidiu acclimate unidades de medida como um recurso de linguagem direta, mas até mesmo TRANSLATION FROM VPE FOR CSHARP e o Visual Basic podem fazer tipos semelhantes de coisas, graças aos genéricos.

Nosso Martin Fowler interna de canalização, vamos começar com uma simples classe dinheiro, conheça o valor (quantidade) e a moeda (tipo) de uma determinada quantia monetária:

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

Superficialmente, isso parece viável, mas antes de muito tempo, vamos querer começar a fazer coisas de valor semelhante com isso, por exemplo, adicionar dinheiro instâncias juntas (bastante comuns algo a ver com dinheiro, quando você 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, o problema vai surgir quando tentamos adicionar U.S. dólares (USD) e Europeu euro (EUR) junto, tais como quando saímos para almoçar (afinal, todo mundo sabe europeus amadurecer a melhor cerveja, mas os americanos fazem a melhor pizza):

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

Qualquer pessoa que leva a uma rápida observação os painéis de controle financeiros vai perceber que alguém está obtendo arrancado — os euros estão sendo convertidas em dólares a uma taxa de 1-1. Para evitar fraude acidental, nós provavelmente deseja certificar-se de que o compilador não sabe para converter USD em euros sem passar por um processo de conversão aprovado que pesquise a atual taxa de conversão (ver Figura 1).

Figura 1 conversão está em ordem

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 como dólares americanos e euros são basicamente apenas espaços reservados, projetados para oferecer o compilador algo para comparação. Se os parâmetros de tipo c dois não são iguais, é um problema.

Claro, também tenhamos perdido a capacidade de combinar os dois, e haverá momentos quando queremos fazer exatamente isso. Fazer isso requer um pouco mais sintaxe paramétrico (ver Figura 2).

Figura 2 combinar tipos intencionalmente

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

Este é um método genérico especializado dentro de uma classe genérica e < a > sintaxe após o nome do método adiciona mais parâmetros de tipo para o escopo do método — neste caso, o tipo de moeda para converter para segundo. Então, comprar uma pizza e cerveja agora se torna 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>();

Se desejável, poderíamos até mesmo usar o operador de conversão (em TRANSLATION FROM VPE FOR CSHARP) para fazer a conversão automaticamente, mas que possam ser mais confusas do que úteis aos leitores do código, dependendo das suas preferências estéticos.

Conclusão

O que está faltando na < > dinheiro exemplo é óbvio: claramente deve haver uma maneira de converter dólares em euros e euros em dólares. Mas parte da meta em designs como esse é evitar um sistema fechado — ou seja, como novas moedas forem necessários (rublos, rúpias, libras, lira ou qualquer outro flutua monetário de barco), seria bom se nós, os designers originais do < de > dinheiro Digite, não precisa ser chamado para adicioná-los. O ideal é que, em um sistema aberto, outros desenvolvedores podem conectá-los à medida que eles precisam e tudo o que "simplesmente funciona".

Não ajustar ainda, porém e certamente não iniciam o código que seja de entrega. Ainda temos alguns ajustes para tornar a < > dinheiro Digite para torná-lo mais eficiente, seguro e extensível. Ao longo do processo, teremos uma olhada na programação funcional e dinâmica.

Mas por enquanto, boa codificação!

Ted Neward é uma entidade de segurança com Neward & Associa uma firma independente especializado em enterprise.NET Framework e Java sistemas da plataforma. Ele escreveu mais de 100 artigos, é um palestrante INETA e de TRANSLATION FROM VPE FOR CSHARP MVP e tem autor e co-autor de doze livros, incluindo "Profissional F # 2. 0" (Wrox 2010). Ele consulta, mentores regularmente — contatá-lo em ted@tedneward.com se você estiver interessado em ter-lhe vir trabalhar com sua equipe, ou ler seu blog em blogs.tedneward.com.

Graças aos seguinte perito técnico para revisão deste artigo: Krzysztof Cwalina