4. Classi, strutture, interfacce
4.1. L'oggetto come esempio
4.1.1. Informazioni generali
Passiamo ora alla programmazione orientata agli oggetti attraverso un esempio. Un oggetto è un'entità contenente dati che ne definiscono lo stato (chiamati campi, attributi, ...) e funzioni (chiamate metodi). Un oggetto viene creato secondo un modello chiamato classe:
public class C1{
Type1 p1 ; // field p1
Type2 p2 ; // p2 field
…
Type3 m3(… ) { // m3 method
…
}
Type4 m4(… ) { // m4 method
…
}
…
}
Dalla classe C1 sopra riportata, è possibile creare numerosi oggetti O1, O2,.. Tutti avranno campi p1, p2,.. e metodi m3, m4, .. Ma avranno valori diversi per i loro campi pi, ciascuno con il proprio stato. Se o1 è un oggetto di tipo C1, o1.p1 designa la proprietà p1 di o1 e o1.m1 il metodo m1 di O1.
Consideriamo un primo modello a oggetti: la classe Person.
4.1.2. Creazione di un progetto C#
Negli esempi precedenti, avevamo un solo file: Program.cs. D'ora in poi, potremo avere diversi file sorgente in un unico progetto. Vi mostreremo come.
![]() |
In [1], create un nuovo progetto. In [2], scegliete Applicazione console. In [3], lasciate il valore predefinito. In [4], confermate. In [5], il progetto generato. Il contenuto di Program.cs è il seguente:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
}
}
}
Salviamo il progetto creato:
![]() |
In [1], l'opzione da salvare. In [2], seleziona la cartella in cui salvare il progetto. In [3], assegna un nome al progetto. In [5], indica che desideri creare una soluzione. Una soluzione è un insieme di progetti. In [4], assegna un nome alla soluzione. In [6], conferma il salvataggio.
![]() |
In [1], il progetto salvato. In [2], aggiungere un nuovo elemento al progetto.
![]() |
In [1], indica che desideri aggiungere una classe. In [2], inserisci il nome della classe. In [3], conferma le informazioni. In [4], il progetto [01] presenta un nuovo file sorgente Personne.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
class Personne {
}
}
Modifica lo spazio dei nomi di ciascun file sorgente in Chap2 ed elimina la necessità di importare spazi dei nomi non necessari:
using System;
namespace Chap2 {
class Personne {
}
}
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
}
}
}
4.1.3. Definizione della classe Person
La definizione della classe Person nel file sorgente [Personne.cs] sarà la seguente:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// method
public void Initialise(string P, string N, int age) {
this.prenom = P;
this.nom = N;
this.age = age;
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Qui abbiamo la definizione di una classe, ovvero un tipo di dati. Quando creiamo variabili di questo tipo, le chiamiamo oggetti o istanze della classe. Una classe è quindi uno stampo da cui vengono costruiti gli oggetti.
I membri o campi di una classe possono essere dati (attributi), metodi (funzioni) o proprietà. Le proprietà sono metodi speciali utilizzati per ottenere o impostare il valore degli attributi di un oggetto. Questi campi possono essere accompagnati da una delle tre parole chiave seguenti:
Un campo privato è accessibile solo dai metodi interni della classe | |
È possibile accedere a un campo pubblico tramite qualsiasi metodo, definito o meno all'interno della | |
Un campo protetto (protected) è accessibile solo dai metodi interni della classe o da un oggetto derivato (vedi il concetto di ereditarietà più avanti). |
In generale, i dati di classe sono dichiarati come privati mentre i suoi metodi e le sue proprietà sono dichiarati pubblici. Ciò significa che l'utente di un oggetto (il programmatore)
- non avrà accesso diretto ai dati privati dell'oggetto
- potrà richiamare i metodi pubblici dell’oggetto, e in particolare quelli che forniscono l’accesso ai suoi dati privati.
La sintassi per dichiarare una classe in C è la seguente:
public class C{
private donnée ou méthode ou propriété privée;
public donnée ou méthode ou propriété publique;
protected donnée ou méthode ou propriété protégée;
}
L'ordine di dichiarazione degli attributi private, protected e public è arbitrario.
4.1.4. Il metodo Initialize
Torniamo alla nostra classe Person dichiarata come:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// method
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Qual è il ruolo di Initializes? Poiché nome, cognome ed età sono dati privati della classe Person, istruzioni:
sono illegali. Dobbiamo inizializzare un oggetto di tipo Persona tramite un metodo pubblico. Questo è il ruolo di Initializes. Scriviamo:
Scrivere p1.Initialise è legale perché Initializes è aperto al pubblico.
4.1.5. L'operatore new
La sequenza di istruzioni
non è corretta. L'istruzione
indica p1 come riferimento a un oggetto di tipo Person. Questo oggetto non esiste ancora, quindi p1 non è inizializzato. È come scrivere:
dove la parola chiave null indica che la variabile p1 non fa ancora riferimento ad alcun oggetto. Quando poi scrivi
si utilizza il metodo Initialize dell'oggetto a cui fa riferimento p1. Se tale oggetto non esiste ancora, il compilatore segnalerà un errore. Per assicurarsi che p1 faccia riferimento a un oggetto, scrivere:
Questo crea un oggetto di tipo Person non ancora inizializzato: gli attributi «name» e «first name», che sono riferimenti a oggetti di tipo String, avranno il valore null, mentre «age» avrà il valore 0. Si tratta quindi di un'inizializzazione predefinita. Ora che p1 fa riferimento a un oggetto, l'istruzione di inizializzazione per questo oggetto
è valida.
4.1.6. La parola chiave this
Diamo un'occhiata al codice per l'inizializzazione:
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
L'istruzione this.prenom=p significa che il nome dell'oggetto corrente (this) riceve il valore p. La parola chiave this indica l'oggetto corrente: quello in cui si trova il metodo eseguito. Come lo sappiamo? Diamo un'occhiata all'inizializzazione dell'oggetto a cui fa riferimento p1 nel programma chiamante:
Questo è il metodo Initializes dell'oggetto p1. Quando questo metodo fa riferimento a this, di fatto si riferisce all'oggetto p1. Il metodo Initializes avrebbe potuto essere scritto anche come segue:
public void Initialise(string p, string n, int age) {
prenom = p;
nom = n;
this.age = age;
}
Quando il metodo di un oggetto fa riferimento a un attributo A di questo oggetto, la scrittura di this.A è implicita. Deve essere utilizzata esplicitamente in caso di conflitto tra identificatori. Questo è il caso di:
this.age=age;
dove «age» indica sia l'attributo dell'oggetto corrente sia il parametro «age» ricevuto dal metodo. L'ambiguità deve quindi essere risolta indicando l'attributo «age» con «this.age».
4.1.7. Un programma di prova
Ecco un breve programma di prova. È scritto nel file sorgente [Program.cs]:
using System;
namespace Chap2 {
class P01 {
static void Main() {
Personne p1 = new Personne();
p1.Initialise("Jean", "Dupont", 30);
p1.Identifie();
}
}
}
Prima di eseguire il progetto [01], potrebbe essere necessario specificare il file sorgente da eseguire:
![]() |
Nelle proprietà del progetto [01], la classe da eseguire è indicata in [1].
I risultati ottenuti al termine sono i seguenti:
4.1.8. Un altro metodo Inizializza
Consideriamo la classe Person e aggiungiamo il seguente metodo:
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
Ora abbiamo due metodi con il nome Initializes: ciò è lecito purché ammettano parametri diversi. È il caso di qui. Il parametro è ora un riferimento p a una persona. Gli attributi della persona p vengono quindi assegnati all'oggetto corrente (this). Si noti che Initializes ha accesso diretto agli attributi dell'oggetto p sebbene questi siano privati. Ciò è sempre vero: un oggetto o1 di una classe C ha sempre accesso agli attributi degli oggetti della stessa classe C.
Ecco un test della nuova classe Person:
using System;
namespace Chap2 {
class Program {
static void Main() {
Personne p1 = new Personne();
p1.Initialise("Jean", "Dupont", 30);
p1.Identifie();
Personne p2 = new Personne();
p2.Initialise(p1);
p2.Identifie();
}
}
}
e i relativi risultati:
4.1.9. Costruttori della classe Person
Un costruttore è un metodo che prende il nome dalla classe e viene chiamato quando viene creato l'oggetto. Viene generalmente utilizzato per inizializzare l'oggetto. Può accettare argomenti, ma non restituisce alcun risultato. Il suo prototipo o la sua definizione non sono preceduti da alcun tipo (nemmeno void).
Se una classe C ha un costruttore che accetta n argomenti argi, la dichiarazione e l'inizializzazione di un oggetto di questa classe possono essere effettuate nella forma:
oppure
Quando una classe C ha uno o più costruttori, uno di questi deve essere utilizzato per creare un oggetto di questa classe. Se una classe C non ha alcun costruttore, ne ha uno predefinito, ovvero il costruttore senza parametri: public C(). Gli attributi dell'oggetto vengono quindi inizializzati con i valori predefiniti. Questo è ciò che è accaduto nei programmi precedenti, dove:
Creiamo due costruttori per la nostra classe Person:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne P) {
Initialise(P);
}
// method
public void Initialise(string p, string n, int age) {
...
}
public void Initialise(Personne p) {
...
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
I nostri due costruttori utilizzano semplicemente gli Initialize studiati in precedenza. Ricordiamo che quando un programmatore utilizza la notazione Initialize(p), ad esempio, il compilatore la traduce in this.Initialize(p). Nel costruttore, l'Initialize viene chiamato per operare sull'oggetto a cui fa riferimento this, ovvero l'oggetto corrente, quello in fase di costruzione.
Ecco un breve programma di prova:
using System;
namespace Chap2 {
class Program {
static void Main() {
Personne p1 = new Personne("Jean", "Dupont", 30);
p1.Identifie();
Personne p2 = new Personne(p1);
p2.Identifie();
}
}
}
e i risultati ottenuti:
4.1.10. Riferimenti agli oggetti
Utilizziamo sempre la stessa Person. Il programma di test diventa:
using System;
namespace Chap2 {
class Program2 {
static void Main() {
// p1
Personne p1 = new Personne("Jean", "Dupont", 30);
Console.Write("p1="); p1.Identifie();
// p2 references the same object as p1
Personne p2 = p1;
Console.Write("p2="); p2.Identifie();
// p3 references an object that will be a copy of the object referenced by p1
Personne p3 = new Personne(p1);
Console.Write("p3="); p3.Identifie();
// change the state of the object referenced by p1
p1.Initialise("Micheline", "Benoît", 67);
Console.Write("p1="); p1.Identifie();
// as p2=p1, the object referenced by p2 must have changed state
Console.Write("p2="); p2.Identifie();
// as p3 does not reference the same object as p1, the object referenced by p3 must not have changed
Console.Write("p3="); p3.Identifie();
}
}
}
I risultati sono i seguenti:
Quando si dichiara la variabile p1 con
p1 fa riferimento all'oggetto Personne("John", "Smith", 30) ma non è l'oggetto stesso. In C, diremmo che si tratta di un puntatore, ovvero l'indirizzo dell'oggetto creato. Se poi scrivi:
Non è l'oggetto Person("John", "Smith",30) che viene modificato, bensì il riferimento p1 che cambia valore. L'oggetto Person("John", "Smith",30) andrà "perso" se non è referenziato da nessun'altra variabile.
Quando scriviamo:
inizializziamo il puntatore p2: esso "punta" allo stesso oggetto (designa lo stesso oggetto) del puntatore p1. Quindi, se modifichiamo l'oggetto "indicato" (o a cui fa riferimento) da p1, modifichiamo anche quello a cui fa riferimento p2.
Quando scriviamo:
viene creato un nuovo oggetto Person. Questo nuovo oggetto sarà referenziato da p3. Se si modifica l'oggetto "indicato" (o referenziato) da p1, verrà modificato anche quello referenziato da p3. Questo è ciò che mostrano i risultati.
4.1.11. Passaggio di parametri di riferimento a oggetti
Nel capitolo precedente abbiamo visto come vengono passati i parametri di funzione quando rappresentano un semplice tipo C# rappresentato da una struttura .NET. Vediamo cosa succede quando il parametro è un :
using System;
using System.Text;
namespace Chap1 {
class P12 {
public static void Main() {
// example 4
StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3;
Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2);
ChangeStringBuilder(sb0, sb1, ref sb2, out sb3);
Console.WriteLine("Dans fonction appelante après appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3);
}
private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) {
Console.WriteLine("Début fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2);
sbf0.Append("*****");
sbf1 = new StringBuilder("essai1*****");
sbf2 = new StringBuilder("essai2*****");
sbf3 = new StringBuilder("essai3*****");
Console.WriteLine("Fin fonction appelée : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3);
}
}
}
- riga 8: definisce 3 StringBuilder. Un oggetto StringBuilder è simile a un oggetto stringa. Quando si gestisce un oggetto stringa, si ottiene un nuovo oggetto stringa in cambio. Quindi nella sequenza di codice:
La riga 1 crea una stringa e s è il suo indirizzo. Nella riga 2, s.ToUpperCase() crea un altro oggetto stringa in memoria. Pertanto, tra la riga 1 e la riga 2, s ha cambiato valore (ora punta al nuovo oggetto). La classe StringBuilder consente di modificare una stringa senza creare un secondo oggetto. Ecco l'esempio riportato sopra:
- riga 8: 4 riferimenti [sb0, sb1, sb2, sb3] a oggetti di tipo StringBuilder
- riga 10: vengono passati a ChangeStringBuilder con diverse modalità: sb0, sb1 con la modalità predefinita, sb2 con la parola chiave ref, sb3 con la parola chiave out.
- righe 15-22: un metodo con parametri formali [sbf0, sbf1, sbf2, sbf3]. Le relazioni tra i parametri formali sbfi e la forza lavoro sbi sono le seguenti:
- sbf0 e sb0 sono, all'inizio del metodo, due riferimenti distinti che puntano allo stesso oggetto (passaggio del valore dell'indirizzo)
- idem per sbf1 e sb1
- sbf2 e sb2 sono, all'inizio del metodo, lo stesso riferimento allo stesso oggetto (parola chiave ref)
- sbf3 e sb3 sono, dopo l'esecuzione del metodo, lo stesso riferimento allo stesso oggetto (parola chiave out)
I risultati sono i seguenti:
Spiegazioni:
- sb0 e sbf0 sono due riferimenti distinti allo stesso oggetto. Questo è stato modificato tramite sbf0 - riga 3. Questa modifica può essere visualizzata tramite sb0 - riga 4.
- sb1 e sbf1 sono due riferimenti distinti allo stesso oggetto. sbf1 metodo e ora punta a un nuovo oggetto - riga 3. Ciò non modifica il valore di sb1 che continua a puntare allo stesso oggetto - riga 4.
- sb2 e sbf2 sono lo stesso riferimento allo stesso oggetto. sbf2 viene modificato nel metodo e ora punta a un nuovo oggetto - riga 3. Poiché sbf2 e sb2 sono un'unica entità, anche il valore di sb2 è stato modificato e sb2 punta allo stesso oggetto di sbf2 - righe 3 e 4.
- prima di chiamare il metodo, sb3 era privo di valore. Dopo il metodo, sb3 riceve il valore di sbf3. Abbiamo quindi due riferimenti allo stesso oggetto - righe 3 e 4
4.1.12. Oggetti temporanei
In un'espressione è possibile richiamare esplicitamente il costruttore di un oggetto: l'oggetto viene creato, ma non è possibile accedervi (ad esempio per modificarlo). Questo oggetto temporaneo viene creato al solo scopo di valutare l'espressione, per poi essere eliminato. Lo spazio di memoria che occupa viene successivamente recuperato automaticamente da un programma chiamato "garbage collector", il cui compito è quello di recuperare lo spazio di memoria occupato da oggetti che non sono più referenziati dai dati del programma.
Consideriamo il seguente nuovo programma di test:
using System;
namespace Chap2 {
class Program {
static void Main() {
new Personne(new Personne("Jean", "Dupont", 30)).Identifie();
}
}
}
e modificare i costruttori della classe Person per visualizzare un messaggio:
// manufacturers
public Personne(String p, String n, int age) {
Console.WriteLine("Constructeur Personne(string, string, int)");
Initialise(p, n, age);
}
public Personne(Personne P) {
Console.Out.WriteLine("Constructeur Personne(Personne)");
Initialise(P);
}
Otteniamo i seguenti risultati:
che mostra la costruzione successiva dei due oggetti temporanei.
4.1.13. Metodi per la lettura e la scrittura degli attributi privati
Aggiungiamo ai metodi Person metodi per leggere o modificare lo stato degli attributi dell'oggetto:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Console.WriteLine("Constructeur Personne(string, string, int)");
Initialise(p, n, age);
}
public Personne(Personne p) {
Console.Out.WriteLine("Constructeur Personne(Personne)");
Initialise(p);
}
// method
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
// accessors
public String GetPrenom() {
return prenom;
}
public String GetNom() {
return nom;
}
public int GetAge() {
return age;
}
//modifiers
public void SetPrenom(String P) {
this.prenom = P;
}
public void SetNom(String N) {
this.nom = N;
}
public void SetAge(int age) {
this.age = age;
}
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Testiamo la nuova classe con il seguente programma:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p = new Personne("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
p.SetAge(56);
Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")");
}
}
}
e otteniamo i risultati:
4.1.14. Le proprietà
Un altro modo per accedere agli attributi di una classe consiste nel creare delle proprietà. Queste ci consentono di manipolare gli attributi privati come se fossero pubblici.
Consideriamo la classe Person, in cui i precedenti accessori e modificatori sono stati sostituiti da proprietà di lettura e scrittura:
using System;
namespace Chap2 {
public class Personne {
// attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Initialise(p, n, age);
}
public Personne(Personne p) {
Initialise(p);
}
// method
public void Initialise(string p, string n, int age) {
this.prenom = p;
this.nom = n;
this.age = age;
}
public void Initialise(Personne p) {
prenom = p.prenom;
nom = p.nom;
age = p.age;
}
// properties
public string Prenom {
get { return prenom; }
set {
// valid first name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//first name
public string Nom {
get { return nom; }
set {
// valid name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//name
public int Age {
get { return age; }
set {
// valid age?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//if
}//age
// method
public void Identifie() {
Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age);
}
}
}
Una proprietà consente di leggere (get) o modificare (set) il valore di un attributo. Una proprietà viene dichiarata come segue:
dove Type deve essere il tipo dell'attributo gestito dalla proprietà. Può avere due metodi chiamati get e set. Il metodo get è solitamente responsabile della restituzione del valore dell'attributo che gestisce (potrebbe restituire qualcos'altro, nulla lo impedisce). Il metodo set riceve un parametro chiamato value che normalmente assegna all'attributo che gestisce. Può sfruttare questa opportunità per verificare la validità del valore ricevuto e, se necessario, generare un'eccezione se il valore risulta non valido. Questo è ciò che fa ici.
Come vengono chiamati questi metodi get e set? Consideriamo il seguente programma di test:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p = new Personne("Jean", "Michelin", 34);
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
p.Age = 56;
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
try {
p.Age = -4;
} catch (Exception ex) {
Console.Error.WriteLine(ex.Message);
}//try-catch
}
}
}
Nel
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");
Stiamo cercando i valori delle proprietà Prenown, Nom e Age della persona p. Si tratta del metodo get di queste proprietà, che viene quindi chiamato e restituisce il valore dell'attributo che gestisce.
Nel
vogliamo impostare il valore della proprietà Age. Questo è il set che viene poi chiamato. Riceverà 56 come valore del parametro.
Una proprietà P di una classe C che definisce solo il get è detta di sola lettura. Se c è un oggetto della classe C, l'operazione c.P=valeur verrà quindi rifiutata dal compilatore.
L'esecuzione del programma di test precedente fornisce i seguenti risultati:
Le proprietà ci consentono di manipolare gli attributi privati come se fossero pubblici. Un'altra caratteristica delle proprietà è che possono essere utilizzate in combinazione con un costruttore utilizzando la seguente sintassi:
Questa sintassi è equivalente al seguente codice:
L'ordine delle proprietà non ha importanza. Ecco un esempio.
La classe Person aggiunge un nuovo costruttore senza parametri:
public Personne() {
}
Il costruttore non inizializza i membri dell'oggetto. Questo è noto come costruttore predefinito. Viene utilizzato quando la classe non definisce un costruttore.
Il codice seguente crea e inizializza (riga 6) una nuova istanza di Person utilizzando la sintassi illustrata sopra:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" };
Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age);
}
}
}
Riga 6 sopra: viene utilizzato il costruttore senza parametri Person(). In questo caso specifico, avremmo potuto anche scrivere
Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };
ma le parentesi del costruttore Person() senza parametri non sono obbligatorie in questa sintassi.
I risultati sono i seguenti:
In molti casi, le proprietà get e set si limitano a leggere e scrivere un campo privato senza ulteriori elaborazioni. In questo scenario, possiamo utilizzare una dichiarazione automatica come segue:
Il campo privato associato alla proprietà non viene dichiarato. Viene generato automaticamente dal compilatore. È accessibile solo tramite la sua proprietà. Quindi, invece di scrivere:
private string prenom;
...
// propriété associée
public string Prenom {
get { return prenom; }
set {
// prénom valide ?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//prenom
possiamo scrivere:
senza dichiarare il campo privato first name. La differenza tra le due proprietà precedenti è che la prima verifica la validità del nome nel set, mentre la seconda no.
Utilizza la proprietà automatica "First name" per dichiarare il campo "First name" come pubblico:
Ci chiediamo se ci sia qualche differenza tra le due dichiarazioni. Dichiarare pubblico un campo di classe non è consigliabile. Ciò viola il concetto di incapsulamento dello stato di un oggetto, che deve essere privato ed esposto tramite metodi pubblici.
Se la proprietà automatica è dichiarata virtuale, può essere ridefinita in una classe figlia:
class Class1 {
public virtual string Prop { get; set; }
}
class Class2 : Class1 {
public override string Prop { get { return base.Prop; } set {... } }
}
Nella riga 2 sopra, la classe figlia Class2 può inserire nel set il codice che verifica la validità del valore assegnato alla proprietà automatica base.Prop della classe padre Class1.
4.1.15. Metodi e attributi di classe
Supponiamo di voler contare il numero di oggetti Person creati in un'applicazione. È possibile gestire un contatore autonomamente, ma si corre il rischio di dimenticare gli oggetti temporanei creati qua e là. Sembrerebbe più sicuro includere nei costruttori della classe Person un'istruzione che incrementi un contatore. Il problema è passare un riferimento a questo contatore in modo che il costruttore possa incrementarlo: è necessario passare un nuovo parametro. In alternativa, è possibile includere il contatore nella definizione della classe. Poiché si tratta di un attributo della classe stessa e non di una particolare istanza di quella classe, lo dichiariamo in modo diverso con la parola chiave static :
private static long nbPersonnes;
Per farvi riferimento, scriviamo Personne.nbPersonnes per indicare che si tratta di un attributo della classe Person stessa. Qui abbiamo creato un attributo privato a cui non si potrà accedere direttamente dall'esterno della classe. Creiamo quindi una proprietà pubblica per dare accesso all'attributo di classe nbPersonnes. Per restituire il valore di nbPersonnes il metodo get di questa proprietà non ha bisogno di un oggetto Person particolare: infatti nbPersonnes è l'attributo di un'intera classe. Abbiamo quindi bisogno di una proprietà dichiarata anch'essa come static :
public static long NbPersonnes {
get { return nbPersonnes; }
}
che dall'esterno verrà chiamato con la sintassi Personne.NbPersonnes. Ecco un esempio.
La classe Ppersonne diventa la seguente:
using System;
namespace Chap2 {
public class Personne {
// class attributes
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// instance attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String p, String n, int age) {
Initialise(p, n, age);
nbPersonnes++;
}
public Personne(Personne p) {
Initialise(p);
nbPersonnes++;
}
...
}
Alle righe 20 e 24, i costruttori incrementano il campo statico alla riga 7.
Con il seguente programma:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Personne p1 = new Personne("Jean", "Dupont", 30);
Personne p2 = new Personne(p1);
new Personne(p1);
Console.WriteLine("Nombre de personnes créées : " + Personne.NbPersonnes);
}
}
}
otteniamo i seguenti risultati:
4.1.16. Una tabella delle persone
Un oggetto è un dato come qualsiasi altro e, in quanto tale, è possibile raggruppare diversi oggetti in una tabella:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// a table of people
Personne[] amis = new Personne[3];
amis[0] = new Personne("Jean", "Dupont", 30);
amis[1] = new Personne("Sylvie", "Vartan", 52);
amis[2] = new Personne("Neil", "Armstrong", 66);
// display
foreach (Personne ami in amis) {
ami.Identifie();
}
}
}
}
- riga 7: crea un array di 3 elementi di tipo Person. Questi 3 elementi sono inizializzati qui con il valore null, ovvero non fanno riferimento ad alcun oggetto. Anche in questo caso si tratta di un uso improprio del termine "array di oggetti", quando in realtà si tratta semplicemente di un array di riferimenti a oggetti. La creazione dell'array di oggetti, che è esso stesso un oggetto (presenza di new), non crea alcun oggetto dello stesso tipo dei suoi elementi.
- righe 8-10: creazione di 3 oggetti di tipo Person
- righe 12-14: visualizzazione del contenuto della tabella friends
Otteniamo i seguenti risultati:
4.2. Eredità attraverso l'esempio
4.2.1. Generale
Introduciamo il concetto di ereditarietà. Lo scopo dell'ereditarietà è quello di "personalizzare" una classe esistente in base alle nostre esigenze. Supponiamo di voler creare una classe Enstructor: un insegnante è una persona speciale. Ha attributi che nessun'altra persona avrebbe: la materia che insegna, per esempio. Ma ha anche gli attributi di qualsiasi altra persona: nome, cognome e età. Un insegnante fa quindi pienamente parte della classe Persona, ma possiede attributi aggiuntivi. Piuttosto che scrivere una classe Enstructor da zero, preferiamo prendere ciò che abbiamo imparato nella classe Persona e adattarlo al carattere particolare degli insegnanti. È il concetto di ereditarietà che rende possibile tutto questo.
Per esprimere che la classe Teacher eredita le proprietà da Person, scriviamo:
Person viene chiamata classe padre, mentre Enstructor è la classe derivata (o figlia). Un oggetto Enstructor possiede tutte le caratteristiche di un oggetto Person: ha gli stessi attributi e metodi. Questi attributi e metodi della classe padre non vengono ripetuti nella definizione della classe figlia: indichiamo semplicemente gli attributi e i metodi aggiunti dalla classe figlia:
Supponiamo che la classe Person sia definita come segue:
using System;
namespace Chap2 {
public class Personne {
// class attributes
private static long nbPersonnes;
public static long NbPersonnes {
get { return nbPersonnes; }
}
// instance attributes
private string prenom;
private string nom;
private int age;
// manufacturers
public Personne(String prenom, String nom, int age) {
Nom = nom;
Prenom = prenom;
Age = age;
nbPersonnes++;
Console.WriteLine("Constructeur Personne(string, string, int)");
}
public Personne(Personne p) {
Nom = p.Nom;
Prenom = p.Prenom;
Age = p.Age;
nbPersonnes++;
Console.WriteLine("Constructeur Personne(Personne)");
}
// properties
public string Prenom {
get { return prenom; }
set {
// valid first name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("prénom (" + value + ") invalide");
} else {
prenom = value;
}
}//if
}//first name
public string Nom {
get { return nom; }
set {
// valid name?
if (value == null || value.Trim().Length == 0) {
throw new Exception("nom (" + value + ") invalide");
} else { nom = value; }
}//if
}//name
public int Age {
get { return age; }
set {
// valid age?
if (value >= 0) {
age = value;
} else
throw new Exception("âge (" + value + ") invalide");
}//if
}//age
// property
public string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);}
}
}
}
Il metodo Identifica è stato sostituito dall'Identità che identifica la persona. Creiamo un Enstructor che eredita da Person :
using System;
namespace Chap2 {
class Enseignant : Personne {
// attributes
private int section;
// manufacturer
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// the section is saved using the Section property
Section = section;
// follow-up
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//manufacturer
// property Section
public int Section {
get { return section; }
set { section = value; }
}// Section
}
}
La classe Teacher aggiunge ai metodi e agli attributi della classe Person :
- riga 4: la classe Teacher deriva dalla classe Person
- riga 6: un attributo Section che rappresenta il numero della sezione a cui l'insegnante appartiene nel corpo docente (approssimativamente una sezione per disciplina). Questo attributo privato è accessibile tramite la proprietà pubblica Section righe 18-21
- riga 9: un nuovo costruttore per inizializzare tutti gli attributi dell'insegnante
4.2.2. Creazione di un oggetto Teacher
Una classe di ragazze non eredita i costruttori della sua classe Parent. Deve quindi definire i propri costruttori. Il costruttore di Enstructor è il seguente:
// manufacturer
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// section is memorized
Section = section;
// follow-up
Console.WriteLine("Construction enseignant(string, string, int, int)");
}//manufacturer
La dichiarazione
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
dichiara che il costruttore riceve quattro parametri (nome, cognome, età, sezione) e altri tre (nome, cognome, età) dalla sua classe base, in questo caso la classe Person. Sappiamo che questa classe dispone di un costruttore Person(string, string, int) che creerà una persona con i parametri passati (nome, cognome, età). Una volta completata la creazione della classe base, la creazione della classe Teacher prosegue con l'esecuzione del corpo del costruttore:
// on mémorise la section
Section = section;
Si noti che a sinistra del segno = non viene utilizzata la proprietà section dell'oggetto, ma la Section ad essa associata. Ciò consente al costruttore di sfruttare eventuali controlli di validità che potrebbero essere eseguiti da questo metodo. In questo modo si evita di doverli inserire in due punti diversi: il costruttore e la proprietà.
In breve, il costruttore di una classe derivata:
- passa alla sua classe base i parametri di cui ha bisogno per costruirsi
- utilizza gli altri parametri per inizializzare i propri attributi
Avremmo forse preferito scrivere:
// constructeur
public Enseignant(string prenom, string nom, int age, int section){
this.prenom=prenom;
this.nom=nom;
this.age=age;
this.section=section;
}
Non è possibile. La classe Person ha dichiarato privati (private) i suoi tre campi nome, cognome ed età. Solo gli oggetti della stessa classe hanno accesso diretto a questi campi. Tutti gli altri oggetti, compresi gli oggetti figli come ici, devono utilizzare metodi pubblici per accedervi. Sarebbe stato diverso se la classe Person avesse dichiarato protetti (protected) i tre campi: ciò avrebbe consentito alle classi derivate di avere accesso diretto ai tre campi. Nel nostro esempio, l'utilizzo del costruttore della classe padre era quindi la soluzione corretta, e questo è il metodo usuale: quando si costruisce un oggetto figlio, si chiama prima il costruttore dell'oggetto padre, poi si completano le inizializzazioni specifiche dell'oggetto figlio (section nel nostro esempio).
Proviamo un primo programma di test [Program.cs] :
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
Questo programma crea semplicemente un oggetto Enstructor (nuovo) e lo identifica. La classe Enstructor non ha un metodo Identite, ma la sua classe padre ne ha uno che è anche pubblico: per ereditarietà, diventa un metodo pubblico della classe Enstructor.
Il progetto complessivo è il seguente:
![]() |
I risultati sono i seguenti:
Possiamo notare che:
- un oggetto Persona (riga 1) è stato costruito prima dell'oggetto Insegnante (riga 2)
- l'identità ottenuta è quella dell'oggetto Person
4.2.3. Ridefinizione di un metodo o di una proprietà
Nell'esempio precedente avevamo l'identità della parte Person, ma mancavano alcune informazioni specifiche della classe Enstructor (la sezione). Questo ci porta a scrivere una proprietà che identifichi l'insegnante:
using System;
namespace Chap2 {
class Enseignant : Personne {
// attributes
private int section;
// manufacturer
public Enseignant(string prenom, string nom, int age, int section)
: base(prenom, nom, age) {
// the section is saved using the Section property
Section = section;
// follow-up
Console.WriteLine("Construction Enseignant(string, string, int, int)");
}//manufacturer
// property Section
public int Section {
get { return section; }
set { section = value; }
}// section
// property Identity
public new string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
}
}
Righe 24-26: la proprietà Identite della classe Enstructor si basa sull'Identite della sua classe padre (baseidentity) (riga 25) per visualizzare la sua "Persona", quindi completa con la sezione specifica dell'Enstructor. Si noti la dichiarazione della proprietà Identity:
public new string Identite{
Sia un oggetto insegnante E. Questo oggetto contiene una Persona:
![]() |
La proprietà Identity è definita sia nella classe Teacher che nella sua classe padre Person. Nella classe Teacher, la proprietà Identity deve essere preceduta dalla parola chiave new per indicare che si sta ridefinendo una nuova proprietà Identity per la classe Teacher.
public new string Identite{
La classe Teacher ha ora due proprietà Identite:
- quella ereditata dalla classe padre Person
- la propria
Se E è un Enstructor, E.Identite indica la proprietà Identite della classe Enstructor. Si dice che la proprietà Identite della classe Enstructor ridefinisce o nasconde la proprietà Identite della classe padre. In generale, se O è un oggetto e M un metodo, per eseguire O.M il sistema cerca il metodo M nel seguente ordine:
- in O
- nella sua classe padre, se ne ha una
- nella classe padre della sua classe padre, se esiste
- ecc.
L'ereditarietà consente di ridefinire metodi/proprietà con lo stesso nome presenti nella classe padre all'interno della classe figlia. Ciò permette di adattare la classe figlia alle proprie esigenze. In combinazione con il polimorfismo, che vedremo tra poco, la ridefinizione di metodi/proprietà è il principale vantaggio dell'ereditarietà.
Consideriamo lo stesso programma di test di cui sopra:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite);
}
}
}
I risultati ottenuti questa volta sono i seguenti:
4.2.4. Polimorfismo
Consideriamo una discendenza di classi: C0 ← C1 ← C2 ← … ←Cn
dove Ci ← Cj indica che Cj deriva da Ci. Ciò significa che Cj possiede tutte le caratteristiche di Ci più altre. Siano Oi oggetti di tipo Ci. È lecito scrivere:
Infatti, per ereditarietà, Cj possiede tutte le caratteristiche della classe Ci più altre. Quindi un Oj di tipo Cj contiene un oggetto di tipo Ci. L'operazione
indica che Oi è un riferimento all'oggetto di tipo Ci contenuto nell'oggetto Oj.
Il fatto che una variabile di classe Ci possa effettivamente fare riferimento non solo a un oggetto della classe Ci, ma anche a qualsiasi oggetto derivato da essa, è chiamato polimorfismo: la capacità di una variabile di fare riferimento a diversi tipi di oggetti.
Facciamo un esempio e consideriamo la seguente funzione indipendente dalla classe (statica):
Potremmo anche scrivere
che
In quest'ultimo caso, il metodo statico Affiche del parametro formale p di tipo Person riceverà un valore di tipo Enstructor. Poiché il tipo Teacher deriva da Person, è lecito.
4.2.5. Ridefinizione e polimorfismo
Completiamo il nostro metodo Affiche:
public static void Affiche(Personne p) {
// displays identity of p
Console.WriteLine(p.Identite);
}//poster
La proprietà p.Identite restituisce una stringa che identifica l'oggetto Person p. Cosa succede nell'esempio precedente se il parametro passato a Poster è un oggetto di tipo Teacher :
Enseignant e = new Enseignant(...);
Affiche(e);
Diamo un'occhiata al seguente esempio:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a teacher
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// a person
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// poster
public static void Affiche(Personne p) {
// displays identity of p
Console.WriteLine(p.Identite);
}//poster
}
}
I risultati sono i seguenti:
L'esecuzione mostra che p.Identite (riga 17) ha eseguito l'Identità di una Persona, prima (riga 7) la persona contenuta nell'Insegnante e, poi (riga 10) la Persona p stessa. Non si è adattata all'oggetto effettivamente passato come parametro a Poster. Avremmo preferito avere l'identità completa di theTeacher e. Ciò avrebbe richiesto la notazione p.Identite, che fa riferimento alla proprietà Identity dell'oggetto effettivamente puntato da p, piuttosto che alla proprietà Identity della parte "Person" dell'oggetto effettivamente puntato da p.
Questo risultato può essere ottenuto dichiarando Identity come proprietà virtuale (virtual) nella classe base Person :
public virtual string Identite {
get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); }
}
La parola chiave virtual ha reso Identity una proprietà virtuale. Questa parola chiave può essere applicata anche ai metodi. Le classi figlie che ridefiniscono una proprietà o un metodo virtuale devono quindi utilizzare la parola chiave override invece di new per qualificare la proprietà/il metodo ridefinito. Pertanto, nella classe Teacher, la proprietà Identity viene ridefinita come segue:
public override string Identite {
get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); }
}
Il programma precedente produce quindi i seguenti risultati:
Questa volta, alla riga 3, abbiamo l'identità completa dell'insegnante. Ora ridefiniamo un metodo anziché una proprietà. La classe object (alias C# di System.Object) è la classe "madre" di tutte le classi C#. Quindi, quando scrivi:
scriviamo implicitamente:
La classe System.Object definisce un metodo virtuale ToString:
![]() |
Il metodo ToString restituisce il nome della classe a cui appartiene l'oggetto, come mostrato nell'esempio seguente:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a teacher
Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString());
// a person
Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString());
}
}
}
I risultati sono i seguenti:
Si noti che, sebbene non abbiamo ridefinito il metodo ToString nelle classi Person e Teacher, possiamo comunque vedere che il metodo ToString della classe Object è stato in grado di visualizzare il vero nome della classe dell'oggetto.
Ridefiniamo il metodo ToString nelle classi Person e Teacher:
// méthode ToString
public override string ToString() {
return Identite;
}
La definizione è la stessa in entrambe le classi. Consideriamo il seguente programma di test:
using System;
namespace Chap2 {
class Program3 {
public static void Main() {
// a teacher
Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61);
Affiche(e);
// a person
Personne p = new Personne("Jean", "Dupont", 30);
Affiche(p);
}
// poster
public static void Affiche(Personne p) {
// displays identity of p
Console.WriteLine(p);
}//Poster
}
}
Esaminiamo il metodo Poster il cui parametro è una persona p. Alla riga 15, la classe Console non ha una variante che accetti un parametro di tipo Person. Tra i vari WriteLine, ce n'è uno che accetta un Object. Il compilatore utilizzerà questo metodo, WriteLine(Object o), poiché questa firma implica che o possa essere di tipo Object o derivato. Poiché Object è la classe padre di tutte le classi, qualsiasi oggetto può essere passato come parametro a WriteLine e quindi un oggetto di tipo Person o Teacher. Il metodo WriteLine(Object o) scrive o.ToString() nel flusso di scrittura Out. Essendo il metodo ToString virtuale, se l'oggetto o (di tipo Object o derivato) ha ridefinito il ToString, verrà utilizzato quest'ultimo. Questo è il caso qui con Person e Teacher.
Ecco cosa mostrano i risultati delle prestazioni:
4.3. Ridefinire il significato di un operatore per una classe
4.3.1. Introduzione
Consideriamo l'istruzione
dove op1 e op2 sono due operandi. È possibile ridefinire il significato dell'operatore +. Se l'operando op1 è un oggetto della classe C1, è necessario definire un metodo statico nella classe C1 con la seguente firma:
Quando il compilatore incontra il
Lo traduce quindi come C1.operator+(op1, op2). Il tipo restituito dall'operatore è importante. Consideriamo l'operazione op1+op2+op3. Viene tradotta dal compilatore come (op1+op2)+op3. Sia res12 il risultato di op1+op2. L'operazione successiva è res12+op3. Se il tipo di res12 è C1, verrà tradotta anch'essa come C1.operator+(res12,op3). Ciò rende possibile concatenare le operazioni.
Anche gli operatori unari con un singolo operando possono essere ridefiniti. Ad esempio, se op1 è un oggetto di tipo C1, l'operazione op1++ può essere ridefinita da un metodo statico di C1 :
Quanto detto qui vale per la maggior parte degli operatori, con poche eccezioni:
- gli operatori == e != devono essere ridefiniti contemporaneamente
- gli operatori &&, ||, [], (), +=, -=, ... non possono essere ridefiniti
4.3.2. Un esempio
Creiamo una classe ListeDePersonnes derivata da ArrayList. Questa classe implementa una lista dinamica e viene presentata nel capitolo seguente. Utilizziamo solo i seguenti elementi di questa classe:
- il metodo L.Add(Object o) per aggiungere alla L un oggetto o. Qui l'oggetto o sarà un oggetto Person.
- la proprietà L.Count che fornisce il numero di elementi nella lista L
- notazione L[i] che fornisce l'elemento i della lista L
La classe ListeDePersonnes erediterà tutti gli attributi, i metodi e le proprietà di ArrayList. La sua definizione è la seguente:
using System;
using System.Collections;
using System.Text;
namespace Chap2 {
class ListeDePersonnes : ArrayList{
// redefine + operator, to add a person to the list
public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) {
// person p is added to the ListeDePersonnes l
l.Add(p);
// we return the ListeDePersonnes l
return l;
}// operator +
// ToString
public override string ToString() {
// render (él1, él2, ..., éln)
// opening parenthesis
StringBuilder listeToString = new StringBuilder("(");
// browse the list of people (this)
for (int i = 0; i < Count - 1; i++) {
listeToString.Append(this[i]).Append(",");
}//for
// last element
if (Count != 0) {
listeToString.Append(this[Count-1]);
}
// closing parenthesis
listeToString.Append(")");
// you must return a string
return listeToString.ToString();
}//ToString
}
}
- riga 6: la classe ListeDePersonnes deriva dalla classe ArrayList
- righe 8-13: definizione dell'operatore + per l'operazione l + p, dove l è di tipo ListeDePersonnes e p di tipo Person o derivato.
- riga 10: la persona p viene aggiunta alla lista l. Qui viene utilizzata la classe padre ArrayList di Add.
- riga 12: il riferimento alla lista l viene reso in modo che gli operatori + possano essere concatenati, come in l + p1 + p2. L'operazione l+p1+p2 sarà interpretata (priorità dell'operatore) come (l+p1)+p2. L'operazione l+p1 crea il riferimento l. L'operazione (l+p1)+p2 diventa quindi l+p2, che aggiunge la persona p2 alla lista di persone l.
- riga 16: ridefiniamo ToString per visualizzare un elenco di persone come (persona1, persona2, ..) dove persona1 è a sua volta il risultato della classe ToString Person.
- riga 19: utilizziamo un oggetto di tipo StringBuilder. Questa classe è più adatta rispetto alla stringa non appena sono necessarie numerose operazioni sulle stringhe, in questo caso le aggiunte. Infatti, ogni operazione su una stringa crea un nuovo oggetto stringa, mentre le stesse operazioni su uno StringBuilder modificano l'oggetto senza crearne uno nuovo. Utilizziamo il metodo Append per concatenare le stringhe.
- riga 21: scorrimento degli elementi dell'elenco delle persone. Questo elenco è qui designato da this. Si tratta dell'oggetto corrente su cui viene eseguito il metodo ToString. La proprietà Count è una proprietà della classe padre ArrayList.
- riga 22: l'elemento n. i nell'elenco corrente this è accessibile tramite la notazione this[i]. Ancora una volta, si tratta di una proprietà di ArrayList. Poiché si tratta di aggiungere stringhe, verrà utilizzato this[i].ToString(). Trattandosi di un metodo virtuale, verrà utilizzato l'oggetto ToString this, di tipo Person o derivato.
- riga 31: dobbiamo restituire un oggetto di tipo stringa (riga 16). La classe StringBuilder dispone di un metodo ToString che consente di passare da uno StringBuilder a un tipo stringa.
Si noti che ListeDePersonnes non ha un costruttore. In questo caso, sappiamo che il
. Questo costruttore non fa altro che chiamare il costruttore senza parametri della sua classe padre:
Una classe di test potrebbe apparire così:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// a list of people
ListeDePersonnes l = new ListeDePersonnes();
// add people
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// display
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
}
}
}
- riga 7: creazione di un elenco di persone l
- riga 9: aggiunta di 2 persone con l'operatore +
- riga 12: insegnante aggiunto
- righe 11 e 13: utilizzo del metodo ridefinito ListeDePersonnes.ToString().
I risultati:
4.4. Definizione di un indicizzatore per una classe
Continuiamo qui a utilizzare la classe ListeDePersonnes. Se l è un oggetto ListeDePersonnes, vogliamo poter utilizzare l[i] per designare la persona n. i nella lista l sia in lettura (Person p=l[i]) che in scrittura (l[i]=new Person(...)).
Per poter scrivere l[i] dove l[i] designa un oggetto Person, classe, dobbiamo definire per ListeDePersonnes il seguente metodo:
public Personne this[int i] {
get { ... }
set { ... }
}
Il metodo si chiama this[int i], un indicizzatore perché dà significato all'espressione obj[i] che ricorda la notazione degli array, mentre obj non è un array ma un oggetto. Il metodo get di questo oggetto obj viene richiamato quando si scrive variable=obj[i] e il metodo set quando si scrive obj[i]=value.
La classe ListeDePersonnes deriva dalla classe ArrayList, che a sua volta dispone di un indice:
Esiste un conflitto tra la classe ListeDePersonnes e:
public Personne this[int i]
e la classe ArrayList di this
public object this[int i]
poiché hanno lo stesso nome e lo stesso tipo di parametro (int). Per indicare che la classe ListeDePersonnes "mette in cache" il metodo omonimo della classe ArrayList, dobbiamo aggiungere la parola chiave new alla dichiarazione della classe ListeDePersonnes. Scriviamo quindi:
public new Personne this[int i]{
get { ... }
set { ... }
}
Completiamo questo metodo. Il metodo this.get viene chiamato quando, ad esempio, variable=l[i], dove l è di tipo ListeDePersonnes. Dobbiamo quindi restituire la persona n° i dalla lista l. Ciò avviene con la notazione base[i], che crea l'oggetto n° i della classe ArrayList sottostante a ListeDePersonnes. L'oggetto restituito è di tipo Object, è quindi necessario un transtyping verso la classe Person.
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { ... }
}
Il metodo set viene chiamato quando l[i]=p, dove p è una Person. Lo scopo è quello di associare la persona p all'elemento i in l.
public new Personne this[int i]{
get { ... }
set { base[i]=value; }
}
Qui, la persona p rappresentata dalla parola chiave value viene assegnata all'elemento n. i della classe base ArrayList.
L'indicizzatore di classe ListeDePersonnes sarà quindi il seguente:
public new Personne this[int i]{
get { return (Personne) base[i]; }
set { base[i]=value; }
}
Ora vogliamo poter scrivere Person p=l["name"], ovvero indicizzare la lista l non in base al numero dell'elemento, ma al nome della persona. Per farlo, definiamo un nuovo indicizzatore:
// indexeur via un nom
public int this[string nom] {
get {
// on recherche la personne
for (int i = 0; i < Count; i++) {
if (((Personne)base[i]).Nom == nom)
return i;
}//for
return -1;
}//get
}
La prima riga
public int this[string nom]
indica che la classe ListeDePersonnes accetta un nome sotto forma di stringa e che il risultato di l[name] è un numero intero. Questo numero intero corrisponderà alla posizione nell'elenco della persona con il nome name oppure a -1 se la persona non è presente nell'elenco. È previsto solo l'accesso in lettura (get), il che impedisce di scrivere l["name"] = value, operazione che avrebbe richiesto la definizione di un set. La parola chiave new non è richiesta nella dichiarazione dell'indicizzatore, poiché la classe base ArrayList non definisce un indicizzatore this[string].
Nel corpo del get, viene eseguita una scansione dell'elenco delle persone alla ricerca del nome passato come parametro. Se viene trovato nella posizione i, viene restituito i, altrimenti viene restituito -1.
Il programma di test precedente viene completato come segue:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// a list of people
ListeDePersonnes l = new ListeDePersonnes();
// add people
l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12);
// display
Console.WriteLine("l=" + l);
l = l + new Enseignant("camille", "germain",27,60);
Console.WriteLine("l=" + l);
// change item 1
l[1] = new Personne("franck", "gallon",5);
// display element 1
Console.WriteLine("l[1]=" + l[1]);
// display list l
Console.WriteLine("l=" + l);
// people search
string[] noms = { "martin", "germain", "xx" };
for (int i = 0; i < noms.Length; i++) {
int inom = l[noms[i]];
if (inom != -1)
Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]);
else
Console.WriteLine("Personne(" + noms[i] + ") n'existe pas");
}//for
}
}
}
La sua esecuzione fornisce i seguenti risultati:
4.5. Le strutture
La struttura in C# è analoga alla struttura del linguaggio C ed è molto vicina al concetto di classe. Una struttura è definita come segue:
Nonostante le dichiarazioni simili, esistono differenze significative tra classe e struttura. Ad esempio, il concetto di ereditarietà non esiste nelle strutture. Se stiamo scrivendo una classe che non deve essere derivata, quali sono le differenze tra struttura e classe che ci aiuteranno a scegliere tra le due? Usiamo il seguente esempio per scoprirlo:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// a sp1 structure
SPersonne sp1;
sp1.Nom = "paul";
sp1.Age = 10;
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
// a sp2 structure
SPersonne sp2 = sp1;
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// sp2 is modified
sp2.Nom = "nicole";
sp2.Age = 30;
// checking sp1 and sp2
Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")");
Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")");
// an op1 object
CPersonne op1=new CPersonne();
op1.Nom = "paul";
op1.Age = 10;
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
// an op2 object
CPersonne op2=op1;
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
// op2 is modified
op2.Nom = "nicole";
op2.Age = 30;
// op1 and op2 verification
Console.WriteLine("op1=CPersonne(" + op1.Nom + "," + op1.Age + ")");
Console.WriteLine("op2=CPersonne(" + op2.Nom + "," + op2.Age + ")");
}
}
// structure SPersonne
struct SPersonne {
public string Nom;
public int Age;
}
// class CPersonne
class CPersonne {
public string Nom;
public int Age;
}
}
- righe 38-41: una struttura con due campi pubblici: Nom, Age
- righe 44-47: una classe con due campi pubblici: Nom, Age
Se eseguiamo questo programma, otteniamo i seguenti risultati:
Laddove prima utilizzavamo una Person, ora utilizziamo una SPersonne :
struct SPersonne {
public string Nom;
public int Age;
}
La struttura qui non ha alcun costruttore. Potrebbe averne uno, come vedremo più avanti. Per impostazione predefinita, ha sempre il costruttore senza parametri, qui SPersonne().
- riga 7 del codice: la dichiarazione
SPersonne sp1;
è equivalente all'istruzione:
SPersonne sp1=new Spersonne();
Viene creata una struttura (Nome,Età) e il valore di sp1 è proprio questa struttura. Nel caso della classe, la creazione dell'oggetto (Nome,Età) deve essere effettuata esplicitamente tramite l'operatore new (riga 22):
CPersonne op1=new CPersonne();
L'istruzione precedente crea un CPersonne (approssimativamente equivalente alla nostra struttura) e il valore di p1 è quindi l'indirizzo (il riferimento) di questo oggetto.
Riassumendo
- nel caso della struttura, il valore di sp1 è la struttura stessa
- nel caso della classe, il valore di op1 è l'indirizzo dell'oggetto creato
![]() |
Quando nel programma scriviamo la riga 12:
SPersonne sp2 = sp1;
viene creata una nuova struttura sp2(Nome,Età) e inizializzata con il valore di sp1, ovvero la struttura stessa.
![]() |
La struttura di sp1 viene duplicata in sp2 [1]. Si tratta di una copia di un valore. Consideriamo ora l'istruzione alla riga 27:
CPersonne op2=op1;
Nel caso delle classi, il valore di op1 viene copiato in op2, ma poiché questo valore è in realtà l'indirizzo dell'oggetto, non viene duplicato [2].
Nel caso delle strutture [1], se modifichiamo il valore di sp2, viene modificato anche il valore di sp1, come mostrato nel programma. Nel caso degli oggetti [2], se modifichiamo l'oggetto puntato da op2, viene modificato anche quello puntato da op1 poiché si tratta dello stesso oggetto. Ciò è dimostrato anche dai risultati del programma.
Queste spiegazioni dimostrano che:
- il valore di una variabile di struttura è la struttura stessa
- il valore di una variabile oggetto è l'indirizzo dell'oggetto a cui si fa riferimento
Una volta compresa questa differenza fondamentale, la struttura è molto simile alla classe, come mostra il seguente nuovo esempio:
using System;
namespace Chap2 {
// structure SPersonne
struct SPersonne {
// private attributes
private string nom;
private int age;
// properties
public string Nom {
get { return nom; }
set { nom = value; }
}//name
public int Age {
get { return age; }
set { age = value; }
}//age
// Manufacturer
public SPersonne(string nom, int age) {
this.nom = nom;
this.age = age;
}//manufacturer
// ToString
public override string ToString() {
return "SPersonne(" + Nom + "," + Age + ")";
}//ToString
}//structure
}//namespace
- righe 8-9: due campi privati
- righe 12-20: proprietà pubbliche associate
- righe 23-26: definizione di un costruttore. Si noti che il costruttore senza parametri SPersonne() è sempre presente e non deve essere dichiarato. La sua dichiarazione viene rifiutata dal compilatore. Nel costruttore alle righe 23-26, si potrebbe essere tentati di inizializzare i campi privati name e age tramite le loro proprietà pubbliche Name e Age. Ciò viene rifiutato dal compilatore. I metodi della struttura non possono essere utilizzati durante la costruzione della struttura.
- righe 29-31: ridefinizione del metodo ToString.
Un programma di prova potrebbe apparire così:
using System;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// one person p1
SPersonne p1=new SPersonne();
p1.Nom="paul";
p1.Age= 10;
Console.WriteLine("p1={0}",p1);
// one person p2
SPersonne p2 = p1;
Console.WriteLine("p2=" + p2);
// p2 is modified
p2.Nom = "nicole";
p2.Age = 30;
// checking p1 and p2
Console.WriteLine("p1=" + p1);
Console.WriteLine("p2=" + p2);
// one person p3
SPersonne p3 = new SPersonne("amandin", 18);
Console.WriteLine("p3=" + p3);
// one person p4
SPersonne p4 = new SPersonne { Nom = "x", Age = 10 };
Console.WriteLine("p4=" + p4);
}
}
}
- riga 7: siamo obbligati a utilizzare esplicitamente il costruttore senza parametri, poiché nella struttura è presente un altro costruttore. Se la struttura non avesse alcun costruttore, l'istruzione
SPersonne p1;
sarebbe stata sufficiente per creare una struttura vuota.
- righe 8-9: la struttura viene inizializzata tramite le sue proprietà pubbliche
- riga 10: il metodo p1.ToString verrà utilizzato in WriteLine.
- riga 21: creazione di una struttura con il costruttore SPersonne(string,int)
- riga 24: creazione di una struttura utilizzando il costruttore senza parametri SPersonne() con, tra parentesi graffe, l'inizializzazione dei campi privati tramite le loro proprietà pubbliche.
Si ottengono i seguenti risultati:
L'unica differenza degna di nota qui tra struttura e classe è che, con una classe, gli oggetti p1 e p2 avrebbero puntato allo stesso oggetto alla fine del programma.
4.6. Interfacce
Un'interfaccia è un insieme di metodi o proprietà prototipo che formano un contratto. Una classe che decide di implementare un'interfaccia si impegna a fornire un'implementazione di tutti i metodi definiti nell'interfaccia. Il compilatore verifica questa implementazione.
Ad esempio, ecco la definizione dell'interfaccia System.Collections.IEnumerator :
public interface System.Collections.IEnumerator
{ // Prop
e rties Object Curren
t { get; }
// Methods
bool MoveNe
xt(); void Reset(); }
Le proprietà e i metodi dell'interfaccia sono definiti solo dalle loro firme. Non sono implementati (non hanno codice). Sono le classi che implementano l'interfaccia a fornire il codice ai metodi e alle proprietà dell'interfaccia.
- riga 1: la classe C implementa la classe IEnumerator. Si noti che il segno : utilizzato per implementare un'interfaccia è lo stesso utilizzato per derivare una classe.
- righe 3-5: implementazione dei metodi e delle proprietà dell'interfaccia IEnumerator.
Si consideri la seguente interfaccia:
namespace Chap2 {
public interface IStats {
double Moyenne { get; }
double EcartType();
}
}
L'interfaccia IStats presenta:
- una proprietà di sola lettura Average: per calcolare la media di una serie di valori
- un metodo EcartType: per calcolare la deviazione standard
Si noti che in nessun punto viene specificata quale serie di valori sia coinvolta. Potrebbe trattarsi della media dei voti di una classe, della media delle vendite mensili di un determinato prodotto, della temperatura media in una data località, ecc. Questo è il principio delle interfacce: si presuppone l'esistenza di metodi nell'oggetto, ma non l'esistenza di dati specifici.
Una prima implementazione della classe IStats potrebbe essere una classe utilizzata per memorizzare i voti degli studenti di una classe in una determinata materia. Uno studente sarebbe caratterizzato dalla struttura Student come segue:
public struct Elève {
public string Nom { get; set; }
public string Prénom { get; set; }
}//Student
Lo studente sarebbe identificato dal nome e dal cognome. Le righe 2-3 mostrano le proprietà automatiche per questi due attributi.
Una nota sarebbe caratterizzata dalla struttura Note come segue:
public struct Note {
public Elève Elève { get; set; }
public double Valeur { get; set; }
}//Note
Il voto sarebbe identificato dallo studente valutato e dal voto stesso. Le righe 2-3 mostrano le proprietà automatiche per questi due attributi.
I voti di tutti gli studenti in una determinata materia sono raggruppati nella classe TableauDeNotes di seguito:
using System;
using System.Text;
namespace Chap2 {
public class TableauDeNotes : IStats {
// attributes
public string Matière { get; set; }
public Note[] Notes { get; set; }
public double Moyenne { get; private set; }
private double ecartType;
// manufacturer
public TableauDeNotes(string matière, Note[] notes) {
// saving via public properties
Matière = matière;
Notes = notes;
// calculating the average score
double somme = 0;
for (int i = 0; i < Notes.Length; i++) {
somme += Notes[i].Valeur;
}
if (Notes.Length != 0) Moyenne = somme / Notes.Length;
else Moyenne = -1;
// standard deviation
double carrés = 0;
for (int i = 0; i < Notes.Length; i++) {
carrés += Math.Pow((Notes[i].Valeur - Moyenne), 2);
}//for
if (Notes.Length != 0)
ecartType = Math.Sqrt(carrés / Notes.Length);
else ecartType = -1;
}//manufacturer
public double EcartType() {
return ecartType;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// concatenate all the notes
for (i = 0; i < Notes.Length-1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
};
//final note
if (Notes.Length != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
}
valeur.Append(")");
// end
return valeur.ToString();
}//ToString
}//class
}
- riga 6: la classe TableauDeNotes implementa l'interfaccia IStats. Deve quindi implementare le proprietà Average ed EcartType. Queste sono implementate alla riga 10 (Average) e alle righe 35-37 (EcartType)
- righe 8-10: tre proprietà automatiche
- riga 8: il materiale di cui l'oggetto memorizza le note
- riga 9: tabella dei voti degli studenti (Student, Grade)
- riga 10: punteggio medio - proprietà che implementa l'interfaccia Average di IStats.
- riga 11: campo che memorizza la deviazione standard dei punteggi - il metodo get associato a EcartType nelle righe 35-37 implementa l'interfaccia EcartType di IStats.
- riga 9: i voti vengono memorizzati in una tabella. Questa viene trasmessa al costruttore delle righe 14-33 al momento della creazione della classe TableauDeNotes.
- righe 14-33: il costruttore. Si presume qui che i punteggi trasmessi al costruttore non cambieranno in futuro. Utilizziamo quindi il costruttore per calcolare immediatamente la media e la deviazione standard di questi punteggi e memorizzarli nei campi delle righe 10-11. La media è memorizzata nel campo privato sottostante la proprietà automatica Average alla riga 10 e la deviazione standard nel campo privato alla riga 11.
- riga 10: il metodo get della proprietà automatica Average renderà il campo privato sottostante.
- righe 35-37: il metodo EcartType restituisce il valore del campo privato alla riga 11.
Ci sono alcune sottigliezze in questo codice:
- riga 23: il metodo set property Average viene utilizzato per effettuare l'assegnazione. Questo metodo è stato dichiarato privato alla riga 10, in modo che l'assegnazione di un valore a Average sia possibile solo all'interno della classe.
- righe 40-54: si utilizza un oggetto StringBuilder per costruire la stringa che rappresenta TableauDeNotes al fine di migliorare le prestazioni. Va notato, tuttavia, che la leggibilità del codice ne risente notevolmente. Questo è il rovescio della medaglia.
Nella classe precedente, le note erano memorizzate in una tabella. Non era possibile aggiungere una nuova nota una volta creato TableauDeNotes. Proponiamo ora una seconda implementazione di IStats, chiamata ListeDeNotes, in cui questa volta le note verrebbero salvate in una lista, con la possibilità di aggiungere note dopo la creazione iniziale dell'oggetto ListeDeNotes.
Il codice della classe ListeDeNotes è il seguente:
using System;
using System.Text;
using System.Collections.Generic;
namespace Chap2 {
public class ListeDeNotes : IStats {
// attributes
public string Matière { get; set; }
public List<Note> Notes { get; set; }
public double moyenne = -1;
public double ecartType = -1;
// manufacturer
public ListeDeNotes(string matière, List<Note> notes) {
// saving via public properties
Matière = matière;
Notes = notes;
}//manufacturer
// add a note
public void Ajouter(Note note) {
// add note
Notes.Add(note);
// mean and standard deviation reset
moyenne = -1;
ecartType = -1;
}
// ToString
public override string ToString() {
StringBuilder valeur = new StringBuilder(String.Format("matière={0}, notes=(", Matière));
int i;
// concatenate all the notes
for (i = 0; i < Notes.Count - 1; i++) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("],");
};
//final note
if (Notes.Count != 0) {
valeur.Append("[").Append(Notes[i].Elève.Prénom).Append(",").Append(Notes[i].Elève.Nom).Append(",").Append(Notes[i].Valeur).Append("]");
}
valeur.Append(")");
// end
return valeur.ToString();
}//ToString
// average score
public double Moyenne {
get {
if (moyenne != -1) return moyenne;
// calculating the average score
double somme = 0;
for (int i = 0; i < Notes.Count; i++) {
somme += Notes[i].Valeur;
}
// we return the average
if (Notes.Count != 0) moyenne = somme / Notes.Count;
return moyenne;
}
}
public double EcartType() {
// standard deviation
if (ecartType != -1) return ecartType;
// average
double moyenne = Moyenne;
double carrés = 0;
for (int i = 0; i < Notes.Count; i++) {
carrés += Math.Pow((Notes[i].Valeur - moyenne), 2);
}//for
// we return the standard deviation
if (Notes.Count != 0)
ecartType = Math.Sqrt(carrés / Notes.Count);
return ecartType;
}
}//class
}
- riga 7: la classe ListeDeNotes implementa l'interfaccia IStats
- riga 10: le note vengono ora visualizzate in un elenco anziché in una tabella
- riga 11: la proprietà automatica della classe Average TableauDeNotes è stata qui abbandonata a favore di un campo privato average, alla riga 11, associato alla proprietà pubblica di sola lettura Average alle righe 48-60
- righe 22-28: ora è possibile aggiungere una nota a quelle già memorizzate, cosa che prima era impossibile.
- righe 15-19: di conseguenza, la media e la deviazione standard non vengono più calcolate nel costruttore, ma nei metodi dell'interfaccia stessi: Average (righe 48-60) ed EcartType (62-76). Tuttavia, il ricalcolo viene riavviato solo se la media e la deviazione standard sono diverse da -1 (righe 50 e 64).
Una classe di test potrebbe apparire così:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program1 {
static void Main(string[] args) {
// some students & english notes
Elève[] élèves1 = { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
// which we save in a TableauDeNotes object
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// average and standard deviation display
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais);
// we put the students and the material in a ListeDeNotes object
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// average and standard deviation display
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
// we add a note
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// average and standard deviation display
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", français.Moyenne, français.EcartType(), français);
}
}
}
- riga 8: creazione di un array di studenti utilizzando il costruttore senza parametri e l'inizializzazione tramite proprietà pubbliche
- riga 9: creazione di una tabella di note utilizzando la stessa tecnica
- riga 11: un oggetto TableauDeNotes la cui media e deviazione standard vengono calcolate alla riga 13
- riga 15: un oggetto ListeDeNotes la cui media e deviazione standard vengono calcolate alla riga 17. La classe List<Note> ha un costruttore che ammette un oggetto che implementa l'interfaccia IEnumerable<Note>. La tabella notes1 implementa questa interfaccia e può essere utilizzata per costruire la List<Note>.
- riga 19: nuova nota aggiunta
- riga 21: ricalcolo della media e della deviazione standard
I risultati sono i seguenti:
Nell'esempio precedente, due classi implementano l'interfaccia IStats. Detto questo, l'esempio non mostra quanto sia utile l'interfaccia IStats. Riscriviamo il programma di test come segue:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// some students & english notes
Elève[] élèves1 = { new Elève { Prénom = "Paul", Nom = "Martin" }, new Elève { Prénom = "Maxime", Nom = "Germain" }, new Elève { Prénom = "Berthine", Nom = "Samin" } };
Note[] notes1 = { new Note { Elève = élèves1[0], Valeur = 14 }, new Note { Elève = élèves1[1], Valeur = 16 }, new Note { Elève = élèves1[2], Valeur = 18 } };
// which we save in a TableauDeNotes object
TableauDeNotes anglais = new TableauDeNotes("anglais", notes1);
// average and standard deviation display
AfficheStats(anglais);
// we put the students and the material in a ListeDeNotes object
ListeDeNotes français = new ListeDeNotes("français", new List<Note>(notes1));
// average and standard deviation display
AfficheStats(français);
// we add a note
français.Ajouter(new Note { Elève = new Elève { Prénom = "Jérôme", Nom = "Jaric" }, Valeur = 10 });
// average and standard deviation display
AfficheStats(français);
}
// display mean and standard deviation of a type IStats
static void AfficheStats(IStats valeurs) {
Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs);
}
}
}
- righe 25-27: il metodo statico AfficheStats riceve un IStats, tipo Interfaccia. Ciò significa che il parametro effettivo può essere qualsiasi oggetto che implementi IStats. Quando si utilizzano dati con un tipo di interfaccia, ciò significa che si utilizzano solo i metodi dell'interfaccia implementati dai dati. Il resto viene ignorato. Si tratta di una proprietà simile al polimorfismo osservato per le classi. Se un insieme di Ci non collegate tramite ereditarietà (quindi non è possibile utilizzare il polimorfismo di ereditarietà) presenta un insieme di metodi con la stessa firma, può essere interessante raggruppare questi metodi in un'interfaccia I implementata da tutte le classi interessate. Le istanze di queste classi Ci possono quindi essere utilizzate come parametri effettivi di funzioni che ammettono un parametro formale di tipo I, c.a.d. funzioni che utilizzano solo i metodi degli oggetti Ci definiti nell'I e non gli attributi e i metodi delle singole classi Ci.
- riga 13: il metodo AfficheStats viene chiamato con un TableauDeNotes che implementa l'IStats
- riga 17: idem con un tipo ListeDeNotes
I risultati di questa esecuzione sono identici a quelli della precedente.
Una variabile può essere di tipo interfaccia. Pertanto, possiamo scrivere:
L'istruzione alla riga 1 indica che stats1 è l'istanza di una classe che implementa l'interfaccia IStats. Questa istruzione implica che il compilatore consentirà l'accesso solo ai metodi dell'interfaccia stats1: Average ed EcartType.
Infine, va notato che le interfacce possono essere implementate in diversi modi, ovvero possono essere scritte come
dove le Ij sono interfacce.
4.7. Classi astratte
Una classe astratta è una classe che non può essere istanziata. È necessario creare classi derivate che possano essere istanziate.
Le classi astratte possono essere utilizzate per fattorizzare il codice di una linea di classi. Si consideri il seguente caso:
using System;
namespace Chap2 {
abstract class Utilisateur {
// fields
private string login;
private string motDePasse;
private string role;
// manufacturer
public Utilisateur(string login, string motDePasse) {
// information is recorded
this.login = login;
this.motDePasse = motDePasse;
// on identifie l'utilisateur
role=identifie();
// identified?
if (role == null) {
throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse));
}
}
// toString
public override string ToString() {
return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role);
}
// identifies
abstract public string identifie();
}
}
- righe 11-21: il costruttore di classe User. Questa classe memorizza le informazioni sull'utente di un'applicazione web. Questa applicazione ha vari tipi di utenti autenticati tramite login/password (righe 6-7). Queste due informazioni vengono verificate con un servizio LDAP per alcuni utenti, con un SGBD per altri, ecc...
- righe 13-14: le informazioni di autenticazione sono memorizzate in memoria
- riga 16: vengono verificate da un metodo identifies. Poiché il metodo di identificazione non è noto, viene dichiarato astratto alla riga 29 con la parola chiave abstract. Il metodo identifies restituisce una stringa che specifica il ruolo dell'utente (in sostanza, ciò che è autorizzato a fare). Se questa stringa è nulla, viene generata un'eccezione alla riga 19.
- riga 4: poiché contiene un metodo astratto, la classe stessa è dichiarata astratta con la parola chiave abstract.
- riga 29: il metodo astratto identifies non ha una definizione. Le classi derivate ne forniranno una.
- righe 24-26: il metodo ToString che identifica un'istanza della classe.
Si presume qui che lo sviluppatore desideri controllare la costruzione delle istanze della classe User e delle classi derivate, forse perché vuole essere sicuro che venga generata un'eccezione di un certo tipo se l'utente non viene riconosciuto (riga 19). Le classi derivate possono fare affidamento su questo costruttore. Per farlo, devono fornire il metodo ToString.
La classe ExceptionUtilisateurInconnu è la seguente:
using System;
namespace Chap2 {
class ExceptionUtilisateurInconnu : Exception {
public ExceptionUtilisateurInconnu(string message) : base(message){
}
}
}
- riga 3: deriva dalla classe Exception
- righe 4-6: ha un unico costruttore che accetta un messaggio di errore come parametro. Questo viene passato alla classe padre (riga 5), che ha lo stesso costruttore.
Ora deriviamo la classe User dalla classe Director delle ragazze:
namespace Chap2 {
class Administrateur : Utilisateur {
// manufacturer
public Administrateur(string login, string motDePasse)
: base(login, motDePasse) {
}
// identifies
public override string identifie() {
// identification LDAP
// ...
return "admin";
}
}
}
- righe 4-6: il costruttore passa semplicemente i parametri ricevuti alla classe padre
- righe 9-12: il metodo identifica la classe Director. Si presume che un amministratore sia identificato da un sistema LDAP. Questo metodo ridefinisce gli identificatori della sua classe padre. Poiché ridefinisce un metodo astratto, è inutile inserire la parola chiave override.
Ora deriviamo la classe User dalla classe Observer:
namespace Chap2 {
class Observateur : Utilisateur{
// manufacturer
public Observateur(string login, string motDePasse)
: base(login, motDePasse) {
}
//identifies
public override string identifie() {
// identification SGBD
// ...
return "observateur";
}
}
}
- righe 4-6: il costruttore si limita a passare i parametri ricevuti alla classe padre
- righe 9-13: il metodo identifica la classe Observer. Si presume che un osservatore venga identificato verificando i suoi dati identificativi in un database.
Alla fine, gli oggetti Director e Observer vengono istanziati dallo stesso costruttore della classe padre User. Questo costruttore utilizzerà le identificazioni fornite da queste classi.
Una terza classe, Unknown, deriva anch'essa da User:
namespace Chap2 {
class Inconnu : Utilisateur{
// manufacturer
public Inconnu(string login, string motDePasse)
: base(login, motDePasse) {
}
//identifies
public override string identifie() {
// unknown user
// ...
return null;
}
}
}
- riga 13: il metodo imposta il puntatore a null per indicare che l'utente non è stato riconosciuto.
Un programma di prova potrebbe essere simile a questo:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
Console.WriteLine(new Observateur("observer","mdp1"));
Console.WriteLine(new Administrateur("admin", "mdp2"));
try {
Console.WriteLine(new Inconnu("xx", "yy"));
} catch (ExceptionUtilisateurInconnu e) {
Console.WriteLine("Utilisateur non connu : "+ e.Message);
}
}
}
}
Si noti che le righe 6, 7 e 9 utilizzano [User].ToString(), che verrà utilizzato da WriteLine.
I risultati sono i seguenti:
4.8. Classi, interfacce e metodi generici
Supponiamo di voler scrivere un metodo che permuti due numeri interi. Questo metodo potrebbe essere il seguente:
public static void Echanger1(ref int value1, ref int value2){
// on échange les références value1 et value2
int temp = value2;
value2 = value1;
value1 = temp;
}
Ora, se volessimo scambiare due riferimenti all'oggetto Person, scriveremmo:
public static void Echanger2(ref Personne value1, ref Personne value2){
// on échange les références value1 et value2
Personne temp = value2;
value2 = value1;
value1 = temp;
}
La differenza tra i due metodi sta nel tipo T dei parametri: int in Exchange1, Person in Exchange2. Le classi generiche e le interfacce soddisfano l'esigenza di metodi che differiscono solo per il tipo di alcuni dei loro parametri.
Con una classe generica, l'Exchange potrebbe essere riscritto come segue:
namespace Chap2 {
class Generic1<T> {
public static void Echanger(ref T value1, ref T value2){
// exchange the value1 and value2 references
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- riga 2: la classe Generic1 è parametrizzata da un tipo indicato con T. È possibile assegnarle qualsiasi nome si desideri. Questo tipo T viene poi riutilizzato nella classe alle righe 3 e 5. Si dice che Generic1 è una classe generica.
- riga 3: definisce i due riferimenti al tipo T da scambiare
- riga 5: la variabile temporanea temp è di tipo T.
Un programma di test per la classe potrebbe essere il seguente:
using System;
namespace Chap2 {
class Program {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic1<int>.Echanger(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic1<string>.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic1<Personne>.Echanger(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- riga 8: quando si utilizza una classe generica parametrizzata dai tipi T1, T2, ... questi devono essere "istanziati". Riga 8: utilizzare il metodo statico Exchange di tipo Generic1<int> per indicare che i riferimenti passati a Exchange sono di tipo int.
- riga 12: si utilizza il metodo statico Exchange di tipo Generic1<string> per indicare che i riferimenti passati a Exchange sono di tipo string.
- riga 16: si utilizza il metodo statico Exchange di tipo Generic1<Person> per indicare che i riferimenti passati a Exchange sono di tipo Person.
I risultati sono i seguenti:
Il metodo Exchange avrebbe potuto essere scritto anche nel modo seguente:
namespace Chap2 {
class Generic2 {
public static void Echanger<T>(ref T value1, ref T value2){
// exchange the value1 and value2 references
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- riga 2: la classe Generic2 non è più generica
- riga 3: il metodo statico Exchange è generico
Il programma di test è quindi il seguente:
using System;
namespace Chap2 {
class Program2 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic2.Echanger<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic2.Echanger<string>(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic2.Echanger<Personne>(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
- righe 8, 12 e 16: richiamano l'Exchange specificando il tipo di parametro tra <>. In realtà, il compilatore è in grado di dedurre la variante dell'Exchange da utilizzare. La seguente istruzione è quindi valida:
Generic2.Echanger(ref i1, ref i2);
...
Generic2.Echanger(ref s1, ref s2);
...
Generic2.Echanger(ref p1, ref p2);
Righe 1, 3 e 5: la variante del metodo Exchange non è più specificata. Il compilatore è in grado di dedurla dalla natura dei parametri effettivi utilizzati.
È possibile applicare vincoli ai parametri generici:

Si consideri il nuovo metodo generico Exchange riportato di seguito:
namespace Chap2 {
class Generic3 {
public static void Echanger<T>(ref T value1, ref T value2) where T : class {
// exchange the value1 and value2 references
T temp = value2;
value2 = value1;
value1 = temp;
}
}
}
- riga 3: il tipo T deve essere un riferimento (classe, interfaccia)
Si consideri il seguente programma di test:
using System;
namespace Chap2 {
class Program4 {
static void Main(string[] args) {
// int
int i1 = 1, i2 = 2;
Generic3.Echanger<int>(ref i1, ref i2);
Console.WriteLine("i1={0},i2={1}", i1, i2);
// string
string s1 = "s1", s2 = "s2";
Generic3.Echanger(ref s1, ref s2);
Console.WriteLine("s1={0},s2={1}", s1, s2);
// Person
Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55);
Generic3.Echanger(ref p1, ref p2);
Console.WriteLine("p1={0},p2={1}", p1, p2);
}
}
}
Il compilatore segnala un errore alla riga 8 perché il tipo int non è una classe né un'interfaccia, ma una struttura:

Consideriamo il nuovo metodo generico Exchange di seguito:
namespace Chap2 {
class Generic4 {
public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 {
// retrieve the value of the 2 elements
int value1 = element1.Value();
int value2 = element2.Value();
// if 1st element > 2nd element, exchange elements
if (value1 > value2) {
T temp = element2;
element2 = element1;
element1 = temp;
}
}
}
}
- riga 3: il tipo T deve implementare l'Interfaccia1. Dispone di un metodo Value, utilizzato alle righe 5 e 6, che restituisce il valore dell'oggetto di tipo T.
- righe 8-12: i due riferimenti element1 ed element2 vengono scambiati solo se il valore di element1 è maggiore del valore di element2.
L'interfaccia Interface1 è la seguente:
namespace Chap2 {
interface Interface1 {
int Value();
}
}
Viene implementata dalla Classe1 di seguito:
using System;
using System.Threading;
namespace Chap2 {
class Class1 : Interface1 {
// object value
private int value;
// manufacturer
public Class1() {
// wait 1 ms
Thread.Sleep(1);
// random value between 0 and 99
value = new Random(DateTime.Now.Millisecond).Next(100);
}
// accessor private field value
public int Value() {
return value;
}
// instance status
public override string ToString() {
return value.ToString();
}
}
}
- riga 5: Class1 implementa l'Interfaccia1
- riga 7: il valore di un'istanza di Class1
- righe 10-14: il valore del campo viene inizializzato con un valore casuale compreso tra 0 e 99
- righe 18-20: il metodo Value dell'interfaccia Interface1
- righe 23-25: il metodo ToString della classe
L'interfaccia Interface1 è implementata anche dalla Classe2 :
using System;
namespace Chap2 {
class Class2 : Interface1 {
// object values
private int value;
private String s;
// manufacturer
public Class2(String s) {
this.s = s;
value = s.Length;
}
// accessor private field value
public int Value() {
return value;
}
// instance status
public override string ToString() {
return s;
}
}
}
- riga 4: Class2 implementa l'Interfaccia1
- riga 6: il valore di un'istanza di Class2
- righe 10-13: il valore del campo viene inizializzato con la lunghezza della stringa passata al costruttore
- righe 16-18: il metodo Value dell'interfaccia Interface1
- righe 21-22: il metodo ToString della classe
Un programma di test potrebbe apparire così:
using System;
namespace Chap2 {
class Program5 {
static void Main(string[] args) {
// exchange instances of type Class1
Class1 c1, c2;
for (int i = 0; i < 5; i++) {
c1 = new Class1();
c2 = new Class1();
Console.WriteLine("Avant échange --> c1={0},c2={1}", c1, c2);
Generic4.Echanger(ref c1, ref c2);
Console.WriteLine("Après échange --> c1={0},c2={1}", c1, c2);
}
// exchange Class2 instances
Class2 c3, c4;
c3 = new Class2("xxxxxxxxxxxxxx");
c4 = new Class2("xx");
Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
Generic4.Echanger(ref c3, ref c4);
Console.WriteLine("Avant échange --> c3={0},c4={1}", c3, c4);
}
}
}
- righe 8-14: le istanze della Classe1 vengono scambiate
- righe 16-22: vengono scambiate le istanze di tipo Class2
I risultati sono i seguenti:
Per illustrare il concetto di interfaccia generica , ordineremo un array di persone prima in base al loro nome, poi in base alla loro età. Il metodo che utilizziamo per ordinare un array è il metodo statico della classe Array di Spell:

Ricordiamo che un metodo statico viene utilizzato anteponendo al metodo il nome della classe e non il nome di un'istanza della classe. Il metodo Spell ha diverse firme (è sovraccaricato). Useremo la seguente firma:
Spell è un metodo generico in cui T indica qualsiasi tipo. Il metodo riceve due parametri:
- T[] table : l'array di elementi T da ordinare
- IComparer<T> comparator : un riferimento a un oggetto che implementa l'interfaccia IComparer<T>.
IComparer<T> è un'interfaccia generica definita come segue:
L'interfaccia IComparer<T> ha un solo metodo. Il metodo Compare :
- accetta due elementi come parametri t1 e t2 di tipo T
- restituisce 1 se t1>t2, 0 se t1==t2, -1 se t1<t2. Spetta allo sviluppatore dare un significato agli operatori <, ==, >. Ad esempio, se p1 e p2 sono due oggetti Person, possiamo dire che p1>p2 se il nome di p1 precede quello di p2 in ordine alfabetico. Ordineremo quindi in ordine crescente per nome. Se si desidera ordinare per età, si dirà che p1>p2 se l'età di p1 è maggiore di quella di p2.
- Per ordinare in ordine decrescente, basta invertire i risultati +1 e -1
Sappiamo abbastanza per ordinare una tabella di persone. Il programma è il seguente:
using System;
using System.Collections.Generic;
namespace Chap2 {
class Program6 {
static void Main(string[] args) {
// a table of people
Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) };
// display
Affiche("Tableau à trier", personnes1);
// sort by name
Array.Sort(personnes1, new CompareNoms());
// display
Affiche("Tableau après le tri selon les nom et prénom", personnes1);
// sorted by age
Array.Sort(personnes1, new CompareAges());
// display
Affiche("Tableau après le tri selon l'âge", personnes1);
}
static void Affiche(string texte, Personne[] personnes) {
Console.WriteLine(texte.PadRight(50, '-'));
foreach (Personne p in personnes) {
Console.WriteLine(p);
}
}
}
// first and last name comparison class
class CompareNoms : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// compare names
int i = p1.Nom.CompareTo(p2.Nom);
if (i != 0)
return i;
// equal names - first names are compared
return p1.Prenom.CompareTo(p2.Prenom);
}
}
// age comparison class
class CompareAges : IComparer<Personne> {
public int Compare(Personne p1, Personne p2) {
// comparing ages
if (p1.Age > p2.Age)
return 1;
else if (p1.Age == p2.Age)
return 0;
else
return -1;
}
}
}
- riga 8: la tabella delle persone
- riga 12: ordina la tabella delle persone in base al nome e al cognome. Il secondo parametro del metodo generico Spell è un'istanza di CompareNoms che implementa l'interfaccia generica IComparer<Person>.
- righe 30-39: la classe CompareNoms che implementa l'interfaccia generica IComparer<Person>.
- righe 31-38: implementazione del metodo generico int CompareTo(T,T) dell'interfaccia IComparer<T>. Il metodo utilizza String.CompareTo, presentato in clipboard 3.3.5.4, per confrontare due stringhe.
- riga 16: ordina la tabella delle persone per età. Il secondo parametro del metodo generico Spell è un'istanza di CompareAges che implementa l'interfaccia generica IComparer<Person> e definita alle righe 42-51.
I risultati sono i seguenti:
4.9. Spazi dei nomi
Per scrivere una riga sullo schermo, usiamo l'istruzione
Se guardiamo la definizione di Console
Namespace: System
Assembly: Mscorlib (in Mscorlib.dll)
scopriamo che fa parte di System. Ciò significa che la Console dovrebbe essere indicata con System.Console e che dovremmo effettivamente scrivere:
Questo si evita utilizzando un using:
Si dice che si importa lo spazio dei nomi System con la clausola using. Quando il compilatore incontra il nome di una classe (in questo caso Console), cercherà di trovarla nei vari spazi dei nomi importati dalla clausola using. In questo caso troverà la classe Console nello spazio dei nomi System. Ora notiamo la seconda informazione associata alla classe Console:
Questa riga indica in quale "assembly" si trova la definizione della classe Console. Quando si compila al di fuori di Visual Studio e si devono fornire i riferimenti alle varie DLL contenenti le classi da utilizzare, questa informazione può essere utile. Per fare riferimento alle DLL necessarie per compilare una classe, scriviamo:
dove CSC è il compilatore C#. Quando creiamo una classe, possiamo inserirla all'interno di uno spazio dei nomi. Lo scopo di questi spazi dei nomi è quello di evitare conflitti di nomi tra le classi, ad esempio quando vengono commercializzate. Consideriamo due aziende, E1 ed E2, che distribuiscono classi contenute rispettivamente nei file dll e1.dll ed e2.dll. Supponiamo che un cliente C acquisti questi due set di classi in cui entrambe le aziende hanno definito una classe Person. Il cliente C compila un programma come segue:
Se il codice sorgente prog.cs utilizza la classe Person, il compilatore non saprà se prendere la Person di e1.dll o quella di e2.dll. Segnalerà un errore. Se l'azienda E1 si preoccupa di creare le proprie classi in uno spazio dei nomi chiamato E1 e l'azienda E2 in uno spazio dei nomi chiamato E2, le due classi Person saranno allora chiamate E1.Person e E2.Personne. Il cliente deve utilizzare E1.Personne o E2.Personne, ma non Person. Lo spazio dei nomi elimina ogni ambiguità.
Per creare una classe in uno spazio dei nomi, scrivere:
4.10. Applicazione di esempio - V2
Ripetiamo il calcolo delle imposte già studiato nel capitolo precedente, paragrafo 3.6, e ora lo affrontiamo utilizzando classi e interfacce. Ricordiamo il problema:
Proponiamo di scrivere un programma per calcolare l'imposta sul reddito di un contribuente. Il caso semplificato è quello di un contribuente che deve dichiarare solo il proprio stipendio (dati del 2004 relativi al reddito del 2003):
- il numero di quote di dipendente viene calcolato come nbParts=nbEnfants/2 +1 se non sposato, nbEnfants/2+2 se sposato, dove nbEnfants è il numero di figli.
- se ha almeno tre figli, ottiene mezza quota in più
- calcola il tuo reddito imponibile R=0,72*S dove S è il suo stipendio annuale
- calcola il tuo coefficiente familiare QF=R/nbParts
- calcola la tua imposta I. Considera la seguente tabella:
4262 | 0 | 0 |
8382 | 0,0683 | 291,09 |
14753 | 0,1914 | 1322,92 |
23888 | 0,2826 | 2668,39 |
38868 | 0,3738 | 4846,98 |
47932 | 0,4262 | 6883,66 |
0 | 0,4809 | 9505,54 |
Ogni riga ha 3 campi. Per calcolare l'imposta I, cerca la prima riga in cui QF<=champ1. Ad esempio, se QF=5000 troviamo la riga
L'imposta I è quindi pari a 0,0683*R - 291,09*nbParts. Se QF è tale che la relazione QF<=champ1 non viene mai verificata, vengono utilizzati i coefficienti dell'ultima riga. Qui:
il che dà l'imposta I=0,4809*R - 9505,54*nbParts.
Per prima cosa, definiamo una struttura in grado di incapsulare una riga dell'array precedente:
namespace Chap2 {
// a tax bracket
struct TrancheImpot {
public decimal Limite { get; set; }
public decimal CoeffR { get; set; }
public decimal CoeffN { get; set; }
}
}
Quindi definiamo un'interfaccia IImpot in grado di calcolare l'imposta:
namespace Chap2 {
interface IImpot {
int calculer(bool marié, int nbEnfants, int salaire);
}
}
- riga 3: metodo di calcolo delle imposte basato su tre dati: se il contribuente è sposato o meno, il numero di figli e lo stipendio
Successivamente, definiamo una classe astratta che implementa questa interfaccia:
namespace Chap2 {
abstract class AbstractImpot : IImpot {
// tax brackets required to calculate tax
// come from an external source
protected TrancheImpot[] tranchesImpot;
// tAX CALCULATION
public int calculer(bool marié, int nbEnfants, int salaire) {
// calculating the number of shares
decimal nbParts;
if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
else nbParts = (decimal)nbEnfants / 2 + 1;
if (nbEnfants >= 3) nbParts += 0.5M;
// calculation of taxable income & family quota
decimal revenu = 0.72M * salaire;
decimal QF = revenu / nbParts;
// tAX CALCULATION
tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
int i = 0;
while (QF > tranchesImpot[i].Limite) i++;
// return result
return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
}//calculate
}//class
}
- riga 2: la classe AbstractImpot implementa l'interfaccia IImpot.
- riga 7: dati per il calcolo dell'imposta annuale sotto forma di campo protetto. La classe AbstractImpot non sa come verrà inizializzato questo campo. Lascia questo compito alle classi derivate. Ecco perché è dichiarata astratta (riga 2), per impedire qualsiasi istanziazione.
- righe 10-25: implementazione dell'interfaccia di calcolo IImpot. Le classi derivate non dovranno riscrivere questo metodo. AbstractImpot funge da classe di fattorizzazione per le classi derivate. È qui che inseriamo ciò che è comune a tutte le classi derivate.
Una classe che implementa la classe IImpot può essere costruita derivando da AbstractImpot. È quello che stiamo facendo ora:
using System;
namespace Chap2 {
class HardwiredImpot : AbstractImpot {
// data tables for tax calculations
decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
public HardwiredImpot() {
// creation of tax bracket table
tranchesImpot = new TrancheImpot[limites.Length];
// filling
for (int i = 0; i < tranchesImpot.Length; i++) {
tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
}
}
}// class
}// namespace
La classe HardwiredImpot definisce, alle righe 7-9, i dati fissi necessari per il calcolo dell'imposta. Il suo costruttore (righe 11-18) utilizza questi dati per inizializzare il campo protetto tranchesImpot della classe padre AbstractImpot.
Un programma di test potrebbe essere il seguente:
using System;
namespace Chap2 {
class Program {
static void Main() {
// interactive Tax calculation program
// l'user types three data into keyboard: married nbEnfants salary
// the program then displays Tax payable
const string syntaxe = "syntaxe : Marié NbEnfants Salaire\n"
+ "Marié : o pour marié, n pour non marié\n"
+ "NbEnfants : nombre d'enfants\n"
+ "Salaire : salaire annuel en F";
// creation of a IImpot object
IImpot impot = new HardwiredImpot();
// infinite loop
while (true) {
// tax calculation parameters are requested
Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
string paramètres = Console.ReadLine().Trim();
// anything to do?
if (paramètres == null || paramètres == "") break;
// check number of arguments in the input line
string[] args = paramètres.Split(null);
int nbParamètres = args.Length;
if (nbParamètres != 3) {
Console.WriteLine(syntaxe);
continue;
}//if
// checking the validity of parameters
// married
string marié = args[0].ToLower();
if (marié != "o" && marié != "n") {
Console.WriteLine(syntaxe + "\nArgument marié incorrect : tapez o ou n");
continue;
}//if
// nbEnfants
int nbEnfants = 0;
bool dataOk = false;
try {
nbEnfants = int.Parse(args[1]);
dataOk = nbEnfants >= 0;
} catch {
}//if
// correct data?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul");
continue;
}
// salary
int salaire = 0;
dataOk = false;
try {
salaire = int.Parse(args[2]);
dataOk = salaire >= 0;
} catch {
}//try-catch
// correct data?
if (!dataOk) {
Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul");
continue;
}
// parameters are correct - Tax is calculated
Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
// next taxpayer
}//while
}
}
}
Il programma sopra riportato consente all'utente di eseguire ripetute simulazioni di calcolo delle imposte.
- riga 16: creazione dell'oggetto tax che implementa l'interfaccia IImpot. Questo oggetto si ottiene istanziando un HardwiredImpot, un tipo che implementa l'interfaccia IImpot. Si noti che alla variabile tax non è stato assegnato il tipo HardwiredImpot, ma l'interfaccia IImpot. Ciò indica che ci interessa solo l'oggetto tax e non il resto.
- righe 19-68: il ciclo di simulazione del calcolo delle imposte
- riga 22: i tre parametri richiesti per il metodo calculate vengono richiesti in una singola riga digitata sulla tastiera.
- riga 26: il metodo [string].Split(null) suddivide [string] in parole. Queste vengono memorizzate in un array args.
- riga 66: chiama l'oggetto calculate tax che implementa l'interfaccia IImpot.
Ecco un esempio di come eseguire il programma:









