Una interfaz es un contrato: una lista con nombre de miembros que un tipo promete proporcionar. Dice lo que un tipo sabe hacer y nada de cómo. Las clases y los structs implementan interfaces, y el código escrito contra la interfaz funciona con todas las implementaciones, incluidas las que se escriban más tarde.
Declarar e implementar una interfaz
Una interfaz declara miembros sin cuerpo. Por convención, su nombre empieza por I. Una clase la implementa indicándola después de dos puntos y proporcionando un miembro público por cada uno:
Salida:
15% off: 68
10 off: 70
Best for 80: 68
Best for 40: 30
BestPrice no sabe nada de PercentOff ni de FixedOff. Solo depende de IDiscount, así que añadir más tarde una clase BuyOneGetOne no exige ningún cambio en él. Ese desacoplamiento es toda la razón de ser de las interfaces.
Algunas reglas de la declaración:
- Los miembros de una interfaz son públicos por defecto, y los miembros que los implementan también deben ser públicos (salvo que se implementen de forma explícita, más abajo).
- Una interfaz puede declarar métodos, propiedades, indexadores y eventos. No puede declarar campos de instancia ni constructores.
- Una clase que omite un miembro no compila: error CS0535, "'FixedOff' does not implement interface member 'IDiscount.Apply(decimal)'".
- No puedes crear una interfaz con
new. Creas una clase que la implementa y puedes guardarla en una variable del tipo de la interfaz:IDiscount d = new FixedOff(5);.
La interfaz como tipo
Una variable, un parámetro, un campo o un tipo de retorno pueden declararse como una interfaz. Así aceptan cualquier objeto que la implemente, y solo exponen los miembros de la interfaz. Es la forma de escribir código al que no le importa qué implementación recibe, y la forma en que las pruebas meten una falsa:
public class Checkout
{
private readonly IPaymentGateway gateway; // not StripeGateway, not PayPalGateway
public Checkout(IPaymentGateway gateway) { this.gateway = gateway; }
public bool Pay(decimal amount) => gateway.Charge(amount);
}
// Production: new Checkout(new StripeGateway(apiKey))
// Unit test: new Checkout(new FakeGateway(alwaysSucceeds: true))
Los contenedores de inyección de dependencias de ASP.NET Core se basan en esto: los servicios se registran y se piden por interfaz.
Implementar varias interfaces
Una clase tiene como mucho una clase base, pero puede implementar cualquier número de interfaces. Primero la clase base, después las interfaces, separadas por comas:
Salida:
INVOICE Studio rent March: 950.00
True
True
Las interfaces también pueden heredar de otras interfaces: interface IRepository<T> : IReadRepository<T> añade miembros a los que hereda, y una clase que implemente IRepository<T> debe proporcionar los dos conjuntos.
Implementación explícita de interfaces
A veces dos interfaces declaran un miembro con el mismo nombre y significados distintos, o un miembro de interfaz no tiene sentido en la superficie pública de la propia clase. Impleméntalo de forma explícita poniendo delante el nombre de la interfaz y omitiendo el modificador de acceso:
Salida:
Desk lamp,34.90
Desk lamp for 34.90
Un miembro implementado de forma explícita solo es accesible a través de la interfaz. El framework lo usa para no ensuciar los tipos habituales: los arrays implementan IList.Add de forma explícita (lanza NotSupportedException, porque los arrays tienen un tamaño fijo), así que el método no aparece cuando escribes array..
Interfaces del framework que implementarás
Implementar una interfaz estándar conecta tu tipo con el código existente del framework. Hay tres que aparecen constantemente.
IComparable<T> da a un tipo un orden natural, que usan List<T>.Sort(), Array.Sort y Max(). CompareTo devuelve un número negativo, cero o un número positivo:
Salida:
1.4, 2.9, 2.10, 10.0
Ordenar las mismas versiones como strings daría 1.4, 10.0, 2.10, 2.9. Sin IComparable<T>, Sort() lanza InvalidOperationException porque no tiene forma de comparar dos objetos Version.
IEnumerable<T> hace que un tipo pueda usarse en foreach y con LINQ. La forma fácil de implementar GetEnumerator es con yield return, lo que se explica en la página de IEnumerable y yield.
IDisposable marca un tipo que contiene algo que debe liberarse (un descriptor de archivo, una conexión, un temporizador). Su único método, Dispose, es lo que llama la sentencia using cuando termina el bloque, aunque se lance una excepción:
Salida:
open sales.txt
write to sales.txt: March total: 12400
close sales.txt
after using
Métodos de interfaz por defecto (C# 8)
Añadir un miembro a una interfaz publicada rompía antes todas las clases que la implementaban. Desde C# 8, un miembro de una interfaz puede tener cuerpo, que las clases que la implementan heredan salvo que aporten el suyo:
public interface ILogger
{
void Write(string message);
// New in version 2 of the library. Existing implementers keep compiling.
void Error(string message) => Write("ERROR: " + message);
}
public class ConsoleLogger : ILogger
{
public void Write(string message) => Console.WriteLine(message);
}
ILogger log = new ConsoleLogger();
log.Error("disk full"); // ERROR: disk full
var direct = new ConsoleLogger();
// direct.Error("x"); // does not compile: the default method belongs to the interface
La última línea es la parte que sorprende: un método por defecto no se hereda en los miembros propios de la clase, así que solo puede llamarse a través del tipo de la interfaz. Los métodos por defecto tampoco pueden usar campos de instancia (las interfaces no tienen); trabajan a través de los demás miembros de la interfaz. Requieren .NET Core 3.0 o posterior y no están disponibles en .NET Framework.
C# 8 también permitió miembros static en las interfaces, y C# 11 añadió los miembros static abstract, que permiten que el código genérico llame a un método static o a un operador sobre T. Sobre eso se construyen las matemáticas genéricas de .NET 7 (INumber<T>, where T : INumber<T>).
¿Interfaz o clase abstracta?
Usa una interfaz para una capacidad que pueden compartir tipos sin relación, cuando los structs deban participar o cuando una clase necesite varios de esos papeles. Usa una clase abstracta cuando las implementaciones sean variaciones de una misma cosa y compartan estado o un algoritmo fijo. La página de clases abstractas tiene una tabla comparativa. Muchos diseños usan las dos: una interfaz para los consumidores y una clase base abstracta que implementa las partes aburridas para los que implementan.
Errores comunes
- Implementación que falta o no es pública. Cada miembro de la interfaz necesita un miembro público con una firma que coincida, o una implementación explícita. Si no, CS0535 (o CS0737 cuando el método existe pero no es público).
- Interfaces con una sola implementación "por si acaso". Una interfaz se gana su sitio cuando hay varias implementaciones o una falsa para las pruebas. Si no, es un archivo más por el que navegar.
- Interfaces enormes. Una interfaz con veinte miembros obliga a cada implementación a escribir veinte miembros. Divídela en papeles más pequeños (
IReader,IWriter) que las clases combinan. - Convertir una interfaz de vuelta a una clase.
((StripeGateway)gateway).Refund()deshace el desacoplamiento. Si quienes llaman necesitanRefund, debe estar en la interfaz. - Esperar llamar a un método de interfaz por defecto sobre la clase. Se llega a él a través del tipo de la interfaz.
Preguntas frecuentes
¿Qué es una interfaz en C#?
Una interfaz es un conjunto con nombre de miembros (métodos, propiedades, eventos, indexadores) que un tipo promete proporcionar, sin estado de instancia. interface IDiscount { decimal Apply(decimal price); } dice "todo lo que sea un IDiscount puede aplicarse a un precio". Las clases y los structs la implementan con dos puntos, y el código puede trabajar después con cualquiera de ellos a través del tipo de la interfaz.
¿Cómo implemento una interfaz en C#?
Indícala después de dos puntos en la declaración de la clase (después de la clase base, si la hay) y proporciona un miembro público por cada miembro de la interfaz: class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }. Un miembro que falta es el error CS0535. Visual Studio y Rider pueden generar los esqueletos con una corrección rápida.
¿Puede una clase de C# implementar varias interfaces?
Sí, tantas como haga falta, separadas por comas: class Invoice : IPrintable, IComparable<Invoice>, IDisposable. Es la respuesta de C# a la herencia múltiple: una clase tiene una clase base pero puede desempeñar muchos papeles. Si dos interfaces declaran el mismo miembro, un solo método público puede satisfacer a las dos, o puedes usar la implementación explícita para dar a cada una el suyo.
¿Qué es la implementación explícita de interfaces en C#?
Implementar un miembro con el nombre de la interfaz delante y sin modificador de acceso: string IExportable.Format() { ... }. Ese miembro solo puede llamarse a través de una variable del tipo de la interfaz, no a través de la clase. Se usa para resolver choques de nombres entre dos interfaces y para mantener fuera de la superficie pública de la clase los miembros de interfaz que rara vez se usan.
¿Qué son los métodos de interfaz por defecto en C#?
Desde C# 8, un miembro de una interfaz puede tener cuerpo: void Error(string m) => Write("ERROR: " + m);. Las clases que la implementan lo reciben gratis y pueden aportar el suyo. La característica existe para que los autores de bibliotecas puedan añadir miembros a una interfaz publicada sin romper a todos los que la implementan. A un método por defecto se llega a través del tipo de la interfaz, no a través de la clase.
¿Por qué los nombres de las interfaces de C# empiezan por I?
Es la convención de nombres de .NET (IEnumerable, IDisposable, IComparable<T>): una I mayúscula seguida de un nombre en PascalCase, a menudo un adjetivo terminado en "-able". El compilador no lo exige, pero seguirla hace que los tipos de interfaz se reconozcan de un vistazo, sobre todo en la declaración de una clase, donde la clase base y las interfaces comparten una misma lista.