Menu

Enum en C#: valores, ToString, Parse, Flags y recorrerlos

Cómo funcionan los enums en C#: declarar constantes con nombre, los valores enteros subyacentes y el cast, convertir un enum a string y un string a enum con Parse y TryParse, listar todos los valores, [Flags] con operadores bit a bit y HasFlag, switch sobre un enum y el manejo de valores no definidos.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Un enum (enumeración) es un tipo cuyos valores son un conjunto fijo de constantes con nombre: estados de un pedido, días de la semana, niveles de log. Por debajo, cada nombre es un entero, pero el sistema de tipos impide que un OrderStatus se confunda con un int normal o con otro enum.

Declarar y usar un enum

Enumera los nombres de los miembros entre llaves. Por defecto, el primero vale 0 y cada uno de los siguientes vale uno más:

Salida:

Paid
On its way
True
2

El enum es un tipo de verdad: un método que recibe un OrderStatus no puede llamarse por error con 3 ni con un LogLevel. Los enums son tipos de valor, así que nunca son null y se comparan con == por valor.

Valores explícitos y el tipo subyacente

Puedes asignar los números tú. Eso importa siempre que el número sale de tu programa (una columna de base de datos, un estado HTTP, un formato de archivo), porque entonces renumerar rompe los datos guardados:

Salida:

404
Created
418
1
Byte

Hay dos cosas en las que fijarse. Hacer un cast de un int a un enum nunca falla: (HttpStatus)418 es un valor válido que simplemente no tiene nombre, y se imprime como el número. Y el tipo subyacente puede ser cualquier tipo entero (byte, short, long, ...), lo que solo importa en código sensible al almacenamiento; int es el valor por defecto y la opción correcta casi siempre.

Cuando añadas miembros más adelante, añádelos al final o dales valores explícitos. Insertar Refunded entre Paid y Shipped cambia en silencio el número de todos los miembros que van detrás.

Enum a string

ToString() devuelve el nombre del miembro, que es también lo que usan Console.WriteLine y la interpolación de strings. Los strings de formato cambian la salida:

Salida:

Warning
2
00000002
[Warning]
Error
Error
Needs attention

Los nombres de los miembros son identificadores, así que no pueden contener espacios y no se traducen. Para el texto que se muestra a los usuarios, asocia tú los valores, como hace Label, o con un Dictionary<LogLevel, string>. Algunas bases de código ponen un atributo [Description("Needs attention")] en cada miembro y lo leen con reflexión; la página de reflexión y atributos muestra cómo funciona esa búsqueda.

String a enum: Parse y TryParse

Enum.Parse convierte un nombre de vuelta en un valor y lanza ArgumentException si nada coincide. Enum.TryParse devuelve false en su lugar, que es lo que quieres con cualquier entrada que no controles:

Salida:

Large
Medium
Parse threw ArgumentException for Huge
small  parsed=True  value=Small  defined=True
XL     parsed=False value=Small  defined=True
2      parsed=True  value=Large  defined=True
7      parsed=True  value=7      defined=False

Las dos últimas filas son la trampa. Los dos métodos aceptan strings numéricos, así que "7" se parsea con éxito en un Size que no tiene nombre. Y un TryParse fallido pone el resultado a 0, que aquí es el Small de aspecto válido. Cuando el texto venga de un parámetro de consulta, un archivo de configuración o un formulario, comprueba siempre tanto el valor devuelto como Enum.IsDefined:

if (Enum.TryParse(input, true, out Size size) && Enum.IsDefined(typeof(Size), size))
{
    // safe to use size
}

.NET Core 2.0 y posteriores añaden un Enum.Parse<Size>("Large") genérico que no necesita cast.

Listar todos los valores

Enum.GetValues devuelve todos los miembros, ordenados por su valor numérico (comparado sin signo, así que los miembros negativos van al final); Enum.GetNames devuelve sus nombres. Así es como se llena una lista desplegable o se valida contra todas las opciones:

Salida:

Free       0 EUR/month
Starter    9 EUR/month
Pro       29 EUR/month
Team      99 EUR/month
Free | Starter | Pro | Team
3 paid plans

Enum.GetValues(typeof(Plan)) devuelve un Array sin tipo, de ahí el Cast<Plan>() antes de LINQ. En .NET 5 y posteriores, Enum.GetValues<Plan>() devuelve directamente un Plan[] con tipo.

Flags: combinar valores

Algunos enums describen un conjunto de opciones en lugar de una elección: permisos de archivo, los días que abre una tienda, canales de notificación. Da a cada miembro su propio bit (1, 2, 4, 8, ...), añade None = 0 y marca el enum con [Flags]. Así los valores se combinan con |:

Salida:

Read, Share
Editor, Share
True
False
Editor
3
Read, Delete
True

Qué hace cada operador: | activa bits, & ~X los desactiva, ^ los invierte, y (value & X) != 0 o value.HasFlag(X) los comprueban. HasFlag(X) significa "todos los bits de X están activados", así que HasFlag(None) es verdadero para cualquier valor, y HasFlag(Editor) exige tanto Read como Write.

