delphi metaclass class-reference

Referencias de clase Delphi... también conocidas como metaclasses... cuándo usarlas



class-reference (5)

He leído la documentación oficial y entiendo las referencias de clase, pero no veo cuándo y por qué son la mejor solución en comparación con las alternativas.

El ejemplo citado en la documentación es TCollection que puede crearse una instancia con cualquier descendiente de TCollectionItem. La justificación para usar una referencia de clase es que le permite invocar métodos de clase cuyo tipo es desconocido en tiempo de compilación (supongo que este es el tiempo de compilación de TCollection). Simplemente no veo cómo usar TCollectionItemClass como argumento es superior al uso de TCollectionItem. TCollection aún podría contener cualquier descendiente de TCollectionItem y aún así poder invocar cualquier método declarado en TCollectionItem. ¿No es así?

Compare esto con una colección genérica. TObjectList parece ofrecer la misma funcionalidad que TCollection con el beneficio adicional de la seguridad de tipo. Se libera del requisito de heredar de TCollectionItem para almacenar su tipo de objeto y puede crear una colección tan específica de tipo como desee. Y si necesita acceder a los miembros del elemento desde la colección, puede usar restricciones genéricas. Además del hecho de que las referencias de clase están disponibles para los programadores antes de Delphi 2009, ¿hay alguna otra razón convincente para usarlas en contenedores genéricos?

El otro ejemplo dado en la documentación es pasar una referencia de clase a una función que actúa como una fábrica de objetos. En este caso, una fábrica para crear objetos de tipo TControl. No es muy claro, pero supongo que la fábrica de TControl invoca al constructor del tipo de descendiente que se le transfirió en lugar del constructor de TControl. Si este es el caso, entonces estoy empezando a ver al menos alguna razón para usar referencias de clase.

Así que supongo que lo que realmente intento comprender es cuándo y dónde las referencias de clase son las más apropiadas y qué es lo que compra un desarrollador.


MetaClasses y "procedimientos de clase"

Las MetaClasses se tratan de "procedimientos de clase". Comenzando con una class básica:

type TAlgorithm = class public class procedure DoSomething;virtual; end;

Como DoSomething es un class procedure podemos llamarlo sin una instancia de TAlgorithm (se comporta como cualquier otro procedimiento global). Podemos hacer esto:

TAlgorithm.DoSomething; // this is perfectly valid and doesn''t require an instance of TAlgorithm

Una vez que tengamos esta configuración, podríamos crear algunos algoritmos alternativos, todos compartiendo algunos bits y paces del algoritmo base. Me gusta esto:

type TAlgorithm = class protected class procedure DoSomethingThatAllDescendentsNeedToDo; public class procedure DoSomething;virtual; end; TAlgorithmA = class(TAlgorithm) public class procedure DoSomething;override; end; TAlgorithmB = class(TAlgorithm) public class procedure DoSomething;override; end;

Ahora tenemos una clase base y dos clases descendientes. El siguiente código es perfectamente válido porque declaramos los métodos como métodos de "clase":

TAlgorithm.DoSomething; TAlgorithmA.DoSomething; TAlgorithmB.DoSomething;

Vamos a presentar la class of tipo:

type TAlgorithmClass = class of TAlgorithm; procedure Test; var X:TAlgorithmClass; // This holds a reference to the CLASS, not a instance of the CLASS! begin X := TAlgorithm; // Valid because TAlgorithmClass is supposed to be a "class of TAlgorithm" X := TAlgorithmA; // Also valid because TAlgorithmA is an TAlgorithm! X := TAlgorithmB; end;

TAlgorithmClass es un tipo de datos que se puede usar como cualquier otro tipo de datos, se puede almacenar en una variable, pasar como parámetro a una función. En otras palabras, podemos hacer esto:

procedure CrunchSomeData(Algo:TAlgorithmClass); begin Algo.DoSomething; end; CrunchSomeData(TAlgorithmA);

En este ejemplo, el procedimiento CrunchSomeData puede usar cualquier variación del algoritmo siempre que sea un descendiente de TAlgorithm.

Aquí hay un ejemplo de cómo se puede usar este comportamiento en una aplicación del mundo real: Imagine una aplicación tipo nómina, donde algunos números deben calcularse de acuerdo con un algoritmo definido por ley. Es concebible que este algoritmo cambie en el tiempo, porque la Ley algunas veces ha cambiado. Nuestra aplicación necesita calcular los salarios tanto para el año actual (utilizando la calculadora actualizada) como para otros años, utilizando versiones anteriores del algoritmo. Así es como se verían las cosas:

// Algorithm definition TTaxDeductionCalculator = class public class function ComputeTaxDeduction(Something, SomeOtherThing, ThisOtherThing):Currency;virtual; end; // Algorithm "factory" function GetTaxDeductionCalculator(Year:Integer):TTaxDeductionCalculator; begin case Year of 2001: Result := TTaxDeductionCalculator_2001; 2006: Result := TTaxDeductionCalculator_2006; else Result := TTaxDeductionCalculator_2010; end; end; // And we''d use it from some other complex algorithm procedure Compute; begin Taxes := (NetSalary - GetTaxDeductionCalculator(Year).ComputeTaxDeduction(...)) * 0.16; end;

