Foros Club Delphi

Foros Club Delphi (http://www.clubdelphi.com/foros/index.php)
-   Trucos (http://www.clubdelphi.com/foros/forumdisplay.php?f=52)
-   -   Ayudante generico para enumerativos (http://www.clubdelphi.com/foros/showthread.php?t=91426)

AgustinOrtu 04-02-2017 20:55:43

Ayudante generico para enumerativos
 
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>

Código PHP:

  /// <summary> Record que contiene metodos estaticos para trabajar con tipos enums </summary>
  
Enum<Trecord> = record
  
public
    
/// <summary> El nombre del tipo enum </summary>
    
class function TypeNamestring; static; inline;
    
/// <summary> El nombre del valor enum </summary>
    
class function ValueName(const ValueT): 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 Identifierstring; const ValueT): 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 Identifierstring; const ValueT; const Default: string ''): string; static; inline;
    
/// <summary> Devuelve todos los nombres con los que fue anotado el enum </summary>
    
class function EnumNames(const Identifierstring): TArray<string>; static; inline;
    
/// <summary> Devuelve el valor enum dado un Ordinal </summary>
    
class function Parse(const OrdinalInteger): T; static; inline;
    
/// <summary> Convierte el valor enum a su correspondiente Ordinal </summary>
    
class function ToInteger(const ValueT): Integer; static; inline;
    
/// <summary> El valor maximo del enum. Equivalente a Ord(High(T)) </summary>
    
class function MaxValueInteger; static; inline;
    
/// <summary> El valor maximo del enum. Equivalente a Ord(Low(T)) </summary>
    
class function MinValueInteger; static; inline;
    
/// <summary> Devuelve True si el valor del tipo enum se encuentra dentro del rango permitido </summary>
    
class function InRange(const ValueT): Booleanoverload; static;
    
/// <summary> Devuelve True si el entero se encuentra dentro del rango permitido del tipo enum </summary>
    
class function InRange(const ValueInteger): Booleanoverload; 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 ValueT; const Namespace, MethodNamestring); static;
    
/// <summary> Cantidad de elementos del enum </summary>
    
class function CountInteger; static;
    
/// <summary> Devuelve un Array con los elementos del enum </summary>
    
class function AsArrayTArray<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:

Código PHP:


type
{$SCOPEDENUMS ON}
  
// podemos usar enumerativos con calificacion completa si queremos sin ningun problema
  
TScoped = (FirstSecondThird);

{
$SCOPEDENUMS OFF}

  
TTestEnumeration = (ttFirstttSecondttThird);

procedure Main;
var
  
TestEnumTTestEnumeration;
  
ScopedEnumTScoped;
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, 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

Código PHP:

  EnumNamesAttribute = class(TCustomAttribute)
  
strict private
    
FIdentifierstring;
    
FNamesTArray<string>;
  public
    
constructor Create(const IdentifierNamesstring; const Delimiterstring ',');
    function 
NameOf<Trecord>(const ValueT): string;
    
property Identifierstring read FIdentifier;
    
property NamesTArray<stringread 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:

Código PHP:

type
{$SCOPEDENUMS ON}

  [
EnumNames('Test''Hello,World')] // utilizamos el atributo
  
TScoped = (FirstSecondThird); // 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:

Código PHP:

constructor Create(const IdentifierNamesstring; const Delimiterstring ','); 

Delphi nos permite ahorrarnos el sufijo "Attribute" cuando anotamos un tipo, y tambien el llamado al metodo Create. Si se fijan en el popup con los parametros al escribir "EnumNames(" el IDE muestra esto:



Que es justamente el constructor

El parametro "Names" es un string, que deberia ser un string delimitado por un algun caracter (personalizable por el parametro "Delimiter", por defecto la coma). Dicho string se separa usando el delimitador y se almacena en un arreglo. En el caso de que el numero de string sea diferente al del enumerativo, aquellos que queden indefinidos se interpretan como "EmptyStr" y lo que sobre se ignora

Que es esa cosa que llame "identificador". Bueno ya que Delphi permite anotar todas las veces que uno quiera con el mismo atributo a un tipo, me parecio una buena idea que la API permitiera
definir distintas representaciones, pero obviamente era necesario algo que "identifique"

Un ejemplo en accion:

Código PHP:

type
{$SCOPEDENUMS ON}

  [
EnumNames('Test''Hello,World')]
  [
EnumNames('MoreNames''OnlyFirst')]
  
TScoped = (FirstSecondThird);

{
$SCOPEDENUMS OFF}

procedure Main;
var
  
sstring;
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

Casimiro Notevi 04-02-2017 22:02:21

^\||/^\||/^\||/

Neftali [Germán.Estévez] 06-02-2017 11:06:58

Muy interesante Agustín.
^\||/^\||/^\||/


La franja horaria es GMT +2. Ahora son las 17:55:20.

Powered by vBulletin® Version 3.6.8
Copyright ©2000 - 2018, Jelsoft Enterprises Ltd.
Traducción al castellano por el equipo de moderadores del Club Delphi