Jump to content


Photo

Ayudante generico para enumerativos

enumrtti helper

  • Please log in to reply
4 replies to this topic

#1 Agustin Ortu

Agustin Ortu

    Advanced Member

  • Moderadores
  • PipPipPip
  • 831 posts
  • LocationArgentina

Posted 04 February 2017 - 01:50 PM

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>


delphi
  1. /// <summary> Record que contiene metodos estaticos para trabajar con tipos enums </summary>
  2. Enum<T: record> = record
  3. public
  4. /// <summary> El nombre del tipo enum </summary>
  5. class function TypeName: string; static; inline;
  6. /// <summary> El nombre del valor enum </summary>
  7. class function ValueName(const Value: T): string; static; inline;
  8. /// <summary> Devuelve el valor del tipo enum anotado por el atributo EnumNames </summary>
  9. /// <summary> Si el enum no esta anotado por el atributo EnumNames, o no esta anotado por un atributo
  10. /// EnumNames con el identificador indicado, se eleva una excepcion EEnumNameNotFound </summary>
  11. /// <remarks> Ver EnumNamesAttribute </remarks>
  12. class function EnumName(const Identifier: string; const Value: T): string; static; inline;
  13. /// <summary> Devuelve el valor del tipo enum anotado por el atributo EnumNames </summary>
  14. /// <summary> En lugar de elevar una excepcion EEnumNameNotFound, se devuelve el valor Default </summary>
  15. /// <summary> Si Default = EmptyStr se devuelve ValueName(Value) </summary>
  16. /// <remarks> Ver EnumNamesAttribute </remarks>
  17. class function EnumNameOrDefault(const Identifier: string; const Value: T; const Default: string = ''): string; static; inline;
  18. /// <summary> Devuelve todos los nombres con los que fue anotado el enum </summary>
  19. class function EnumNames(const Identifier: string): TArray<string>; static; inline;
  20. /// <summary> Devuelve el valor enum dado un Ordinal </summary>
  21. class function Parse(const Ordinal: Integer): T; static; inline;
  22. /// <summary> Convierte el valor enum a su correspondiente Ordinal </summary>
  23. class function ToInteger(const Value: T): Integer; static; inline;
  24. /// <summary> El valor maximo del enum. Equivalente a Ord(High(T)) </summary>
  25. class function MaxValue: Integer; static; inline;
  26. /// <summary> El valor maximo del enum. Equivalente a Ord(Low(T)) </summary>
  27. class function MinValue: Integer; static; inline;
  28. /// <summary> Devuelve True si el valor del tipo enum se encuentra dentro del rango permitido </summary>
  29. class function InRange(const Value: T): Boolean; overload; static;
  30. /// <summary> Devuelve True si el entero se encuentra dentro del rango permitido del tipo enum </summary>
  31. class function InRange(const Value: Integer): Boolean; overload; static;
  32. /// <summary> Eleva una excepcion EEnumOutOfRange si el valor del tipo enum esta fuera del rango
  33. // permitido </summary>
  34. /// <param name="Value"> El valor a testear </param>
  35. /// <param name="Namespace"> Describe el "contexto" de quien invoca a este metodo (ej clase o unidad) </param>
  36. /// <param name="MethodName"> Nombre del metodo que invoco a esta rutina </param>
  37. class procedure CheckInRange(const Value: T; const Namespace, MethodName: string); static;
  38. /// <summary> Cantidad de elementos del enum </summary>
  39. class function Count: Integer; static;
  40. /// <summary> Devuelve un Array con los elementos del enum </summary>
  41. class function AsArray: TArray<T>; static;
  42. 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:


delphi
  1. type
  2. {$SCOPEDENUMS ON}
  3. // podemos usar enumerativos con calificacion completa si queremos sin ningun problema
  4. TScoped = (First, Second, Third);
  5.  
  6. {$SCOPEDENUMS OFF}
  7.  
  8. TTestEnumeration = (ttFirst, ttSecond, ttThird);
  9.  
  10. procedure Main;
  11. var
  12. TestEnum: TTestEnumeration;
  13. ScopedEnum: TScoped;
  14. begin
  15. // Imprime los valores "0", "1", "2"
  16. for TestEnum in Enum<TTestEnumeration>.AsArray do
  17. begin
  18. Write('Enum<TTestEnumeration>.ToInteger: ');
  19. Writeln(Enum<TTestEnumeration>.ToInteger(TestEnum));
  20. end;
  21. Writeln;
  22.  
  23. // Imprime los valores "First", "Second", "Third"
  24. for ScopedEnum in Enum<TScoped>.AsArray do
  25. begin
  26. Write('Enum<TScoped>.ValueName: ');
  27. Writeln(Enum<TScoped>.ValueName(ScopedEnum));
  28. end;
  29. Writeln;
  30.  
  31. // imprime "TTestEnumeration"
  32. Writeln(Enum<TTestEnumeration>.TypeName);
  33.  
  34. // imprime "TScoped"
  35. Write(Enum<TScoped>.TypeName);
  36. // "TScoped" tiene 3 elementos
  37. Writeln(' tiene ' + Enum<TScoped>.Count.ToString + ' elementos');
  38. Writeln('El valor maximo es ' + Enum<TScoped>.MaxValue.ToString);
  39. Writeln('El valor minimo es ' + Enum<TScoped>.MinValue.ToString);
  40.  
  41. Write('5 es un ordinal dentro del rango permitido de TScoped? ');
  42. // evalua False
  43. Writeln(Enum<TScoped>.InRange(5));
  44.  
  45. Write('2 es un ordinal dentro del rango permitido de TScoped? ');
  46. // evalua True
  47. Writeln(Enum<TScoped>.InRange(2));
  48. 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

 


