Tema 14: 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 que 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 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 <literales>, 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 ha:
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 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 muchos 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 y 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.
-
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:
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); // Almacena 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)); // Almacena true
O lo que es lo mismo:
bool permisoLectura = ( (obj & ModificadorArchivo.Lectura) != 0);
// Almacena true
Del mismo, 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án 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.