CodeGym /Corsi /C# SELF /Confronto tra interfacce e classi astratte

Confronto tra interfacce e classi astratte

C# SELF
Livello 24 , Lezione 2
Disponibile

1. Brevemente sulle differenze classiche

Se qualcuno ti dice: «Un'interfaccia è solo un insieme di firme», chiedi: «Ma con quale versione di C# stai lavorando?» A partire da C# 8 e versioni successive, le interfacce sono diventate molto più potenti. È ora di confrontarle con le classi astratte — non solo per le proprietà classiche, ma anche considerando tutte le nuove chicche della piattaforma .NET.

Se torniamo indietro di qualche anno — a C# 7, — era tutto semplice. Una classe astratta può definire campi e metodi parzialmente implementati, un'interfaccia — solo firme (di metodi, proprietà, eventi, indicizzatori).
L'ereditarietà di una classe astratta si realizza tramite la relazione "è un" (Is-a), le interfacce realizzano l'ereditarietà multipla del comportamento ("sa fare", can-do).

Caratteristica Classe astratta Interfaccia (fino a C# 8)
Relazione is-a can-do
Ereditarietà Solo una Multipla
Campi Può contenere Non può
Implementazione dei metodi Può Non può
Costruttori Può Non può
Modificatori di accesso Diversi (public, protected, ...) Solo implicitamente public

Come vedi, prima le classi astratte erano davvero i "fratelli maggiori" delle interfacce — più potenti e flessibili. Ma tutto cambia!

2. Interfacce con implementazione di default

Con l'arrivo di C# 8 (e ancora di più in C# 14 e .NET 9) le interfacce hanno ottenuto una nuova superpotenza — metodi con implementazione di default, chiamati "Default Interface Methods" (DIM).

Come appare?


public interface IAnimal
{
    void SayHello();

    // Metodo con implementazione di default!
    void Walk()
    {
        Console.WriteLine("Sto camminando...");
    }
}

Wow! All'improvviso si è scoperto che l'interfaccia ora può contenere l'implementazione di un metodo. E non solo uno, ma quanti vuoi. Ma c'è una cosa: questi metodi devono essere dichiarati esplicitamente con il corpo, e tutto il resto (campi, metodi privati, costruttori) — ancora non si può.

3. Funzionalità moderne delle interfacce

Nuove chicche che ogni sviluppatore .NET moderno dovrebbe conoscere:

  • Metodi con implementazione di default.
  • Metodi privati dentro l'interfaccia (solo per scopi di supporto, accessibili solo da altri metodi della stessa interfaccia).
  • Metodi statici (da C# 8).
  • Proprietà con implementazione di default.
  • Campi statici (da C# 14 — "static interface members").
  • Membri statici astratti ("abstract static members" — sì, ora l'interfaccia può richiedere certe static methods nell'implementazione!).

Esempio di interfaccia moderna completa:


public interface ILogger
{
    static int LoggerCount { get; set; } // C# 14

    void Log(string message); // Firma (contratto)

    // Default implementation
    void LogWarning(string warning)
    {
        Log("[WARNING]: " + warning);
    }
    
    // Metodo privato di supporto nell'interfaccia (C# 8+)
    private void FormatAndLog(string level, string msg)
    {
        Log($"{level}: {msg}");
    }

    // Metodo statico nell'interfaccia (C# 8+)
    static void PrintLoggerInfo()
    {
        Console.WriteLine("Interfaccia ILogger — il tuo miglior aiutante!");
    }
}

Immagina solo — prima era semplicemente impossibile, è come se qualcuno permettesse a un gatto di fare il guardiano del server.

4. Classi astratte: cosa c'è di nuovo?

Le classi astratte... come dire... non sono molto evolute nell'ultimo decennio. Possono ancora contenere:

  • Campi (anche privati, protetti e statici).
  • Metodi implementati e astratti.
  • Costruttori (sì, puoi creare classi astratte con logica di inizializzazione).
  • Proprietà, eventi, indicizzatori.
  • Membri statici e di istanza.

Esempio di classe astratta:


public abstract class Animal
{
    public string Name { get; set; }

    public abstract void Speak();

    public virtual void Walk()
    {
        Console.WriteLine($"{Name} cammina sulle zampette!");
    }

    protected void Eat()
    {
        Console.WriteLine($"{Name} mangia il cibo.");
    }
}

La classe astratta rimane ancora un ottimo posto per conservare logica comune, stato e comportamento per una gerarchia di classi.

5. Confronto moderno: tabella con le nuove funzionalità

Caratteristica Classe astratta Interfaccia (C# 14+, .NET 9)
Relazione is-a (è un) can-do (sa fare)
Ereditarietà Solo una Multipla
Campi Sì, qualsiasi Solo statici* (C# 14+)
Costruttori No
Implementazione dei metodi Sì (virtual/abstract) Sì (default, static, abstract static)
Proprietà con implementazione Sì (default implementation)
Membri privati Sì (solo metodi, C# 8+)
Membri statici Sì (C# 8+, con limitazioni)
Campi statici Sì * (C# 14+)
Modificatori di accesso Qualsiasi Di default public o private

* — Nelle interfacce i campi statici sono usati di solito in casi particolari, ed è una feature molto recente del linguaggio.

6. Dove usare un'interfaccia e dove una classe astratta: consigli moderni

Le interfacce (e ora anche con implementazione di default) sono uno strumento per creare contratti tra componenti. La loro caratteristica chiave: molteplicità. La tua classe può implementare anche dieci interfacce diverse, rendendola un vero soldato universale.

La classe astratta rimane la tua scelta se:

  • Serve uno stato comune (campi), logica e comportamento che vengono ereditati da altre classi.
  • Hai bisogno di logica standard ma sovrascrivibile (usa virtual).
  • Vuoi centralizzare l'inizializzazione tramite il costruttore.

Nei progetti reali spesso si trova questo schema: i "contratti puri" si formulano nelle interfacce, e se serve codice comune o infrastruttura per le sottoclassi — si crea una classe base astratta.


.                     ┌────────────────────────┐
                      │      Interfaccia       │
                      │  (contratto: cosa sa)  │
                      └─────────┬──────────────┘
                                │
                 ┌──────────────┼──────────────┐
                 │              │              │
           Implementazione 1   Implementazione 2 ...  Implementazione N
             MyLogger         CloudLogger         FileLogger
    
        (puoi combinare con ereditarietà da classe astratta)
Schema: dove e cosa usare

7. Scenari — quando vince cosa

Implementazione multipla:
Supponiamo che tu abbia un'interfaccia IDrivable e una classe astratta Vehicle. Ora la classe Car può ereditare la base — Vehicle — e allo stesso tempo implementare più interfacce (IDrivable, IRepairable, IInsurable). Se avessi una classe astratta Repairable, dovresti scegliere — o Vehicle, o Repairable! Qui le interfacce vincono chiaramente.

Logica e stato comuni:
Supponiamo che tutti i "veicoli" abbiano un campo "numero". Questo dovrebbe essere un campo della classe astratta. Nell'interfaccia i campi (almeno non statici) non si possono avere.

Evoluzione delle API:
Una delle storie rivoluzionarie con i Default Interface Methods — ora puoi evolvere le interfacce senza rischiare di rompere i consumatori esistenti.
Per esempio, hai aggiunto un nuovo metodo con implementazione di default nell'interfaccia — tutto funziona, tutte le vecchie implementazioni dell'interfaccia non si rompono! Prima era doloroso (o, anche dimenticando il dolore, comunque impossibile).

8. Esempi pratici

Nella nostra app didattica stiamo aggiungendo gradualmente il logging. Creiamo la nostra interfaccia ILogger con implementazione di default:


public interface ILogger
{
    void Log(string message);

    // L'implementazione di default è disponibile a tutte le implementazioni dell'interfaccia!
    void LogInfo(string info)
    {
        Log("[INFO] " + info);
    }

    // Metodo statico dell'interfaccia
    static void PrintHelp()
    {
        Console.WriteLine("Usa ILogger per loggare gli eventi");
    }
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

// Da qualche parte nel codice:
ILogger logger = new ConsoleLogger();
logger.LogInfo("Sistema avviato!"); // funziona grazie all'implementazione di default

// Chiamata al metodo statico dell'interfaccia
ILogger.PrintHelp();

Se aggiungessimo un nuovo metodo con implementazione di default nell'interfaccia, tutte le implementazioni esistenti (ad esempio, ConsoleLogger) riceverebbero automaticamente questo nuovo metodo — niente panico e nessun codice rotto.

9. Errori e sfumature: pratica e trappole

Sappi che non è tutto rose e fiori come sembra a prima vista. Ad esempio, se la tua interfaccia contiene un'implementazione di default, ma il consumatore accede all'oggetto tramite il tipo della classe, e non tramite il tipo dell'interfaccia, l'implementazione di default è disponibile solo tramite l'interfaccia.


ConsoleLogger log = new ConsoleLogger();
log.LogInfo("Hello"); // Non compila: LogInfo non è definito nella classe!

ILogger log2 = log;
log2.LogInfo("Hello"); // Tutto ok!

Questo assomiglia a un caso particolare di implementazione esplicita dell'interfaccia. A volte questo comportamento è comodo per nascondere API "inutili", a volte — sorprende i principianti.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION