Istanziare e Utilizzare Oggetti in C#
Per ora ci concentreremo su istanziare e usare oggetti nei nostri programmi.
Lavoreremo con classi già definite e soprattutto con le classi di sistema del .NET Core Framework. Le particolarità della definizione delle nostre classi personalizzate le vedremo più avanti, nelle prossime lezioni.
Istanziare e rilasciare oggetti
L'istanziazione di oggetti a partire da classi già definite durante l'esecuzione del programma viene effettuata tramite l'operatore new
.
Il nuovo oggetto creato viene di solito assegnato a una variabile il cui tipo coincide con la classe dell'oggetto (questo, però, non è obbligatorio come vedremo più avanti).
In questa assegnazione l'oggetto non viene copiato: nella variabile viene memorizzato soltanto un riferimento al nuovo oggetto (il suo indirizzo in memoria). Ecco un semplice esempio di come funziona:
Gatto qualcheGatto = new Gatto();
Alla variabile qualcheGatto
di tipo Gatto
assegniamo la nuova istanza della classe Gatto
. La variabile qualcheGatto
rimane nello stack, mentre la sua istanza è allocata all'interno dello heap, ossia quella parte di memoria dinamica che viene utilizzata per allocare oggetti e array:
Creare oggetti con parametri impostati
Consideriamo ora una variante leggermente diversa dell'esempio precedente, nella quale impostiamo dei parametri al momento della creazione dell'oggetto:
Gatto qualcheGatto = new Gatto("Tom", "Marrone");
In questo caso vogliamo che l'oggetto qualcheGatto
rappresenti un gatto di nome Tom e di colore Marrone. Lo indichiamo inserendo le parole "Tom" e "Marrone" tra parentesi dopo il nome della classe.
Quando si crea un oggetto con l'operatore new
avvengono due cose:
- viene riservata memoria per l'oggetto;
- i suoi membri dati vengono inizializzati.
L'inizializzazione è eseguita da un metodo speciale chiamato costruttore. Nell'esempio precedente i parametri di inizializzazione sono in realtà i parametri del costruttore della classe.
Parleremo dei costruttori più avanti. Poiché le variabili membro nome
e colore
della classe Gatto
sono di tipo riferimento (classe String
), anch'esse vengono memorizzate nella memoria dinamica (heap) e all'interno dell'oggetto sono conservati i loro riferimenti (indirizzi/puntatori).
La figura seguente illustra come l'oggetto Gatto
è rappresentato nella memoria del computer (le frecce mostrano i riferimenti da un oggetto all'altro):
Rilasciare gli oggetti
Una caratteristica importante del lavoro con gli oggetti in C# è che di solito non è necessario distruggerli manualmente né liberare la memoria da essi occupata. In altri linguaggi di programmazione, come C++, è necessario distruggere gli oggetti manualmente quando non sono più necessari, altrimenti si rischia di esaurire la memoria disponibile.
Ciò è possibile grazie al sistema integrato nel CLR di .NET per la pulizia della memoria (garbage collector), che si occupa di rilasciare gli oggetti non più utilizzati al posto nostro.
Gli oggetti ai quali, in un determinato momento, non è più riferito alcun puntatore nel programma vengono rilasciati automaticamente e la memoria che occupano viene liberata, prevenendo così molti potenziali bug e problemi.
Se volessimo rilasciare manualmente un oggetto specifico, dovremmo distruggere il riferimento ad esso, ad esempio così:
someCat = null;
Questo non distrugge immediatamente l'oggetto, ma lo pone in uno stato in cui non è più accessibile al programma; la prossima volta che il garbage collector pulirà la memoria, l'oggetto verrà eliminato:
Accesso ai campi di un oggetto
L'accesso ai campi e alle proprietà di un oggetto avviene tramite l'operatore .
(punto) posto tra il nome dell'oggetto e il nome del campo (o della proprietà). L'operatore .
non è necessario se accediamo a un campo o a una proprietà di una classe all'interno del corpo di un metodo della stessa classe.
Possiamo accedere ai campi e alle proprietà sia per estrarre dati sia per assegnare nuovi dati. Nel caso di una proprietà, l'accesso avviene esattamente nello stesso modo che per un campo – C# ci offre questa possibilità. Ciò è realizzato tramite le parole chiave get
e set
nella definizione della proprietà, che eseguono rispettivamente l'estrazione del valore e l'assegnazione di un nuovo valore. Nella definizione della classe Gatto
(vista sopra) le proprietà sono Nome e Colore.
Accesso alla memoria e alle proprietà di un oggetto – esempio
Ecco un esempio di utilizzo di una proprietà di un oggetto, basato sulla classe Gatto
definita in precedenza. Creiamo un'istanza mioGatto
e assegniamo "Tom" alla proprietà Nome
, quindi stampiamo il nome del nostro gatto:
class ManipolazioneGatto
{
static void Main()
{
Gatto mioGatto = new Gatto();
mioGatto.Nome = "Tom";
Console.WriteLine("Il nome del mio gatto è {0}.",
mioGatto.Nome);
}
}
Dopo l'esecuzione del programma, sullo standard output comparirà:
Il nome del mio gatto è Tom.
Chiamare i metodi degli oggetti
La chiamata ai metodi di un oggetto avviene tramite l'operatore di invocazione ()
e con l'operatore .
(punto). Quest'ultimo non è obbligatorio solo quando il metodo è chiamato all'interno del corpo di un altro metodo della stessa classe. Un metodo si chiama con il suo nome seguito da ()
o da (<parametri>)
se gli vengono passati argomenti. Abbiamo già visto come invocare metodi nel capitolo Metodi.
I metodi delle classi hanno modificatori di accesso public
, private
o protected
, che ne limitano la visibilità. Approfondiremo questi modificatori nelle prossime lezioni. Per ora basta sapere che il modificatore public
non introduce restrizioni: il metodo è quindi liberamente invocabile.
Chiamare i metodi degli oggetti – esempio
Completiamo l'esempio precedente chiamando il metodo Miagola
della classe Gatto
:
class ManipolazioneGatto
{
static void Main()
{
Gatto mioGatto = new Gatto();
mioGatto.Nome = "Tom";
Console.WriteLine("Il nome del mio gatto è {0}.",
mioGatto.Nome);
mioGatto.Miagola();
}
}
Dopo l'esecuzione del programma, sullo standard output comparirà:
Il nome del mio gatto è Tom.
Il gatto Tom ha detto: Miaooooo!
Costruttori
Il costruttore è un metodo speciale della classe che viene chiamato automaticamente alla creazione di un oggetto di quella classe e si occupa di inizializzare i suoi dati (questo è il suo scopo).
Un costruttore non ha tipo di valore restituito e il suo nome deve coincidere con il nome della classe.
Un costruttore può essere con o senza parametri. Un costruttore privo di parametri è detto anche parameterless constructor (costruttore senza parametri).
Costruttore con parametri
Un costruttore può accettare parametri come qualsiasi altro metodo.
Una classe può avere un numero qualunque di costruttori con un'unica restrizione: numero e tipi dei parametri devono differire (firma diversa). Quando si crea un oggetto della classe, viene invocato uno dei suoi costruttori.
Se in una classe sono presenti più costruttori, sorge spontanea la domanda: quale costruttore viene chiamato quando si istanzia l'oggetto?
Il problema è risolto in modo intuitivo (analogamente ai metodi): il compilatore sceglie automaticamente il costruttore più adatto in base all'insieme di parametri forniti al momento della creazione dell'oggetto. Si applica cioè il principio del best match.
Chiamare i costruttori – Esempio
Rivediamo la definizione della classe Gatto
e, in particolare, i suoi due costruttori. Per ora non ci interessano i dettagli della classe, ma solo i costruttori:
public class Gatto
{
// Campo name
private string nome;
// Campo color
private string colore;
// ...
// Costruttore senza parametri
public Gatto()
{
this.name = "Senza nome";
this.color = "grigio";
}
// Costruttore con parametri
public Gatto(string nome, string colore)
{
this.nome = nome;
this.colore = colore;
}
// ...
}
Utilizzeremo questi costruttori per mostrare la differenza tra costruttori con e senza parametri.
Per la classe Gatto
definita in questo modo, creeremo due istanze:
- un gatto generico (usando il costruttore senza parametri);
- il nostro gatto marrone di nome Tom (usando il costruttore con parametri).
In seguito chiameremo il metodo Miagola
per ciascun gatto e analizzeremo il risultato.
Il codice sorgente è il seguente:
class ManipolazioneGatto
{
static void Main()
{
Gatto qualcheGatto = new Gatto();
qualcheGatto.Miagola();
Console.WriteLine("Il colore del gatto {0} è {1}.",
qualcheGatto.Nome, qualcheGatto.Colore);
qualcheGatto = new Gatto("Tom", "marrone");
qualcheGatto.Miagola();
Console.WriteLine("Il colore del gatto {0} è {1}.",
qualcheGatto.Nome, qualcheGatto.Colore);
}
}
In questo esempio abbiamo creato un oggetto Gatto
senza parametri e successivamente lo abbiamo sostituito con un oggetto Gatto
con parametri. Il metodo Miagola
è stato chiamato per entrambi gli oggetti.
All'esecuzione del programma, sullo standard output verrà stampato:
Il colore del gatto Senza nome è grigio.
Il gatto Senza nome ha detto: Miaooooo!
Il colore del gatto Tom è marrone.
Il gatto Tom ha detto: Miaooooo!
Campi e metodi statici
I membri dati che abbiamo analizzato finora rappresentano lo stato degli oggetti e sono direttamente collegati alle singole istanze delle classi.
Nella programmazione orientata agli oggetti esistono però particolari campi e metodi associati al tipo di dato (la classe) e non alla singola istanza: li chiamiamo membri statici.
Essi hanno le seguenti caratteristiche:
- sono indipendenti dai singoli oggetti;
- possono essere usati senza creare alcuna istanza della classe che li contiene;
- possono essere campi, metodi oppure costruttori.
Un campo o metodo statico si dichiara con la parola chiave static
posta prima del tipo di ritorno (per i metodi) o del tipo del campo.
Per un costruttore statico la parola static
precede il nome del costruttore. Studieremo i costruttori statici più avanti.
Quando usare campi e metodi statici?
Per rispondere a questa domanda occorre comprendere bene la differenza fra membri statici e non-statici.
Pensiamo alla classe come a una “categoria” di oggetti e a ciascun oggetto come al “rappresentante” di tale categoria.
- I membri statici descrivono lo stato e il comportamento della categoria nel suo complesso.
- I membri non statici descrivono lo stato e il comportamento dei singoli rappresentanti di quella categoria.
Inizializzazione dei campi statici e non statici
I campi non statici vengono inizializzati dal costruttore quando si crea un'istanza della classe.
I campi statici, invece, non possono essere inizializzati in quel momento perché possono essere usati prima di creare un'istanza. È importante ricordare quanto segue:
Inizializzazione dei campi statici
I campi statici vengono inizializzati la prima volta che il tipo (la classe) viene utilizzato durante l'esecuzione del programma.
Campi e metodi statici – Esempio pratico
Supponiamo di voler implementare un metodo che, a ogni chiamata, restituisca un intero incrementato di 1 rispetto al valore restituito dalla chiamata precedente, partendo da 0. In pratica, vogliamo generare la sequenza dei numeri naturali.
Poiché il valore restituito non dipende da alcuna istanza specifica, sia il campo che memorizza l'ultimo valore sia il metodo che lo incrementa devono essere statici.
public class Sequenza
{
// Campo statico che memorizza il valore corrente della sequenza
private static int valoreCorrente = 0;
// Costruttore privato: impedisce l'istanziazione della classe
private Sequenza()
{
}
// Metodo statico che restituisce il prossimo valore della sequenza
public static int ProssimoValore()
{
valoreCorrente++;
return valoreCorrente;
}
}
Classi di utilità
Una classe che possiede solo costruttori privati non può essere istanziata.
Di solito contiene soltanto membri statici ed è detta “utility class”.
Approfondiremo i modificatori di accesso public
, private
e protected
nelle prossime lezioni.
Utilizzo della classe Sequenza
class ManipolazioneSequenza
{
static void Main()
{
Console.WriteLine("Sequenza[1...3]: {0}, {1}, {2}",
Sequence.ProssimoValore(), Sequence.ProssimoValore(),
Sequence.ProssimoValore());
}
}
Output del programma:
Sequenza[1...3]: 1, 2, 3
Se provassimo a creare istanze diverse di Sequence
, otterremmo un errore di compilazione perché il suo costruttore è privato.
In Sintesi
In questa lezione abbiamo visto:
- come istanziare gli oggetti a partire da classi definite utilizzando l'operatore
new
; - come rilasciare gli oggetti;
- come accedere ai campi e alle proprietà di un oggetto;
- come chiamare i metodi di un oggetto;
- come usare i costruttori sia con che senza parametri;
- come usare i campi e metodi statici.
Anche se non abbiamo ancora approfondito la definizione delle classi, abbiamo visto come istanziare e utilizzare oggetti in C#. Questo è un passo fondamentale per poter utilizzare le classi di sistema, argomento che tratteremo nella prossima lezione.