C# 7–Default method implementation?

El otro día estuve hablando con Joan Jané, sobre la funcionalidad que se está valorando para C#7 o (probablemente, dado su estado) más adelante. A falta de un nombre mejor llamaré a esa funcionalidad “Default method implementation” porque así se conoce en Java, donde esa funcionalidad ya existe.

Joan y yo teníamos puntos de vista totalmente opuestos a dicha característica, mientras que para mi era un añadido muy interesante al lenguaje, Joan se alineaba más con las tesis que Fernando Escolar expone en un post en su blog. Para Fer, esa feature es la peor idea que ha tenido Java en los últimos años. A mi me da la sensación que verlo así es no entender exactamente que añade esa característica y analizarla desde una posición demasiado rígida. Joan argumentaba problemas relacionados con SOLID, generalmente con el SRP y con la segregación de interfaces. En este post voy a comentar lo que, desde mi punto de vista, permitiría esa característica de estar disponible. Y por qué, no solo no es una mala idea, si no, a priori, todo lo contrario (tanto en Java como en C# por más que se empecine Fer en decir lo contrario).

¿En qué consiste esa característica?

Bueno, básicamente consiste en que las interfaces pueden definir una implementación para sus métodos. Dicho de golpe y porrazo esto parece una aberración. Plantean algo parecido a lo siguiente (copio y pego el código que pone Fer en su blog):

interface ISomeInterface
{
    string Property { get; }
    default string Format()
    {
        return string.Format("{0} ({1})", GetType().Name, Property);
    }
}

class SomeClass : ISomeInterface
{
    public string Property { get; set; }
}

Visto así, parece realmente algo sin sentido. ¿Interfaces definiendo métodos? ¿Nos hemos vuelto locos?

El primer argumento que se esgrime contra esa característica es el mantra de que “las interfaces definen un contrato y por lo tanto no deben poder implementar métodos”. De esta afirmación se infiere que no se puede añadir comportamiento a una interfaz. Todo muy bonito, pero extremadamente limitante en un sistema de tipos tan estricto como el de C# (y el de Java). Igual, alguien se pregunta qué sentido tiene agregarle comportamiento a una interfaz. Bien, quien se pregunte eso, debería revisar por ejemplo Linq. Linq no es nada más que agregar comportamiento a la interfaz IEnumerable<T>. Si en C# no se hubieran inventado un mecanismo para poder hacer eso, ahora existirían N implementaciones de Linq (una por cada clase que implementa IEnumerable<T>). Y, por supuesto, cada clase futura que implementase IEnumerable<T> (ya fuese o no del framework) debería implementar su propia versión de Linq. ¿Qué… ya no suena tan bonito eh? Simplemente a veces es interesante que las interfaces puedan tener un comportamiento por defecto.

Por supuesto C# dio con un mecanismo para ello, que es los métodos de extensión sobre interfaces. Al final, si la idea es proponer un método Format() que sirva para todos los objetos ISomeInterface podemos declarar un método de extensión:

interface ISomeInterface
{
    string Property { get; }
}

class SomeClass : ISomeInterface
{
    public string Property { get; set; }
}

class SomeInterfaceExtensions {
    public static string Format(this ISomeInterface self) {
        return string.Format("{0} ({1})", 
            self.GetType().Name, self.Property);
    }
}

Con tal de que añadamos el using correspondiente al namespace donde está declarada la clase SomeInterfaceExtensions (si es que está en un namespace distinto del de la interfaz ISomeInterface) ya podemos llamar a Format() sobre cualquier objeto que implemente ISomeInterface. Exactamente igual que en el caso anterior. Efectivamente, Linq está implementado como un conjunto de métodos de extensión sobre IEnumerable<T>. Pero debemos entender que para el tema que nos ocupa es irrelevante si dichos métodos se implementan en como métodos estáticos en una clase separada y el compilador hace el “truco del almendruco” o bien dichos métodos están implementados en la propia interfaz. Es irrelevante porque en ambos casos la idea fundamental es la misma: añadir comportamiento a una interfaz.

En definitiva, si te sientes cómodo con los métodos de extensión sobre interfaces y te parecen bien, y ves como una aberración la implementación de métodos en una interfaz… es que no entiendes el motivo de los métodos de extensión. Porque ambas técnicas, en el fondo, persiguen lo mismo.

Por supuesto, dado que C# ya tiene métodos de extensión es lícito preguntarnos si la implementación de métodos en interfaces aporta algo al lenguaje (en Java la respuesta es mucho más clara, ya que no hay métodos de extensión). Yo pienso que sí, que la implementación de métodos en una interfaz aporta soluciones incluso teniendo ya los métodos de extensión, y voy a dedicar el resto del post en hablar de eso.

Métodos de extensión vs default implementation methods

Nota: Cuando analice el funcionamiento de los default implementation methods voy a hacerlo en base a como funciona en Java. En C# se está discutiendo, así que las decisiones que se tomen en base a como dicha característica aterriza (si finalmente aterriza) a C# pueden afectar lo que diga a continuación.

Ambas características sirven a un propósito similar, pero lo interesante es que son complementarias en tanto que el punto “fuerte” de cada una de ellas es el punto “débil” de la otra:

  • Con métodos de extensión podemos extender interfaces que no controlamos. Eso es, interfaces que no son “nuestros”. Podemos extender IEnumerable<T> a pesar de que IEnumerable<T> es una interfaz del framework. Con los default implementation methods no podemos hacer eso, ya que para añadir un método default a IEnumerable<T> debemos modificar el código de IEnumerable<T> ya que el método se añade en la propia interfaz.
  • Los default extension methods son resueltos en tiempo de ejecución, no de compilación, por lo que pueden ser redefinidos en una clase derivada, y la implementación redefinida será usada en lugar de la implementación propuesta por la interfaz, con independencia del tipo de la referencia. Vamos, que son métodos virtuales. Eso no ocurre con los métodos de extensión, que se resuelven en tiempo de compilación:
class Program 
{
    static void Main(string[] args)
        {
            IA a = new A();
            a.Test();
        }
    }
}
interface IA { }
class A : IA {
    public void Test()
    {
        Console.WriteLine("A::Test");
    }
}
static class AExtensions
{
    public static void Test(this IA a)
    {
        Console.WriteLine("AExtensions::Test");
    }
}