php
  1.   EnumNamesAttribute = class(TCustomAttribute)
  2.   strict private
  3.     FIdentifier: string;
  4.     FNames: TArray<string>;
  5.   public
  6.     constructor Create(const Identifier, Names: string; const Delimiter: string = ',');
  7.     function NameOf<T: record>(const Value: T): string;
  8.     property Identifier: string read FIdentifier;
  9.     property Names: TArray<string> read FNames;
  10.   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:


delphi
  1. type
  2. {$SCOPEDENUMS ON}
  3.  
  4. [EnumNames('Test', 'Hello,World')] // utilizamos el atributo
  5. TScoped = (First, Second, Third); // declaracion del tipo, como toda la vida
  6.  
  7. {$SCOPEDENUMS OFF}
  8.  
  9. procedure Main;
  10. begin
  11. // extraer el valor
  12. Writeln(Enum<TScoped>.EnumName('Test', TScoped.First)); // imprime "Hello"
  13. 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:


php
  1. constructor Create(const Identifier, Names: string; const Delimiter: string = ',');

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:
 
Attached File  enum.png   8.2KB   0 downloads
 
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:


delphi
  1. type
  2. {$SCOPEDENUMS ON}
  3.  
  4. [EnumNames('Test', 'Hello,World')]
  5. [EnumNames('MoreNames', 'OnlyFirst')]
  6. TScoped = (First, Second, Third);
  7.  
  8. {$SCOPEDENUMS OFF}
  9.  
  10. procedure Main;
  11. var
  12. s: string;
  13. begin
  14. // imprime "Hello"
  15. Writeln(Enum<TScoped>.EnumName('Test', TScoped.First));
  16. // Writeln(Enum<TScoped>.EnumName('test', TScoped.First)); // excepcion, sensible a mayusculas
  17.  
  18. // imprime "OnlyFirst"
  19. Writeln(Enum<TScoped>.EnumName('MoreNames', TScoped.First));
  20.  
  21. // imprimen string vacio
  22. Writeln(Enum<TScoped>.EnumName('MoreNames', TScoped.Second));
  23. Writeln(Enum<TScoped>.EnumName('MoreNames', TScoped.Third));
  24.  
  25. // Writeln(Enum<TScoped>.EnumName('blabla', TScoped.Third)); // excepcion ya que no fue anotado con "blabla"
  26.  
  27. // imprime "Third", es decir, el valor del tipo enumerado tal y como fue declarado
  28. Writeln(Enum<TScoped>.EnumNameOrDefault('blabla', TScoped.Third));
  29.  
  30. // imprime "LoQueSea"
  31. Writeln(Enum<TScoped>.EnumNameOrDefault('blabla', TScoped.Third, 'LoQueSea'));
  32.  
  33. Writeln;
  34. // arreglo con todos los nombres bajo el identificador "Test"
  35. // imprime Hello World
  36. for s in Enum<TScoped>.EnumNames('Test') do
  37. Writeln(s);
  38. end;

Pueden descargar la unidad aca

 


  • 1

#2 Agustin Ortu

Agustin Ortu

    Advanced Member

  • Moderadores
  • PipPipPip
  • 831 posts
  • LocationArgentina

Posted 04 February 2017 - 05:46 PM

Por cierto, si se quiere obviar la sintaxis Enum<T> se puede definir una variable y especializar el generico, asi


delphi
  1. type
  2. TmyEnum = (Foo, Bar);
  3.  
  4. var
  5. e: Enum<TmyEnum>;
  6. begin
  7. Writeln(e.TypeName);
  8. Readln;
  9. end.


  • 0

#3 Agustin Ortu

