O objetivo da componentização clara é derrotado pelo compartilhamento de muitas informações de tipo entre bibliotecas. Talvez você precise de um armazenamento de dados fortemente tipado e eficiente, mas seria muito caro se você precisasse atualizar o esquema do seu banco de dados toda vez que o modelo de objeto evoluísse, então você faria o mesmo?
em vez disso, inferir seu esquema de tipo em tempo de execução? Você precisa entregar componentes que aceitem objetos de usuário arbitrários emanipulá
-los de alguma maneira inteligente. Você deseja que o compilador da biblioteca seja capaz de lhe dizer programaticamente quais são seus tipos?
para manter estruturas de dados fortemente tipadas e, ao mesmo tempo, maximizar a flexibilidade do tempo de execução, você provavelmente desejará considerar a reflexão e como ela pode melhorar seu software. Nesta coluna, explorarei o namespace System.Reflection no Microsoft .NET Framework e como ele pode beneficiar sua experiência de desenvolvimento. Começarei com alguns exemplos simples e terminarei explicando como lidar com situações de serialização do mundo real. Ao longo do caminho, mostrarei como a reflexão e o CodeDom funcionam juntos para lidar com dados de tempo de execução com eficiência.
Antes de me aprofundar no System.Reflection, gostaria de discutir a programação reflexiva em geral. Primeiro, a reflexão pode ser definida como qualquer funcionalidade fornecida por um sistema de programação que permite aos programadores inspecionar e manipular entidades de código sem conhecimento prévio de sua identidade ou estrutura formal. Há muito o que abordar nesta seção, então abordarei um por um.
Primeiro, o que a reflexão proporciona? O que você pode fazer com ela? Tenho tendência a dividir as tarefas típicas centradas na reflexão em duas categorias: inspeção e manipulação. A inspeção requer a análise de objetos e tipos para coletar informações estruturadas sobre sua definição e comportamento. Para além de algumas disposições básicas, isto é muitas vezes feito sem qualquer conhecimento prévio das mesmas. (Por exemplo, no .NET Framework, tudo herda de System.Object, e uma referência a um tipo de objeto costuma ser o ponto de partida geral para reflexão.)
As operações invocam código dinamicamente usando informações coletadas por meio de inspeção, criação de novas instâncias ou até mesmo tipos e objetos podem ser facilmente reestruturados dinamicamente. Um ponto importante a ser destacado é que, para a maioria dos sistemas, a manipulação de tipos e objetos em tempo de execução resulta na degradação do desempenho em comparação com a execução estática de operações equivalentes no código-fonte. Esta é uma compensação necessária devido à natureza dinâmica da reflexão, mas há muitas dicas e práticas recomendadas para otimizar o desempenho da reflexão (consulte msdn.microsoft.com/msdnmag/issues/05 para obter informações mais detalhadas sobre como otimizar o uso de reflexão /07/Reflexão).
Então, qual é o objetivo da reflexão? O que o programador realmente inspeciona e manipula? Na minha definição de reflexão, usei o novo termo "entidade de código" para enfatizar o fato de que, da perspectiva do programador, as técnicas de reflexão às vezes confundem os limites? objetos e tipos tradicionais. Por exemplo, uma tarefa típica centrada na reflexão pode ser:
começar com um identificador para o objeto O e usar a reflexão para obter um identificador para sua definição associada (tipo T).
Examine o tipo T e obtenha um identificador para seu método M.
Chame o método M de outro objeto O' (também do tipo T).
Observe que estou passando de uma instância para seu tipo subjacente, desse tipo para um método e, em seguida, usando o identificador do método para chamá-lo em outra instância - obviamente, isso está usando a programação C # tradicional no código-fonte. A tecnologia não pode alcançá-lo. Depois de discutir o System.Reflection do .NET Framework abaixo, explicarei essa situação novamente com um exemplo concreto.
Algumas linguagens de programação fornecem reflexão nativamente por meio de sintaxe, enquanto outras plataformas e frameworks (como o .NET Framework) a fornecem como uma biblioteca de sistema. Independentemente de como a reflexão é proporcionada, as possibilidades de utilização da tecnologia de reflexão em uma determinada situação são bastante complexas. A capacidade de um sistema de programação fornecer reflexão depende de muitos fatores: O programador faz bom uso dos recursos da linguagem de programação para expressar seus conceitos? O compilador incorpora informações estruturadas suficientes (metadados) na saída para facilitar análises futuras? Interpretação? Existe um subsistema de tempo de execução ou interpretador de host que digere esses metadados. A biblioteca da plataforma apresenta os resultados dessa interpretação de uma forma que seja útil para os programadores
se você tiver em mente um sistema de tipo complexo e orientado a objetos
?aparece como uma função simples no estilo C no código e não há nenhuma estrutura de dados formal, então é obviamente impossível para o seu programa inferir dinamicamente que o ponteiro de uma determinada variável v1 aponta para uma instância de objeto de um certo tipo T . Afinal, o tipo T é um conceito na sua cabeça; ele nunca aparece explicitamente nas suas instruções de programação. Mas se você usar uma linguagem orientada a objetos mais flexível (como C#) para expressar a estrutura abstrata do programa e introduzir diretamente o conceito do tipo T, o compilador converterá sua ideia em algo que poderá posteriormente ser passado pelo Lógica apropriada para entender o formulário, conforme fornecido pelo Common Language Runtime (CLR) ou algum intérprete de linguagem dinâmica.
A reflexão é uma tecnologia totalmente dinâmica e de tempo de execução? Simplificando, não é. Muitas vezes, durante o ciclo de desenvolvimento e execução, a reflexão está disponível e é útil para os desenvolvedores. Algumas linguagens de programação são implementadas por meio de compiladores independentes que convertem código de alto nível diretamente em instruções que a máquina pode entender. O arquivo de saída inclui apenas entrada compilada e o tempo de execução não possui lógica de suporte para aceitar objetos opacos e analisar dinamicamente suas definições. Este é exatamente o caso de muitos compiladores C tradicionais. Como há pouca lógica de suporte no executável de destino, você não pode fazer muita reflexão dinâmica, mas os compiladores fornecem reflexão estática de tempos em tempos - por exemplo, o onipresente operador typeof permite que os programadores verifiquem os identificadores de tipo em tempo de compilação.
Uma situação completamente diferente é que as linguagens de programação interpretadas sempre são executadas por meio do processo principal (as linguagens de script geralmente se enquadram nesta categoria). Como a definição completa do programa está disponível (como o código-fonte de entrada), combinada com a implementação completa da linguagem (como o próprio interpretador), todas as técnicas necessárias para apoiar a autoanálise estão em vigor. Esta linguagem dinâmica fornece frequentemente capacidades de reflexão abrangentes, bem como um rico conjunto de ferramentas para análise dinâmica e manipulação de programas.
O .NET Framework CLR e suas linguagens hospedeiras, como C#, estão no meio. O compilador é usado para converter o código-fonte em IL e metadados. Este último é de nível inferior ou menos "lógico" que o código-fonte, mas ainda retém muita estrutura abstrata e informações de tipo. Depois que o CLR inicia e hospeda esse programa, a biblioteca System.Reflection da biblioteca de classes base (BCL) pode usar essas informações e retornar informações sobre o tipo de objeto, membros de tipo, assinaturas de membros e assim por diante. Além disso, também pode suportar chamadas, incluindo chamadas de ligação tardia.
Reflexão no .NET
Para aproveitar as vantagens da reflexão ao programar com o .NET Framework, você pode usar o namespace System.Reflection. Este namespace fornece classes que encapsulam muitos conceitos de tempo de execução, como assemblies, módulos, tipos, métodos, construtores, campos e propriedades. A tabela na Figura 1 mostra como as classes em System.Reflection são mapeadas para suas contrapartes conceituais de tempo de execução.
Embora importantes, System.Reflection.Assembly e System.Reflection.Module são usados principalmente para localizar e carregar novo código no tempo de execução. Nesta coluna, não discutirei essas partes e assumirei que todo o código relevante já foi carregado.
Para inspecionar e manipular o código carregado, o padrão típico é principalmente System.Type. Normalmente, você começa obtendo uma instância System.Type da classe de tempo de execução de interesse (via Object.GetType). Você pode então usar vários métodos de System.Type para explorar a definição do tipo em System.Reflection e obter instâncias de outras classes. Por exemplo, se você estiver interessado em um método específico e quiser obter uma instância System.Reflection.MethodInfo desse método (talvez por meio de Type.GetMethod). Da mesma forma, se você estiver interessado em um campo e quiser obter uma instância System.Reflection.FieldInfo deste campo (talvez através de Type.GetField).
Depois de ter todos os objetos de instância de reflexão necessários, você poderá continuar seguindo as etapas de inspeção ou manipulação conforme necessário. Ao verificar, você usa várias propriedades descritivas na classe reflexiva para obter as informações necessárias (este é um tipo genérico? Este é um método de instância?). Ao operar, você pode chamar e executar métodos dinamicamente, criar novos objetos chamando construtores e assim por diante.
Verificando Tipos e Membros
Vamos passar para o código e explorar como verificar usando reflexão básica. Vou me concentrar na análise de tipo. Começando com um objeto, recuperarei seu tipo e depois examinarei alguns membros interessantes (veja a Figura 2).
A primeira coisa a notar é que na definição da classe, à primeira vista parece que há muito mais espaço para descrever os métodos do que eu esperava. De onde vêm esses métodos extras? Qualquer pessoa versada na hierarquia de objetos do .NET Framework reconhecerá esses métodos herdados da própria classe base comum Object. (Na verdade, usei primeiro Object.GetType para recuperar seu tipo.) Além disso, você pode ver a função getter da propriedade. Agora, e se você precisar apenas das funções explicitamente definidas do próprio MyClass. Em outras palavras, como você oculta as funções herdadas? Ou talvez você só precise das funções de instância explicitamente definidas.
Basta dar uma olhada online no MSDN e você saberá
?Descobri que todos estão dispostos a usar o segundo método sobrecarregado de GetMethods, que aceita o parâmetro BindingFlags. Ao combinar diferentes valores da enumeração BindingFlags, você pode fazer com que uma função retorne apenas o subconjunto desejado de métodos. Substitua a chamada GetMethods por:
GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly |BindingFlags.Public)
Como resultado, você obtém a seguinte saída (observe que não há funções auxiliares estáticas e funções herdadas de System.Object).
Exemplo de demonstração de reflexão 1
Nome do tipo: MyClass
Nome do método: MyMethod1
Nome do método: MyMethod2
Nome do método: get_MyProperty
Nome da propriedade: MyProperty
E se você souber o nome do tipo (totalmente qualificado) e os membros de antemão? conversão? Com o código dos dois primeiros exemplos, você já possui os componentes básicos para implementar um navegador de classe primitivo. Você pode localizar uma entidade de tempo de execução por nome e depois enumerar suas diversas propriedades relacionadas.
Chamando código dinamicamente
Até agora obtive identificadores para objetos de tempo de execução (como tipos e métodos) apenas para fins descritivos, como imprimir seus nomes. Mas como fazer mais? Como realmente chamar um método.
Alguns pontos-chave neste exemplo são: primeiro, uma instância System.Type é recuperada de uma instância de MyClass, mc1 e, em seguida, uma instância de MethodInfo é recuperada de uma instância de MethodInfo. esse tipo. Finalmente, quando MethodInfo é chamado, ele é vinculado a outra instância MyClass (mc2), passando-o como o primeiro parâmetro da chamada.
Conforme mencionado anteriormente, este exemplo confunde a distinção entre tipos e uso de objetos que você esperaria ver no código-fonte. Logicamente, você recupera um identificador para um método e depois chama o método como se ele pertencesse a um objeto diferente. Para programadores familiarizados com linguagens de programação funcionais, isso pode ser fácil, mas para programadores familiarizados apenas com C#, pode não ser tão intuitivo separar a implementação de objetos e a instanciação de objetos.
Juntando tudo
Até agora eu discuti os princípios básicos de check e call, e agora vou juntá-los com exemplos concretos. Imagine que você deseja entregar uma biblioteca com funções auxiliares estáticas que devem manipular objetos. Mas em tempo de design, você não tem ideia dos tipos desses objetos. Depende das instruções do chamador da função sobre como ele deseja extrair informações significativas desses objetos! A função aceitará uma coleção de objetos e um descritor de string do método. Ele irá então percorrer a coleção, chamando os métodos de cada objeto e agregando os valores de retorno com alguma função.
Para este exemplo, vou declarar algumas restrições. Primeiro, o método descrito pelo parâmetro string (que deve ser implementado pelo tipo subjacente de cada objeto) não aceitará nenhum parâmetro e retornará um número inteiro. O código irá percorrer a coleção de objetos, chamando o método especificado e calculando gradualmente a média de todos os valores. Finalmente, como este não é um código de produção, não preciso me preocupar com validação de parâmetros ou estouro de número inteiro ao somar.
Ao navegar no código de exemplo, você pode ver que o acordo entre a função principal e o auxiliar estático ComputeAverage não depende de nenhuma informação de tipo além da classe base comum do próprio objeto. Em outras palavras, você pode alterar completamente o tipo e a estrutura do objeto que está sendo transferido, mas contanto que você sempre possa usar uma string para descrever um método que retorna um número inteiro, ComputeAverage funcionará bem
. está oculto em O último exemplo está relacionado ao MethodInfo (reflexão geral). Observe que no loop foreach do ComputeAverage, o código apenas captura um MethodInfo do primeiro objeto da coleção e o vincula à chamada de todos os objetos subsequentes. Como mostra a codificação, funciona bem - este é um exemplo simples de cache do MethodInfo. Mas há uma limitação fundamental aqui. Uma instância MethodInfo só pode ser chamada por uma instância do mesmo tipo hierárquico do objeto que ela recupera. Isso é possível porque as instâncias de IntReturner e SonOfIntReturner (herdadas de IntReturner) são passadas.
No código de exemplo, foi incluída uma classe chamada EnemyOfIntReturner, que implementa o mesmo protocolo básico que as outras duas classes, mas não compartilha nenhum tipo compartilhado comum. Em outras palavras, as interfaces são logicamente equivalentes, mas não há sobreposição no nível do tipo. Para explorar o uso de MethodInfo nesta situação, tente adicionar outro objeto à coleção, obtenha uma instância por meio de "new EnemyOfIntReturner(10)" e execute o exemplo novamente. Você encontrará uma exceção indicando que MethodInfo não pode ser usado para chamar o objeto especificado porque não tem absolutamente nada a ver com o tipo original do qual o MethodInfo foi obtido (mesmo que o nome do método e o protocolo subjacente sejam equivalentes). Para deixar seu código pronto para produção, você precisa estar preparado para enfrentar essa situação.
Uma solução possível poderia ser analisar você mesmo os tipos de todos os objetos recebidos, mantendo a interpretação de sua hierarquia de tipos compartilhada (se houver). Se o tipo do próximo objeto for diferente de qualquer hierarquia de tipos conhecida, um novo MethodInfo precisará ser obtido e armazenado. Outra solução é capturar TargetException e obter novamente uma instância MethodInfo. Ambas as soluções mencionadas aqui têm seus prós e contras. Joel Pobar escreveu um excelente artigo para a edição de maio de 2007 desta revista sobre buffer do MethodInfo e desempenho de reflexão, que eu recomendo fortemente.
Esperamos que este exemplo demonstre a adição de reflexão a um aplicativo ou estrutura para adicionar mais flexibilidade para personalização ou extensibilidade futura. É certo que usar a reflexão pode ser um pouco complicado em comparação com a lógica equivalente em linguagens de programação nativas. Se você acha que adicionar vinculação tardia baseada em reflexão ao seu código é muito complicado para você ou seus clientes (afinal, eles precisam que seus tipos e códigos sejam contabilizados em sua estrutura de alguma forma), então isso pode ser necessário apenas com moderação e flexibilidade para alcançar algum equilíbrio.
Tratamento eficiente de tipos para serialização
Agora que cobrimos os princípios básicos da reflexão do .NET por meio de vários exemplos, vamos dar uma olhada em uma situação do mundo real. Se o seu software interage com outros sistemas através de serviços Web ou outras tecnologias de comunicação remota fora do processo, você provavelmente encontrou problemas de serialização. A serialização converte essencialmente objetos ativos que ocupam memória em um formato de dados adequado para transmissão on-line ou armazenamento em disco.
O namespace System.Xml.Serialization no .NET Framework fornece um mecanismo de serialização poderoso com XmlSerializer, que pode pegar qualquer objeto gerenciado e convertê-lo em XML (os dados XML também podem ser convertidos de volta em uma instância de objeto digitado no futuro. Este processo é chamado de desserialização). A classe XmlSerializer é um software poderoso e pronto para empresas que será sua primeira escolha se você enfrentar problemas de serialização em seu projeto. Mas, para fins educacionais, vamos explorar como implementar a serialização (ou outras instâncias de manipulação de tipo de tempo de execução semelhantes).
Considere o seguinte: você está entregando uma estrutura que pega instâncias de objetos de tipos de usuários arbitrários e as converte em algum formato de dados inteligentes. Por exemplo, suponha que você tenha um objeto residente na memória do tipo Endereço conforme mostrado abaixo:
(pseudocódigo)
endereço de classe
{
ID do endereço;
Rua das Cordas, Cidade;
EstadoTipoEstado;
ZipCodeTipo ZipCode;
}
Como gerar uma representação de dados apropriada para uso posterior Talvez uma simples renderização de texto resolva este problema:
Endereço: 123
Rua: 1 Microsoft Way
Cidade: Redmond
Estado: WA
CEP: 98052
Se os dados formais que precisam ser convertidos forem totalmente compreendidos? digite antecipadamente (por exemplo, ao escrever o código você mesmo), as coisas se tornam muito simples:
foreach(Address a in AddressList)
{
Console.WriteLine(“Endereço:{0}”, a.ID);
Console.WriteLine(“tRua:{0}”, a.Rua);
... // e assim por diante
}
No entanto, as coisas podem ficar realmente interessantes se você não souber antecipadamente quais tipos de dados encontrará em tempo de execução. Como você escreve um código de estrutura geral como este
MyFramework.TranslateObject (entrada de objeto, saída MyOutputWriter)
Primeiro, você precisa decidir quais membros de tipo são úteis para serialização. As possibilidades incluem capturar apenas membros de um tipo específico, como tipos de sistema primitivos, ou fornecer um mecanismo para autores de tipos indicarem quais membros precisam ser serializados, como usar propriedades personalizadas como marcadores em membros de tipo). Você só pode capturar membros de um tipo específico, como tipos de sistema primitivos, ou o autor do tipo pode indicar quais membros precisam ser serializados (possivelmente usando propriedades personalizadas como marcadores nos membros do tipo).
Depois de documentar os membros da estrutura de dados que precisam ser convertidos, o que você precisa fazer é escrever a lógica para enumerá-los e recuperá-los dos objetos recebidos. O Reflection faz o trabalho pesado aqui, permitindo consultar estruturas e valores de dados.
Para simplificar, vamos projetar um mecanismo de conversão leve que pega um objeto, obtém todos os seus valores de propriedade pública, os converte em strings chamando ToString diretamente e então serializa os valores. Para um determinado objeto chamado "input", o algoritmo é aproximadamente o seguinte:
chame input.GetType para recuperar uma instância System.Type, que descreve a estrutura subjacente da entrada.
Use Type.GetProperties e o parâmetro BindingFlags apropriado para recuperar propriedades públicas como instâncias PropertyInfo.
As propriedades são recuperadas como pares de valores-chave usando PropertyInfo.Name e PropertyInfo.GetValue.
Chame Object.ToString em cada valor para convertê-lo (de maneira básica) para o formato de string.
Empacote o nome do tipo de objeto e a coleção de nomes de propriedades e valores de string no formato de serialização correto.
Este algoritmo simplifica significativamente as coisas, ao mesmo tempo que captura o ponto de pegar uma estrutura de dados de tempo de execução e transformá-la em dados autodescritivos. Mas há um problema: desempenho. Como mencionado anteriormente, a reflexão é muito cara tanto para processamento de tipo quanto para recuperação de valor. Neste exemplo, executo uma análise de tipo completa em cada instância do tipo fornecido.
E se fosse possível capturar ou preservar de alguma forma sua compreensão da estrutura de um tipo para que você pudesse recuperá-la sem esforço mais tarde e lidar com eficiência com novas instâncias desse tipo? Em outras palavras, pule para a etapa 3 no algoritmo de exemplo. A novidade é que é possível fazer isso usando recursos do .NET Framework. Depois de entender a estrutura de dados de um tipo, você pode usar o CodeDom para gerar dinamicamente código que se liga a essa estrutura de dados. Você pode gerar um assembly auxiliar que contém uma classe auxiliar e métodos que fazem referência ao tipo de entrada e acessam suas propriedades diretamente (como qualquer outra propriedade no código gerenciado), portanto, a verificação de tipo afeta o desempenho apenas uma vez.
Agora vou consertar esse algoritmo. Novo tipo:
Obtenha a instância System.Type correspondente a este tipo.
Use os vários acessadores System.Type para recuperar o esquema (ou pelo menos o subconjunto do esquema útil para serialização), como nomes de propriedades, nomes de campos, etc.
Use as informações do esquema para gerar um assembly auxiliar (via CodeDom) que se vincule ao novo tipo e lide com instâncias com eficiência.
Use código em um assembly auxiliar para extrair dados de instância.
Serialize os dados conforme necessário.
Para todos os dados recebidos de um determinado tipo, você pode pular para a etapa 4 e obter uma grande melhoria de desempenho em relação à verificação explícita de cada instância.
Desenvolvi uma biblioteca básica de serialização chamada SimpleSerialization que implementa esse algoritmo usando reflexão e CodeDom (pode ser baixado nesta coluna). O componente principal é uma classe chamada SimpleSerializer, que é construída pelo usuário com uma instância de System.Type. No construtor, a nova instância SimpleSerializer analisa o tipo fornecido e gera um assembly temporário usando classes auxiliares. A classe auxiliar está fortemente ligada ao tipo de dados fornecido e trata a instância como se você estivesse escrevendo o código com conhecimento prévio completo do tipo.
A classe SimpleSerializer possui o seguinte layout:
class SimpleSerializer
{
classe pública SimpleSerializer (Tipo dataType);
public void Serialize (entrada de objeto, gravador SimpleDataWriter);
}
Simplesmente incrível! O construtor faz o trabalho pesado: ele usa reflexão para analisar a estrutura do tipo e depois usa CodeDom para gerar o assembly auxiliar. A classe SimpleDataWriter é apenas um coletor de dados usado para ilustrar padrões de serialização comuns.
Para serializar uma instância simples da classe Address, use o seguinte pseudocódigo para concluir a tarefa:
SimpleSerializer
mySerializer = new SimpleSerializer(typeof(Address));
SimpleDataWriter Writer = new SimpleDataWriter(
)
;
Recomendamos que você experimente o código de exemplo, especialmente a biblioteca SimpleSerialization. Adicionei comentários a algumas partes interessantes do SimpleSerializer, espero que ajude. Obviamente, se você precisar de serialização estrita no código de produção, precisará realmente confiar nas tecnologias fornecidas no .NET Framework (como XmlSerializer). Mas se você achar que precisa trabalhar com tipos arbitrários em tempo de execução e lidar com eles de forma eficiente, espero que adote minha biblioteca SimpleSerialization como sua solução.