Divertimento: Cadenas de longitud máxima fija en C#

Aviso: Este post es un divertimento que ha surgido a raíz del siguiente tweet de Juan Quijano. En este tweet básicamente Juan preguntaba si había alguna manera de limitar la longitud de una cadena. Por supuesto todas las respuestas que le dan son correctísimas, a saber:

  1. Validarlo en el setter
  2. Usar DataAnnotations y validación con atributos
  3. Usar [StringLength] en el caso de ASP.NET MVC
  4. Y otras que se podrían dar aquí.

Pero me he preguntado cuan sencillo sería crear en C# una clase cadena de longitud fija pero que se comportase como una cadena. Es decir que desde el punto de vista del usuario no haya diferencia entre objetos de esta clase y las cadenas estándar.

En este post os cuento a la solución a la que he llegado, que no tiene porque ser la única ni la mejor, y los “problemas” que me he encontrado. Además la solución me da una excusa para contar una capacidad de C# que mucha gente no conoce ni usa que son las conversiones implícitas personalizadas 🙂

Conversiones implícitas personalizadas

Las conversiones implícitas personalizadas son uno de los dos puntos clave de la solución a la que he llegado (el otro son los genéricos).

Una conversión implícita personalizada es la capacidad de los valores de un tipo para convertirse automáticamente en valores de otro tipo cuando es necesario. P. ej. hay una conversión implícita de int a float:

  1. float f = 10;

En esta línea la constante 10 es de tipo int, pero en cambio f es de tipo float. La asignación funciona no porque 10 sea un float si no porque hay una conversión implícita entre int (10) y float.

En cambio no hay una conversión implícita de double a float y es por ello que esa línea no compila:

  1. float f = 100.0;

Esta línea no compila porque las constantes decimales son de tipo double. Y aunque 100.0 es un valor válido para un float (pues entra dentro de su rango y capacidad), para el compilador es un double y no puede transformar un double en un float porque no hay una transformación implícita.

Por supuesto podemos asignar 100.0 a un float, usando este código:

  1. float f = (float)100.0;

Ahora estamos usando una conversión explícita. Y dado que hay definida una conversión explícita entre double (100.0) y float, el código compila. Que la conversión sea explícita tiene su lógica: la conversión de double a float puede generar una pérdida de rango y/o precisión. Por ello no hay una conversión implícita (que sucedería automáticamente y podría generar errores). Por ello la conversión es explícita, obligando al desarrollador a indicar (mediante el casting) que quiere realizarla y que es consciente de los peligros que pueda haber.

El operador de casting en C# pues invoca a una conversión explícita, que debe estar definida. P. ej. el siguiente código no compila, por más que usemos el casting:

  1. float f = (float) "100.0";

Y no compila porque NO hay definida ninguna conversión explícita de string (“100.0”) a float.

Pues bien, en C# una clase puede definir conversiones explícitas e implícitas desde y a otros tipos.

Yo empecé mi solución con una clase muy simple: Una clase que contuviera nada más que una cadena pero que se convirtiese implícitamente desde y a string:

Code Snippet
  1. sealed class FixedLengthString
  2. {
  3.     private string _buffer;
  4.  
  5.     public FixedLengthString()
  6.     {
  7.         _buffer = string.Empty;
  8.     }
  9.  
  10.     public FixedLengthString(string initialValue)
  11.     {
  12.         _buffer = initialValue;
  13.     }
  14.  
  15.  
  16.     public override string ToString()
  17.     {
  18.         return _buffer;
  19.     }
  20.  
  21.     public static implicit operator string(FixedLengthString value)
  22.     {
  23.         return value._buffer;
  24.     }
  25.  
  26.     public static implicit operator FixedLengthString(string value)
  27.     {
  28.         return new FixedLengthString(value);
  29.     }
  30.  
  31.     public override bool Equals(object obj)
  32.     {
  33.         if (obj == null)
  34.         {
  35.             return false;
  36.         }
  37.  
  38.         if (obj is string)
  39.         {
  40.             return obj.Equals(_buffer);
  41.         }
  42.  
  43.         return (obj is FixedLengthString) ?
  44.             ((FixedLengthString)obj)._buffer == _buffer :
  45.             base.Equals(obj);
  46.     }
  47.  
  48.     public override int GetHashCode()
  49.     {
  50.         return (_buffer ?? string.Empty).GetHashCode();
  51.     }
  52. }

Fíjate que esta clase no es nada más que un contenedor para una cadena (_buffer), pero la clave está en los dos métodos estáticos:

Conversiones Implicitas
  1. public static implicit operator string(FixedLengthString value)
  2. {
  3.     return value._buffer;
  4. }
  5.  
  6. public static implicit operator FixedLengthString(string value)
  7. {
  8.     return new FixedLengthString(value);
  9. }

