Jan 18, 2011

Emulando en C# los enumerados de Java

Entre los lenguajes de Java y C# existen varias diferencias, entre las cuales, como una de las más mencionadas, están los enumerados. En Java, la declaración enum define una clase (con todas sus posibilidades) llamada "tipo enumerado". Los enumerados en Java pueden implementar interfaces, definir métodos, campos, propiedades, entre otros. Sin embargo en C#, los enumerados no son más que una forma eficiente de definir constantes ordinales que pueden ser asignadas a una variable.

Dada la gran flexibilidad de los enumerados en Java, puede resultar un tanto complejo conseguir un comportamiento similar en C#. Portar un código desde Java que haga uso de enumerados puede no ser tan simple como pinta, sobre todo cuando se explota todo el potencial que brinda el lenguaje. Como siempre digo, la solución que adoptemos debe estar en concordancia con el nivel de complejidad particular que necesitemos, pero creo que es un buen ejercicio analizar dos variantes diferentes y comparar sus ventajas y desventajas.

¿Cómo lo hace Java?

Veamos uno de los propios ejemplos de Sun (ahora Oracle) que aparece en la documentación de los enumerados:
public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7),
    PLUTO   (1.27e+22,  1.137e6);

    private final double mass;   
    private final double radius; 

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    public double mass()   { return mass; }
    public double radius() { return radius; }

    public static final double G = 6.67300E-11;

    public double surfaceGravity() {
        return G * mass / (radius * radius);
    }
    public double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
}
El enumerado "Planet" define varios métodos y campos, y permite la inicialización de cada uno de sus valores a través de un constructor. Veamos ahora un ejemplo de la utilización de este enumerado:
public static void main(String[] args) {
    double earthWeight = 175;
    double mass = earthWeight/EARTH.surfaceGravity();
    for (Planet p : Planet.values()) {
        System.out.printf("Your weight on %s is %f%n", 
                          p, p.surfaceWeight(mass));
    }
}
¡Beautiful! Los que estén acostumbrados al C# de seguro se morirán de envidia al ver esto (al menos a mi me duele la cabeza cada vez que me toca hacer algún enum en C#). Indiscutiblemente Java dio en el clavo con su implementación de enumerados, pero como en la vida no todo lo que toca es la tacita de café, hagámos el intento de acercarnos lo más posible a esta implementación en C#.

Primera variante: Usando una clase para definir el enumerado

En la primera solución en nuestro intento de emular los enumerados de Java en C# vamos a utilizar una clase para definir lo que será nuestro enumerado "Planet" y luego definiremos cada valor del enumerado como un campo estático y de solo lectura dentro de la clase:
public class Planet
{
    public const double G = 6.67300E-11;

    public static readonly Planet MERCURY;
    public static readonly Planet VENUS;
    public static readonly Planet EARTH;
    public static readonly Planet MARS;
    public static readonly Planet JUPITER;
    public static readonly Planet SATURN;
    public static readonly Planet URANUS;
    public static readonly Planet NEPTUNE;
    public static readonly Planet PLUTO;

    private readonly string name;
    private readonly double mass;   
    private readonly double radius; 

    static Planet() {
        Planet.MERCURY = new Planet("MERCURY", 3.303e+23, 2.4397e6);
        Planet.VENUS = new Planet("VENUS", 4.869e+24, 6.0518e6);
        Planet.EARTH = new Planet("EARTH", 5.976e+24, 6.37814e6);
        Planet.MARS = new Planet("MARS", 6.421e+23, 3.3972e6);
        Planet.JUPITER = new Planet("JUPITER", 1.9e+27, 7.1492e7);
        Planet.SATURN = new Planet("SATURN", 5.688e+26, 6.0268e7);
        Planet.URANUS = new Planet("URANUS", 8.686e+25, 2.5559e7);
        Planet.NEPTUNE = new Planet("NEPTUNE", 1.024e+26, 2.4746e7);
        Planet.PLUTO = new Planet("PLUTO", 1.27e+22, 1.137e6);
    }

    private Planet(string name, double mass, double radius)
    {
        this.name = name;
        this.mass = mass;
        this.radius = radius;
    }

    public double SurfaceGravity()
    {
        return G * this.mass / (this.radius * this.radius);
    }

    public double SurfaceWeight(double otherMass)
    {
        return otherMass * SurfaceGravity();
    }

