Ereditarietà in C#

Ereditarietà in C#

  • Definizione di nuove classi a partire da esistenti
  • Classe base e classe derivata
  • Polimorfismo e override

Motivazioni

  • L'ereditarietà, insieme all'incapsulamento e al polimorfismo, è una dei tre pilastri della programmazione orientata agli oggetti.
  • consente di creare nuove classi che riutilizzano, estendono e modificano il comportamento definito in altre classi.
  • Quando si definisce una classe da derivare da un'altra classe, la classe derivata ottiene tutti i membri della classe base, ad eccezione dei costruttori.
  • La classe derivata riutilizza il codice nella classe di base senza doverlo riscrivere.
  • È possibile aggiungere altri membri nella classe derivata.
  • La classe derivata quindi estende la funzionalità della classe base.

Esempio

class Auto
{
    private string _marca;
    private int    _vMax;

    public Auto(string marca, int vMax) { ... } // costruttore
    public void Avvia() { Console.WriteLine("Avvio motore"); }
    public void Ferma() { Console.WriteLine("Freno"); }
}

class Moto
{
    private string _marca;    // duplicato!
    private int    _vMax;     // duplicato!

    public Moto(string marca, int vMax) { ... }
    public void Avvia() { Console.WriteLine("Avvio motore"); } // duplicato!
    public void Ferma() { Console.WriteLine("Freno"); }        // duplicato!
}

La soluzione: la gerarchia di classi

  • Classe base (o superclasse): contiene i dati e i comportamenti comuni
  • Classe derivata (o sottoclasse): estende la classe base con caratteristiche specifiche
  • La derivata eredita automaticamente tutti i membri della base
  • Relazione "è un": una Moto è un Veicolo

Sintassi

// Classe BASE
class Veicolo
{
    // campi e metodi comuni a tutti i veicoli
}

// Classe DERIVATA: il simbolo ':' indica "estende"
class Auto : Veicolo
{
    // solo i dati e metodi SPECIFICI di Auto
}

class Moto : Veicolo
{
    // solo i dati e metodi SPECIFICI di Moto
}

In C# ogni classe può ereditare da una sola classe base (ereditarietà singola).

Classe Base e Derivata

La classe base Veicolo

class Veicolo
{
    private string _marca;
    private int    _velocitaMax;

    public string Marca       { get { return _marca; } }
    public int    VelocitaMax { get { return _velocitaMax; } }

    public Veicolo(string marca, int velocitaMax)
    {
        _marca       = marca;
        _velocitaMax = velocitaMax > 0 ? velocitaMax : 0;
    }

    public void Avvia()
    {
        Console.WriteLine($"{_marca}: motore avviato.");
    }

    public void Ferma()
    {
        Console.WriteLine($"{_marca}: veicolo fermo.");
    }

    public override string ToString()
    {
        return $"{_marca} (max {_velocitaMax} km/h)";
    }
}

La classe derivata Auto


class Auto : Veicolo
{
    private int _numeroPosti;

    public int NumeroPosti { get { return _numeroPosti; } }

    // Il costruttore della derivata chiama quello della base con 'base(...)'
    public Auto(string marca, int velocitaMax, int posti)
        : base(marca, velocitaMax)
    {
        _numeroPosti = posti > 0 ? posti : 2;
    }

    public void ApriCofano()
    {
        Console.WriteLine("Cofano aperto.");
    }

    public override string ToString()
    {
        return base.ToString() + $", {_numeroPosti} posti";
    }
}

La parola chiave base

  • base(...) nel costruttore: chiama il costruttore della classe base
  • base.Metodo(): chiama la versione del metodo definita nella classe base
  • Va usata prima di qualsiasi altra istruzione nel costruttore
  • Se non si scrive base(...), C# chiama automaticamente il costruttore senza parametri della base (errore se non esiste)

Usare la gerarchia

Veicolo v  = new Veicolo("Generico", 200);
Auto    a  = new Auto("Fiat", 180, 5);

v.Avvia();          // Generico: motore avviato.
a.Avvia();          // Fiat: motore avviato.   ← ereditato da Veicolo
a.ApriCofano();     // Cofano aperto.           ← specifico di Auto

Console.WriteLine(v);  // Generico (max 200 km/h)
Console.WriteLine(a);  // Fiat (max 180 km/h), 5 posti

Auto ha tutti i metodi di Veicolo più i propri.

Modificatori di Accesso e Ereditarietà

protected: visibile alle classi derivate

Modificatore Stessa classe Classe derivata Fuori dalla gerarchia
public
protected no
private no no

Usare protected per campi o metodi che devono essere accessibili alle classi derivate ma non all'esterno.

Esempio con protected

class Veicolo
{
    protected string _marca;   // accessibile nelle classi derivate
    private   int    _codiceInterno; // non accessibile nelle derivate

    public Veicolo(string marca)
    {
        _marca = marca;
    }
}

class Auto : Veicolo
{
    public Auto(string marca) : base(marca) { }

    public void Descrivi()
    {
        Console.WriteLine(_marca);        // OK: è protected
        // Console.WriteLine(_codiceInterno); // ERRORE: è private
    }
}

Polimorfismo

Cos'è il polimorfismo

  • Dal greco: molte forme
  • Stesso metodo, comportamento diverso a seconda del tipo reale dell'oggetto
  • In C# si realizza con virtual e override
  • Permette di scrivere codice generico che funziona con qualsiasi tipo della gerarchia

Esempio


class Veicolo
{
    public string Marca { get; }

    public Veicolo(string marca) { Marca = marca; }

    // virtual: le classi derivate POSSONO ridefinire questo metodo
    public virtual string Descrizione()
    {
        return $"Veicolo: {Marca}";
    }
}

class Auto : Veicolo
{
    public int NumeroPosti { get; }

    public Auto(string marca, int posti) : base(marca)
    {
        NumeroPosti = posti;
    }

    // override: ridefinisce il metodo virtuale della base
    public override string Descrizione()
    {
        return $"Auto: {Marca}, {NumeroPosti} posti";
    }
}

override con chiamata a base

class Camion : Veicolo
{
    public decimal PortataKg { get; }

    public Camion(string marca, decimal portata) : base(marca)
    {
        PortataKg = portata;
    }

    public override string Descrizione()
    {
        // Riusa la descrizione della base e aggiunge informazioni
        return base.Descrizione() + $" [portata: {PortataKg} kg]";
    }
}

base.Descrizione() chiama la versione di Veicolo, evitando di duplicare la logica.

Polimorfismo in azione

// Un array di Veicolo può contenere qualsiasi tipo derivato
Veicolo[] parco = {
    new Veicolo("Bici",   30),
    new Auto("Toyota",   160, 5),
    new Camion("Scania", 18000)
};

// Lo stesso codice funziona per tutti i tipi
foreach (Veicolo v in parco)
{
    Console.WriteLine(v.Descrizione());
}
Veicolo: Bici
Auto: Toyota, 5 posti
Veicolo: Scania [portata: 18000 kg]

Il principio di sostituzione di Liskov

  • Un oggetto di una classe derivata può sempre essere usato al posto di un oggetto della classe base
  • Veicolo v = new Auto("Fiat", 5); è sempre lecito
  • Il contrario non è vero: Auto a = new Veicolo(...); è un errore

Overloading dei metodi

Cos'è l'overloading

  • Possibilità di definire più metodi con lo stesso nome nella stessa classe
  • I metodi devono differire nella firma (numero, tipo o ordine dei parametri)
  • Il compilatore sceglie quale versione chiamare in base agli argomenti passati
  • È detto anche polimorfismo statico (la scelta avviene a tempo di compilazione)
  • A differenza di override, l'overloading non richiede ereditarietà
  • Permette di offrire la stessa operazione su dati diversi senza inventare nomi nuovi (SommaInt, SommaDouble, …)

Regole

Due metodi si considerano diversi se cambia almeno una di queste caratteristiche:

  • il numero dei parametri
  • il tipo dei parametri
  • l'ordine dei tipi dei parametri

Non è sufficiente, invece:

  • cambiare solo il tipo di ritorno
  • cambiare solo il nome dei parametri

Esempio: classe Calcolatrice

class Calcolatrice
{
    // 1) Somma di due interi
    public int Somma(int a, int b)
    {
        return a + b;
    }

    // 2) Somma di tre interi (diverso NUMERO di parametri)
    public int Somma(int a, int b, int c)
    {
        return a + b + c;
    }

    // 3) Somma di due double (diverso TIPO di parametri)
    public double Somma(double a, double b)
    {
        return a + b;
    }

    // 4) Somma di tutti gli elementi di un array
    public int Somma(int[] valori)
    {
        int totale = 0;
        foreach (int v in valori) totale += v;
        return totale;
    }
}

Utilizzo

Calcolatrice c = new Calcolatrice();

Console.WriteLine(c.Somma(2, 3));              // chiama la 1) → 5
Console.WriteLine(c.Somma(2, 3, 4));           // chiama la 2) → 9
Console.WriteLine(c.Somma(1.5, 2.5));          // chiama la 3) → 4
Console.WriteLine(c.Somma(new[] {1, 2, 3, 4})); // chiama la 4) → 10

Il compilatore individua la versione corretta osservando tipi e numero degli argomenti.

Overloading dei costruttori

Anche i costruttori possono essere sovraccaricati: è utile per fornire versioni con meno parametri e valori di default.

class Veicolo
{
    public string Marca       { get; }
    public int    VelocitaMax { get; }

    // Costruttore completo
    public Veicolo(string marca, int velocitaMax)
    {
        Marca       = marca;
        VelocitaMax = velocitaMax;
    }

    // Costruttore ridotto: delega al completo con 'this(...)'
    public Veicolo(string marca) : this(marca, 130) { }

    // Costruttore senza parametri
    public Veicolo() : this("Generico", 0) { }
}

this(...) evita di duplicare la logica di inizializzazione.

Overloading ed ereditarietà

  • Una classe derivata può aggiungere overload di un metodo della base
  • Gli overload della base restano comunque accessibili sull'oggetto derivato
class Stampante
{
    public void Stampa(string testo)   { Console.WriteLine(testo); }
}

class StampanteAvanzata : Stampante
{
    // nuovo overload: stessa "famiglia" di metodi, firma diversa
    public void Stampa(string testo, int copie)
    {
        for (int i = 0; i < copie; i++) Console.WriteLine(testo);
    }
}

StampanteAvanzata s = new StampanteAvanzata();
s.Stampa("ciao");       // ereditato dalla base
s.Stampa("ciao", 3);    // nuovo overload

Overloading vs Overriding

Aspetto Overloading Overriding
Firma diversa uguale
Richiede eredità no
Parole chiave nessuna virtual / override
Quando viene scelto a compile time a run time
Scopo stessa operazione, dati diversi specializzare comportamento

Classi Astratte

Il problema: la classe base "incompleta"

Talvolta la classe base rappresenta un concetto astratto che non ha senso istanziare direttamente:

// Ha senso creare un "Animale" generico?
Animale a = new Animale();   // ambiguo: che suono fa?

Meglio impedire la creazione diretta e obbligare le classi derivate a completare l'implementazione.

Classe astratta e metodo astratto

// 'abstract': non si può fare new Animale()
abstract class Animale
{
    public string Nome { get; }

    public Animale(string nome) { Nome = nome; }

    // 'abstract': nessuna implementazione qui,
    // le classi derivate DEVONO fare override
    public abstract string FaiVerso();

    // metodo normale: ereditato da tutti
    public void Presentati()
    {
        Console.WriteLine($"Sono {Nome} e dico: {FaiVerso()}");
    }
}

Classi derivate dalla classe astratta

class Cane : Animale
{
    public Cane(string nome) : base(nome) { }

    public override string FaiVerso()   // OBBLIGATORIO
    {
        return "Bau!";
    }
}

class Gatto : Animale
{
    public Gatto(string nome) : base(nome) { }

    public override string FaiVerso()   // OBBLIGATORIO
    {
        return "Miao!";
    }
}

Se una classe derivata non implementa tutti i metodi astratti, deve essere astratta a sua volta.

Uso della classe astratta

// Animale a = new Animale("x");  // ERRORE: classe astratta

Animale[] zoo = {
    new Cane("Rex"),
    new Gatto("Whiskers"),
    new Cane("Fido")
};

foreach (Animale a in zoo)
{
    a.Presentati();
}
Sono Rex e dico: Bau!
Sono Whiskers e dico: Miao!
Sono Fido e dico: Bau!

Esempio Completo

Gerarchia: Figura geometrica

abstract class Figura
{
    public string Colore { get; set; }

    public Figura(string colore)
    {
        Colore = colore;
    }

    public abstract double Area();
    public abstract double Perimetro();

    public override string ToString()
    {
        return $"{GetType().Name} ({Colore}): " +
               $"area={Area():F2}, perimetro={Perimetro():F2}";
    }
}

Cerchio e Rettangolo

class Cerchio : Figura
{
    public double Raggio { get; }

    public Cerchio(double raggio, string colore) : base(colore)
    {
        Raggio = raggio > 0 ? raggio : 0;
    }

    public override double Area()      => Math.PI * Raggio * Raggio;
    public override double Perimetro() => 2 * Math.PI * Raggio;
}

class Rettangolo : Figura
{
    public double Base   { get; }
    public double Altezza { get; }

    public Rettangolo(double b, double h, string colore) : base(colore)
    {
        Base    = b > 0 ? b : 0;
        Altezza = h > 0 ? h : 0;
    }

    public override double Area()      => Base * Altezza;
    public override double Perimetro() => 2 * (Base + Altezza);
}

Utilizzo della gerarchia

List<Figura> figure = new List<Figura>
{
    new Cerchio(5.0, "rosso"),
    new Rettangolo(4.0, 6.0, "blu"),
    new Cerchio(3.0, "verde")
};

foreach (Figura f in figure)
{
    Console.WriteLine(f);
}

// Area totale — funziona per qualsiasi Figura
double totale = 0;
foreach (Figura f in figure)
    totale += f.Area();

Console.WriteLine($"Area totale: {totale:F2}");
Cerchio (rosso): area=78.54, perimetro=31.42
Rettangolo (blu): area=24.00, perimetro=20.00
Cerchio (verde): area=28.27, perimetro=18.85
Area totale: 130.81

Concetti chiave

Concetto Sintassi C#
Classe derivata class B : A { ... }
Costruttore derivata public B(...) : base(...) { ... }
Membro protetto protected tipo _campo;
Concetto Sintassi C#
Metodo ridefinibile public virtual T Metodo() { ... }
Ridefinizione public override T Metodo() { ... }
Chiamata alla base base.Metodo()
Classe astratta abstract class A { ... }
Metodo astratto public abstract T Metodo();
Overload di metodo stesso nome, firma diversa
Concatena costruttori public B(...) : this(...) { ... }

Quando usare cosa

  • Ereditarietà: quando esiste una relazione "è un" chiara e stabile
  • protected: quando le classi derivate devono accedere allo stato interno
  • virtual / override: quando il comportamento varia tra i tipi della gerarchia
  • Classe astratta: quando la classe base non ha senso come oggetto autonomo
  • Overloading: quando la stessa operazione concettuale va offerta su dati diversi

Fine

Uno sguardo critico all'OOP: https://cscalfani.medium.com/goodbye-object-oriented-programming-a59cda4c0e53