Este código imprime “AExtensions::Test” en lugar de “A::Test” por la pantalla. A pesar de que el objeto es de tipo A y a pesar de que la clase A proporciona su propia implementación del método de extensión. Pero como la referencia es de tipo IA, se usa el método de extensión. Eso, a la práctica, impide que una clase redefina un método de extensión (con una implementación más eficiente o más adaptada) con la total seguridad de que ese método será usado en todos los casos posibles. Una pena, pero claro, cuando el compilador hace “trucos” estamos limitados a los trucos que el compilador puede hacer. Igual te parece que eso no es relevante, pero lo es. Imagina un método Last() declarado como método de extensión sobre IEnumerable<T>. Este método puede implementarse más o menos eficientemente en función del tipo real del IEnumerable. P. ej. si tenemos acceso directo y sabemos el número de elementos (como una List<T>) podemos devolver el último elemento de forma directa. Ahora bien, el método de extensión Last() no puede hacer eso, debe recorrer todo el IEnumerable, porque lo único que se puede hacer con un IEnumerable es recorrerlo. Por supuesto se podría usar reflection o una serie de “ifs con is” y preguntar si el IEnumerable es realmente una IList (y Linq hace eso en algunos casos) pero es una solución que ya se ve que no escala y que no es de uso general.

Lo que realmente necesitaríamos en estos casos es que la clase List<T> definiese su propio método Last() y que se llamase a ese método siempre que llamara a Last() de cualquier objeto que fuese una List<T> a pesar de que la referencia fuese de tipo IEnumerable<T>. Precisamente, lo que conseguiríamos si Last() fuese un método “default” implementado en IEnumerable<T> y redefinido en List<T>.

Por lo tanto: sí. Hay lugar para los default implementation methods a pesar de tener ya métodos de extension.

Anda… ¡y traits!

Otra ventaja de los default implementation methods es que abren de par en par las puertas de los traits a C#. Los traits son un tipo particular de herencia múltiple, que no experimenta los problemas de la herencia múltiple genérica. Un trait no es nada más que “un pedazo de funcionalidad” (es decir un conjunto de métodos) pensada para ser añadida a cualquier clase existente. No es la idea de un trait que se creen instancias de él.