    public override string ToString()
    {
        return this.name;
    }

    public static IEnumerable<Planet> Values
    {
        get
        {
            yield return MERCURY;
            yield return VENUS;
            yield return EARTH;
            yield return MARS;
            yield return JUPITER;
            yield return SATURN;
            yield return URANUS;
            yield return NEPTUNE;
            yield return PLUTO;
        }
    }

    public string Name { get { return this.name; } }
    public double Mass { get { return this.mass; } }
    public double Radius { get { return this.radius; } }
}
Si prestamos atención, veremos que esta implementación no dista mucho del propio ejemplo en Java, con la diferencia de que aquí estamos utilizando una clase en vez de un tipo enumerado, tenemos una propiedad llamada "Name" donde almacenaremos el nombre del planeta, y otra propiedad "Values" la cual retorna un IEnumerable de forma tal que podamos iterar a través de todos los elementos. El resto es bien similar.

Veamos ahora cómo utilizar nuestro "intento de enumerado":
static void Main(string[] args)
{
    Planet pEarth = Planet.EARTH;
    double earthRadius = pEarth.Radius;
    double earthWeight = 175;
    double mass = earthWeight / pEarth.SurfaceGravity();
    foreach (Planet p in Planet.Values)
    {
        Console.WriteLine("Your weight on {0} is {1}", 
                          p, p.SurfaceWeight(mass));
    }
}
Descontando algunas sutilizas entre ambos lenguajes, la forma de utilizar nuestro ejemplo es extremadamente similar a la vista anteriormente escrita en Java. Sin lugar a dudas pudimos reproducir de forma relativamente simple la flexibilidad del enumerado "Planet" en Java a nuestra clase "Planet" en C#.

No tan rápido que aquí no acaba la historia

A pesar que nuestra clase "Planet" cumple con todos los requisitos necesarios para reproducir nuestro ejemplo en Java, no es una solución perfecta e incluso puede ser un problema en dependencia de nuestras necesidades. Imaginemos que queremos hacer algo como esto en nuestro código en C#:
switch (Planet)
{
    Planet.EARTH: // do something 
        break;
    Planet.MARS: // do something else
        break;
    ...
}
El código anterior no compilará, dado que nuestros planetas no son valores ordinales que puedan ser utilizados en una cláusula switch. Este es el precio que hemos tenido que pagar para portar el enumerado "Planet" desde Java a C# de la forma más simple posible. Sin embargo, todo en la vida tiene solución, así que veamos nuestra segunda implementación.

Segunda variante: Usando enumerados combinados con atributos

Esta segunda implementación es un poco más complicada pero algo más flexible que la anterior. En este caso sacrificaremos la simplicidad de nuestra primera implementación con el objetivo de utilizar nuestro enumerado de una forma mucho más consistente en todo el proyecto. Empecemos con la definición general de nuestro nuevo "Planet":
public enum Planet
{
    MERCURY,
    VENUS,
    EARTH,
    MARS,
    JUPITER,
    SATURN,
    URANUS,
    NEPTUNE,
    PLUTO
}
Simplemente hemos definido un enumerado en C# con todos sus posibles valores. Esto es prácticamente lo más lejos que podemos llegar con el tipo de dato enum en C#, sin embargo, utilizando atributos podemos hacer que la historia se vuelva un poco más interesante. Definamos un atributo que luego vamos a aplicarle a cada uno de los valores de nuestro enumerado:
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, 
                Inherited = true)]
public class PlanetAttribute : Attribute
{
    private double mass;
    private double radius;

    public PlanetAttribute(double mass, double radius)
    {
        this.mass = mass;
        this.radius = radius;
    }

    public double Mass { get { return this.mass; } }
    public double Radius { get { return this.radius; } }
}
Nuestro atributo almacenará los valores del radio y la masa de cada planeta, para lo cual creamos los campos, propiedades, y el constructor correspondiente. Además, utilizando "AttributeUsage" vamos a especificar que nuestro attributo "PlanetAttribute" solamente podrá ser utilizado a nivel de campos, no permitirá múltiples apariciones del mismo en cada campo, y podrá ser heredados por clases hijas. Apliquemos el atributo ahora a cada uno de nuestros planetas:
public enum Planet
{
    [Planet(3.303e+23, 2.4397e6)]
    MERCURY,

    [Planet(4.869e+24, 6.0518e6)]
    VENUS,

