Novedades de C#8: Interfaces. ¿Qué podemos esperar?

Tiempo de lectura: 6 minutos
La imagen muestra el logotipo de c# 8.0 para la entrada de interfaces

Llegamos a la última entrada del año y es el momento de acabar con las novedades «grandes» que nos ha introducido C#8 hablando de las interfaces. En entradas anteriores hemos hablado de Índices y Rangos, del nuevo Pattern Matching y de IAsyncEnumerable que aunque no son las únicas novedades que trae C#8, sí son las más grandes (a mi humilde criterio siempre).

Esta quizás sea una entrada controvertida y es que este cambio no ha gustado a todos por igual en la comunidad.

¿Qué es una interfaz?

Hasta ahora cuando hablábamos de una interfaz nos estábamos refiriendo a un «contrato» que incluía una serie de métodos que toda clase que la implementase se comprometía a cumplir (o de lo contrario se genera un error de compilación).

public interface IVehiculo
{
    void Avanzar();
    void Girar(int grados);
}

public class Coche : IVehiculo
{
    public void Avanzar()
    {
        //....
    }

    public void Girar(int grados)
    {
        //....
    }
}

public class Bicicleta : IVehiculo
{
    public void Avanzar()
    {
        //....
    }

    public void Girar(int grados)
    {
        //....
    }
}

Precisamente este «contrato» nos daba la capacidad de trabajar abstrayéndonos completamente de que había por debajo al utilizar la interfaz. Para nosotros trabajando con la interfaz no nos importa si lo que hay detrás es una bicicleta o un coche. Un caso claro es cuando recibimos esa interfaz en un método.

void AvanzarHacia(IVehiculo vehiculo, int grados)
{
    vehiculo.Girar(grados);
    vehiculo.Avanzar();
}

Hasta aquí no te estoy contando nada nuevo, esto son las interfaces de siempre.

¿Por qué han cambiado las interfaces en C#8?

Hasta ahora una interfaz que ya se había distribuido y se estaba utilizando era inmutable. En el ejemplo del código anterior, si la estamos utilizando y dentro de un tiempo nos damos cuenta de que hay que añadir un nuevo método y lo añadimos, vamos a romper la compilación. Esto es porque precisamente la interfaz nos obliga a implementar todos sus métodos.

¿Imaginas que pasaría si el equipo de desarrollo de Microsoft se diese cuenta de que IDisposable necesita añadirle más métodos?

La respuesta es muy simple: Romperían todo el ecosistema. Todos los proyectos que tengan alguna clase que implementase IDisposable dejarían de compilar al instante hasta que se añadiese ese método extra de la interfaz. Si echamos un ojo al código de .Net o .Net Core podemos comprobar que son miles las clases que implementan IDisposable. Eso solo dentro del propio framework, si hablamos de su uso por parte de los desarrolladores en librerías y productos, el número de clases que se romperían es tan grande que no podemos ni imaginarlo.

Por supuesto un cambio así que es un breaking change en toda la expresión del término no se puede aplicar sin hacer un cambio de versión «major». Hacerlo sin actualizar la versión sería el final del lenguaje, pero aun actualizando la versión… ¿Estarías dispuesto a rehacer todo el código de un proyecto solo para actualizarlo de versión? ¿Y si no ha cambiado solo IDisposable? Desde luego la acogida de esa nueva versión sería mucho más complicada. Aquí es donde entran los cambios que nos trae C#8. Pero…

¿En que han cambiado las interfaces con C#8?

Precisamente la novedad que tenemos con respecto a las interfaces es que ahora si pueden tener una implementación por defecto. Espera…

WHAT???!! ¿Si una interfaz tiene una implementación por defecto no es una clase abstracta?

Sí. Con anterioridad a C#8 si queríamos tener una implantación por defecto elegíamos una clase abstracta y sino una interfaz. La separación estaba clara. Esto ahora ha cambiado y si por ejemplo después del tiempo nos diésemos cuenta de que la interfaz del ejemplo que hemos puesto no es útil porque en vez de especificar los grados queremos solo decirle también que gire a izquierda y derecha podríamos hacer una interfaz así:

public interface IVehiculo
{
    void Avanzar();
    void Girar(int grados);
    void GirarDerecha() => Girar(90); //Implementación por defecto
    void GirarIzquierda() => Girar(-90); //Implementación por defecto
}

El hecho de actualizar esta interfaz no va a provocar un error de compilación ya que si no encuentra una implementación en la clase para ese método va a utilizar la implementación por defecto.

Este cambio en las interfaces de C#8 no viene solo. Hasta ahora todos los métodos de una interfaz pública eran públicos y esto tenía todo el sentido. Si se definían unos métodos que la clase tenía que implementar, lógicamente tenían que ser accesibles. Como ahora ya no es obligatorio que esos métodos estén todos implementados.

Nuevos modificadores de las interfaces para C#8

Precisamente de lo que hemos visto anteriormente el siguiente paso natural es que una interfaz no sea completamente pública, puede que necesitemos cierta lógica no pública para poder manejar esos casos por defecto. Es por eso que ahora los modificadores de disponibles son:

Con ellos vamos a poder aplicar la lógica necesaria para poder generar funcionamiento por defecto para las interfaces. Puedes ver un ejemplo en que ofrece el equipo de dotnet directamente en github.

