jueves, 16 de octubre de 2014

Conversiones de usuario en C#

Las conversiones de usuario permiten definir conversiones personalizadas en C#, tanto explícitas como implícitas, desde un tipo origen a un tipo destino.

Lo cierto es que no suelo utilizarlas mucho (por no decir rara y extrañísima vez), pero como siempre estamos buscando una oportunidad para poner en juego características del lenguaje, recientemente me he encontrado con un caso que he resuelto con una conversión de usuario.

Para poner en situación, primero veremos un ejemplo de cómo son y qué hacen.
using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var age = 38;
            Person person = age; // implícita de int a Person
            Console.WriteLine(person.Age); // 38 
            age = (int)person; // explícita, necesario cast, de Person a int            
            Console.WriteLine(age); // 38
            Console.ReadKey();
        }
    }

    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public static implicit operator Person(int age)
        {
            return new Person() { Age = age };
        }

        public static explicit operator int(Person person)
        {
            return person.Age;
        }
    }
}
Como se puede ver en el ejemplo, la sintaxis de una conversión de usuario es:
public static {implicit|explicit} operator TipoDestino (TipoOrigen identificador) 
{
 // do something…
return TipoDestino;
}
Las restricciones que se aplican a una conversión de usuario son:

  • Sólo son válidas para clases y estructuras.
  • TipoDestino y TipoOrigen tienen que ser de distinto tipo.
  • No puede haber una relación de herencia entre TipoDestino y TipoOrigen
  • TipoDestino y TipoOrigen no pueden ser de un tipo interface ni object.
  • No se puede declarar la misma conversión tanto de forma implícita como explícita
  • No se puede usar el operador is
  • No se puede usar el operador as

Después de la teoría y ya sabiendo que es una conversión de usuario, el ejemplo donde he aplicado una conversión de usuario es en un método que devuelve si se puede o no enviar un pedido. El problema está en que si no se puede enviar el pedido, querría saber también el motivo. Es decir, el siguiente código no me servía.
    public class Order
    {
        public string CustomerId { get; set; }
        public DateTime? DeliveryDate { get; set; }

        public bool CanBeSent()
        {
            if (string.IsNullOrEmpty(CustomerId))
            {
                return false;
            }
            if (DeliveryDate == null)
            {
                return false;
            }
            return true;
        }
    }
Para solucionar el problema expuesto y haciendo uso de las conversiones de usuario, podemos hacer que el método devuelve un tipo complejo indicando si es o no posible enviar el pedido y, en caso negativo, también especificar el motivo. Además, el valor devuelto se puede trabajar tanto como un booleano como un tipo complejo cuando sea necesario.
using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var order = new Order();
            var result = order.CanBeSent();
            if (result)
            {
                Console.WriteLine("Se puede enviar el pedido");
            }
            else
            {
                Console.WriteLine("No se puede enviar el pedido");
                Console.WriteLine(result.Reason);
            }
            Console.ReadKey();
        }
    }

    public class CanBeResult
    {
        public bool Value { get; set; }
        public string Reason { get; set; }

        public CanBeResult(bool value)
        {
            Value = value;
        }

        public CanBeResult(bool value, string reason)
            : this(value)
        {
            Reason = reason;
        }

        public static implicit operator bool(CanBeResult result)
        {
            return result.Value;
        }

        public static implicit operator CanBeResult(bool value)
        {
            return new CanBeResult(value);
        }
    }

    public class Order
    {
        public string CustomerId { get; set; }
        public DateTime? DeliveryDate { get; set; }

        public CanBeResult CanBeSent()
        {
            if (string.IsNullOrEmpty(CustomerId))
            {
                return new CanBeResult(false, "El pedido no tiene cliente");
            }
            if (DeliveryDate == null)
            {
                return new CanBeResult(false, "El pedido no tiene fecha de entrega");
            }
            return true;
        }
    }
}
Un saludo!

2 comentarios:

  1. Hola Sergio,

    Muy buena explicación. Un caso en que suelo utilizar las conversiones (además implícitas) es cuando creo builders para tests (https://gist.github.com/jmhdez/8332445).

    Así puedo escribir código como:

    order.AddLine(Build.Product("Camiseta").Size("L").Color("Rojo"), 2);

    De esta forma el fluent interface del ProductBuilder, que devuelve en cada paso una referencia a un ProductBuilder, se puede convertir en cualquier momento de forma implícita a un objeto Product para pasarlo como parámetro de un método.

    Un saludo,

    Juanma.

    ResponderEliminar
    Respuestas
    1. Gracias Juanma,

      De hecho tú fuiste el germen de este post :)

      Las conversiones era lo típico que había leído pero nunca había visto usarlas, después entre tu post (http://blog.koalite.com/2014/01/aplicando-el-patron-builder-para-escribir-tests-unitarios/) y el uso de librería netfx (patrón especificación para LINQ https://www.nuget.org/packages/netfx-Patterns.LinqSpecs/) que hace un uso intensivo de estas conversiones me picó el gusanillo :)

      Un saludo.

      Eliminar