Constructores virtuales

Un Delphi Constructor funciona como una "función de clase"; Si tenemos una metaclase y la metaclase conoce un constructor virtual, podemos crear instancias de tipos de descendientes. Esto es utilizado por el Editor IDE de TCollection para crear nuevos elementos cuando presiona el botón "Agregar". Todo lo que TCollection necesita para que esto funcione es una MetaClass para un TCollectionItem.


En algunas de mis aplicaciones, tengo un mecanismo que conecta clases a formas capaces de editar instancias de una o más de esas clases. Tengo una lista central donde se almacenan esos pares: una referencia de clase y una referencia de clase de formulario. Por lo tanto, cuando tengo una instancia de una clase, puedo buscar la clase de formulario correspondiente, crear un formulario y dejar que edite la instancia.

Por supuesto, esto también podría implementarse haciendo que un método de clase devuelva la clase de formulario apropiada, pero eso requeriría que la clase conozca la clase de formulario. Mi enfoque hace un sistema más modular. El formulario debe tener en cuenta la clase, pero no al revés. Este puede ser un punto clave cuando no puede cambiar las clases.


Los genéricos son muy útiles, y estoy de acuerdo en que TObjectList<T> es (por lo general) más útil que TCollection . Pero las referencias de clase son más útiles para diferentes escenarios. Son realmente parte de un paradigma diferente. Por ejemplo, las referencias de clase pueden ser útiles cuando tiene un método virtual que debe ser anulado. Las anulaciones de métodos virtuales deben tener la misma firma que el original, por lo que el paradigma de Generics no se aplica aquí.

Un lugar donde las referencias de clase se usan mucho es en la transmisión de formularios. Vea un DFM como texto en algún momento, y verá que a cada objeto se lo denomina por nombre y clase. (Y el nombre es opcional, en realidad). Cuando el lector de formularios lee la primera línea de una definición de objeto, obtiene el nombre de la clase. Lo busca en una tabla de búsqueda y recupera una referencia de clase, y utiliza esa referencia de clase para invocar la anulación de esa clase de TComponent.Create(AOwner: TComponent) para que pueda instanciar el tipo correcto de objeto, y luego comienza a aplicar las propiedades descrito en el DFM. Este es el tipo de cosas que las referencias de clase le compran, y no se puede hacer con genéricos.


Sí, una colección aún podría contener cualquier descendiente de TCollectionItem e invocar métodos en él. PERO, no podría instanciar una nueva instancia de ningún descendiente de un TCollectionItem. Llamar TCollectionItem.Create construye una instancia de TCollectionItem, mientras que

private FItemClass: TCollectionItemClass; ... function AddItem: TCollectionItem; begin Result := FItemClass.Create; end;

Construiría una instancia de cualquier clase de descendiente TCollectionItem que se encuentre en FItemClass.

No he hecho mucho con contenedores genéricos, pero creo que si tuviera una opción, optaría por el contenedor genérico. Pero en cualquier caso, aún tendría que usar una metaclase si quería tener la instancia de la lista y hacer lo que sea necesario hacer cuando se agrega un elemento en el contenedor y no sé la clase exacta de antemano.

Por ejemplo, un descendiente TObjectList observable (o contenedor genérico) podría tener algo como:

function AddItem(aClass: TItemClass): TItem; begin Result := Add(aClass.Create); FObservers.Notify(Result, cnNew); ... end;

Creo que, en resumen, la ventaja / beneficio de las metaclases es cualquier método / clase que solo tenga conocimiento de

type TMyThing = class(TObject) end; TMyThingClass = class of TMyThing;

es capaz de construir instancias de cualquier descendiente de TMyThing donde sea que hayan sido declaradas.


También usaría una metaclase siempre que sea necesario para hacer una fábrica que pueda construir no solo una clase codificada, sino cualquier clase que herede de mi clase base.

Sin embargo, las metaclases no son el término con el que estoy familiarizado en los círculos de Delphi. Creo que los llamamos referencias de clase, que tiene un nombre que suena menos "mágico", por lo que es genial que pones los monikers comunes en tu pregunta.

Un ejemplo concreto de un lugar que he visto utilizar bien es en los componentes JvLink JvDocking, donde un componente de "estilo de acoplamiento" proporciona información de metaclases al conjunto de componentes base de acoplamiento, de modo que cuando un usuario arrastra su mouse y conecta un formulario de cliente al docking host form, los formularios "tab host" y "conjuntar host" que muestran barras de captura (de apariencia similar a la barra de título de una ventana desacoplada normal) pueden ser de una clase de complemento definida por el usuario, que proporciona una apariencia personalizada y funcionalidad de tiempo de ejecución personalizada, en un plug-in.