Fantasias sobre o assunto de metaclasses em C #

Programadores como eu, que chegaram ao C # com vasta experiência em Delphi, geralmente não têm o que Delphi é chamado de referência de classe e, no trabalho teórico, metaclasse. Várias vezes em vários fóruns me deparei com uma discussão que ocorreu da mesma maneira. Começa com uma pergunta de um ex-delphist sobre como criar uma metaclasse em C #. Os Sharpists simplesmente não entendem o problema, tentando esclarecer que tipo de animal é esse - uma metaclasse, golfistas como eles podem explicar, mas as explicações são curtas e incompletas e, como resultado, os sharpers estão completamente perdidos por que tudo isso é necessário. Afinal, o mesmo pode ser feito com a ajuda de fábricas de reflexão e de classe.

Neste artigo, tentarei explicar o que são as metaclasses para aqueles que nunca as encontraram. Além disso, todos decidam por si mesmos se seria bom ter uma coisa dessas na linguagem ou se a reflexão é suficiente. Tudo o que estou escrevendo aqui são apenas fantasias sobre como poderia ter sido se as metaclasses realmente existissem em C #. Todos os exemplos deste artigo estão escritos nesta versão hipotética do C #, nem um único compilador existente no momento pode compilá-los.

O que é uma metaclasse?


Então, o que é uma metaclasse? Este é um tipo especial que serve para descrever outros tipos. Há algo muito semelhante em C # - o tipo Type. Mas apenas semelhante. Um valor do tipo Type pode descrever qualquer tipo, uma metaclasse pode descrever apenas os herdeiros da classe especificada quando a metaclasse foi declarada.

Para fazer isso, nossa versão hipotética do C # adquire o tipo Type <T>, que é o sucessor do Type. Mas o Tipo <T> é adequado apenas para descrever o tipo T ou seus descendentes.
Vou explicar isso com um exemplo:

class A { } class A2 : A { } class B { } static class Program { static void Main() { Type<A> ta; ta = typeof(A); //   ta = typeof(A2); //    ta = typeof(B); //   – Type<B>   Type<A> ta = (Type<A>)typeof(B); //      -   Type tx = typeof(A); ta = tx; //   –    Type  Type<A> ta = (Type<A>)tx; //    Type<B> tb = (Type<B>)tx; //  } } 

O exemplo acima é o primeiro passo para o surgimento de metaclasses. Tipo Tipo <T> permite restringir quais tipos podem ser descritos pelos valores correspondentes. Esse recurso pode ser útil por si só, mas as possibilidades de metaclasses não se limitam a isso.

Metaclasses e membros de classe estática


Se alguma classe X tiver membros estáticos, a metaclasse Tipo <X> obterá membros semelhantes, não mais estáticos, pelos quais você pode acessar os membros estáticos do X. Vamos explicar esta frase confusa com um exemplo.

 class X { public static void DoSomething() { } } static class Program { static void Main() { Type<X> tx = typeof(X); tx.DoSomething(); //   ,     X.DoSomething(); } } 

Aqui, de um modo geral, surge a pergunta - e se na classe X for declarado um método estático, cujo nome e conjunto de parâmetros coincidam com o nome e o conjunto de parâmetros de um dos métodos da classe Type, cujo herdador é o Tipo <X>? Existem várias opções bastante simples para resolver esse problema, mas não vou me deter nelas - por simplicidade, acreditamos que em nossa linguagem de fantasia de conflitos não há nomes mágicos.

O código acima para qualquer pessoa normal deve ser desconcertante - por que precisamos de uma variável para chamar um método se podemos chamá-lo diretamente? De fato, nesta forma, essa oportunidade é inútil. Mas o benefício vem quando você adiciona métodos de classe a ele.

Métodos de classe


Métodos de classe são outra construção que o Delphi possui, mas está ausente no C #. Quando declarados, esses métodos são marcados com a classe de palavras e são um cruzamento entre métodos estáticos e métodos de instância. Como métodos estáticos, eles não estão vinculados a uma instância específica e podem ser chamados pelo nome da classe sem criar uma instância. Mas, diferentemente dos métodos estáticos, eles têm um parâmetro implícito this. Somente isso neste caso não é uma instância da classe, mas uma metaclasse, ou seja, se o método da classe for descrito na classe X, esse parâmetro será do tipo Tipo <X>. E você pode usá-lo assim:

 class X { public class void Report() { Console.WriteLine($”    {this.Name}”); } } class Y : X { } static class Program { static void Main() { X.Report() // : «    X» Y.Report() // : «    Y» } } 

Esse recurso não é muito impressionante até agora. Mas, graças a isso, os métodos de classe, diferentemente dos métodos estáticos, podem ser virtuais. Mais precisamente, os métodos estáticos também podem ser virtualizados, mas não está claro o que fazer em seguida com essa virtualidade. Mas com métodos de classe, esses problemas não surgem. Considere isso com um exemplo.

 class X { protected static virtual DoReport() { Console.WriteLine(“!”); } public static Report() { DoReport(); } } class Y : X { protected static override DoReport() { Consloe.WriteLine(“!”); } } static class Program { static void Main() { X.Report() // : «!» Y.Report() // : ??? } } 

Pela lógica das coisas, ao chamar Y.Report, “Bye!” Deve ser exibido. Mas o método X.Report não possui informações sobre de qual classe ele foi chamado, portanto, não pode escolher dinamicamente entre X.DoReport e Y.DoReport. Como resultado, o X.Report sempre chamará X.DoReport, mesmo que o Relatório tenha sido chamado por meio de Y. Não faz sentido tornar virtual o método DoReport. Portanto, o C # não permite virtualizar métodos estáticos - seria possível virtualizá-los, mas você não poderá se beneficiar da virtualidade deles.

Outra coisa são os métodos de classe. Se o Relatório no exemplo anterior não fosse estático, mas classe, ele "saberia" quando foi chamado por meio de X e por Y. Assim, o compilador poderia gerar código que selecionaria o DoReport desejado e resultaria em uma chamada para Y.Report. até a conclusão "Tchau!".

Esse recurso é útil por si só, mas se torna ainda mais útil se você adicionar a capacidade de chamar variáveis ​​de classe por meio de metaclasses. Algo assim:

 class X { public static virtual Report() { Console.WriteLine(“!”); } } class Y : X { public static override Report() { Consloe.WriteLine(“!”); } } static class Program { static void Main() { Type<X> tx = typeof(X); tx.Report() // : «!» tx = typeof(Y); tx.Report() // : «!» } } 

Para atingir esse polimorfismo sem metaclasses e métodos de classe virtual, para a classe X e cada um de seus descendentes teria que escrever uma classe auxiliar com o método virtual usual. Isso requer muito mais esforço, e o controle do compilador não será tão completo, o que aumenta a probabilidade de cometer algum erro. Enquanto isso, situações em que o polimorfismo é necessário no nível do tipo, e não no nível da instância, são encontradas regularmente e, se a linguagem suportar esse polimorfismo, essa é uma propriedade muito útil.

Construtores virtuais


Se metaclasses aparecerem no idioma, os construtores virtuais precisarão ser adicionados a eles. Se um construtor virtual for declarado em uma classe, todos os seus descendentes deverão se sobrepor, ou seja, tenha seu próprio construtor com o mesmo conjunto de parâmetros, por exemplo:

 class A { public virtual A(int x, int y) { ... } } class B : A { public override B(int x, int y) : base(x, y) { } } class C : A { public C(int z) { ... } } 

Nesse código, a classe C não deve ser compilada, porque não possui um construtor com os parâmetros int x, int y, mas a classe B é compilada sem erros.

Outra opção é possível: se o construtor virtual do ancestral não se sobrepuser ao herdeiro, o compilador o sobrepõe automaticamente, assim como agora cria automaticamente o construtor padrão. Ambas as abordagens têm prós e contras óbvios, mas isso não é importante para o quadro geral.

Um construtor virtual pode ser usado sempre que um construtor regular pode ser usado. Além disso, se a classe tiver um construtor virtual, sua metaclasse terá o método CreateInstance com o mesmo conjunto de parâmetros que o construtor, e esse método instanciará a classe, conforme mostrado no exemplo abaixo.

 class A { public virtual A(int x, int y) { ... } } class B : A { public override B(int x, int y) : base(x, y) { } } static class Program { static void Main() { Type<A> ta = typeof(A); A a1 = ta.CreateInstance(10, 12); //    A ta = typeof(B); A a2 = ta.CreateInstance(2, 7); //    B } } 

Em outras palavras, temos a oportunidade de criar objetos cujo tipo é determinado em tempo de execução. Agora isso também pode ser feito usando Activator.CreateInstance. Mas esse método funciona através da reflexão, portanto, a correção do conjunto de parâmetros é verificada apenas no estágio de execução. Mas se tivermos metaclasses, o código com os parâmetros errados simplesmente não será compilado. Além disso, ao usar a reflexão, a velocidade do trabalho deixa muito a desejar, e as metaclasses permitem minimizar os custos.

Conclusão


Sempre me surpreendi por que Halesberg, que é o principal desenvolvedor do Delphi e do C #, não fazia metaclasses em C #, embora eles se mostrassem tão bem no Delphi. Talvez o ponto aqui seja que no Delphi (nessas versões que Halesberg fez) quase não há reflexão, e simplesmente não há alternativa para as metaclasses, o que não pode ser dito sobre C #. De fato, todos os exemplos deste artigo não são tão difíceis de refazer, usando apenas as ferramentas que já estão no idioma. Mas tudo isso funcionará visivelmente mais devagar do que poderia com as metaclasses, e a correção das chamadas será verificada no tempo de execução, não na compilação. Portanto, minha opinião pessoal é que o C # se beneficiaria muito se metaclasses aparecessem nele.

Source: https://habr.com/ru/post/pt464141/


All Articles