El primero de los dos define la conversion de FixedLengthString a cadena y el segundo la conversión desde cadena a FixedLengthString.

Fíjate la sintaxis:

  • El método es static
  • Se usa implicit operator para indicar que es una conversión implícita (usaríamos explicit operator para indicar una de explícita).
  • Como valor de retorno colocamos el del tipo al que nos convertimos
  • Recibimos un parámetro del tipo desde el que nos convertimos.

Gracias a estas conversiones implícitas, el siguiente código es válido:

  1. FixedLengthString str = "YYY";
  2. Console.WriteLine(str);

En la primera línea estamos invocando la conversión implícita de cadena (“YYY”) a FixedLengthString y en la segunda la conversión contraria (Console.WriteLine espera un parámetro de tipo string y str es un FixedLengthString).

Bien, ahora tenía una clase que envolvía una cadena y que para el desarrollador se comportaba como una cadena. Sólo había que añadir la longitud máxima y listos.

Pero no era tan fácil.

La primera solución que se nos puede ocurrir pasa por declarar un campo con la longitud máxima de la cadena y en el constructor de FixedLengthString pasar que longitud máxima queremos. Crear la clase FixedLengthString para que contenga cadenas de como máximo una longitud determinada es fácil y no tiene ningún secreto. El problema está en mantener las conversiones implícitas, especialmente la conversión implícita desde una cadena hacia una FixedLengthString.

Supongamos que definimos la clase FixedLengthString para que acepte un parámetro en el constructor que defina la longitud máxima. Entonces podríamos declarar una variable para contener el DNI así:

  1. var dni = new FixedLengthString(9);

Ahora si usáramos métodos definidos en la clase (p. ej. supongamos que la clase FixedLengthString definiese un método SetValue o algo así) podríamos controlar fácilmente que el valor del buffer interno no excediese nunca de 9 carácteres. Pero yo no quería eso: yo quería que la clase se pudiese usar como una cadena estándar se tratase. Es decir poder hacer:

  1. dni = "12345678A";

En esta línea se invoca la conversión implícita desde cadena hacia FixedLengthString… ¿Y cual es el problema? Que es estática. Mira de nuevo el código de dicha conversion:

  1. public static implicit operator FixedLengthString(string value)
  2. {
  3.     return new FixedLengthString(value);
  4. }

Dentro de la conversión no puedo saber cual es el valor de la longitud máxima porque la conversión es estática y el valor de la longitud máxima está en un campo de instancia (el valor de longitud máxima puede ser distinto en cada objeto). Hablando claro: La conversión implícita devuelve un nuevo objeto, y NO puede acceder a las propiedades del objeto anterior si lo hubiese (en mi caso el objeto anterior guardado en dni).

Parece que estamos en un callejon sin salida…

En este punto he empezado un proceso de pensamiento que ha discurrido más o menos así:

  1. El problema es que el campo de longitud máxima es un campo de objeto (no estático) y la conversión es estática.
  2. Entonces si guardo el campo de longitud máxima en una variable estática, podré acceder a dicho valor en la conversión…
  3. … Aunque claro, este enfoque tiene un problema: El valor de longitud máxima es compartido por todos los objetos de la clase. No puedo tener un objeto FixedLengthString de longitud máxima 9 (para un DNI p. ej.) y otro de longitud máxima 5 (p. ej. para un código postal).

Evidentemente el punto 3, parece descartar la idea pero… ¿Y si pudiésemos tener varias clases distintas pero todas con el mismo código, pero tan solo cambiando el valor de longitud máxima? Entonces… ¡todo funcionaría!

Y… qué mecanismo conocéis en C# que permite generar clases distintas con el mismo código? Exacto: Los genéricos.

Si había una solución pasaba por usar genéricos.

Genéricos al rescate

Pero había un pequeño temilla: el parámetro que yo quería generalizar era el valor de longitud máxima, que es un int y esto no está permitido en genéricos. En los templates de C++ (que vienen a ser como los genéricos de .NET pero hipervitaminados) es posible generalizar parámetros de un tipo específico, pero en .NET no. En .NET los parámetros de los genéricos definen siempre un tipo, no un valor. P. ej. en el genérico List<T> el parámetro T es siempre un tipo (si T vale int tienes List<int> y si T vale string tienes List<string>). Y lo mismo ocurre en cualquier genérico que definas en .NET.

En fin… no era perfecto pero ya tenía la idea montada en mi mente. No era perfecta porque me obligaba a crear un tipo específico, distinto, por cada valor de longitud máxima que quisiera, pero bueno, al menos serían tipos muy sencillos. De hecho, serían tipos vacíos, tan solo decorados con un atributo.

Empecé definiendo el atributo que usaría:

FixedLengthMaxAttribute
  1. public class FixedStringLengthMaxAttribute : Attribute
  2. {
  3.     public int Length { get; set; }
  4.  
  5.     public FixedStringLengthMaxAttribute(int len)
  6.     {
  7.         this.Length = len;
  8.     }
  9. }

La idea era la siguiente: Por cada valor de longitud máxima que se quisiera se crea una clase vacía y se decora con este atributo con el valor máximo deseado.

Luego se crea una instancia de la clase FixedLengthString<T> y se pasa como valor del tipo genérico T la clase creada y decorada con el atributo. Para declarar un DNI de 9 carácteres sería:

  1. [FixedStringLengthMaxAttribute(9)]
  2. internal class NifSize
  3. {
  4. }

  1. var nif = new FixedLengthString<NifSize>();

Una vez tenemos el objeto nif podemos convertirlo desde y a cadena sin ningún problema (como hemos visto antes) y se mantiene la longitud máxima de 9 carácteres (en mi implementación se trunca si la cadena desde la que convertimos es más larga).

Ah si… Y falta la implementación de la clase FixedLenghString<T>. Básicamente es la misma que la original FixedLengthString pero con un constructor estático que lee via reflection el atributo [FixedStringLength] aplicado al tipo T y guarda el valor de la propiedad Length de este atributo en un campo estático:

FixedLengthString<T>
  1. sealed class FixedLengthString<T>
  2. {
  3.     private string _buffer;
  4.     private static int _maxlen;
  5.  
  6.     static FixedLengthString()
  7.     {
  8.         var type = typeof (T);
  9.         var attr = type.GetCustomAttribute<FixedStringLengthMaxAttribute>();
  10.         if (attr == null)
  11.         {
  12.             _maxlen = Int32.MaxValue;
  13.         }
  14.         else
  15.         {
  16.             _maxlen = attr.Length;
  17.         }
  18.     }
  19.  
  20.  
  21.     public FixedLengthString()
  22.     {
  23.         _buffer = string.Empty;
  24.     }
  25.  
  26.     public FixedLengthString(string initialValue)
  27.     {
  28.         _buffer = initialValue.Length < _maxlen ? initialValue : initialValue.Substring(0, _maxlen);
  29.     }
  30.  
  31.      
  32.     public override string ToString()
  33.     {
  34.         return _buffer;
  35.     }
  36.  
  37.     public static implicit operator string(FixedLengthString<T> value)
  38.     {
  39.         return value._buffer;
  40.     }
  41.  
  42.     public static implicit operator FixedLengthString<T>(string value)
  43.     {
  44.         return new FixedLengthString<T>(value);
  45.     }
  46.  
  47.     public override bool Equals(object obj)
  48.     {
  49.         if (obj == null)
  50.         {
  51.             return false;
  52.         }
  53.  
  54.         if (obj is string)
  55.         {
  56.             return obj.Equals(_buffer);
  57.         }
  58.  
  59.         return (obj is FixedLengthString<T>) ?
  60.             ((FixedLengthString<T>)obj)._buffer == _buffer :
  61.             base.Equals(obj);
  62.     }
  63.  
  64.     public override int GetHashCode()
  65.     {
  66.  
  67.         return (_buffer ?? string.Empty).GetHashCode();
  68.     }
  69. }

¡Y listos!

Por supuesto puedo crear una clase con una propiedad FixedLengthString<T>:

  1. class Persona
  2. {
  3.     public string Nombre { get; set; }
  4.     public FixedLengthString<NifSize> NIF { get; set; }
  5.     public Persona()
  6.     {
  7.         NIF = new FixedLengthString<NifSize>();
  8.     }
  9. }

Y operar con el NIF de esas personas como si fuesen cadenas normales:

  1. Persona p = new Persona();
  2. Console.WriteLine(p.NIF);
  3. p.NIF = "12345678A";
  4. Console.WriteLine(p.NIF);
  5. p.NIF = "1234567890987654321Z";
  6. Console.WriteLine(p.NIF);

La salida de este programa es:

image

Se puede observar como la cadena se trunca a 9 caracteres.

Bueno… Llegamos al final de este post, espero que os haya resultado interesante. Por supuesto no digo que esta sea la solución para cadenas de tamaño máximo, si no que como he dicho al principio es un simple divertimento 😉

Saludos!

2 comentarios sobre “Divertimento: Cadenas de longitud máxima fija en C#”

  1. Sin duda una solución muy interesante por el uso combinado de las diferentes opciones que ofrece C#. Yo lo consideraría una «code kata», aunque normalmente este termino se suele vincular a soluciones que exigen más desarrollo a nivel de algoritmo.

    El código queda elegante salvo, como has apuntado, el tener que crear un tipo específico por cada longitud máxima.

Responder a etomas Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *