Enumeraciones

Concepto de enumeración

    Una enumeración o tipo enumerado es un tipo especial de estructura en la que los literales de los valores que pueden tomar sus objetos se indican explícitamente al definirla. Por ejemplo, una enumeración de nombre Tamaño cuyos objetos pudiesen tomar los valores literales Pequeño, Mediano o Grande se definiría así:

 
 enum Tamaño
 {
  Pequeño,
  Mediano,
  Grande
 }

    Para entender bien la principal utilidad de las enumeraciones vamos a ver antes un problema muy típico en programación: si queremos definir un método que pueda imprimir por pantalla un cierto texto con diferentes tamaños, una primera posibilidad sería dotarlo de un parámetro de algún tipo entero que indique el tamaño con el que se desea mostrar el texto. A estos números que los métodos interpretan con significados específicos se les suele denominar números mágicos, y su utilización tiene los inconvenientes de que dificulta la legibilidad del código (hay que recordar qué significa para el método cada valor del número) y su escritura (hay que recordar qué número ha pasársele al método para que funcione de una cierta forma)

    Una alternativa mejor para el método anterior consiste en definirlo de modo que tome un parámetro de tipo Tamaño para que así el programador usuario no tenga que recordar la correspondencia entre tamaños y números. Véase así como la llamada (2) del ejemplo que sigue es mucho más legible que la (1):


obj.MuestraTexto(2);                   // (1)
obj.MuestraTexto(Tamaño.Mediano);      // (2)

    Además, estos literales no sólo facilitan la escritura y lectura del código sino que también pueden ser usados por herramientas de documentación, depuradores u otras aplicaciones para sustituir números mágicos y mostrar textos muchos más legibles.

    Por otro lado, usar enumeraciones también facilita el mantenimiento del código. Por ejemplo, si el método (1) anterior se hubiese definido de forma que 1 significase tamaño pequeño, 2 mediano y 3 grande, cuando se quisiese incluir un nuevo tamaño intermedio entre pequeño y mediano habría que darle un valor superior a 3 o inferior a 1 ya que los demás estarían cogidos, lo que rompería el orden de menor a mayor entre números y tamaños asociados. Sin embargo, usando una enumeración no importaría mantener el orden relativo y bastaría añadirle un nuevo literal.

    Otra ventaja de usar enumeraciones frente a números mágicos es que éstas participan en el mecanismo de comprobación de tipos de C# y el CLR. Así, si un método espera un objeto Tamaño y se le pasa uno de otro tipo enumerado se producirá, según cuando se detecte la incoherencia, un error en compilación o una excepción en ejecución. Sin embargo, si se hubiesen usado números mágicos del mismo tipo en vez de enumeraciones no se habría detectado nada, pues en ambos casos para el compilador y el CLR serían simples números sin ningún significado especial asociado.