    [Planet(5.976e+24, 6.37814e6)]
    EARTH,

    [Planet(6.421e+23, 3.3972e6)]
    MARS,

    [Planet(1.9e+27, 7.1492e7)]
    JUPITER,

    [Planet(5.688e+26, 6.0268e7)]
    SATURN,

    [Planet(8.686e+25, 2.5559e7)]
    URANUS,

    [Planet(1.024e+26, 2.4746e7)]
    NEPTUNE,

    [Planet(1.27e+22, 1.137e6)]
    PLUTO
}
Ya en estos momentos cada uno de los planetas almacena toda la información que necesitamos. En este caso no es necesario definir una propiedad para el nombre del planeta ya que los enumerados en C# nos permiten con un simple ToString() obtener este valor. Ahora las preguntas pendientes son: ¿Cómo acceder a esta información desde nuestro código? ¿Dónde ponemos nuestros métodos? ¿Cómo reproducir el ejemplo en Java que usa el enumerado?

Con un poco de reflection, y la ayuda de las extensiones en C# podemos conseguir nuestro objetivo:
public static class PlanetExtensions
{
    public const double G = 6.67300E-11;

    public static double GetMass(this Planet planet)
    {
        return ((PlanetAttribute)typeof(Planet).GetField(planet.ToString()).
            GetCustomAttributes(true)[0]).Mass;
    }

    public static double GetRadius(this Planet planet)
    {
        return ((PlanetAttribute)typeof(Planet).GetField(planet.ToString()).
            GetCustomAttributes(true)[0]).Radius;
    }

    public static double SurfaceGravity(this Planet planet)
    {
        return G * planet.GetMass() / 
               (planet.GetRadius() * planet.GetRadius());
    }

    public static double SurfaceWeight(this Planet planet, double otherMass)
    {
        return otherMass * planet.SurfaceGravity();
    }
}
En la clase "PlanetExtensions" definimos cuatro métodos que van a extender nuestro enumerado "Planet". En el caso de "GetMass" y "GetRadius", haciendo uso de reflection, vamos a devolver el valor de estas propiedades almacenadas en los atributos con los que anteriormente habíamos marcado cada uno de los planetas. Desafortunadamente, en C# 4.0 no existen "Property Extensions" así que tendremos que conformarnos con métodos en vez de propiedades, pero esto no es mayor problema. La implementación de "SurfaceGravity" y "SurfaceWeight" es completamente intuitiva tal y como habíamos visto en nuestra primera implementación.

Veamos cómo quedaría el código usando nuestra nueva implementación de "Planet":
static void Main(string[] args)
{
    Planet pEarth = Planet.EARTH;
    double earthRadius = pEarth.GetRadius();
    double earthWeight = 175;
    double mass = earthWeight / pEarth.SurfaceGravity();
    foreach (Planet p in Enum.GetValues(typeof(Planet)))
    {
        Console.WriteLine("Your weight on {0} is {1}", 
                          p, p.SurfaceWeight(mass));
    }
}
Las dos únicas diferencias aquí serían el uso del método "GetRadius" ya que no podemos convertirlo a una propiedad, y la manera de iterar por todos los valores de un enumerado que, en el caso de C#, se realiza utilizando la clase auxiliar "Enum" a través del método "GetValues". El resto es exactamente igual.

¿Y ahora qué pasa con el switch?

Dado que nuestra segunda implementación está basada en un enumerado real de C#, podremos utilizar "Planet" en una cláusula swith sin ningún tipo de problemas. Del mismo modo, podremos disfrutar de todas las ventajas que tienen los enumerados en C#, con el valor añadido de que nuestro "Planet" aparentemente se comporta como su versión en Java.

El fin de la historia

Después de ver los dos ejemplos de cómo conseguir en C# acercarnos a los enumerados en Java, hay que tener en cuenta que, en dependencia del problema en cuestión, podremos necesitar más o menos de lo aquí presentado. Cada situación tiene sus particularidades, y en nuestras manos queda determinar lo mejor en cada caso. La primera variante es muy sencilla, pero menos flexible que la segunda. Sin embargo, esto no quiere decir que una implementación con atributos sea siempre lo más adecuado... incluso, es muy probable que lo único que necesitemos sea el simple enumerado de C# sin muchos más inventos... ¿quién sabe? Habría que ver.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.