Fantasías sobre el tema de las metaclases en C #

Los programadores como yo, que vinimos a C # con una amplia experiencia en Delphi, a menudo carecen de lo que Delphi se llama referencia de clase y, en el trabajo teórico, metaclase. Varias veces en varios foros me encontré con una discusión que tuvo lugar en la misma línea. Comienza con una pregunta de un ex delfín sobre cómo hacer una metaclase en C #. Los agudos simplemente no entienden el problema, tratando de aclarar qué tipo de bestia es esta: una metaclase, los delfines como pueden explicar, pero las explicaciones son cortas e incompletas, y como resultado, los más agudos no saben por qué se necesita todo esto. Después de todo, se puede hacer lo mismo con la ayuda de fábricas de reflexión y clase.

En este artículo, intentaré decirle qué metaclases son para aquellos que nunca los han encontrado. Además, que cada uno decida por sí mismo si sería bueno tener algo así en el idioma, o si es suficiente reflexión. Todo lo que escribo aquí son solo fantasías sobre cómo podría haber sido si las metaclases realmente existieran en C #. Todos los ejemplos en el artículo están escritos en esta versión hipotética de C #, ni un solo compilador existente en este momento puede compilarlos.

¿Qué es una metaclase?


Entonces, ¿qué es una metaclase? Este es un tipo especial que sirve para describir otros tipos. Hay algo muy similar en C #: el tipo Tipo. Pero solo similar. Un valor de tipo Tipo puede describir cualquier tipo, una metaclase solo puede describir a los herederos de la clase especificada cuando se declaró la metaclase.

Para hacer esto, nuestra versión hipotética de C # adquiere el tipo Type <T>, que es el sucesor de Type. Pero el Tipo <T> solo es adecuado para describir el tipo T o sus descendientes.
Explicaré esto con un ejemplo:

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

El ejemplo anterior es el primer paso para la aparición de metaclases. Tipo Tipo <T> le permite restringir qué tipos pueden ser descritos por los valores correspondientes. Esta característica puede resultar útil en sí misma, pero las posibilidades de las metaclases no se limitan a esto.

Metaclases y miembros de clase estática


Si alguna clase X tiene miembros estáticos, la metaclase Tipo <X> obtiene miembros similares, ya no estáticos, a través de los cuales puede acceder a los miembros estáticos de X. Expliquemos esta frase confusa con un ejemplo.

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

Aquí, en términos generales, surge la pregunta: ¿qué sucede si en la clase X se declara un método estático, cuyo nombre y conjunto de parámetros coincide con el nombre y el conjunto de parámetros de uno de los métodos de la clase Type, cuyo heredero es Type <X>? Hay varias opciones bastante simples para resolver este problema, pero no me detendré en ellas: por simplicidad, creemos que en nuestro lenguaje fantástico de conflictos no hay nombres mágicos.

El código anterior para cualquier persona normal debería ser desconcertante: ¿por qué necesitamos una variable para llamar a un método si podemos llamar a este método directamente? De hecho, en esta forma, esta oportunidad es inútil. Pero el beneficio viene cuando le agregas métodos de clase.

Métodos de clase


Los métodos de clase son otra construcción que tiene Delphi, pero falta en C #. Cuando se declaran, estos métodos se marcan con la clase de palabra y son un cruce entre los métodos estáticos y los métodos de instancia. Al igual que los métodos estáticos, no están vinculados a una instancia específica y pueden llamarse a través del nombre de la clase sin crear una instancia. Pero, a diferencia de los métodos estáticos, tienen un parámetro implícito esto. Solo esto en este caso no es una instancia de la clase, sino una metaclase, es decir si el método de clase se describe en la clase X, entonces este parámetro será del tipo Tipo <X>. Y puedes usarlo así:

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

Esta característica no es muy impresionante hasta ahora. Pero gracias a él, los métodos de clase, a diferencia de los métodos estáticos, pueden ser virtuales. Más precisamente, los métodos estáticos también podrían hacerse virtuales, pero no está claro qué hacer a continuación con esta virtualidad. Pero con los métodos de clase, tales problemas no surgen. Considere esto con un ejemplo.

 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() // : ??? } } 

