C#: Conversiones (explícitas o implícitas) e interfaces

Una de las características más útiles, aunque más potencialmente peligrosas de C# es la posibilidad de sobrecargar los operadores de conversión (casting) y concretamente el de conversión implícita.

Poder sobrecargar el operador de conversión explícita, aunque lo entiendo como una característica que agrega ortogonalidad al lenguaje, no es algo que me guste. Antes de eso prefiero crear un método AsXXX(). De hecho me parece que un cliente de mi clase encontrará más lógico un método AsXXX() que no «un casting a XXX» que debes saber que se puede hacer para hacerlo.

Pero el casting implícito ya es otra historia: es especialmente útil cuando tenemos clases que son poco más que un envoltorio simple de otro tipo y queremos que usarlo en el entorno del otro tipo sea lo más sencillo posible. Aunque te pueda parecer una aberración que un tipo se convierta de forma implícita (es decir, sin que tu hagas nada) eso es algo relativamente habitual:

long l=10;

Ale, aquí ya tienes una conversión implícita, donde un valor de tipo int, se convierte automáticamente en un valor de tipo long.

Bien, sobrecargar el operador de conversión implícita tiene una limitación, y es que solo funciona con tipos concretos, no con interfaces. Lo siguiente no funciona (te copio parte de la clase para que tengas el contexto):

class DynamicViewport
{
    private readonly Func<IViewportFactory, IViewport> _vpFunc;
    private readonly IViewportFactory _viewportFactory;
    public DynamicViewport(IViewportFactory factory, Func<IViewportFactory, IViewport> viewportFunc)
    {
        _vpFunc = viewportFunc;
        _viewportFactory = factory;
    }
    public static implicit operator IViewport(DynamicViewport dp)
    {
        return dp._vpFunc(dp._viewportFactory);
    }
}

Observa que el código es a priori coherente: _vpFunc es un delegado que devuelve un IViewport así que por ahí no se prevee un problema.

Bueno la razón por la que eso no funciona es porque está explícitamente prohibido por la especificación de C#. Ya, pero… ¿por qué está prohibido? ¿Hay alguna razón?

Permitir sobrecargar el operador de casting es cuanto menos peliagudo ya que hay varios casos que nos pueden hacer confundir fácilmente. Uno es el de la pérdida de identidad de los objetos. Si, dado el siguiente código:

object obj = new MyClass();
var myObj = (MyClass)obj;
var same = Object.ReferenceEquals(obj, myObj);

Os pregunto por el valor de same, supongo que una gran mayoría de vosotros me diréis que same vale true.  El operador de casting explícito existente en el lenguaje no modifica los objetos, solo las referencias. Ahora, imaginad que el siguiente operador estuviera permitido:

public static explicit operator MyClass(object o) { return new MyClass(); }

Entonces, el valor de same del código anterior sería false. Así, que para evitar convertir el desarrollo de C# en algo parecido a la física cuántica, y que algunas conversiones modificasen la identidad y otras no, los desarrollades del lenguaje decidieron prohibir cualquier conversión explícita o implícita siempre que ya exista una (y la conversión de tipo base a tipo derivado existe ya en C#).

Bueno, pues el mismo criterio aplica a las interfaces:

interface IMyClass { }
class MyClass : IMyClass { }
class OtherClass { }

class Program
{
    static void Main(string[] args)
    {
        var other = new OtherClass();
        var iFirst = (IMyClass)other;
    }
}

El siguiente código compila, ya que C# nos permite hacer casting de una clase cualquiera a un interfaz y sabemos que esa conversión tiene dos posibles resultados:

  1. O bien rebienta en ejecución si OtherClass no implementa realmente IMyClass
  2. O bien iFirst es un IMyClass pero Object.ReferenceEquals(iFirst, other) es true. Es decir son «el mismo» objeto.

Es un caso análogo al de pasar de clase base a derivada, a pesar de que ser una conversión entre dos tipos que no tienen relación alguna (OtherClass y IMyClass).

Pues, a caso análogo, razonamiento análogo. Si pudiéramos tener una conversión (implícita o no) propia podríamos hacer eso:

class OtherClass
{
    public static explicit operator IMyClass (OtherClass oc) => new MyClass();
}

Y entonces otheriFirst del código anterior no tendrían la misma referencia. Así que para evitar esas situaciones, los diseñadores del lenguaje decidieron cortar por lo sano.

Leyendo el último ejemplo igual te preguntas como narices C# lo permite (es decir, que compila). Lo copio otra vez:

interface IMyClass { }
class MyClass : IMyClass { }
class OtherClass { }

class Program
{
    static void Main(string[] args)
    {
        var other = new OtherClass();
        var iFirst = (IMyClass)other;
    }
}

Yo no sé vosotros, pero a mi no me hace falta un CLR que me diga que other no implementa la interfaz. Es decir:

  • Tengo una variable other que es de tipo OtherClass
  • La clase OtherClass no implementa IMyClass
  • Luego es de cajón que el casting fallará.

Estamos todos de acuerdo, ¿no?. ¿En qué pensaban los diseñadores del lenguaje cuando decidieron soportar esas conversiones sin sentido?

Ah, calla que igual pensaban en soportar eso:

interface IMyClass { }
class MyClass : IMyClass { }
class OtherClass
{
}

class OtherClassChild : OtherClass, IMyClass {}

class Program
{
    static void Main(string[] args)
    {
        OtherClass other = new OtherClassChild();
        var iFirst = (IMyClass)other;
    }
}

Como podéis ver cuando tenemos interfaces por en medio, todo se complica…

Bueno, en fin, resumiendo, que no hay una razón técnica por la que las conversiones propias desde/a interfaces no estén permitidas. La razón es porque abren la puerta a determinados escenarios que los diseñadores del lenguaje no quisieron abrir y por ello las prohibieron.

Y, ya, por último, para que veáis que las conversiones no son tan sencillas, os presento el siguiente código:

class A
{
    public int Foo { get; set; }
}


class B
{
    public static implicit operator A(B b) => new B();
}

class Program
{
    static void Main(string[] args)
    {
        A a = new B();
        var x = a.Foo;
    }
}

Qué créis que hace este código? Compila? No? Y en caso de qué compile… Qué ocurre? Y por qué?

Saludos!

PD: Por supuesto, yo no soy diseñador del lenguaje de C#, así que cuando afirmo la decisión que tomaron y las razones es porque ellos mismos así lo han comentado: https://stackoverflow.com/questions/9229928/more-on-implicit-conversion-operators-and-interfaces-in-c-sharp-again/9231325#9231325 🙂

Deja un comentario

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