Agustin Ortu

    Advanced Member

  • Moderadores
  • PipPipPip
  • 831 posts
  • LocationArgentina

Posted 04 February 2017 - 08:29 PM

Se puede jugar un poco con operadores de clase para realizar conversiones sin tanto tipeo (Enum<Tipo>.). Para ello he definido un nuevo tipo


delphi
  1. ContainedEnum<T: record {: enum}> = record
  2. strict private
  3. FEnumValue: T;
  4. constructor Create(const Value: T);
  5. public
  6. class operator Implicit(const Value: T): ContainedEnum<T>; inline;
  7. class operator Implicit(const Value: Integer): ContainedEnum<T>; inline;
  8. class operator Implicit(const Value: string): ContainedEnum<T>; inline;
  9. class operator Implicit(const Value: ContainedEnum<T>): string; inline;
  10. class operator Implicit(const Value: ContainedEnum<T>): Integer; inline;
  11. class operator Implicit(const Value: ContainedEnum<T>): T; inline;
  12. function ToInteger: Integer; inline;
  13. function ToString: string; inline;
  14. property Value: T read FEnumValue;
  15. end;
  16.  
  17. constructor ContainedEnum<T>.Create(const Value: T);
  18. begin
  19. FEnumValue := Value;
  20. end;
  21.  
  22. class operator ContainedEnum<T>.Implicit(const Value: T): ContainedEnum<T>;
  23. begin
  24. Result := ContainedEnum<T>.Create(Value);
  25. end;
  26.  
  27. class operator ContainedEnum<T>.Implicit(const Value: Integer): ContainedEnum<T>;
  28. begin
  29. Result := ContainedEnum<T>.Create(Enum<T>.Parse(Value));
  30. end;
  31.  
  32. class operator ContainedEnum<T>.Implicit(const Value: string): ContainedEnum<T>;
  33. begin
  34. Result := ContainedEnum<T>.Create(Enum<T>.Parse(Value));
  35. end;
  36.  
  37. class operator ContainedEnum<T>.Implicit(const Value: ContainedEnum<T>): Integer;
  38. begin
  39. Result := Enum<T>.ToInteger(Value.FEnumValue);
  40. end;
  41.  
  42. class operator ContainedEnum<T>.Implicit(const Value: ContainedEnum<T>): string;
  43. begin
  44. Result := Enum<T>.ValueName(Value.FEnumValue);
  45. end;
  46.  
  47. class operator ContainedEnum<T>.Implicit(const Value: ContainedEnum<T>): T;
  48. begin
  49. Result := Value.FEnumValue;
  50. end;
  51.  
  52. function ContainedEnum<T>.ToInteger: Integer;
  53. begin
  54. Result := Self;
  55. end;
  56.  
  57. function ContainedEnum<T>.ToString: string;
  58. begin
  59. Result := Self;
  60. end;

Los metodos Enum<T>Parse y Enum<T>.AsArray ahora devuelven este tipo en lugar del generico T

 

Lo interesante es que ahora se puede escribir este codigo:


delphi
  1. var
  2. Value: ContainedEnum<TAlign>;
  3. AlignValue: TAlign;
  4. IntValue: Integer;
  5. StrValue: string;
  6. begin
  7. AlignValue := Enum<TAlign>.Parse('alClient');
  8. IntValue := Enum<TAlign>.Parse('alLeft');
  9. IntValue := Enum<TAlign>.Parse(2);
  10. StrValue := Enum<TAlign>.Parse('alBottom');
  11. Value := TAlign.alClient;
  12. Value := 'alTop';
  13. Value := 3;
  14. Readln;
  15. end.

Puede servir de manera generica y reusable para jugar con enums. Los enum suelen ser muy utiles por su expresividad, y se los suele usar como "parametros" para configurar aplicaciones. Esos parametros normalmente hay que guardarlos en algun lado y leerlos. De esta manera se puede tener una manera clara y facil de trabajar con enumerativos para estos casos ahorrandonos codigo y problemas

 

Lo que realmente me gustaria ahora es encontrar alguna forma de escribir esto, ya que es una sintaxis que siempre me agrado mas


delphi
  1. TAlign.alClient.ToString

Eso se puede hacer con un record helper para TAlign, pero eso implica definir e implementar un helper para cada enumerativo que quiero 

 

Lo que me molesta es que la implementacion para colmo es de lo mas trivial !!