En efecto una interfaz con todos sus métodos implementados como default methods es ni más ni menos que un trait. Quizá te preguntes porque no podemos usar una clase abstracta con todos sus métodos implementados (es decir no abstractos) como trait. Pues muy fácil: en C# solo puedo heredar de una clase. Por lo tanto eso me limitaría a aplicar como máximo un trait a cada clase (y además impediría que dicha clase heredase de otra clase). No, las clases abstractas no nos sirven… Pero una interfaz con todos sus métodos implementados como métodos default es casi idéntico a una clase abstracta y ¡una clase puede implementar los que desee! Así, aplicar un trait se convierte en implementar una interfaz. Pero, dado que dicha interfaz tiene todos sus métodos implementados como métodos default, a la clase “no le cuesta nada” implementar dicha interfaz (es decir, no debe añadirse código alguno a la clase).

Protocolos

El concepto de “interface” que nos parece tan natural y “tan OOP” no existe en muchos lenguajes orientados a objetos. Muchos de esos lenguajes trabajan con un concepto parecido al de interface, pero no igual, que es el de protocolo. Básicamente un protocolo es una “funcionalidad (conjunto de métodos) que está disponible para un tipo determinado”. Parece lo mismo que una interface, pero no lo es. Una diferencia entre protocolo y la interface clásica de C#/Java es que el primero puede definir comportamiento (recuerda que el protocolo se define como un comportamiento disponible para un tipo). Es decir, exactamente lo mismo que los default implementation methods. Lenguajes como Swift o Objective-C usan constantemente protocolos. Así, que la idea de “interfaces que definen métodos” no es tan aberrante como pueda parecer.

Otros lenguajes, como Elixir, llevan el concepto más allá y permiten “aplicar un protocolo a un tipo”. Eso es, hacer que las instancias de un determinado tipo X sean también instancias del protocolo P, aplicando el protocolo P a X. Esa aplicación ocurre fuera de X y consiste en definir los métodos de P basados en una instancia de X. Es parecido a los métodos de extensión pero el efecto es radicalmente distinto: con un método de extensión conseguimos extender una interfaz I, pero si el tipo A no implementa I, seguirá sin hacerlo. Aplicando protocolos lo que conseguimos es añadir los métodos de I al tipo A, de forma que A ahora implementa I, pero lo hemos hecho sin modificar A.

En resúmen…

Por supuesto hay aspectos que deben analizarse sobre como añadir los default implementation methods en C# y hay muchos aspectos que no he considerado en este post. Para solucionar algunos de ellos basta fijarse en Java y para solucionar otros más especificos de C# (p. ej. que ocurre con implementaciones explícitas) se debe analizar en más profundidad.

Pero lo que está clarísimo es que el concepto de que una interfaz implemente métodos no es, ni de lejos, una aberración. Y que añadirlo al lenguaje, lo enriquece. Por supuesto, como característica puede usarse mal. Pero eso ya ocurre con cualquier característica actual, así que no debería ser motivo para descartarla. Para mí, incluso en su forma más sencilla (parecida a la actual de Java) aporta lo suficiente como para valer la pena. Personalmente me gustaría ver aplicación de protocolos en C#, pero me temo que eso está mucho más lejos (si los default implementatio methods requieren ya ayuda del CLR, eso mucho más todavía).

¿Y vosotros? ¿Qué opináis? ¿Os parece interesante esa característica, os da igual u os parece una aberración? Cualquiera está invitado, como siempre, a comentarlo 🙂

Saludos!

