IL POLIMORFISMO IN C#

C sharpIl polimorfismo in c sharp è il terzo pilastro della programmazione Object Oriented, Letteralmente vuol dire “avere molte forme” significa nella pratica che partendo da una classe base che ha un certo comportamento, questo comportamento lo possiamo ridefinire nelle sue classi derivate. Vediamo la teoria che sta alla base del polimorfismo con alcune immagini.

POLIMORFISMO IN C SHARP L’OVERRIDE

Prima forma override
Seconda forma override
Classi e metodi abstract

METODI VIRTUALI E OVERRIDE

Passiamo dalla teoria alla pratica guardando quali sono i meccanismi che il linguaggio C# ci mette a disposizione per realizzare il polimorfismo in c sharp.

using System;
namespace Polimorfismo
{
    class Program
    {
        static void Main(string[] args)
        {
            Insetto i = new Insetto();
            i.MyMethod();
        }
        public class Animale
        {
            public void MyMethod()
            {
                Console.WriteLine("Sto eseguendo MyMethod() della Superclasse");
            }
        }
        public class Insetto:Animale
        {
            public void MyMethod()
            {
                 Console.WriteLine("Sto eseguendo MyMethod() della Sottoclasse");
            }
        }
    }
}

POLIMORFISMO IN C SHARP WARNING DEL COMPILATORE

Se proviamo ad eseguire questo codice il compilatore ci darà un warning (MyMethod() nasconde il membro ereditato ‘Program.Animale.MyMethod()’. Se questo comportamento è intenzionale, usare la parola chiave new) in quanto è in grado di comprendere che stiamo effettuando l’override di un metodo della classe base ma non lo abbiamo esplicitamente dichiarato nel codice. Se creiamo un’istanza della classe Insetto verrà comunque eseguito il codice del metodo di tale classe.

LE KEYWORD VIRTUAL E OVERRIDE

Per rimuovere tale warning occorre usare la parola chiave virtual nella classe base che indica al compilatore che stiamo ridefinendo volontariamente il metodo MyMethod () della superclasse in una sottoclasse. Usando la keyword virtual stiamo indicando al compilatore che è possibile eseguire l’override del metodo. Nelle classi derivate occorre specificare la keyword override per rimuovere la segnalazione del compilatore e gestire correttamente il codice.

Warning compilatore

Vediamo come si gestisce correttamente l’override (se necessario) di un metodo della Superclasse nella classe o classi derivate. Se nella classe derivata vogliamo prima richiamare il metodo della Superclasse e poi aggiungere codice supplementare lo dobbiamo fare usando la parola chiave base seguita dal punto e da MyMethod();

using System;
namespace Polimorfismo
{
    class Program
    {
        static void Main(string[] args)
        {
            Insetto i = new Insetto();
            i.MyMethod();
        }
        public class Animale
        {
            public virtual void MyMethod()
            {
                Console.WriteLine("Sto eseguendo MyMethod() della Superclasse.");
            }
        }
        public class Insetto:Animale
        {
            public override void MyMethod()
            {
                 Console.WriteLine("Sto eseguendo MyMethod() della Sottoclasse.");
            }
        }
    }
}

LE CLASSI ASTRATTE

Nel polimorfismo in c sharp se dichiariamo un metodo con la parola chiave abstract tale metodo deve essere semplicemente un segnaposto privo di implementazione che indica alle classi derivate che devono obbligatoriamente implementare tale metodo. Abbiamo visto che se un metodo viene dichiarato astratto allora tutta la classe deve essere abstract. Le classi derivate dovranno continuare ad usare la keyword override. Le classi astratte possono contenere metodi di classe static che hanno una loro implementazione, quindi possono essere implementate parzialmente.

using System;
namespace Polimorfismo
{
    class Program
    {
        static void Main(string[] args)
        {
            Animale i = new Insetto();
            i.MyMethod();
            Animale.MyMethod2();
        }
    }
    public abstract class Animale
    {
        public abstract void MyMethod();

        public static void MyMethod2()
        {
            Console.WriteLine("Sto eseguendo il metodo statico MyMethod2() della Superclasse.");
        } 
    }
    public class Insetto:Animale
    {
        public override void MyMethod()
        {
            Console.WriteLine("Sto eseguendo MyMethod() della Sottoclasse.");
        }
    }
}

Vediamo alcuni importanti meccanismi del polimorfismo con del codice di esempio.