Por la lógica de las cosas, cuando se llama Y.Report, se debe mostrar "Bye!". Pero el método X.Report no tiene información sobre la clase desde la que se llamó, por lo que no puede elegir dinámicamente entre X.DoReport e Y.DoReport. Como resultado, X.Report siempre llamará a X.DoReport, incluso si se llamó a Report a través de Y. No tiene sentido hacer que el método DoReport sea virtual. Por lo tanto, C # no permite que los métodos estáticos sean virtuales: sería posible hacerlo virtual, pero no podrá beneficiarse de su virtualidad.

Otra cosa son los métodos de clase. Si Informe en el ejemplo anterior no fuera estático, sino de clase, "sabría" cuándo se llamó a través de X y cuándo a través de Y. En consecuencia, el compilador podría generar código que seleccionaría el DoReport deseado, y se generaría una llamada a Y.Report a la conclusión "¡Adiós!".

Esta característica es útil en sí misma, pero se vuelve aún más útil si le agrega la capacidad de llamar a las variables de clase a través de metaclases. Algo como esto:

 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 lograr tal polimorfismo sin metaclases y métodos de clase virtual, para la clase X y cada uno de sus descendientes tendría que escribir una clase auxiliar con el método virtual habitual. Esto requiere un esfuerzo significativamente mayor, y el control por parte del compilador no será tan completo, lo que aumenta la probabilidad de cometer un error en alguna parte. Mientras tanto, se encuentran situaciones en las que se necesita polimorfismo a nivel de tipo, y no a nivel de instancia, y si el lenguaje admite dicho polimorfismo, esta es una propiedad muy útil.

Constructores virtuales


Si las metaclases aparecieron en el lenguaje, entonces se les debe agregar constructores virtuales. Si un constructor virtual se declara en una clase, todos sus descendientes deben superponerse, es decir, tener su propio constructor con el mismo conjunto de parámetros, por ejemplo:

 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) { ... } } 

En este código, la clase C no debe compilarse, ya que no tiene un constructor con parámetros int x, int y, pero la clase B se compila sin errores.

Otra opción es posible: si el constructor virtual del antepasado no se superpone en el heredero, el compilador se superpone automáticamente, al igual que ahora crea automáticamente el constructor predeterminado. Ambos enfoques tienen ventajas y desventajas obvias, pero esto no es importante para el panorama general.

Se puede usar un constructor virtual donde sea que se pueda usar un constructor regular. Además, si la clase tiene un constructor virtual, su metaclase tiene un método CreateInstance con el mismo conjunto de parámetros que el constructor, y este método creará una instancia de la clase, como se muestra en el siguiente ejemplo.

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

En otras palabras, tenemos la oportunidad de crear objetos cuyo tipo se determina en tiempo de ejecución. Ahora esto también se puede hacer usando Activator.CreateInstance. Pero este método funciona a través de la reflexión, por lo que la corrección del conjunto de parámetros se verifica solo en la etapa de ejecución. Pero si tenemos metaclases, entonces el código con los parámetros incorrectos simplemente no se compilará. Además, cuando se usa la reflexión, la velocidad del trabajo deja mucho que desear, y las metaclases le permiten minimizar los costos.

Conclusión


Siempre me sorprendió por qué Halesberg, que es el desarrollador principal de Delphi y C #, no hizo metaclases en C #, aunque demostraron ser tan buenos en Delphi. Quizás el punto es que en Delphi (en esas versiones que hizo Halesberg) casi no hay reflexión, y simplemente no hay alternativa a las metaclases, lo que no se puede decir sobre C #. De hecho, todos los ejemplos de este artículo no son tan difíciles de rehacer, utilizando solo aquellas herramientas que ya están en el idioma. Pero todo esto funcionará notablemente más lento de lo que podría hacerlo con metaclases, y la corrección de las llamadas se verificará en tiempo de ejecución, no en compilación. Entonces, mi opinión personal es que C # se beneficiaría enormemente si aparecieran metaclases en él.

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


All Articles