delphi
  1. type
  2. TAlignHelper = record helper for TAlign
  3. public
  4. function AsString: string;
  5. function AsInteger: Integer;
  6. end;
  7.  
  8. function TAlignHelper.AsString: string;
  9. var
  10. This: ContainedEnum<TAlign> absolute Self;
  11. begin
  12. Result := This;
  13. end;
  14.  
  15. function TAlignHelper.AsInteger: Integer;
  16. var
  17. This: ContainedEnum<TAlign> absolute Self;
  18. begin
  19. Result := This;
  20. end;
  21.  
  22. procedure Main;
  23. begin
  24. Writeln(TAlign.alBottom.AsString);
  25. Writeln(TAlign.alBottom.AsInteger);
  26. end;


  • 0

#4 Delphius

Delphius

    Advanced Member

  • Administrador
  • 6295 posts
  • LocationArgentina

Posted 04 February 2017 - 11:32 PM

Interesante propuesta Agustín.

Me gusta lo que has comentado en el último post. Si puedo en los próximos días haré una prueba en Lazarus para comprobar si también es viable.

 

Saludos,


  • 1

#5 Agustin Ortu

Agustin Ortu

    Advanced Member

  • Moderadores
  • PipPipPip
  • 831 posts
  • LocationArgentina

Posted 05 February 2017 - 02:07 AM

Interesante propuesta Agustín.
Me gusta lo que has comentado en el último post. Si puedo en los próximos días haré una prueba en Lazarus para comprobar si también es viable.
 
Saludos,

 
Bueno en FPC que yo sepa no hay atributos, quitando esa parte es lo mismo. Lo unico que no me permite FPC y no se porque es una sobrecarga de operadores del ContainedEnum

delphi
  1. ContainedEnum<T> = record
  2. strict private
  3. FEnumValue: T;
  4. constructor Create(const Value: T);
  5. public
  6. class operator Implicit(const Value: T): ContainedEnum<T>; inline;
  7. class operator Implicit(const Value: Integer): ContainedEnum<T>;
  8. class operator Implicit(const Value: string): ContainedEnum<T>;
  9. class operator Implicit(const Value: ContainedEnum<T>): string;
  10. class operator Implicit(const Value: ContainedEnum<T>): Integer;
  11. // class operator Implicit(const Value: ContainedEnum<T>): T; // no se puede :(
  12. function ToInteger: Integer;
  13. function ToString: string;
  14. property Value: T read FEnumValue;
  15. end;

El operador comentado no deja declararlo. Falla el compilador diciendo que ya existe una declaracion asi. Probe comentar el primero (que involucra los mismos tipos pero a la inversa) y el problema sigue

delphi
  1. Compile Project, Target: C:\Users\Agustin\AppData\Local\Temp\project1.exe: Exit code 1, Errors: 3
  2. unit1.pas(27,20) Error: Function is already declared Public/Forward "operator :=(const ContainedEnum$1):<undefined type>;"
  3. unit1.pas(27,20) Error: Function is already declared Public/Forward "operator :=(const ContainedEnum$1$crcB84BCE15):<undefined type>;"
  4. unit1.pas(27,20) Error: Function is already declared Public/Forward "operator :=(const ContainedEnum$1$crcB84BCE15):<undefined type>;"

Una picardia, porque es muy util

delphi
  1. type
  2.   TAlign = (alNone, alTop, alBottom, alLeft, alRight, alClient, alCustom);
  3.  
  4. procedure TMyApplication.DoRun;
  5. var
  6.   ErrorMsg: string;
  7.   Value: ContainedEnum<TAlign>;
  8.   AlignValue: TAlign;
  9.   IntValue: Integer;
  10.   StrValue: string;
  11. begin
  12.   // es necesario el .Value, en Delphi no
  13.   AlignValue := Enum<TAlign>.Parse('alClient').Value;
  14.   IntValue := Enum<TAlign>.Parse('alLeft');
  15.   IntValue := Enum<TAlign>.Parse(2);
  16.   StrValue := Enum<TAlign>.Parse('alBottom');
  17.   Value := TAlign.alClient;
  18.   Value := 'alTop';
  19.   Value := 3;
  20.  
  21.   // la consola de lazarus acepta enumerativos
  22.   Writeln(TAlign.alBottom);
  23.   Writeln(Enum<TAlign>.Parse('alClient').Value);
  24.  
  25.   // FPC tiene esta alternativa que tambien funciona, esto almacena alBottom en la variable string
  26.   // lo malo es que no tiene la conversion para el otro lado
  27.   WriteStr(StrValue, TAlign.alBottom);
  28.  
  29.   Readln;
  30.   Terminate;
  31. end;   

 
En la implementacion tuve que toquetear algunas cosas porque el compilador medio se enloquecia con los genericos y alguna que otra funcion sobrecargada, pero es casi identica a la Delphi
 
Codigo FPC: http://pastebin.com/B3HL1hMQ

Edited by Agustin Ortu, 05 February 2017 - 02:07 AM.

  • 0




IP.Board spam blocked by CleanTalk.