Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

19 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 27 de mayo de 2025
Desarrolladores estirando .NET

Cuando usamos tipos enumerados en C#, muchas veces buscamos seguridad. Los valores de un enum son constantes conocidas de antemano y se comprueban en tiempo de compilación, lo que evita asignaciones inválidas que podrían derivar en errores en tiempo de ejecución.

También, el hecho de poder acceder a los valores del enum utilizando nombres o identificadores descriptivos da lugar a un código más legible y fácil de mantener. Y encima, las ayudas para el autocompletado y descubrimiento de valores posibles que nos proporcionan los IDEs modernos nos ayudan a ser más productivos. Todo son ventajas 🙂

Sin embargo, los enum de C# sólo permiten que sus valores subyacentes sean numéricos ( byte, sbyte, short, ushort, int -el tipo por defecto-, uint, long o ulong). Esto puede suponer una limitación en casos en los que nos vendría bien disfrutar de todos los beneficios anteriores, pero usando valores string... es decir, deberíamos poder definir un tipo que pueda contener únicamente un conjunto de cadenas de texto predefinidas.

Algunos lenguajes como TypeScript, Python o Swift soportan de forma nativa la creación de enumerados de texto; en cambio, en C# no tenemos esta posibilidad. Pero bueno, no todo está perdido... gracias a la flexibilidad del lenguaje, podemos conseguir algo bastante parecido 😉

Simulando enumerados de texto en C#

Realmente, C# nos ofrece herramientas para acercarnos bastante a lo que estamos buscando. Fijaos en la siguiente clase, donde hacemos una primera aproximación simulando un enumerado de texto en C#:

public sealed class Color
{
    public static readonly Color Red = new("Red");
    public static readonly Color Blue = new("Blue");
    public static readonly Color Black = new("Black");

    private readonly string _value;
    private Color(string value) => _value = value;

    public override string ToString() => _value;
}

Los puntos principales de este código son:

  • La clase Color es sellada para evitar que se pueda heredar de ella, como los enums.
  • Los campos Red, Blue y Black son estáticos y de sólo lectura, y son instancias de la propia clase Color. De esta forma, cada uno de estos campos es un valor constante de tipo Color.
  • El constructor de la clase es privado, lo que significa que no se puede instanciar la clase desde fuera de ella. Esto garantiza que los únicos valores posibles de tipo Color son los que hemos definido como campos estáticos.
  • El método ToString se sobrescribe para que devuelva el valor de la propiedad _value, que es la cadena de texto que representa el color.

Esta clase nos permitiría un uso bastante parecido al de los enum tradicionales, como podemos ver a continuación:

var red = Color.Red;
PrintColor(red);
PrintColor(Color.Blue);
PrintColor("Green"); // Error en tiempo de compilación: no es un objeto "Color"

void PrintColor(Color color)
{
    Console.WriteLine(color);
}

Algunas limitaciones y cómo superarlas

Aunque este primer acercamiento puede ser suficiente para algunos escenarios simples, tiene bastantes limitaciones.

Por ejemplo, dado que los valores disponibles no son realmente tipos primitivos, no podemos hacer de forma directa un switch sobre los valores de tipo Color. Pero obviamente, en caso de necesidad podremos usar los if de siempre, o si nos empeñamos, podríamos usar switch con pattern matching, aunque será algo más engorroso:

switch (color)
{
    case var c when c == Color.Red:
        Console.WriteLine("This is Red");
        break;
    case var c when c == Color.Blue:
        Console.WriteLine("This is Blue");
        break;
    ...
}

Otra limitación es que, aunque el tipo Color envuelve un string, no se comporta como él en algunos casos. Por ejemplo, no podríamos pasar un objeto Color a un método que espera un string de forma directa, o compararlo con otro string:

Console.WriteLine(Color.Blue == "Blue");  // Error: '==' cannot be applied to operands 
                                          // of type 'Color' and 'string'

PrintString(blue);  // Error: Cannot convert from 'Color' to 'string'

void PrintString(string color) => Console.WriteLine(color);

Sin embargo, estos dos problemas los eliminamos de un plumazo si añadimos a la clase la conversión implícita al tipo string. Como vemos a continuación, se consigue de forma muy sencilla:

public sealed class Color
{
    ... // El código anterior de la clase

    public static implicit operator string(Color? color) 
        => color is null? string.Empty: color._value;
}

Fijaos que estamos teniendo en cuenta el hecho de que, al ser un tipo referencia, los objetos Color podrían contener un null, retornando una cadena vacía en este caso.

Al tener el operador implícito de conversión a string, ya podemos usar un objeto Color donde se espere un string, como en llamadas a métodos o comparaciones. Por tanto, el código que vimos antes ya funcionaría:

Console.WriteLine(Color.Blue == "Blue"); // Imprime "True"
PrintString(blue);                       // Imprime "Blue"

void PrintString(string color) => Console.WriteLine(color);

Creación de objetos Color a partir de cadenas de texto

La conversión inversa, es decir, si tenemos una cadena de texto como "Red", no tenemos una forma directa de convertirla a un objeto Color. La primera solución a este problema podría ser añadir un método estático a la clase que actúe como factoría:

public sealed class Color
{
    ... // El código anterior de la clase

    public static Color FromString(string color)
    {
        return new Color(color);
    }
}

Sin embargo, es fácil ver que esto no va a funcionar demasiado bien, principalmente por dos motivos. Primero, no estamos comprobando que la cadena de texto sea un valor válido, y, segundo, porque al crear nuevas instancias de Color estamos rompiendo la idea de que sólo existan los valores predefinidos y nos podrían fallar comparaciones de igualdad por referencia:

var yellow = Color.FromString("Yellow"); // Funciona, pero no debería permitírnoslo
Console.WriteLine(yellow); // Muestra "Yellow"

var red1 = Color.Red;
var red2 = Color.FromString("Red"); // Crea otro objeto "Red" distinto al que tenemos en red1
Console.WriteLine(red1 == red2); // Muestra "False" porque son objetos diferentes

Ambos problemas tienen fácil solución. Para el primero, podríamos añadir una comprobación en el método FromString() que lance una excepción si el valor no es válido. Y para el segundo, podríamos modificar el método para que devuelva una referencia al valor constante en lugar de crear una nueva instancia:

public sealed class Color
{
    ... // El código anterior de la clase

    public static Color FromString(string value)
    {
        switch (value)
        {
            case "Red": return Red;
            case "Blue": return Blue;
            case "Black": return Black;
            default: throw new ArgumentException("Invalid color value", nameof(value));
        }
    }
}

Sin embargo, esta fórmula no es muy escalable, ya que si añadimos nuevos valores a la clase Color, tendremos que modificar este método. Una solución más elegante sería utilizar un diccionario estático que recoja los valores de los distintos colores en el constructor y utilizarlo luego en la factoría:

public sealed class Color
{
    private static readonly Dictionary<string, Color> _allowedColors 
        = new(StringComparer.OrdinalIgnoreCase);
    public static readonly Color Red = new("Red");
    public static readonly Color Blue = new("Blue");
    public static readonly Color Black = new("Black");

    private readonly string _value;

    private Color(string value)
    {
        _value = value;
        _allowedColors[_value] = this; // Aquí podríamos chequear previamente que no exista
    }

    public override string ToString() => _value;

    public static implicit operator string(Color? color) 
        => color is null? string.Empty: color._value;

    public static Color FromString(string color) 
    {
        return _allowedColors.TryGetValue(color, out var result) 
            ? result 
            : throw new ArgumentOutOfRangeException(nameof(color), color);
    }
}

Observad cómo hemos creado el diccionario _allowedColors como un campo estático de la clase, y aprovechamos el constructor para añadir a él cada instancia de Color que vamos creando al definir las propiedades. De esta forma, el método FromString() puede buscar el valor en el diccionario y devolverlo si existe, o lanzar una excepción si no. También, hemos utilizado StringComparer.OrdinalIgnoreCase para que la búsqueda sea insensible a mayúsculas y minúsculas.

Un ejemplo de uso sería el mostrado a continuación:

var red1 = Color.FromString("red");
var red2 = Color.FromString("Red");
Console.WriteLine(red1); // Muestra "Red"
Console.WriteLine(red1 == red2); // Muestra "True"
Console.WriteLine(red1 == Color.Red); // Muestra "True"

Ya con esta infraestructura, sería sencillo ampliar la clase con métodos para comprobar si un valor es válido, obtener una lista de los valores posibles, etc:

public sealed class Color
{
    ... // El código anterior de la clase

    public static bool IsValid(string? color)
    {
        if (color is null)
            return false;

        return _allowedColors.ContainsKey(color);
    } 

    public static IEnumerable<string> GetAllowedValues() => _allowedColors.Keys;

    public static bool TryFromString(string? value, out Color? result)
    {
        if (value is null)
        {
            result = null;
            return false;
        }

        return _allowedColors.TryGetValue(value, out result);
    }
}

Conclusión

En este post hemos recorrido el camino para llegar a una clase que permite simular un enumerado de texto en C#, comenzando por una implementación simple hasta llegar a una más completa que incluye comprobaciones de validez, conversiones implícitas a cadena, y métodos para obtener los valores permitidos y crear instancias a partir de cadenas de texto.

El código de la clase completa es el siguiente:

public sealed class Color
{
    private static readonly Dictionary<string, Color> _allowedColors 
        = new(StringComparer.OrdinalIgnoreCase);

    public static readonly Color Red = new("Red");
    public static readonly Color Blue = new("Blue");
    public static readonly Color Black = new("Black");

    private readonly string _value;

    private Color(string value)
    {
        _value = value;
        _allowedColors[_value] = this;
    }

    public override string ToString() => _value;

    public static implicit operator string(Color? color)
        => color is null ? string.Empty : color._value;

    public static Color FromString(string color)
    {
        return _allowedColors.TryGetValue(color, out var result)
            ? result
            : throw new ArgumentOutOfRangeException(nameof(color), color);
    }

    public static bool IsValid(string? color)
    {
        if (color is null)
        {
            return false;
        }
        return _allowedColors.ContainsKey(color);
    } 

    public static IEnumerable<string> GetAllowedValues() => _allowedColors.Keys;

    public static bool TryFromString(string? value, out Color? result)
    {
        if (value is null)
        {
            result = null;
            return false;
        }

        return _allowedColors.TryGetValue(value, out result);
    }
}

A la vista del código no parece tan complicado, ¿verdad? 😉 No es perfecto y aún quedarían algunos aspectos por resolver, como la serialización y deserialización (nada que no se pueda resolver fácilmente con un custom converter), pero es un punto de partida interesante y, sobre todo, nos ha permitido jugar un poco con conversiones implícitas y enfrentarnos a algunos problemas poco habituales.

¡Espero que os sea de utilidad!

Publicado en Variable not found.

4 Comentarios:

Anónimo dijo...

Como siempre, artículos bien explicados y con alguna enseñanza. Larga vida a este blog y a su autor.

José María Aguilar dijo...

:D Muchas gracias!

Albert dijo...

¡Guau! Me parece buenísima la idea de meter _allowedColors[_value] = this; en el constructor.
A primera vista, no veía cómo se construía el diccionario con todos los valores.

Uso esta estructura en la capa de dominio de todas mis aplicaciones. La mayoría de las veces, con un Id y un Nombre, pero el concepto es el mismo.

A falta de un mejor nombre, los llamo enumeradores de clase (inventado completamente).

Un último apunte: ¿hay alguna razón que desconozco para usar esta firma?
public static bool TryFromString(string? value, out Color? result)

Yo me inclinaría por:
public static Color? TryFromString(string? value)

Como siempre, ¡gracias por compartir! :)

José María Aguilar dijo...

Hola, Albert!

Ante todo, muchas gracias por comentar :)

El tema de la firma de TryFromString() es simplemente por alineación con el patrón que se usa en métodos como int.TryParse() y similares, donde el método retorna por un lado si ha tenido éxito o no, y por otro el elemento resultante. De hecho, me venía bien también para usar el TryGetValue() del diccionario 😉Pero vaya, tu enfoque sería totalmente válido también.

Saludos!