Saludos,
Una buena introduccion a este tema seria leer este otro ¿String a Enumerativo con record helper? ¿Es posible?
La idea general consiste en tener un ayudante para los tipos enumerados. Parece que se llega a una encrucijada porque los record helper no soportan herencia, lo cual nos impide reutilizar el codigo para tal fin. Esto termina en una explosion de helpers los cuales hacen todos exactamente lo mismo, la unica variante es el tipo sobre el cual opera el helper
Tomando varias ideas de la web he escrito un record generico que permite manipular los tipos enumerativos de Delphi de manera sencilla, y gracias al generico, se puede permitir usar cualquier tipo enumerativo. La idea es envolver la RTTI para obtener informacion dinamicamente del tipo enumerado.
Si bien no lo he probado, es necesario Delphi 2010 como minimo que es en donde entra en juego esta nueva RTTI extendida
Sin mas, la interface que se expone consiste en dos tipos fundamentales, el primero y mas importante es Enum<T>
/// <summary> Record que contiene metodos estaticos para trabajar con tipos enums </summary> Enum<T: record> = record public /// <summary> El nombre del tipo enum </summary> class function TypeName: string; static; inline; /// <summary> El nombre del valor enum </summary> class function ValueName(const Value: T): string; static; inline; /// <summary> Devuelve el valor del tipo enum anotado por el atributo EnumNames </summary> /// <summary> Si el enum no esta anotado por el atributo EnumNames, o no esta anotado por un atributo /// EnumNames con el identificador indicado, se eleva una excepcion EEnumNameNotFound </summary> /// <remarks> Ver EnumNamesAttribute </remarks> class function EnumName(const Identifier: string; const Value: T): string; static; inline; /// <summary> Devuelve el valor del tipo enum anotado por el atributo EnumNames </summary> /// <summary> En lugar de elevar una excepcion EEnumNameNotFound, se devuelve el valor Default </summary> /// <summary> Si Default = EmptyStr se devuelve ValueName(Value) </summary> /// <remarks> Ver EnumNamesAttribute </remarks> class function EnumNameOrDefault(const Identifier: string; const Value: T; const Default: string = ''): string; static; inline; /// <summary> Devuelve todos los nombres con los que fue anotado el enum </summary> class function EnumNames(const Identifier: string): TArray<string>; static; inline; /// <summary> Devuelve el valor enum dado un Ordinal </summary> class function Parse(const Ordinal: Integer): T; static; inline; /// <summary> Convierte el valor enum a su correspondiente Ordinal </summary> class function ToInteger(const Value: T): Integer; static; inline; /// <summary> El valor maximo del enum. Equivalente a Ord(High(T)) </summary> class function MaxValue: Integer; static; inline; /// <summary> El valor maximo del enum. Equivalente a Ord(Low(T)) </summary> class function MinValue: Integer; static; inline; /// <summary> Devuelve True si el valor del tipo enum se encuentra dentro del rango permitido </summary> class function InRange(const Value: T): Boolean; overload; static; /// <summary> Devuelve True si el entero se encuentra dentro del rango permitido del tipo enum </summary> class function InRange(const Value: Integer): Boolean; overload; static; /// <summary> Eleva una excepcion EEnumOutOfRange si el valor del tipo enum esta fuera del rango // permitido </summary> /// <param name="Value"> El valor a testear </param> /// <param name="Namespace"> Describe el "contexto" de quien invoca a este metodo (ej clase o unidad) </param> /// <param name="MethodName"> Nombre del metodo que invoco a esta rutina </param> class procedure CheckInRange(const Value: T; const Namespace, MethodName: string); static; /// <summary> Cantidad de elementos del enum </summary> class function Count: Integer; static; /// <summary> Devuelve un Array con los elementos del enum </summary> class function AsArray: TArray<T>; static; end;
Lamentablemente en Delphi no tenemos como constraint (restriccion) para los genericos algo que limite al generico a solamente aceptar tipos enumerativos. Esto implica que deba usar el contraint record, que se traduce en que se puede pasar como parametro generico cualquier tipo "no nullable". Por ejemplo, enumerativos, integer, etc
Los metodos son bastante sencillos de entender y tienen su pequeño comentario sobre que hacen
Una pequeña muestra de codigo de lo que se puede lograr es la siguiente:
type {$SCOPEDENUMS ON} // podemos usar enumerativos con calificacion completa si queremos sin ningun problema TScoped = (First, Second, Third); {$SCOPEDENUMS OFF} TTestEnumeration = (ttFirst, ttSecond, ttThird); procedure Main; var TestEnum: TTestEnumeration; ScopedEnum: TScoped; begin // Imprime los valores "0", "1", "2" for TestEnum in Enum<TTestEnumeration>.AsArray do begin Write('Enum<TTestEnumeration>.ToInteger: '); Writeln(Enum<TTestEnumeration>.ToInteger(TestEnum)); end; Writeln; // Imprime los valores "First", "Second", "Third" for ScopedEnum in Enum<TScoped>.AsArray do begin Write('Enum<TScoped>.ValueName: '); Writeln(Enum<TScoped>.ValueName(ScopedEnum)); end; Writeln; // imprime "TTestEnumeration" Writeln(Enum<TTestEnumeration>.TypeName); // imprime "TScoped" Write(Enum<TScoped>.TypeName); // "TScoped" tiene 3 elementos Writeln(' tiene ' + Enum<TScoped>.Count.ToString + ' elementos'); Writeln('El valor maximo es ' + Enum<TScoped>.MaxValue.ToString); Writeln('El valor minimo es ' + Enum<TScoped>.MinValue.ToString); Write('5 es un ordinal dentro del rango permitido de TScoped? '); // evalua False Writeln(Enum<TScoped>.InRange(5)); Write('2 es un ordinal dentro del rango permitido de TScoped? '); // evalua True Writeln(Enum<TScoped>.InRange(2)); end;
Todo esto esta muy bien, pero ahora, para responder a la inquietud del hilo inicial de Delphius, esto al usar RTTI no puede hacer "mucho mas" que devolver el nombre con el que declaramos la enumeracion.
Que pasa si quiero tener distintas representaciones en string del mismo enumerativo? Una forma bastante elegante y que permite el reuso de codigo es el uso de atributos.
Quien nunca haya leido o usado atributos le invito a explorar la documentacion
El segundo tipo importante que exporta la unidad es el atributo EnumNamesAttribute
EnumNamesAttribute = class(TCustomAttribute) strict private FIdentifier: string; FNames: TArray<string>; public constructor Create(const Identifier, Names: string; const Delimiter: string = ','); function NameOf<T: record>(const Value: T): string; property Identifier: string read FIdentifier; property Names: TArray<string> read FNames; end;
Los atributos basicamente son informacion extra adicional que se usa para "decorar", o "anotar" un determinado tipo (en realidad los atributos se pueden usar sobre muchas mas cosas, para ver que es posible anotar con atributos, mejor referirse a la documentacion)
Como es informacion que se almacena en el ejecutable, como si fuera "una rtti mas", deben ser valores constantes, que se puedan resolver en tiempo de compilacion.
Una limitante de Delphi es que, aunque realmente un "array of TMiEnumerativo of string" es constante, no lo permite en atributos. Es ese el motivo por el cual el atributo EnumNamesAttribute se implementa usando un string delimitado por algun caracter
Otro pormenor es que si bien el tipo enumerado puede ser anotado con atributos, no sucede lo mismo con los valores del enumerado (el codigo compila, ni da advertencias, pero luego no hay forma de extraer la informacion)
Pasemos a un ejemplo que creo que va a ser mas practico:
type {$SCOPEDENUMS ON} [EnumNames('Test', 'Hello,World')] // utilizamos el atributo TScoped = (First, Second, Third); // declaracion del tipo, como toda la vida {$SCOPEDENUMS OFF} procedure Main; begin // extraer el valor Writeln(Enum<TScoped>.EnumName('Test', TScoped.First)); // imprime "Hello" end;
Que esta pasando aqui?
Bueno, en realidad cuando anotamos al tipo TScoped con el atributo, en realidad lo que estamos haciendo es "invocar a su constructor". Si se fijan en el constructor, esta definido asi:
constructor Create(const Identifier, Names: string; const Delimiter: string = ',');
definir distintas representaciones, pero obviamente era necesario algo que "identifique"
Un ejemplo en accion:
type {$SCOPEDENUMS ON} [EnumNames('Test', 'Hello,World')] [EnumNames('MoreNames', 'OnlyFirst')] TScoped = (First, Second, Third); {$SCOPEDENUMS OFF} procedure Main; var s: string; begin // imprime "Hello" Writeln(Enum<TScoped>.EnumName('Test', TScoped.First)); // Writeln(Enum<TScoped>.EnumName('test', TScoped.First)); // excepcion, sensible a mayusculas // imprime "OnlyFirst" Writeln(Enum<TScoped>.EnumName('MoreNames', TScoped.First)); // imprimen string vacio Writeln(Enum<TScoped>.EnumName('MoreNames', TScoped.Second)); Writeln(Enum<TScoped>.EnumName('MoreNames', TScoped.Third)); // Writeln(Enum<TScoped>.EnumName('blabla', TScoped.Third)); // excepcion ya que no fue anotado con "blabla" // imprime "Third", es decir, el valor del tipo enumerado tal y como fue declarado Writeln(Enum<TScoped>.EnumNameOrDefault('blabla', TScoped.Third)); // imprime "LoQueSea" Writeln(Enum<TScoped>.EnumNameOrDefault('blabla', TScoped.Third, 'LoQueSea')); Writeln; // arreglo con todos los nombres bajo el identificador "Test" // imprime Hello World for s in Enum<TScoped>.EnumNames('Test') do Writeln(s); end;
Pueden descargar la unidad aca