C#

LE CLASSI - ereditarietà

Dopo l'incapsulamento eccoci a parlare del secondo pilastro, come si usa dire, della programmazione a oggetti, ovvero l'ereditarietà. Si tratta della possibilità di estendere le funzionalità di una classe creandone una nuova derivata da questa. Ora, chiariremo tra breve cosa significa questo termine "derivata", di certo non è difficile capire le implicazioni a livello di architettura dei programmi, che questo strumento introduce. L'ereditarietà permette quindi estendere ma anche modificare le classi esistenti potendo dare origine a sistemi di grandi espressività e potenza rappresentativa ed elaborativa essendo inoltre possibile riutilizzare le classi esistenti e quindi il codice già scritto che le rappresenta. Come vi sarà facile reperire in rete, esiste un dualismo tra ereditarietà singola e multipla, ovvero la possibilià di per una classe di ereditare rispettivamente da una o più classi. Non entro nel merito del dibattito, diciamo comunque che C# ammette solo l'ereditarietà singola (come Java, ad esempio e diversamente da C++ e Python per dirne un paio che usano l'approccio multiplo) ma, come vedremo, è possibile ereditare da più interfacce, vedremo cosa sono in un altro paragrafo.
Vediamo ora un semplice esempio che permette di comprendere di cosa stiamo parlando:

  Esempio 23.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;

class Persona
{
  public string nome;
  public string cognome;
  public int eta;
}

class Impiegato : Persona
{
  public string impiego;
  public int stipendio;
}

class program
{
  public static void Main()
  {
    Impiegato im01 = new Impiegato();
    im01.nome = "Mario";
    im01.cognome = "Rossi";
    im01.eta = 32;
    im01.impiego = "segretario";
    im01.stipendio = 1500;
  }
}

La classe "Impiegato", alla riga 10 ha una definizione che, come vedete, prevede una sintassi particolare, ovvero i due punti seguiti dal nome di un'altra classe che è la classe dalla quale si eredita. Qui si realizza l'ereditarietà, ovvero la classe Impiegato riceve dalla classe Persona i suoi campi, in questo caso particolare le 3 proprietà nome, cognome ed età. Queste, come si vede alle righe 21, 22, 23, sono liberamente utilizzabili da ogni istanza della classe "Impiegato", nell'esempio da quella definita alla riga 20. La cosa vale per le proprietà ma ovviamente anche i metodi. Lo schema di base dell'ereditarietà pertanto è:

classe A
{
  dati pubblici
}

classe B : A
{
  dati della classe B
}

ovvero

classe derivata : classe base

Il concetto di base fondamentale che va ripetuto e compreso,  è quello del riutilizzo da parte della classe derivata dei campi della classe padre. E' molto importante, se ci pensate un attimo, per quanto riguarda l'archiettura che è possibile dare ai vostri progetti e per la cooperazione che è possibile instaurare tra progetti diversi. Esempi ne sono le numerosissime librerie che contengono oggetti che potete usatilizzare direttamente ma dai quali potete ricavare i vostri modificati opportunamente.
 
Le cose però possono non essere sempre coì semplici e vediamo il seguente esempio:

  Esempio 23.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;

class A
{
  public static int x1;
  private static int x2;
}

class B : A
{
  public int x3 = x1 + x2;
}

class program
{
  public static void Main()
  {
    B b = new B();
  }
}

Questo programma non compila e il messaggio è il seguente:

error CS0122: 'A.x2' non è accessibile a causa del livello di protezione

Il motivo è semplice, x2 è marcata private e quindi risulta utilizzabile solo ed esclusivamente all'interno della classe A. Qual'è il modificatore che permette l'uso di un campo in una classe e nelle sue derivate? Come espresso nel capitolo dedicato, la parolina giusta è protected. Quindi se alla riga 6 sostituite private con protected le cose funzionano (a parte il fatto che il programma in questione non fa assolutamente nulla e si becca pure due giusti warning). Questo significa anche che quando progettate le vostre classi dovete pensare anche ad eventuali problematiche di questo genere se pensate che possano essere riutilizzate.
Un altro punto critico riguarda i costruttori. I costruttori non vengono ereditati e questo è un fatto importante. Ecco di seguito un esempio:

  Esempio 23.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;

class A
{
  int x1;
  public A (int a)
  {
    x1 = a;
  }
}

class B : A
{
  public int x2 = 0;
}

class program
{
  public static void Main()
  {
    B b = new B();
  }
}

Il compilatore si lamenta:

error CS7036: Non sono stati specificati argomenti corrispondenti al parametro formale obbligatorio 'a' di 'A.A(int)'

Il costruttore non passa direttamente e quindi la classe B adopera  quello di default, non avendone uno suo, e questo a sua volta ovrebbe essere applicato alla classe A che però ne ha uno che prevede dei parametri. Di qui l'errore. Se alla riga 6 il costruttore non avesse parametri il programma compilerebbe. Non va invece bene creare un costruttore per B, di nuovo il compilatore vorrebbe un parametro per il costruttore di A. Una soluzione, a questo punto, è chiamare il costruttore della classe "padre", ad esempio:

class B : A
{
  int x2;
  public B (int b) : base(b)
  {}
}

Qui incontriamo la keyword base che, come intuibile, richiama la classe da cui la nostra deriva, quella a livello immediatamente superiore. Sfruttando la possibilità di avere due costruttori in una clase si può anche risolvere nella seguente maniera:

  Esempio 23.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;

class A
{
  int x1;
  public A (int a)
  {
    x1 = 4;
  }
  public A()
  {}
}

class B : A
{

}

class program
{
  public static void Main()
  {
    B b = new B();
  }
}

E' presente un secondo costruttore privo di parametri ed ecco che allora anche la classe derivata non è costretta ad averne.

In questo paragrafo abbiamo scrostato la superficie del discorso relativo all'ereditarietà... c'è ancora molto da dire e lo vedremo poco per volta.