1. Dal "contratto puro" all’architettura flessibile
Prima di C# 8 un interface era come un contratto rigido: vuoi implementare un interface? Devi implementare tutto, fino all’ultima virgola. Se venivano aggiunti nuovi membri, tutte le implementazioni esistenti dovevano subito aggiungerli, altrimenti il compilatore non ti lasciava compilare il progetto.
Ma la vita è più complicata. Immagina che tu mantieni una libreria usata da centinaia di progetti, e all’improvviso ti serve aggiungere un nuovo metodo all’interfaccia. Non vuoi rompere la retrocompatibilità? Ecco che entrano in scena i metodi di default (Default Interface Methods, DIM)!
Qual è il punto?
I metodi di default ti permettono di dichiarare una implementazione del metodo direttamente nell’interfaccia. Ora il contratto diventa più flessibile: se una classe non implementa la “novità”, verrà usata l’implementazione di default. È come una controfigura nei film: se l’attore non vuole saltare dal ponte, lo fa lo stuntman.
2. Sintassi dei Default Interface Methods
Come dichiarare metodi con implementazione nell’interfaccia?
È molto simile ai metodi normali, solo che ora il corpo del metodo si può (e si deve) scrivere direttamente nell’interfaccia:
public interface ILogger
{
void Log(string message);
// Nuovo metodo con implementazione di default!
void LogWarning(string message)
{
Log("[WARNING] " + message);
}
}
Qui LogWarning ha già una sua implementazione! Qualsiasi classe che implementa ILogger deve solo implementare Log, mentre LogWarning avrà la versione di default (a meno che non venga sovrascritta).
Confronta: firma classica vs. moderna
| Versione | Dichiarazione nell’interfaccia |
|---|---|
| Prima di C# 8 | |
| C# 8 e successivi | |
Dettagli importanti della sintassi
- Per un metodo con implementazione devi scrivere il corpo tra parentesi graffe.
- I metodi di default non possono essere abstract.
- Tutti i metodi dell’interfaccia sono comunque implicitamente public.
- Puoi dichiarare anche proprietà con get/set di default (vedi sotto).
3. Esempi pratici
Esempio 1. Garantire la retrocompatibilità
Supponiamo che nella tua app ci sia un’interfaccia per salvare dati:
public interface ISaveable
{
void Save(string filePath);
}
Poi decidi di aggiungere il salvataggio su cloud. Non vuoi modificare cento classi? Aggiungi un metodo di default!
public interface ISaveable
{
void Save(string filePath);
// Nuovo metodo con implementazione "di default"!
void SaveToCloud(string cloudService)
{
Console.WriteLine($"Salvo su cloud {cloudService} (di default — non faccio nulla)");
}
}
Ora tutte le vecchie classi “sanno” già salvare su cloud (anche se per ora solo stampano un messaggio).
Esempio 2. Estendere l’interfaccia del logger
Prima avevamo una semplice interfaccia di logging:
public interface ILogger
{
void Log(string message);
}
Aggiungiamo un metodo di default per il logging degli errori:
public interface ILogger
{
void Log(string message);
void LogError(string message)
{
Log("[ERROR] " + message);
}
}
La classe che implementa ILogger può non implementare LogError — funzionerà la versione di default:
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
// Non implementiamo LogError — userà la versione di default!
}
ILogger logger = new ConsoleLogger();
logger.Log("Tutto ok!");
logger.LogError("Ops, qualcosa è andato storto!"); // Chiama la versione di default
Esempio 3. Metodi di default + app estendibile
La tua app supporta diversi tipi di export: su file, su DB, su rete. Interfaccia:
public interface IExporter
{
void Export(string data, string destination);
// Nuova funzionalità — export in archivio
void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("Archiviazione di default non supportata.");
}
}
I plugin scritti da altri sviluppatori continueranno a funzionare anche se non sanno nulla del nuovo metodo.
4. Come funziona la chiamata dei metodi di default dell’interfaccia?
Scenario "classe vecchia — interfaccia nuova"
Se una classe non implementa il metodo di default dell’interfaccia, quando lo chiami tramite riferimento all’interfaccia viene usata l’implementazione dell’interfaccia. Se lo implementa — viene usata la sua.
public class FileExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("Salvo su file...");
}
// Non implementiamo ExportToArchive — userà la versione di default
}
IExporter exporter = new FileExporter();
exporter.Export("dati", "file.txt"); // Usa l’implementazione di FileExporter
exporter.ExportToArchive("dati", "file.zip"); // Usa la versione di default!
Scenario "la classe sovrascrive il metodo di default"
public class AdvancedExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("Salvo in modalità avanzata...");
}
public void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("Archiviazione supportata!");
}
}
IExporter exporter = new AdvancedExporter();
exporter.ExportToArchive("dati", "file.zip"); // Ora chiama l’implementazione della classe!
5. Cos’altro si può fare con i Default Interface Methods?
Proprietà ed eventi di default
Puoi dichiarare proprietà con implementazione di default, se hanno il corpo get o set:
public interface IHasId
{
// Restituisce automaticamente 42, finché non viene sovrascritta
int Id => 42;
}
public class Person : IHasId {}
Console.WriteLine(new Person().Id); // 42
Chiamare metodi di default dal codice dell’interfaccia
Dentro l’interfaccia i metodi di default e gli altri membri possono chiamarsi tra loro:
public interface IDemo
{
void Foo() => Bar();
void Bar() => Console.WriteLine("BAR");
}
6. Limitazioni e particolarità dei Default Interface Methods
Si possono dichiarare campi, costruttori?
No. Anche con i metodi di default, un’interfaccia non è una classe. Non puoi avere campi, costruttori o distruttori.
Si può usare base con un’interfaccia?
Sì, ma con delle particolarità. Dentro un metodo di default dell’interfaccia puoi chiamare il metodo dell’interfaccia base tramite specifica esplicita:
public interface IBase
{
void Greet() => Console.WriteLine("Hello from IBase");
}
public interface IDerived : IBase
{
void IBase.Greet()
{
Console.WriteLine("Ciao da IDerived!");
IBase.Greet(this); // Chiamiamo il metodo dell’interfaccia base esplicitamente
}
}
Ma serve raramente nei casi base.
Cosa succede in caso di conflitto tra implementazioni di default?
public interface IA { void Foo() { Console.WriteLine("A"); } }
public interface IB { void Foo() { Console.WriteLine("B"); } }
// La classe non implementa Foo esplicitamente:
public class C : IA, IB { }
// Errore di compilazione: non è chiaro quale implementazione scegliere!
7. Errori tipici, limitazioni e particolarità
Errore tipico divertente:
Alcuni provano a dichiarare campi nell’interfaccia dopo aver scoperto i DIM — ma i campi ancora non si possono. Inoltre, se provi a implementare un metodo statico di default — prima di C# 8 non si può. (Dalla versione 8 puoi avere metodi statici negli interface, ma le implementazioni di default per i metodi statici sono un altro discorso.)
Particolarità: Diamond Problem ("problema del diamante")
Se la tua classe implementa due interfacce con lo stesso metodo di default, devi implementare esplicitamente quel metodo:
public class ConflictClass : IA, IB
{
public void Foo() // devi scegliere tu quale versione!
{
// Chiamata esplicita all’implementazione desiderata tramite l’interfaccia (se serve)
((IA)this).Foo();
// oppure
((IB)this).Foo();
}
}
Non esagerare!
I metodi di default salvano la retrocompatibilità, ma se ne abusi puoi ottenere un’architettura “sporca”, dove parte della logica si sparge negli interface. Cerca di tenere tutto ciò che è importante — nelle classi, e usa l’interfaccia davvero come “contratto”.
GO TO FULL VERSION