using System;
namespace Polimorfismo
{
    class Program
    {
        static void Main(string[] args)
        {
            Animale a  = new Insetto();
            Animale b = new Mammifero();
            a.MyMethod();//VIENE ESEGUITO IL METODO DELLA CLASSE DERIVATA INSETTO
            b.MyMethod();//VIENE ESEGUITO IL METODO DELLA CLASSE DERIVATA MAMMIFERO
        }
    }
    public class Animale
    {
        public virtual void MyMethod()
        {
            Console.WriteLine("Sto eseguendo MyMethod() della Superclasse.");
        }
            
    }
    public class Insetto:Animale
    {
        public override void MyMethod()
        {
            Console.WriteLine("Sto eseguendo MyMethod() della Sottoclasse Insetto.");
        }
    }
    public class Mammifero:Animale
    {
        public override void MyMethod()
        {
            Console.WriteLine("Sto eseguendo MyMethod() della Sottoclasse Mammifero.");
        }
    }
}

Abbiamo definito due variabili oggetto relative alla classe Animale a e b le abbiamo inizializzate con la classe Insetto e Mammifero, a e b sono di fatto due oggetti appartenenti alla classe Animale se proviamo ad eseguire MyMethod() su queste due variabili oggetto il codice che viene eseguito è quello relativo alle sottoclassi e non alla classe base.

IN SINTESI

Il polimorfismo rappresenta il principio in funzione del quale diverse classi derivate possono implementare uno stesso comportamento definito nella classe base in modo differente. Dato che in .NET 5 ogni classe deriva direttamente o indirettamente da System.Object, ogni oggetto presenta un certo numero di comportamenti comuni, alcuni dei quali possono essere specializzati sfruttando il polimorfismo. Tra questi comportamenti figurano le funzioni ToString, GetHashCode e Equals, la cui implementazione predefinita è inclusa nella classe base System.Object.

COMPRENSIONE DELLA GERARCHIA DI EREDITARIETA’ USATA NEL CODICE

Riprendendo l’esempio proposto nel caso dell’ereditarietà, consideriamo due comportamenti comuni a tutti gli animali: Respira e Mangia. Nel mondo animale questi comportamenti vengono messi in atto secondo modalità peculiari a seconda delle specie: un carnivoro mangia in modo differente rispetto a un erbivoro, un pesce respira in modo diverso rispetto a un uccello. Sulla base di queste considerazioni, i comportamenti Mangia e Respira, definiti nelle diverse classi derivate da Animale, possono essere implementati in modo specifico e del tutto indipendente dalle altre implementazioni (e, in particolar modo, da quello della classe base Animale).

ESERCITAZIONE

Ridefiniamo la property Denominazione presente nella classe base Persona nella classe derivata Utente aggiungendo l’ID Utente. Ridefiniamo inoltre il metodo ToString() presente nella classe capostipite della catena di ereditarietà degli oggetti C# ossia la classe Object.

using System;

namespace EsercitazionePolimorfismo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Utente utente1 = new(){Id = "1234560",Nome = "Mario",Cognome = "Rossi",NumeroConto = "4356789"};
            utente1.Saldo = 13000m;
            Utente utente2 = new(){Id="12345678", Nome = "Mario",Cognome = "Verdi",NumeroConto = "435679879"};
            utente2.Saldo = 30000m;
            utente1.SaldoCorrente();
            utente2.SaldoCorrente();
            var b1 = new Banca("1234567890", "UBI Banca");
            var b2 = new Banca("1234576543", "UBI Banca");
            b1.Preleva(3000m,utente1);
            b2.Preleva(10000m,utente2);
            utente1.SaldoCorrente();
            utente2.SaldoCorrente();
            Console.WriteLine(utente2.Denominazione);
            Console.WriteLine(utente2.ToString()); //VIENE RICHIAMATO IL METODO RIDEFINITO NELLA CLASSE UTENTE
        }

        internal class Persona
        {
            public string Nome { get; set; }
            public string Cognome { get; set; }

            public virtual string Denominazione => "Utente " + Nome + " " + Cognome;
        }

        internal class Utente : Persona
        {
            private decimal saldo;

            public string Id { get; set; }

            public decimal Saldo
            {
                get => saldo;
                set
                {
                    if (value < 0)
                        Console.WriteLine($"Impossibile impostare il saldo. Valore passato: {value}");
                    else
                        saldo = value;
                }
            }

            public string NumeroConto { get; set; }

            public override string Denominazione => "Utente " + Id + " " + Nome + " " + Cognome;

            internal void SaldoCorrente()
            {
                Console.WriteLine($"Utente {Nome + " " + Cognome} saldo corrente: {Saldo}");
            }

            //ToString() è un metodo predefinito nella classe object. Tutti i tipi in C Sharp
            //Ereditano da questa classe che è la capostitipe di tutta la gerarchia di ereditarietà
            //degli oggetti C#
            public override string ToString()
            {
                return "Utente: " + Nome + " " + Cognome + " " + NumeroConto;
            }
        }

        internal class Banca
        {
            public Banca(string id, string denominazione, string abi, string cab)
            {
                Id = id;
                Denominazione = denominazione;
                Abi = abi;
                Cab = cab;
            }

            public Banca(string id, string denominazione) :
                this(id, denominazione, "06098", "14400")
            {
            }

            public string Id { get; set; }
            public string Denominazione { get; set; }
            public string Abi { get; set; }
            public string Cab { get; set; }
            public Utente Utente { get; set; }

            internal void Preleva(decimal importo, Utente utente)
            {
                this.Utente = utente;
                var saldo = Utente.Saldo;
                if (importo > saldo)
                {
                    Console.WriteLine($"Operazione di prelievo non ammessa, importo richiesto {importo}");
                    return;
                }

                if (importo < 0)
                {
                    Console.WriteLine($"Importo prelevato non valido: {importo}");
                    return;
                }

                Utente.Saldo -= importo;
                this.Utente = null;
            }

            internal void Accredito(decimal importo, Utente utente)
            {
                this.Utente = utente;
                if (importo < 0)
                {
                    Console.WriteLine($"Importo accreditato non valido: {importo}");
                    return;
                }

                Utente.Saldo += importo;
                this.Utente = null;
            }
        }
    }
}

APPROFONDIMENTO SUL POLIMORFISMO

Il polimorfismo in C# è un concetto fondamentale della programmazione orientata agli oggetti che permette agli oggetti di essere trattati come istanze della loro classe base piuttosto che della loro classe derivata. Ciò significa che una singola interfaccia può essere utilizzata per diversi tipi di oggetti. In C#, il polimorfismo si ottiene principalmente attraverso l’ereditarietà e l’uso delle interfacce.

Tipi di Polimorfismo in C#

1. Polimorfismo Statico (o Compile-Time Polymorphism):

Overloading dei Metodi: Consente di avere più metodi con lo stesso nome ma con firme diverse (differenti parametri).

Overloading degli Operatori: Permette di definire o ridefinire il comportamento degli operatori per tipi definiti dall’utente.

2. Polimorfismo Dinamico (o Run-Time Polymorphism):

Override dei Metodi: Permette a una classe derivata di fornire una specifica implementazione di un metodo già definito nella classe base. Questo si ottiene utilizzando le parole chiave virtual nella classe base e override nella classe derivata.

Interfacce: Definiscono un contratto che le classi devono implementare. Le classi che implementano l’interfaccia possono essere trattate in modo uniforme.

Esempio di Polimorfismo Dinamico con Override dei Metodi

using System;

class Animale
{
          public virtual void FaiSuono()
          {
              Console.WriteLine(“L’animale fa un suono.“);
          }
}

class Cane : Animale
{
          public override void FaiSuono()
          {
             Console.WriteLine(“Il cane abbaia.“);
          }
}

class Gatto : Animale
{
          public override void FaiSuono()
          {
             Console.WriteLine(“Il gatto miagola.“);
          }
}

class Programma
{
          static void Main(string[] args)
          {
                 Animale mioAnimale = new Animale();
                 Animale mioCane = new Cane();
                 Animale mioGatto = new Gatto();

                 mioAnimale.FaiSuono(); // Output: L’animale fa un suono.
                 mioCane.FaiSuono(); // Output: Il cane abbaia.
                 mioGatto.FaiSuono(); // Output: Il gatto miagola.
           }
}

In questo esempio, la classe Animale ha un metodo FaiSuono che è definito come virtual. Le classi Cane e Gatto ereditano da Animale e sovrascrivono il metodo FaiSuono utilizzando la parola chiave override. Nel metodo Main, gli oggetti di tipo Cane e Gatto vengono trattati come oggetti di tipo Animale, ma chiamano le loro rispettive implementazioni del metodo FaiSuono.

Il polimorfismo permette quindi di scrivere codice più flessibile e riutilizzabile, facilitando la manutenzione e l’estensibilità del software.

LINK AI POST PRECEDENTI

LINK AL CODICE SU GITHUB