MaCocoa 005

Capitolo 005 - Objective C

Ok la programmazione object-oriented; ma in pratica? Ci occorre un linguaggio di programmazione.

Sorgenti: Segue da Objective C

Primo inserimento: Settembre-Ottobre 2001

Objective C

Objective C è una estensione del linguaggio C con alcuni costrutti aggiuntivi per poter programmare secondo la metodologia OOP. Pare addirittura che ObjC sia in realtà un semplice C mascherato.

Parentesi aperta. Normalmente, un compilatore trasforma le istruzioni scritte in una determinata forma in codice macchina eseguibile. Molti linguaggi possiedono un pre-compilatore che si occupa di preparare in maniera acconcia la lista delle istruzioni, ma non è concettualmente un oggetto che esegue compiti fondamentali. Tuttavia, in questo caso, il pre-compilatore ObjC trasforma il linguaggio ObjC in linguaggio C vero e proprio. Poi, un normale compilatore C è in grado di produrre l'applicazione che interessa. Verificheremo questa cosa più avanti, quando andremo a fare conoscenza con il compilatore. Chiusa parentesi.

Per il momento, parliamo del linguaggio ObjC e vediamo come è fatto. In questo documento non parlerò assolutamente di C, lo darò per scontato; così non fosse, ci vorrebbe un intero libro per parlarne, e quindi vi rimando a questi, che trovate abbondantemente (beh, insomma... ormai sono quasi tutti sul C++).

D'altra parte, la OOP richiede l'appropriazione di concetti un po' differenti, e quindi credo che conoscere come si fa a definire una matrice di puntatori a funzioni che hanno come argomento un puntatore a struttura non sia fondamentale per imparare a programmare in ObjC. Ritengo quindi che una conoscenza superficiale del C sia sufficiente per muovere i primi passi senza necessariamente essere colti da dubbi in ogni momento.

ObjC è un sovrainsieme del C, nel senso che contiene al suo interno l'intero C; il compilatore ObjC (ammesso che esista e non si tratti del solo precompilatore) è in grado quindi di trattare tranquillamente programmi scritti in C normale. I file scritti in linguaggio C ed i file scritti in linguaggio ObjC si distinguono per l'estensione del file (lo so, lo so, non è bello in un ambiente Macintosh, ma siamo stati abituati bene... e poi, credetemi, tante volte è molto comodo aggiungere un'estensione, fin troppo comodo...). Un file in C ha l'estensione .C, mentre un file in ObjC ha l'estensione .M (non so perché). L'estensione del file dovrebbe distinguere se sul file opera il preprocessore standard (i file C) oppure il preprocessore speciale ObjC (i file M).

Dicevo delle estensioni del C con orientamento agli oggetti che danno luogo al ObjC; la maggior parte di queste ricordano in maniera stretta costrutti del linguaggio Smalltalk, uno dei linguaggi OOP puri, in cui tutto (ma proprio tutto) è un oggetto.

Intanto, comincio a parlare di Oggetti. non mi stancherò mai di ripetere: Un Oggetto è l'insieme di una Struttura Dati, definita dalla Classe, e da una serie di operazioni che usano questi dati. Le operazioni si chiamano Metodi, ed i metodi operano sui valori della struttura dati, le Variabili d'Istanza. Un oggetto, o meglio la sua realizzazione concreta, un'Istanza, è un costrutto che raccoglie come un tutto indissolubile una struttura dati valorizzata (variabili d'istanza) con un gruppo di procedure (metodi). I metodi dell'oggetto sono l'unico modo per accedere o manipolare le variabili d'istanza. Se non c'è un metodo per manipolare direttamente la variabile, non ci si può far nulla. Inoltre, i metodi sono legati all'oggetto, nel senso che i metodi si applicano solo all'oggetto per cui sono definiti.

Tipo d'oggetto

In ObjC, un oggetto è dichiarato come un id, cioè una variabile in grado di identificare un oggetto di tipo id, come un numero intero è di tipo int o un numero floating point di tipo float:

id ilMioOggetto;

sospetto che il tipo id non sia altro che un puntatore a void, un metodo classico in C per indicare un puntatore a qualcosa che non si sa bene cosa.

Ecco, sospetto infondato. Sono andato a guardare la definizione di id; è un puntatore ad una struttura, che contiene una variabile Class, che NON è un tipo predefinito, ma una struttura piuttosto complessa e complicata che non voglio indagare oltre. Dove ho trovato tutto ciò? nei file objc.h e parenti, di difficile reperibilità...

Il tipo id non convoglia altre informazioni che non siano: è un oggetto. Come si fa allora a sapere con che razza di oggetto abbiamo a che fare?

Una della variabili d'istanza di ciascun oggetto è in effetti isa, che è un puntatore alla classe cui l'oggetto appartiene (ma la variabile d'istanza non può essere letta dall'esterno! beh, da qualche parte ci sarà un metodo per conoscerla..., sempre ammesso che occorra conoscerla: il binding dinamico ed il polimorfismo si basano proprio sul fatto di ignorare il tipo dell'oggetto). Utilizzando questa variabile il compilatore e l'ambiente di sviluppo sono in grado di capire che tipo di oggetto stanno considerando, e di attivare le operazioni conseguenti.

Messaggi

Per inviare un messaggio ad un oggetto, si usa la seguente sintassi:

[ oggetto-ricevente messaggio ]

Visto che questa è una istruzione C, alla fine dell'invio del messaggio deve esserci un ';'. Abbiamo poi anche detto che un messaggio provoca l'esecuzione di un metodo, che può generare un risultato. In definitiva, una semplice istruzione in ObjC che invia un messaggio ad un oggetto e mette da parte il risultato è:

risultato = [ ricevente messaggio ];

Ora, i messaggi possono avere degli argomenti; questi si scrivono nel seguente modo:

<nome metodo>: argomento <etichetta>: argomento <etichetta>: argomento ...

Ad esempio, avendo un oggetto mouse che riceve messaggi di movimento ad una locazione di coordinate X e coordinata Y, potremmo avere un messaggio formulato così:

[ mouse moveToX: 100 moveToY: 50 ]

che dice al mouse di spostarsi alla coordinata (100,50). Ci sono due appunti. Il metodo, in questo caso, non si chiama moveToX, ma in maniera completa: moveToX:moveToY:. L'etichetta può non essere presente, ma allora il metodo cambia nome (è un altro metodo); ad esempio, si potrebbe avere il metodo moveToXY:: (si noti la presenza del doppio due punti)

[ mouse moveToXY: 100 : 50 ]

C'è infine un altro metodo per dare argomenti, ovvero mettere più argomenti assieme, separandoli da una virgola; il metodo a questo punto si chiama moveToXY: (si noti UN solo due punti):

[ mouse moveToXY: 100, 50 ]

Ho messo le possibilità in ordine di preferenza stilistica: è molto più bello avere metodi completi di tutte le etichette (sono più chiari e leggibili, quasi discorsivi), meno bello non avere etichette, assolutamente brutto avere argomenti multipli.

Sfruttando il fatto che l'esecuzione di un metodo produce un risultato, si possono inviare più messaggi all'interno di una stessa riga di istruzione:

[ mouse moveToX: 100 moveToY: [ mouse clickV] ];

Questa immaginaria istruzione sposta la coordinata y del mouse alla coordinata y dell'ultima locazione dove il mouse ha fatto clic.

Classi

Le classi sono oggetti. Sono oggetti di tipo speciale, perché servono essenzialmente a produrre altri oggetti, istanze della classe. La classe è l'oggetto ideale, di cui tutte le istanze sono immagini (un concetto un po' platonico, ma tant'è). Una classe dichiara le variabili d'istanza (ma non le definisce... c'è una sottile distinzione non banale), e definisce tutti i metodi che gli oggetti istanza della classe possono utilizzare.

Intervallo: la distinzione tra dichiarare e definire una variabile; questo discorso va bene anche per altri linguaggi di programmazione (ad esempio, il C). Quando dichiaro qualcosa (una classe, una variabile), affermo solamente la sua esistenza. Non è costruito alcun spazio informatico per questa cosa: dichiarare una variabile col nome aloha significa che, da qualche parte all'interno dell'applicazione, esiste un posto indicato appunto come aloha dove è conservato il valore di questa variabile; il compilatore, incontrando una dichiarazione, la deve risolvere, ovvero andare a trovare questo posto, in modo che poi sappia rintracciare il valore. Ma il posto deve esistere. Creare il posto è compito della definizione. Definendo una variabile col nome aloha, si crea il posto dove immagazzinare questa variabile. All'interno di una applicazione deve esistere una ed una sola definizione e quante si vogliono dichiarazioni.

Riprendendo la classe: dichiara le variabili d'istanza, cioè dice che esistono, ma non ne crea lo spazio. In pratica, non ne ha; dichiarare le variabili d'istanza è utile al momento della definizione di una istanza (definizione!), che crea lo spazio necessario per conservare tutte le variabili d'istanza.

Una classe definisce invece tutti i metodi: vale a dire, tutti i metodi devono prevedere il loro bravo pezzo di codice che verrà eseguito al ricevimento del messaggio apposito. Tutte le istanze di questa classe condividono questi metodi, ed è per questo che i metodi sono definiti dalla classe. Invece, tutte le istanze hanno le proprie variabili d'istanza, ed è per questo che la classe dichiara le variabili d'istanza. Per convenzione, agli oggetti Classe si danno nomi che cominciano con lettera maiuscola (come ad esempio LaMiaClasse), mentre agli oggetti Istanza della classe si danno nomi che cominciano con lettera minuscola, eccoLaIstanza.

Abbiamo detto altrove che ogni classe è sottoclasse di un'altra classe, tranne una classe che avevano chiamato primitiva; un termine più usato è root, radice. Nel caso di Cocoa, questa classe è chiamata NSObject. Tutte le classi di Cocoa sono discendenti (più o meno vicine) di questa Classe, la madre di tutte le Classi.

Mirko Viviani mi corregge: le classi root sono in realtà almeno due (basta leggere l'introduzione della documentazione relativa al Foundation Framework, anzi, basta guardare la figura): le classi root presenti all'interno del framework sono NSObject, la più importante ed utilizzata, e NSProxy, che prende momentaneamente il posto di un'altra classe, perché quest'ultima è lontana oppure non abbiamo voglia di gestirla.

Benchè sia concettualmente possibile creare una nuova gerarchia di Classi a partire da un nuovo oggetto da noi definito, nella pratica la cosa è così complicata e inutile che non viene mai fatta.

Mirko Viviani mi corregge ancora; in effetti costruire una nuova gerarchia di classi a partire da un diverso oggetto root non è molto difficile; ma, per quanto riguarda Cocoa, in pratica non serve.

La gerarchia delle classi funziona in questo modo: in principio c'era NSObject. Attraverso il meccanismo dell'ereditarietà, da NSObject sono state definite nuove classi figlie; e le figlie hanno generato altre figlie. Ogni figlia riceve dalla madre (superclasse) tutte le sue funzionalità (variabili d'istanza e metodi); alcune figlie aggiungono caratteristiche (variabili d'istanza), altre funzionalità (nuovi metodi), altre ancora sia metodi che variabili, altre figlie un po' più ribelli riscrivono metodi...

Sempre per merito di Mirko Viviani mi vedo costretto a commentare il testo seguente, che non è preciso dal punto di vista informatico, lo è di più dal punto di vista pratico.

In mezzo a tutto questo bailamme, si nota l'esistenza di classi un po' bizzarre, nullafacenti, con nessuno scopo nella vita se non quella di servire ad esempio. A ben vedere, è proprio il caso di NSObject, che di suo non ha proprio nulla di interessante.

NSObject non è una classe nullafacente, anzi. Realizza una moltitudine di metodi importantissimi per lo sviluppo delle applicazioni; ma sono metodi di uso generale: per avere funzioni specifiche dell'applicazione, bisogna costruire sottoclassi.

Per avere una classe che serve a qualcosa, bisogna verosimilmente aggiungere qualche variabile d'istanza e qualche metodo interessante, che altrimenti NSObject è piuttosto noioso. Classi progettate in modo da non avere vita propria, ma destinate a funzionare solo come matrice per altre sottoclassi sono chiamate Classi Astratte. Non esistono cioè istanze della classe astratta, ma solo istanze generate a partire da sottoclassi di questa classe astratta.

In effetti, il concetto di classe astratta è differente. Una classe è astratta quando devono essere necessariamente estese per poter funzionare. Se non fossero estese, non possono proprio funzionare e generare errore. NSObject non è una classe astratta. Invece, a sentire la documentazione, NSProxy lo è.

In pratica una classe astratta nasce per raccogliere assieme una serie di funzionalità comuni ad un insieme di oggetti, ma che devono essere rifinite a mano per funzionare correttamente. Un po' come quelle solette mangiadore da inserire all'interno delle scarpe: al supermercato esiste lo stampo già fatto, ma per utilizzare la soletta, dovete ritagliare la sagoma delle dimensioni del vostro piede...

Classi e tipo

Una Classe è a tutti gli effetti un tipo, ovvero la definizione di una classe definisce un tipo di dato (qui tipo ha lo stesso significato dei tipi standard del C, come ad esempio int, float, eccetera). È quindi perfettamente lecito utilizzare il nome della classe MiaClasse in costrutti come sizeof( MiaClasse), e soprattutto per dichiarare oggetti (ah, finalmente ci siamo arrivati):

MiaClasse    * ilMioPrimoOggetto ;

Ecco, ci siamo: questa è una dichiarazione (dichiarazione: dico che esiste da qualche parte una istanza che si chiama ilMioPrimoOggetto, che è di tipo MiaClasse) di una istanza di oggetto, ed il fatto di aver indicato esplicitamente il tipo di questo oggetto significa che ho utilizzato il binding statico. Alternativamente, avrei potuto utilizzare una dichiarazione più neutra, del tipo

id ilMioSecondoOggetto ;

Il vantaggio principale del binding statico è che, sapendo bene come stanno le cose, il compilatore produce codice più efficiente e veloce. D'altra parte, si perde in flessibilità (questa è una caratteristica standard del mondo, nota come congettura della coperta corta: ogni cosa è come una coperta corta; se copri i piedi, rimane fuori la testa, se copri la testa, rimangono fuori i piedi).

Si può fare binding statico anche utilizzando come tipo una delle classi ancestrali dell'oggetto che ci interessa; se MadreClasse è una superclasse di MiaClasse, la dichiarazione

MadreClasse *ilMioPrimoOggetto ;

avrebbe dichiarato un oggetto di tipo MadreClasse. Dove sta il vantaggio? Il compilatore ottimizza il codice trattando ilMioPrimoOggetto come una MadreClasse, ma poi, durante l'esecuzione, esegue ugualmente il binding dinamico per arrivare (se il caso) ad usare i metodi di MiaClasse; così facendo si risparmia un po' di lavoro al binding dinamico (c'è meno strada da fare per arrivare in fondo), aumentando un po' le prestazioni...

Ancora una volta, Mirko Viviani mi corregge: ad esempio, il codice prodotto per gestire:

id stringa;

o

NSString *stringa;

è assolutamente lo stesso. Ciò che si guadagna con l'esplicita tipizzazione della variabile è un controllo dei metodi al momento della compilazione, con conseguente generazione di avvertimenti se il metodo utilizzato non è definito per l'oggetto in esame.

Quando, durante l'esecuzione, si invoca un metodo, sempre e comunque è utilizzata una specifica funzione, chiamata objc_msgSend(), che fa parte del cosidetto meccanismo di run-time dello ObjC. Il manuale del linguaggio ObjC fornito con la documentazione dei Developer's Tool sconsiglia caldamente la chiamata diretta di questa funzione, ma di lasciar fare al compilatore.

Oggetti Classe

Devo aver già detto che una classe è un oggetto (speciale, senza variabili, cose del genere), e definisce tutti i metodi che poi sono utilizzati dagli oggetti creati a partire da quella classe. Ma un oggetto Classe, che si può individuare esplicitamente tipizzandolo come tipo Class, può avere metodi propri. Anzi, i metodi propri sono verosimilmente gli unici utilizzabili su di oggetto Classe, in quanto gli altri metodi utilizzano quasi certamente i valori delle variabili d'istanza (che un oggetto Classe non ha in quanto le dichiara ma non può definirle).

Insomma, gli oggetti Class sono oggetti a tutti gli effetti, che possono essere tipizzati dinamicamente (eh, che brutta locuzione: significa che posso dichiararli come id, ma poi usarli come Class, sono cioé soggetti al binding dinamico), possono ricevere messaggi, ereditare metodi da altre classi, eccetera. Sono speciali perchè sono creati direttamente dal compilatore (non devo definirli esplicitamente), non hanno variabili d'istanza e servono come stampi per la produzione di oggetti.

Quindi, ecco come si fa a generare oggetti: per prima cosa, definisco la variabile che individua l'oggetto (cioè, definisco un puntatore all'oggetto).

id ilMioOggetto;

Avrei potuto anche scrivere con binding statico:

MiaClasse *ilMioOggetto ;

A questo punto ho definito la variabile che serve a contenere l'oggetto, ma non ho ancora creato l'oggetto; per questo, occorre una nuova istruzione:

ilMioOggetto = [ MiaClasse alloc ] ;

Cosa succede? Ho inviato il messaggio alloc alla classe MiaClasse; ogni classe eredita dalla madre di tutte le classi (NSObject, ricordo) il metodo alloc, che appunto crea lo spazio necessario a conservare un oggetto delle dimensioni di MiaClasse.

Ma non è ancora finita. Lo spazio c'è, ma è ancora vuoto ed inutilizzabile. Occorre inizializzarlo, utilizzando un altro metodo ereditato da NSObject, chiamato init; in realtà le operazioni si fanno normalmente assieme, con una istruzione come la seguente:

ilMioOggetto = [[MiaClasse alloc] init];

a questo punto, ilMioOggetto è pronto a funzionare.

L'unico problema, è che non abbiano ancora idea di come sia fatta la classe...

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).