¿Cómo se comporta este funcionamiento por defecto?

En primer lugar, toda la implementación está en la propia interfaz y no en la clase que la implementa. Estas implementaciones por defecto no son un tipo de herencia múltiple encubierta. Es decir, si la interfaz tiene una implementación por defecto de un método que la clase no tiene, solo será accesible desde la interfaz y no desde la clase. De hecho, no es una multiherencia ni si quiera a nivel de interfaz, si por ejemplo heredamos nuestra interfaz de dos o más que si tienen el mismo método por defecto, vamos a necesitar especificar cual exactamente queremos utilizar o se generará un error de ambigüedad. Por ejemplo:

public interface IVehiculoTerrestre
{
    void Avanzar() => Console.WriteLine("Avanzar por tierra");
}

public interface IVehiculoNaval
{
    void Avanzar() => Console.WriteLine("Avanzar por mar");
}

public interface IVehiculoHibrido : IVehiculoTerrestre, IVehiculoNaval
{
}

public class VehiculoHibrido : IVehiculoHibrido
{
}


IVehiculoHibrido vehiculo = new VehiculoHibrido();

vehiculo.Avanzar(); //Error de ambigüedad. No compila
((IVehiculoTerrestre)vehiculo).Avanzar(); //Avanzar por tierra
((IVehiculoNaval)vehiculo).Avanzar(); //Avanzar por mar

Y… ¿Dónde está el problema entonces?

De lo que hemos visto más arriba podemos sacar en claro que, aunque es una utilidad interesante y que va a permitir actualizar las interfaces sin miedo a romper un montón de código. El problema viene de que la línea que he separado históricamente las interfaces y las clases abstractas se ha difuminado tanto que se ha convertido en buenas prácticas que en una separación real de funciones. Precisamente por eso muchos opinan que es una característica que dificulta el lenguaje y lo hace confuso porque el lenguaje ya tenía una manera de resolver eso antes de las interfaces de C#8, las clases abstractas. Si alguien quería proveer una implementación por defecto era una clase abstracta lo que debía usar.

Otra de las razones de los detractores de este cambio es que al haber una implementación por defecto ya no hay errores de compilación si la interfaz que estas implementando en tu clase tiene métodos no implementados en la propia clase. Esto puede llevar a errores difíciles de detectar cuando trabajas con la abstracción que te da una interfaz… Imagina que tu método recibe una interfaz y cuando llamas a su método ‘x’ actúa una implementación por defecto sin que tú te estés dando cuenta… Dependiendo de la situación esto puede ser un gran problema.

Personalmente pienso que una característica como las interfaces de C#8 no es ni buena ni mala por si sola. Puede ser muy útil en determinados casos y provocar fallos muy graves en otros y es nuestra labor como desarrolladores conseguir que el mundo sea mejor.

La imagen muestra una escena de Spiderman donde el Tio Ben dice su famosa frase: "Un gran poder conlleva una gran responsabilidad"

¿Qué opinas tú de esta nueva característica? ¿Crees que mejora o que empeora el lenguaje? No dudes en dejar un comentario dando tu opinión sobre el tema.

4 comentarios en «Novedades de C#8: Interfaces. ¿Qué podemos esperar?»

  1. Que te pueda ahorrar escribir código, puede.
    Que huele a problemas de herencia y comportamientos inesperados, también.
    No soy partidario de añadir funcionalidades a un lenguaje solo porque se pueda hacer, prefiero que limite posibilidades para evitar problemas futuros

    Responder
    • Buenas Jesús,
      Muchas gracias por tu comentario!
      La verdad es que yo todas estas cosas las tomo siempre con mucha cautela. En este caso creo puede ser bastante peligroso si ahora las interfaces valen para todo… Pero como característica me parece muy interesante la verdad.

      Responder
  2. «Imagina que tu método recibe una interfaz y cuando llamas a su método ‘x’ actúa una implementación por defecto sin que tú te estés dando cuenta…»

    ¿No se supone que si la clase re-implementa un método de la interfaz, el compilador es suficientemente inteligente para llamar a la versión de la clase y no la de la interfaz?

    «Precisamente por eso muchos opinan que es una característica que dificulta el lenguaje y lo hace confuso porque el lenguaje ya tenía una manera de resolver eso antes de las interfaces de C#8, las clases abstractas.»

    Solo existe herencia de un solo tipo, pero puedes tener múltiples interfaces. Por otro lado, desde hace ya bastante tiempo que se viene promoviendo la «composición por sobre la herencia», las clases abstractas tienen su uso pero no son la solución a todo.

    Responder
    • Buenas lrhlpz,
      Gracias por tu comentario!
      En respuesta a tus pregunta sí, el hecho de implementar la interfaz en la clase invalida la implementación por defecto. Esta implementación por defecto solo afecta si ampliar la interfaz y no la clase.
      Estoy de acuerdo contigo en la que las clases abstractas no son la solución a todo, de hecho yo prefiero el uso de interfaces incluso cuando hay una clase abstracta de por medio, ya que creo que aporta flexibilidad a la hora del diseño.

      Un abrazo!

      Responder

Deja un comentario