Definición de enumeraciones

    Ya hemos visto un ejemplo de cómo definir una enumeración. Sin embargo, la sintaxis completa que se puede usar para definirlas es:


 enum <nombreEnumeración> : <tipoBase>
 {
  <literales>
 }

    En realidad una enumeración es un tipo especial de estructura (luego System.ValueType será tipo padre de ella) que sólo puede tener como miembros campos públicos constantes y estáticos. Esos campos se indican en , y como sus modificadores son siempre los mismos no hay que especificarlos (de hecho, es erróneo hacerlo)

    El tipo por defecto de las constantes que forman una enumeración es int, aunque puede dárseles cualquier otro tipo básico entero (byte, sbyte, short, ushort, uint, int, long o ulong) indicándolo en <tipoBase>. Cuando se haga esto hay que tener muy presente que el compilador de C# sólo admite que se indiquen así los alias de estos tipos básicos, pero no sus nombres reales (System.Byte, System.SByte, etc.)

    Si no se especifica valor inicial para cada constante, el compilador les dará por defecto valores que empiecen desde 0 y se incrementen en una unidad para cada constante según su orden de aparición en la definición de la enumeración. Así, el ejemplo del principio del tema es equivalente a:

 
 enum Tamaño:int
 {
  Pequeño = 0,
  Mediano = 1,
  Grande = 2
 }

     Es posible alterar los valores iniciales de cada constante indicándolos explícitamente como en el código recién mostrado. Otra posibilidad es alterar el valor base a partir del cual se va calculando el valor de las siguientes constantes como en este otro ejemplo:

 
 enum Tamaño
 {
  Pequeño,
  Mediano = 5,
  Grande
 }

    En este último ejemplo el valor asociado a Pequeño será 0, el asociado a Mediano será 5, y el asociado a Grande será 6 ya que como no se le indica explícitamente ningún otro se considera que este valor es el de la constante anterior más 1.

    Obviamente, el nombre que se de a cada constante ha de ser diferente al de las demás de su misma enumeración y el valor que se de a cada una ha de estar incluido en el rango de valores admitidos por su tipo base. Sin embargo, nada obliga a que el valor que se de a cada constante tenga que ser diferente al de las demás, y de hecho puede especificarse el valor de una constante en función del valor de otra como muestra este ejemplo:

 
 enum Tamaño
 {
  Pequeño,
  Mediano = Pequeño,
  Grande = Pequeño + Mediano
 }

    En realidad, lo único que importa es que el valor que se dé a cada literal, si es que se le da alguno explícitamente, sea una expresión constante cuyo resultado se encuentre en el rango admitido por el tipo base de la enumeración y no provoque definiciones circulares. Por ejemplo, la siguiente definición de enumeración es incorrecta ya que en ella los literales Pequeño y Mediano se han definido circularmente:

 
 enum TamañoMal
 {
  Pequeño = Mediano,
  Mediano = Pequeño,
  Grande
 }

    Nótese que también la siguiente definición de enumeración también sería incorrecta ya que en ella el valor de B depende del de A implícitamente (sería el de A más 1):


 enum EnumMal
 {
  A = B,
  B
 }

Uso de enumeraciones

    Las variables de tipos enumerados se definen como cualquier otra variable (sintaxis <nombreTipo> <nombreVariable>) Por ejemplo:

 Tamaño t; 

    El valor por defecto para un objeto de una enumeración es 0, que puede o no corresponderse con alguno de los literales definidos para ésta. Así, si la t del ejemplo fuese un campo su valor sería Tamaño.Pequeño. También puede dársele otro valor al definirla, como muestra el siguiente ejemplo donde se le da el valor Tamaño.Grande:


Tamaño t = Tamaño.Grande;   // Ahora t vale Tamaño.Grande

    Nótese que a la hora de hacer referencia a los literales de una enumeración se usa la sintaxis <nombreEnumeración>.<nombreLiteral>, como es lógico si tenemos en cuenta que en realidad los literales de una enumeración son constantes publicas y estáticas, pues es la sintaxis que se usa para acceder a ese tipo de miembros. El único sitio donde no es necesario preceder el nombre del literal de <nombreEnumeración>. es en la propia definición de la enumeración, como también ocurre con cualquier constante estática.

    En realidad los literales de una enumeración son constantes de tipos enteros y las variables de tipo enumerado son variables del tipo entero base de la enumeración. Por eso es posible almacenar valores de enumeraciones en variables de tipos enteros y valores de tipos enteros en variables de enumeraciones. Por ejemplo:





int i = Tamaño.Pequeño;     // Ahora i vale 0
Tamaño t = (Tamaño) 0;      // Ahora t vale Tamaño.Pequeño (=0)
t = (Tamaño) 100;           // Ahora t vale 100, que no se corresponde
                             // con ningún literal

    Como se ve en el último ejemplo, también es posible darle a  una  enumeración valores enteros que no se correspondan con ninguno de sus literales.

    Dado que los valores de una enumeración son enteros, es posible aplicarles muchas de  las operaciones que se pueden aplicar a los mismos: ==, !=, <, >, <=, >=, +, -, ^, &, |, ~, ++, -- y sizeof. Sin embargo, hay que concretar que los operadores binarios + y no pueden aplicarse entre dos operandos de enumeraciones, sino que al menos uno de ellos ha de ser un tipo entero; y que |, & y ^ sólo pueden aplicarse entre enumeraciones.

La clase System.Enum

Todos los tipos enumerados derivan de System.Enum, que deriva de System.ValueType  y ésta a su vez deriva de la  clase primigenia System.Object. Aparte de los métodos heredados de estas clases padres ya estudiados, toda enumeración también dispone de otros métodos heredados de System.Enum, los principales de los cuales son:

  • static Type getUnderlyingType(Type enum): Devuelve un objeto System.Type con información sobre el tipo base de la enumeración representada por el objeto System.Type que se le pasa como parámetro[13].

  • string ToString(string formato): Cuando a un objeto de un tipo enumerado se le aplica el método ToString() heredado de object, lo que se muestra es una cadena con el nombre del literal almacenado en ese objeto. Por ejemplo (nótese que WriteLine() llama automáticamente al ToString() de sus argumentos no string):


 Tamaño t = Color.Pequeño;
 Console.WriteLine(t); // Muestra por pantalla la cadena "Pequeño"

Como también puede resultar interasante obtener el valor numérico del literal, se ha sobrecargado System.Enum el método anterior para que tome como parámetro una cadena que indica cómo se desea mostrar el literal almacenado en el objeto. Si esta cadena es nula, vacía o vale "G" muestra el literal como si del método ToString() estándar se tratase, pero si vale "D" o "X" lo que muestra es su valor numérico (en decimal si vale "D" y en hexadecimal si vale "X") Por ejemplo:

 
 Console.WriteLine(t.ToString("X")); // Muestra 0
 Console.WriteLine(t.ToString("G")); // Muestra Pequeño

En realidad, los valores de formato son insensibles a la capitalización y da igual si en vez de "G" se usa "g" o si en vez de "X" se usa "x".

  • static string Format(Type enum, object valorLiteral, string formato): Funciona de forma parecida a la sobrecarga de ToString() recien vista, sólo que ahora no es necesario disponer de ningún objeto del tipo enumerado cuya representación de literal se desea obtener sino que basta indicar el objeto Type que lo representa y el número del literal a obtener. Por ejemplo:


Console.Write(Enum.Format(typeof(Tamaño), 0, "G"); // Muestra Pequeño

Si el valorLiteral indicado no estuviese asociado a ningún literal del tipo enumerador representado por enum, se devolvería una cadena con dicho número. Por el contrario, si hubiesen varios literales en la enumeración con el mismo valor numérico asociado, lo que se devolvería sería el nombre del declarado en último lugar al definir la enumeración.

  • static object Parse(Type enum, string nombre, bool mayusculas?): Crea un objeto de un tipo enumerado cuyo valor es el correspondiente al literal de nombre asociado nombre. Si la enumeración no tuviese ningún literal con ese nombre se lanzaría una ArgumentException, y para determinar cómo se ha de buscar el nombre entre los literales de la enumeración se utiliza el tercer parámetro (es opcional y por defecto vale false) que indica si se ha de ignorar la capitalización al buscarlo. Un ejemplo del uso de este método es:


Tamaño t = (Tamaño) Enum.Parse(typeof(Tamaño), "Pequeño");
Console.WriteLine(t) // Muestra Pequeño

Aparte de crear objetos a partir del nombre del literal que almacenarán, Parse() también permite crearlos a partir del valor numérico del mismo. Por ejemplo:


Tamaño t = (Tamaño) Enum.Parse(typeof(Tamaño), "0");
Console.WriteLine(t) // Muestra Pequeño

En este caso, si el valor indicado no se correspondiese con el de ninguno de los literales de la enumeración no saltaría ninguna excepción, pero el objeto creado no almacenaría ningún literal válido. Por ejemplo:


Tamaño t = (Tamaño) Enum.Parse(typeof(Tamaño), "255");
Console.WriteLine(t) // Muestra 255

  • static object[] GetValues(Type enum): Devuelve una tabla con los valores de todos los literales de la enumeración representada por el objeto System.Type que se le pasa como parámetro. Por ejemplo:


object[] tabla = Enum.GetValues(typeof(Tamaño));
Console.WriteLine(tabla[0]); // Muestra 0, pues Pequeño =  0
Console.WriteLine(tabla[1]); // Muestra 1, pues Mediano = 1
Console.WriteLine(tabla[2]); // Muestra 1, pues Grande = Pequeño+Mediano

  • static string GetName(Type enum, object valor): Devuelve una cadena con el nombre del literal de la enumeración representada por enum que tenga el valor especificado en valor. Por ejemplo, este código muestra Pequeño por pantalla:


Console.WriteLine(Enum.GetName(typeof(Tamaño), 0)); //Imprime Pequeño

    Si la enumeración no contiene ningún literal con ese valor devuelve null, y si tuviese varios con ese mismo valor devolvería sólo el nombre del último. Si se quiere obtener el de todos es mejor usar GetNames(), que se usa como GetName()       pero devuelve un string[] con los nombres de todos los literales que tengan el valor indicado ordenados según su orden de definición en la enumeración.

  • static bool isDefined (Type enum, object valor): Devuelve un booleano que indica si algún literal de la enumeración indicada tiene el valor indicado.

Enumeraciones de flags

    Muchas veces interesa dar como valores de los literales de una enumeración únicamente valores que sean potencias de dos, pues ello permite que mediante operaciones de bits & y | se puede tratar los objetos del tipo enumerado como si almacenasen simultáneamente varios literales de su tipo. A este tipo de enumeraciones las llamaremos enumeraciones de flags, y un ejemplo de ellas es el siguiente:


  enum ModificadorArchivo
  {
   Lectura = 1,
   Escritura = 2,
   Oculto = 4,
   Sistema = 8
  }

    Si queremos crear un objeto de este tipo que represente los modificadores de un archivo de lectura-escritura podríamos hacer:


ModificadorArchivo obj =
ModificadorArchivo.Lectura | ModificadorArchivo.Escritura

    El valor del tipo base de la enumeración que se habrá almacenado en obj es 3, que es el resultado de hacer la operación OR entre los bits de los valores de los literales Lectura y Escritura. Al ser los literales de ModificadorArchivo potencias de dos sólo tendrán un único bit a 1 y dicho bit será diferente en cada uno de ellos, por lo que la única forma de generar un 3 (últimos dos bits a 1) combinando literales de ModificadorArchivo es combinando los literales Lectura (último bit a 1) y Escritura (penúltimo bit a 1) Por tanto, el valor de obj identificará unívocamente la combinación de dichos literales.

    Debido a esta combinabilidad no se debe determinar el valor literal de los objetos ModificadorArchivo tal y como si sólo pudiesen almacenar un único literal, pues su valor numérico no tendría porqué corresponderse con el de ningún literal de la enumeración Por ejemplo:


bool permisoLectura = (obj == ModificadorArchivo.Lectura);// false

    Aunque los permisos representados por obj incluían permiso de lectura, se devuelve false porque el valor numérico de obj es 3 y el del ModificadorArchivo.Lectura es 1. Si lo que queremos es comprobar si obj contiene permiso de lectura, entonces habrá que usar el operador de bits & para aislarlo del resto de literales combinados que contiene:

 
bool permisoLectura =
 (ModificadorArchivo.Lectura == (obj & ModificadorArchivo.Lectura));//true

   O, lo que es lo mismo:

 
bool permisoLectura = ( (obj & ModificadorArchivo.Lectura) != 0);//true

    Asimismo, si directamente se intenta mostrar por pantalla el valor de un objeto de una enumeración que almacene un valor que sea combinación de literales, no se obtendrá el resultado esperado (nombre del literal correspondiente a su valor) Por ejemplo, dado:


Console.Write(obj);  // Muestra 3

    Se mostrará un 3 por pantalla ya que en realidad ningún literal de ModificadorArchivo tiene asociado dicho valor. Como lo natural sería que se desease obtener un mensaje de la forma Lectura, Escritura, los métodos ToString() y Format() de las enumeraciones ya vistos admiten un cuarto valor "F" para su parámetro formato  (su nombre viene de flags) con el que se consigue lo anterior. Por tanto:

 
Console.Write(obj.ToString("F")); // Muestra Lectura, Escritura

    Esto se debe a que cuando Format() detecta este indicador (ToString() también, pues para generar la cadena llama internamente a Format()) y el literal almacenado en el objeto no se corresponde con ninguno de los de su tipo enumerado, entonces lo que hace es mirar uno por uno los bits a uno del valor numérico asociado de dicho literal y añadirle a la  cadena a devolver el nombre de cada literal de la enumeración cuyo valor asociado sólo tenga ese bit a uno, usándo como separador entre nombres un carácter de coma.

    Nótese que nada obliga a que los literales del tipo enumerado tengan porqué haberse definido como potencias de dos, aunque es lo más conveniente para que "F" sea útil, pues si la enumeración tuviese algún literal con el valor del objeto de tipo enumerado no se realizaría el proceso anterior y se devolvería sólo el nombre de ese literal.

    Por otro lado, si alguno de los bits a 1 del valor numérico del objeto no tuviese el correspondiente literal con sólo ese bit a 1 en la enumeración no se realizaría tampoco el proceso anterior y se devolvería una cadena con dicho valor numérico.

    Una posibilidad más cómoda para obtener el mismo efecto que con "F" es marcar la definición de la enumeración con el atributo Flags, con lo que ni siquiera sería necesario indicar formato al llamar a ToString() O sea, si se define ModificadorArchivo así:

 
 [Flags]
 enum ModificadorArchivo
 {
  Lectura = 1, 
  Escritura = 2,
  Oculto = 4,
  Sistema = 8
 }

    Entonces la siguiente llamada producirá como salida Lectura, Escritura:


Console.Write(obj); // Muestra Lectura, Escritura

    Esto se debe a que en ausencia del modificador "F", Format() mira dentro de los metadatos del tipo enumerado al que pertenece el valor numérico a mostrar si éste dispone del atributo Flags. Si es así funciona como si se le hubiese pasado "F".

    También cabe destacar que, para crear objetos de enumeraciones cuyo valor sea una combinación de valores de literales de su tipo enumerado, el método método Parse() de Enum permite que la cadena que se le especifica como segundo parámetro cuente con múltiples literales separados por comas. Por ejemplo, un objeto ModificadorArchivo que represente modificadores de lectura y ocultación puede crearse con:


 ModificadorArchivo obj =
 (ModificadorArchivo) Enum.Parse(typeof(ModificadorArchivo)
,"Lectura,Oculto"));

    Hay que señalar que esta capacidad de crear objetos de enumeraciones cuyo valor almacenado sea una combinación de los literales definidos en dicha enumeración es totalmente independiente de si al definirla se utilizó el atributo Flags o no.

Enumeraciones
José Antonio González Seco

José Antonio es experto en tecnologias Microsoft. Imparte cursos y conferencias en congresos sobre C# y .NET en Universidades de toda España (Sevilla, Barcelona, San Sebastián, Valencia, Oviedo, etc.) en representación de grandes empresas como Microsoft.
Fecha de alta:07/11/2006
Última actualizacion:07/11/2006
Visitas totales:38621
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com