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:

Istanziazione di un oggetto
Figura 1: Istanziazione di un oggetto

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:

  1. viene riservata memoria per l'oggetto;
  2. 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):

Oggetto Gatto e Sue Variabili Membro in memoria
Figura 2: Oggetto Gatto e Sue Variabili Membro in memoria

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:

Rilascio della memoria di un oggetto
Figura 3: Rilascio della memoria di un oggetto

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:

Nota

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;
    }
}
Definizione

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.