10 comentarios sobre “C# 7–Default method implementation?”

  1. Enhorabuena Edu! Me ha parecido un post muy bueno. Y la temática es muy interesante.

    Según lo que me ha parecido entender, lo más interesante de esta «feature» es conseguir multiherencia, sin los problemas de la multiherencia. Es muy interesante esta posibilidad, que siempre he echado de menos en los lenguajes manejados…

    Pero si van a realizar esta implementación, con todo lo que ello implica, no podría ser más interesante de copiar la multiherencia de c++ directamente? Creo que el reto es casi el mismo y el resultado nos daría mucha más versatilidad. Esto sin entrar en los peligros de esta característica.

    A mí personalmente me gustan las interfaces como contratos y las clases abstractas para hacer implementación «default». Si la interfaz tiene implementación y se puede heredar de varias interfaces con implementación (que esto está por ver), casi prefiero que le cambien el nombre por «class» y digan que han implementado multiherencia 😀

    1. Buenas Fer,
      Si copiasen la herencia de C++, entonces estaríamos hablando de otra cosa.

      Observa que en C++ NO se necesitan las «interfaces». Y no se necesitan, porque en el fondo son un añadido «artificial» para soportar polimorfismo múltiple en un sistmea de herencia simple. Si tienes herencia múltiple… ¿para qué necesitas interfaces? Para nada.

      Los «default extension methods» están a medio camino entre la herencia múltiple y el sistema actual de C#. Y como añadido sirven perfectamente. Sobre lo de seguir llamándoles «interfaces» ya te digo… me da igual, porque estamos hablando de un concepto artificial, así que me da igual el nombre que le quieran poner.

      Y finalmente, sobre el uso de clases abstractas para implementaciones «default»… el problema es que dado que tienes herencia simple NO puedes obtener la implementación «default» de N interfaces, porque no puedes heredar de N clases. De ahí que, si estas son las reglas del juego, mucho mejor que los propios interfaces te den dicha implementación «default» 😉

  2. Muy buen post Edu!!
    Me ha resultado curioso como has sabido ver el potencial de esta nueva característica para sortear la limitación de la herencia múltiple. Me ha parecido mucho más interesante de lo que imaginaba, sin duda.

    Por otro lado, le he estado dando vueltas y si quisiéramos encontrar una alternativa para «simular» la idea de traits, siempre podemos tirar de composición y delegados para construir una clase que implementa varias interfaces y cuya implementación de los métodos usa delegados.

    He implementado un ejemplo, creo que un código vale más que mil palabras, jeje.

    https://gist.github.com/joanjane/ebe5a4d61382e1e4dfc412341cce0838

    Tiene alguna desventaja a parte de escribir ligeramente más código?

    1. Buenas Joan,
      Para simular los traits usar la composición puede funcionar pero no solo escribes más código si no que debes pasar todas esas composiciones en el constructor de la clase. Aunque esto último se podría llegar a superar usando genéricos. Pero la pregunta es… ¿Qué aporta? Nada. Solo complejidad.

      Al margen de eso, hay otra casuística que ni usando composición puedes simular. La del método Last(). Por qué no la puedes simular con composición? Pues sencillamente porque desde las clases que implementan las interfaces NO puedes acceder a datos «privados» de la clase (en tu ejemplo, desde DefaultAnotherImplementation no puedes acceder a variables privadas de SomeClass). Observa que si la implementación de Last() está en DefaultAnotherImplementation, ya no puedes acceder a ninguna variable privada de SomeClass y eso te puede impedir implementar dicho método.

      Yo soy un ferviente seguidor de YAGNI. Veo la complejidad del código que presentas y veo lo sencillo que sería usando «default extension methods» y lo tengo clarísimo 🙂 Lo sencillo siempre suele ser lo mejor.

    1. Buenas!
      MMM… no me queda claro tu comentario 😉

      Cuando se diseñó C#, el modelo que se tomó fue Java. En Java no hay herencia múltiple, porque sus diseñadores creyeron que aportaba demasiada complejidad al lenguaje para los escenarios que solucionaba.

      Todo son puntos de vista…

    2. Aportar poco?

      c# 3: Los métodos de extensión nos trajeron Linq, si eso es poco, apaga y vámonos. Agrega las expresiones lambda, y los tipos anónimos.
      c# 4: los tipos dinámicos, covariant y contravariant en genéricos.
      c# 5: Async/Await. Eso es no aportar nada no?
      c# 6: using static puede que no aporte nada a quien no implementa algo de programación funcional, pero para los que optamos por ese paradigma es una gran aportación. A eso le sumamos varios pequeños pero muy útiles detalles, el «null prograpator», salva vidas cuando quieres disminuir bugs por el manejo de bugs, la interpolación de cadenas, azúcar sintáctica, si (como casi todo), pero azúcar muy buena, el operador nameof, que es muy útil, sobretodo después de refactorizar.
      c# 7: Algo que al parecer muy pocos han entendido, la mejora en el manejo de awaitables, además de la incorporación de ValueTask, que ayuda muchísimo en el rendimiento de tareas asíncronas, pattern matching, que es otra de las novedades que veo que pocos usan (o entienden) y que permite un mejor (y mas corto) código, tuplas, etc.

      Mas bien creo que eres un poco cerrado y te falta investigar un poco mas sobre las novedades, en realidad aportan mucho, muchísimo en algunos casos (el ejemplo de los que implementamos prog. funcional), sobretodo en el día a día (la escritura de código se hace muchísimo mas fácil).

    1. Desde el punto de un diagrama de clases yo consideraría las interfaces con algún método default, como clases abstractas base. Y la relación de implementación es de herencia.

Deja un comentario

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