Fíjate en la segunda línea: cuando una combinación con nombre cubre algunos de los bits activados, ToString la usa, así que Read | Write | Share se imprime como Editor, Share. Tenlo en cuenta antes de parsear la salida de ToString con algo que no sea Enum.Parse.

El atributo no cambia la aritmética. Cambia el formato: sin [Flags], Read | Share se imprime como 9, porque ningún miembro tiene ese valor. Con él, ToString y Parse trabajan los dos con la forma separada por comas. Los miembros deben seguir siendo potencias de dos; escribir Read, Write, Delete con la numeración por defecto (0, 1, 2) hace que Write | Delete valga 3, un valor sin sentido.

switch sobre un enum

switch es la forma natural de actuar sobre un enum. Incluye una rama default, porque una variable enum puede contener valores sin nombre:

switch (status)
{
    case OrderStatus.Pending:
    case OrderStatus.Paid:
        return "Preparing";
    case OrderStatus.Shipped:
        return "On the way";
    case OrderStatus.Delivered:
        return "Delivered";
    default:
        return "Unknown";
}

Desde C# 8, una expresión switch es más corta. Sin un brazo _, el compilador avisa: CS8509 cuando falta un miembro con nombre, y CS8524 cuando se manejan todos los nombres pero no los valores sin nombre como (OrderStatus)7:

string text = status switch
{
    OrderStatus.Pending or OrderStatus.Paid => "Preparing",   // 'or' pattern: C# 9
    OrderStatus.Shipped => "On the way",
    OrderStatus.Delivered => "Delivered",
    OrderStatus.Cancelled => "Cancelled",
    _ => throw new ArgumentOutOfRangeException(nameof(status)),
};

Valores por defecto y no definidos

El valor por defecto de cualquier enum es 0, tenga o no un miembro ese valor. Los campos, los elementos de un array y un TryParse fallido lo producen. Diseña pensando en ello:

  • Haz que 0 sea un miembro con sentido de "sin asignar" (None, Unknown) en lugar de una opción real. Si no, un campo sin inicializar se lee en silencio como la primera opción real.
  • Valida los números que vienen de fuera con Enum.IsDefined. En los enums [Flags], IsDefined devuelve false para las combinaciones que no tienen nombre (Read | Share), así que comprueba los bits en su lugar: (value & ~Permissions.All) == 0 con un miembro All que cubra todos los bits.

Errores comunes

  • Fiarse solo de TryParse. Los strings numéricos se parsean, y un parseo fallido da 0. Añade Enum.IsDefined.
  • Depender de la numeración implícita para valores guardados. Insertar un miembro renumera los que van detrás. Asigna valores explícitos a cualquier enum que se persista.
  • Flags sin potencias de dos. La numeración por defecto (0, 1, 2, 3) solapa bits. Usa 1, 2, 4, 8 o 1 << n.
  • Mostrar ToString() a los usuarios. Los nombres de los miembros son identificadores de código. Asocia los valores a un texto para mostrar.
  • Ningún default en un switch. Un enum puede contener valores fuera de sus miembros con nombre.

Preguntas frecuentes

¿Cómo convierto un enum a string en C#?

Llama a ToString(): OrderStatus.Shipped.ToString() devuelve "Shipped", y la interpolación de strings hace lo mismo. ToString("D") da el número en su lugar. Para un nombre conocido en tiempo de compilación, nameof(OrderStatus.Shipped) es una constante. Para texto de cara al usuario con espacios o traducciones, asocia tú los valores a strings (un switch o un diccionario) en lugar de depender del nombre del miembro.

¿Cómo convierto un string a enum en C#?

Usa Enum.TryParse<OrderStatus>(text, true, out var status), que devuelve false en lugar de lanzar una excepción cuando el texto no coincide con ningún miembro (el true hace que no distinga mayúsculas). Enum.Parse(typeof(OrderStatus), text) lanza ArgumentException ante una entrada incorrecta. Los dos aceptan también strings numéricos como "42", así que comprueba el resultado con Enum.IsDefined cuando la entrada venga de los usuarios.

¿Cómo convierto entre un enum y un int en C#?

Haz un cast en cualquiera de los dos sentidos: int code = (int)OrderStatus.Paid; y var status = (OrderStatus)2;. El cast desde int nunca falla, ni siquiera con números sin miembro correspondiente; el resultado es un valor del enum que se imprime como el número. Valídalo con Enum.IsDefined(typeof(OrderStatus), value) cuando el número venga de fuera.

¿Cómo recorro todos los valores de un enum en C#?

foreach (OrderStatus s in Enum.GetValues(typeof(OrderStatus))) visita todos los miembros en el orden de sus valores numéricos. Desde .NET 5 hay una versión genérica, Enum.GetValues<OrderStatus>(), que no necesita cast. Enum.GetNames(typeof(OrderStatus)) devuelve los nombres como strings.

¿Qué hace [Flags] en un enum de C#?

Marca un enum cuyos valores son bits pensados para combinarse con |, como Read | Write. Da a cada miembro una potencia de dos (1, 2, 4, 8) y añade un None = 0. El atributo hace que ToString() imprima las combinaciones como "Read, Write" y que Enum.Parse pueda leer ese formato. Comprueba un bit con HasFlag o con (value & Permissions.Write) != 0.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR