CodeGym /Corsi /C# SELF /Metodi e proprietà astratti

Metodi e proprietà astratti

C# SELF
Livello 22 , Lezione 2
Disponibile

1. Metodi astratti

Nella lezione precedente abbiamo gettato le basi: abbiamo conosciuto le classi astratte e visto che possono contenere metodi e proprietà astratti. Abbiamo capito che questo è un "contratto" che obbliga le classi derivate a fornire la propria implementazione.

Ora andiamo più a fondo nei dettagli e vediamo scenari più complessi e pratici. In questa lezione ci concentriamo sulle sfumature della sintassi, su come funziona l'astrazione nelle gerarchie multilivello e distinguiamo chiaramente quando usare abstract e quando — virtual.

Un metodo astratto è come una ricetta misteriosa da un vecchio libro di cucina: "aggiungi un ingrediente segreto" — quale, non si sa, ma tutti i cuochi successivi devono inventarsi qualcosa!

Un metodo astratto sta sempre in una classe astratta

Se provi a dichiarare un metodo astratto in una classe normale (non astratta), otterrai un errore di compilazione. Perché? Perché una classe normale si può istanziare direttamente, e cosa succede se provi a chiamare un metodo non descritto? Solo uno sguardo perplesso dell'IDE.


// Questo genera un errore di compilazione:
public class WrongClass
{
    public abstract void Oops(); // Non si può fare!
}

Se una classe contiene almeno un metodo astratto, deve essere marcata come abstract!

Implementazione dei metodi astratti nelle classi derivate

L'implementazione si fa con la parola chiave override. Una classe che eredita da una classe astratta e non implementa tutti i metodi astratti deve anch'essa essere astratta (altrimenti il compilatore si arrabbia).

Continuiamo con l'idea della nostra app:

Nelle lezioni precedenti abbiamo creato la classe Shape (figura), dove abbiamo dichiarato un metodo astratto per calcolare l'area. Ora creiamo figure concrete:


public class Rectangle : Shape
{
    public double Width { get; }
    public double Height { get; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double CalculateArea()
    {
        return Width * Height;
    }
}

public class Circle : Shape
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

Perché i metodi astratti sono così importanti?

I metodi astratti sono il tuo modo di dire: "Ragazzi, se ereditate questa classe, decidete voi come deve funzionare!" Questo rende l'architettura del programma più chiara, protegge da dimenticanze accidentali e, soprattutto, permette di usare il polimorfismo al massimo.

2. Proprietà astratte (properties)

Cos'è una proprietà astratta

Una proprietà astratta è un contratto proprio come un metodo astratto, solo che riguarda una proprietà (property). In C# è molto naturale, perché le proprietà sono uno dei modi principali per incapsulare i dati. Una proprietà astratta dice: "Lascia che i figli decidano da soli dove prendere (e magari come impostare) il valore di questa proprietà".

Esempio:
Aggiungiamo la proprietà Name alla figura — ogni figlio deve averla, ma ognuno decide cosa restituire.


public abstract class Shape
{
    public abstract string Name { get; }

    public abstract double CalculateArea();
}

Ora ogni figlio deve implementare questa proprietà:


public class Rectangle : Shape
{
    public double Width { get; }
    public double Height { get; }
    public override string Name => "Rettangolo";

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public override double CalculateArea()
    {
        return Width * Height;
    }
}

public class Circle : Shape
{
    public double Radius { get; }
    public override string Name => "Cerchio";

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

Particolarità della sintassi

Una proprietà astratta è simile a una di un'interfaccia: si dichiara senza corpo, ma specificando se avrà solo il getter o anche il setter.


public abstract class Creature
{
    // proprietà sola lettura
    public abstract string Species { get; }

    // proprietà lettura-scrittura
    public abstract int Age { get; set; }
}

Nella classe che implementa puoi definire la logica per ottenere (e se serve — modificare) il valore.

Perché servono le proprietà astratte

Una proprietà astratta è uno strumento top quando vuoi che ogni figlio decida come ottenere o memorizzare il valore. Serve spesso nella modellazione, nel lavoro con business model, view model, e in generale dove la logica della proprietà è più complessa di un semplice ritorno di campo.

Per esempio, se una figura calcola il nome con una formula e un'altra lo restituisce come costante, puoi "nascondere" tutto dietro una proprietà astratta.

3. Schema: come sono collegati classe astratta, metodi e proprietà

Dai un'occhiata a questo schema per vedere come funziona:


┌─────────────┐
│ abstract    │
│   Shape     │
│-------------│
│ +Name: str  │  <-- proprietà astratta
│ +Area(): dbl│  <-- metodo astratto
└─────┬───────┘
      │
 ┌────▼────┐      ┌───────┐
 │Rectangle│      │ Circle│
 │........ │ .... │ ......│
 │+Name    │      │+Name  │
 │+Area()  │      │+Area()│
 └─────────┘      └───────┘

Così garantiamo UNA sola interfaccia per lavorare con qualsiasi figlio della classe Shape, ma ogni figlio può fare "sotto il cofano" quello che vuole.

Diagramma UML per metodo e proprietà astratti:


┌────────────────────────────┐
│        abstract class      │
│           Animal           │
│────────────────────────────│
│+ Name: string {abstract}   │
│+ MakeSound(): void {abstract}│
└─────────────┬──────────────┘
              │
     ┌────────┴──────────┐
     │                   │
┌────────────┐     ┌─────────────┐
│    Cat     │     │    Dog      │
│────────────│     │─────────────│
│+ Name      │     │+ Name       │
│+ MakeSound()│    │+ MakeSound()│
└────────────┘     └─────────────┘

4. Scenari pratici e utilità nei progetti reali

Metodi e proprietà astratti sono il tuo strumento se stai progettando gerarchie di classi che devono evolvere. Sono la base per grandi applicazioni business, modelli multilivello di dominio, sistemi di plugin, UI framework, dove per le fondamenta del sistema si stabilisce una "legge" obbligatoria per tutti i figli.

Nei progetti veri queste soluzioni "pulite" permettono dopo mesi e anni di aggiungere nuove entità e funzioni senza paura, sapendo che il vecchio codice già tiene conto di tutti i comportamenti possibili.

La domanda "perché" e ritorno al polimorfismo

Il polimorfismo sembra magia, ma in realtà è solo un contratto: "puoi chiamare un certo metodo su qualsiasi oggetto della famiglia e sempre otterrai il risultato giusto". Metodi e proprietà astratti sono il modo per costringere tutta la gerarchia a parlare la stessa lingua, ma senza una realizzazione "di default" rigida.

Questo approccio si usa spesso nei sistemi di plugin (IDE, editor grafici, CRM), quando sviluppatori esterni creano le proprie estensioni ma devono implementare un set minimo di funzioni richieste dalla piattaforma. Per esempio, se scrivi un modulo per gestire file, la classe base può avere una proprietà astratta FileExtension e un metodo astratto Open(), così ogni plugin può gestire correttamente i file del suo tipo.

Differenze tra membri astratti, virtuali e normali della classe

Caratteristica Metodo/proprietà normale virtual abstract
Ha implementazione No
Override obbligatorio No No (puoi ridefinire) Sì (obbligatorio nel figlio)
Si può chiamare direttamente No
Puo' stare in una classe normale No
Puo' essere marcato sealed No No

5. Applicazione nella nostra app didattica

Ora, con queste nuove conoscenze, torniamo alla nostra app con le figure e vediamo come proprietà e metodi astratti lavorano insieme per creare un sistema flessibile e chiaro.


// Per esempio, nel metodo principale del programma
List<Shape> shapes = new List<Shape>
{
    new Rectangle(4, 5),
    new Circle(2.5)
};

foreach (Shape shape in shapes)
{
    // Grazie alla proprietà astratta Name e al metodo CalculateArea,
    // non ci interessa il tipo concreto della figura
    Console.WriteLine($"{shape.Name}, Area: {shape.CalculateArea():F2}");
}

Risultato:

Rettangolo, Area: 20.00
Cerchio, Area: 19.63

Che bello: il codice non sa e non si preoccupa di che figura si tratta — chiama solo le proprietà e i metodi giusti, e la CLR di .NET si occupa della giusta implementazione!

6. Errori frequenti e particolarità di implementazione

Errore n°1: metodo astratto del genitore non implementato.
Se in una classe derivata almeno un metodo o proprietà astratta resta senza implementazione, e la classe stessa non è marcata come abstract, il compilatore non ti fa compilare il progetto. È una protezione utile: non puoi creare un oggetto con logica "bucata" — tipo una figura senza il metodo CalculateArea().

Errore n°2: metodo dichiarato abstract, ma la classe no.
Anche questo codice non si compila. Se aggiungi un abstract-metodo in una classe, anche la classe deve essere astratta:


public abstract class Polygon : Shape
{
    // Non implementiamo CalculateArea(), la classe resta abstract
}

Errore n°3: tentativo di implementare un metodo astratto con virtual.
Un metodo astratto va implementato con la parola chiave override, non virtual. Però se vuoi che l'implementazione possa essere ridefinita più avanti nella gerarchia, puoi prima ridefinire il metodo e poi dichiararlo virtual nella classe figlia:


public override double CalculateArea()
{
    // Implementazione di default...
}

In un figlio più profondo puoi ridefinire di nuovo il metodo. Ma non puoi più renderlo abstract, perché ormai c'è già un'implementazione.

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