Sistema di esecuzione
Objective-C sposta quante piu' decisioni possibili dalla compilazione al tempo di esecuzione, cercando di eseguire dinamicamente operazioni come la creazione di oggetti e la determinazione di quale metodo invocare. Il nostro linguaggio, quindi, richiede non solo un compilatore ma anche un sistema di esecuzione, per interpretare correttamente il codice compilato. Il sistema di esecuzione dinamica agisce come una sorta di sistema operativo per il linguaggio.


Il tipo id
In Objective-C, gli identificatori di oggetti sono un tipo a parte chiamato id. Ogni oggetto, indipendentemente dalla classe di appartenza e' di questo tipo. Id puo' essere utilizzato sia per le istanze che per le classi stesse; esso e' definito come un puntatore ad una struttura di questo tipo:

typedef struct objc_object {

Class isa;

} *id;



Tutti gli oggetti hanno una variabile isa che indica la classe di appartenenza. Allo stesso modo degli array in C, ogni oggetto e' identificato dal suo indirizzo (puntatore). La parola chiave nil e' definita come un oggetto nullo o un id con valore 0. Id, nil e tutti i tipi base di Objective-C sono definiti nel file di header objc/objc.h .



Dynamic Typing
Il tipo id e' assolutamente non restrittivo: esso non contiene alcuna informazione aggiuntiva. Semplicemente indica che stiamo dialogando con un oggetto. Ovviamente gli oggetti non sono tutti uguali: un rettangolo non avra' gli stessi metodi e variabili d'istanza di un oggetto che rappresenta un'immagine.
Ad un certo punto della sua esecuzione, ogni programma ha bisogno di trovare informazioni specifiche circa gli oggetti che ha al suo interno - variabili di istanza contenute, metodi che puo' eseguire e cosi' via. Dal momento che id non puo' fornire queste informazioni al compilatore, ogni oggetto deve poterle fornire in qualche altra maniera durante l'esecuzione del codice.

La variabile di istanza isa identifica la classe dell'oggetto. Ogni rettangolo sara' in grado di dire che esso e' un oggetto di tipo rettangolo ed ogni cerchio sapra' di essere un cerchio. Oggetti con lo stesso comportamento (metodi) e con gli stessi dati (variabili di istanza) sono definiti essere membri della stessa classe.

Gli oggetti, quindi, sono tipati durante l'esecuzione. Quando serve, il sistema di esecuzione puo' trovare la classe corrispondente, semplicemente chiedendolo.

La variabile isa, inoltre permette di ottenere il concetto di introspezione. Il compilatore conserva informazioni circa le definizioni di classe in particolari strutture dati, specifiche per essere usate dal sistema di esecuzione. Quest'ultimo usa la variabile isa per recuperarle quando serve. Utilizzando il sistema di esecuzione dinamica, ad esempio, e' possibile determinare quando un oggetto implementa un particolare metodo oppure ottenere il nome della superclasse dalla quale si eredita.



Gestione della memoria
In un programma Objective-C e' importante assicurarsi che gli oggetti siano deallocati correttamente quando non sono piu' necessari all'esecuzione, per evitare pericolosi sprechi di memoria - leaks. Inoltre, e' altrettanto importante accertarsi che gli oggetti deallocati non siano piu' utilizzati.

Objective-C 2.0 offre due sistemi di gestione della memoria:

- Reference counting: in questo caso saremo noi i responsabili diretti della vita del nostro oggetto

- Garbage collection: in questo caso passeremo la responsabilita' circa l'allocazione dei nostri oggetti ad un sistema automatizzato

Nella programmazione Objective-C ed iPhone si fa' un uso ibrido di queste due tecnologie. In questo modo avremo la liberta' di decidere se il nostro oggetto deve essere gestito automaticamente o meno.

La gestione della memoria segue due semplici regole che proveremo a riassumere in questo modo:

Regola 1 - Se un oggetto viene creato usando una alloc o una copy, e' necessario liberare la memoria quando sono state completate le operazioni sull'oggetto.

Regola 2 - Se l'oggetto non viene creato direttamente (regola sopra), non bisogna assolutamente tentare di rilasciare la memoria con release.

Ad esempio, il seguente oggetto e' stato creato con una alloc. I responsabili, quindi, del rilascio della memoria siamo noi.

TestClass *ptr = [[TestClass alloc] init];

...

[ptr release];


Proviamo a vedere, invece, cosa accade quando la responsabilita' per l'allocazione dell'oggetto non e' nostra.

NSString *str;
str = [NSString stringWithFormat:@"Some string here..."];
NSLog(str);



In quest'esempio utilizziamo un metodo di classe - stringWithFormat - per la creazione di una nuova stringa da associare al puntatore str. Il metodo non utilizza direttamente la chiamata alloc; in questo caso non saremo noi a dover gestire il rilascio della memoria.

NSString *str;
// No need to release this object
str = [TestClass firstName];



In questo secondo esempio, utilizziamo un metodo di istanza che ritorna un puntatore ad un oggetto stringa. Se vogliamo trattenere la memoria per quest'oggetto, dovremo farlo in maniera esplicita utilizzando il metodo retain.
Piu' in generale, se semplicemente ci serve un puntatore all'oggetto stringa sopra, possiamo assegnarlo ad una variabile, senza preoccuparci di rilasciare la memoria.

Nel caso dell'utilizzo del metodo di classe new, valgono le regole di cui al punto 1; saremo responsabili,cioe', del rilascio della memoria.

MyObject *str;
str = [MyObject new];
...
...
[str release];





Inviare messaggi a nil
In Objective-C e' perfettamente valido inviare un messaggio a nil: semplicemente non avra' nessun effetto. Esistono molti esempi nel framework Cocoa che traggono spunto da questa caratteristica. Il valore ritornato inviando un messaggio a nil, potrebbe essere anch'esso valido:

Se il metodo ritorna un oggetto, allora un messaggio inviato a nil ritorna 0. Ad esempio:

Persona *madre = [[unaPersona sposa] madre];




Se non siamo sposati (il messaggio sposa, ritorna nil), allora il messaggio madre sara' inviato a nil:

[nil madre];



Se il metodo ritorna un qualunque tipo puntatore o un qualunque tipo base, allora un messaggio inviato a nil ritorna 0.



Inizializzazione degli oggetti e NIL
L'ordine degli avvenimenti durante l'inizializzazione di un oggetto e' di fondamentale importanza per il corretto funzionamento del nostro codice. Ad esempio, proviamo ad osservare il codice seguente:

TestClass *ptr = [TestClass alloc];
[ptr init];

// Do something with the object
if (ptr)
...



Allochiamo un oggetto ed inviamo un messaggio di inizializzazione. Il problema risiede proprio nella temporizzazione di questi eventi. Se il metodo init ritorna nil (ogni buon metodo init dovrebbe ritornare nil se qualcosa fallisce durante l'inizializzazione), il test alla riga 5 verra' passato con successo: ptr, infatti, non sara' nil.
Proviamo a confrontare quest'approccio con una versione piu' precisa:

TestClass *ptr = [[TestClass alloc] init];

// Do something with the object
if (ptr)
...


In questo caso, se il metodo di inizializzazione fallisce, ptr verra' impostato a nil ed il test alla riga 4 non verra' passato.



Passaggio di argomenti multipli ad un metodo
Continuiamo a lavorare con la nostra classe Fraction, facendo alcune aggiunte importanti su di essa. Sarebbe interessante poter aggiungere un metodo che configura in maniera atomica numeratore e denominatore. E' possibile definire metodi che prendono argomenti multipli, semplicemente elencando tutti gli argomenti uno dopo l'altro seguiti da un segno di due punti (:). Tutti i parametri diventeranno parte del nome stesso del metodo (firma).
Ad esempio, il metodo addEntryWithName:andEmail: e' un metodo che prende due argomenti. Allo stesso modo, il metodo addEntryWithName:andEmail:andPhone: sara' un metodo che prende tre argomenti.

Ritornando a noi, un metodo che configura contemporanemanete numeratore e denominatore, potrebbe essere scritto cosi':

[myFraction setTo:1 over:3];



aggiungiamo questo nuovo metodo alla nostra interfaccia:

@interface Fraction: Object {
int numerator;
int denominator;
}
- (void) print;
- (void) setNumerator: (int) n;
- (void) setDenominator: (int) d;
- (void) setTo: (int) n over: (int) d;
@end



ed alla nostra implementazione:

@implementation Fraction;
-(void) print {
printf(" %i / %i ", numerator, denominator);
}

-(void) setNumerator: (int) n {
numerator = n;
}

-(void) setDenominator: (int) d {
denominator = d;
}

-(void) setTo: (int) n over: (int) d {
numerator = n;
denominator = d;
}

@end



Il nuovo metodo prende due argomenti interi e configura le variabili di istanza, interne all'oggetto, in maniera opportuna.



Passaggio di parametri: variabili locali ed oggetti
Continuiamo il nostro lavoro con la classe Fraction. Prima di tutto, scriveremo un metodo che ci permettera' di sommare una frazione ad un'altra. Chiameremo il nostro metodo add:. Esso prendera' come argomento una frazione.

-(void) add: (Fraction *)f;


Ricordiamo brevemente il concetto di messaggio. Ogni qualvolta inviamo un messaggio ad un oggetto, eseguiamo un suo metodo di istanza. Nel nostro caso, inviare un messaggio di addizione ad un oggetto, significhera' invocare il suo metodo add:.
La frazioneA che riceve un messaggio di addizione, riceve contestualmente anche un oggetto - un puntatore - come argomento del messaggio. L'argomento passato e' un puntatore alla frazioneB da addizionare.

-(void) add: (Fraction *)f {
numerator = (numerator * [f denominator]) + (denominator * [f numerator]);
denominator = denominator * [f denominator];
}


I nomi utilizzati per riferirsi ai parametri passati ad un metodo, sono a tutti gli effetti delle variabili locali che possiedono una copia del valore passato e vengono create e distrutte nell'arco di vita del nostro metodo. Non risultera' possibile, quindi, modificare il valore passato come parametro, dal momento che dialoghiamo con una semplice copia della variabile originale.

Nel caso dell'esempio sopra, quando passiamo un oggetto come parametro le regole sono leggermente diverse. Se guardiamo attentamente l'esempio, infatti, ci accorgeremo che il nostro oggetto frazione viene passato sottoforma di puntatore (asterisco prima del nome). Questo significa che il metodo che ricevera' questo messaggio - e quindi il parametro con il puntatore alla frazione argomento - dialoghera' direttamente con l'oggetto. In questo caso, ogni variazione alle variabili di istanza dell'oggetto parametro, risultera' valida anche alla fine del ciclo di vita del nostro metodo.



La keyword self
La nostra classe Fraction adesso e' in grado di sommare due frazioni tra loro. Se osserviamo il
codice, ci accorgeremo del fatto che la somma non tiene conto delle riduzioni tra numeratore e denominatore.
Se sommiamo alla frazione avente valore 1/2, la frazione con valore 1/4, otteniamo come risultato 6/8 e non 3/4. Il nostro metodo di somma non tiene conto della riduzione. Per risolvere il problema, possiamo procedere in due modi; possiamo modificare il nostro metodo di somma per effettuare un'ulteriore operazione di riduzione, oppure possiamo creare un nuovo metodo chiamato riduzione ed usarlo dove necessario:

- (void) reduce {
int u = numerator;
int v = denominator;
int temp;

while(v != 0) {
temp = u % v;
u = v;
v = temp;
}

numerator /= u;
denominator /= u;
}



Il metodo effettua la riduzione di numeratore e denominatore in rapporto al modulo tra l'uno e l'altro. Aggiungiamo la dichiarazione all'interfaccia:

- (void) reduce;



Adesso siamo pronti per utilizzarlo dove necessario. Dal momento che si tratta di un normale metodo di istanza, puo' essere richiamato da qualunque istanza di frazione:

...
...
[aFraction add:bFraction];
[aFraction reduce];
...
...


Questa soluzione, pero', implica che dobbiamo cambiare tutto il codice in cui occorre l'uso della funzione di somma. Un'alternativa sarebbe quella di richiamare il metodo di riduzione, direttamente all'interno del metodo di somma, modificandone quindi il funzionamento.

Per fare questo, e' necessario introdurre la nozione di self. Ogni istanza di classe, puo' utilizzare questa parola chiave per riferirsi a se stessa come istanza. Essendo un puntatore ad istanza completamente valido, nulla ci vieta di lanciare un messaggio su di esso. Con queste premesse, potremo riscrivere il metodo di somma come segue:

-(void) add: (Fraction *)f {
numerator = (numerator * [f denominator]) + (denominator * [f numerator]);
denominator = denominator * [f denominator];

[self reduce]; //launch a reduce message to myself
}





Allocazione e ritorno di oggetti all'interno dei metodi
Se lanciamo il codice con le modifiche descritte, noteremo che il metodo add: cambia il valore dell'oggetto che riceve questo messaggio. Vediamo come creare un metodo che crea una nuova frazione con il risultato dell'operazione di somma senza alterare i valori del mittente. Per fare questo, avremo bisogno di creare una nuova frazione risultato e restituirla al mittente del messaggio.

-(Fraction *)add: (Fraction *)f {
Fraction *result = [[Fraction alloc] init];
int resultNum, resultDenom;

resultNum = (numerator * [f denominator] + (denominator * [f denominator]);
resultDenom = denominator * [f denominator];

[result setTo: resultNum over: resultDenom];
[result reduce];

return result;
}


Il metodo alloca ed inizializza una nuovo oggetto frazione chiamato result. I valori delle variabili locali resultNum e resultDenom sono utilizzate per ospitare il risultato dell'operazione e, successivamente, come parametri per il metodo setTo:over:.
Dopo aver effettuato le operazioni di riduzione, ritorniamo il nuovo oggetto creato al chiamante. Nota che la memoria allocata non verra' in nessun modo rilasciata; sara' l'oggetto che riceve il risultato a doversi preoccupare di deallocarla quando non sara' piu' utilizzata.



L'oggetto root: Object
Abbiamo gia' parlato dell'idea di ereditarieta' quando abbiamo discusso del padre di una classe (lezione 2). Ovviamente, anche una classe padre puo' avere un padre. La classe che non ha nessun padre, si trova alla radice dell'albero genealogico ed e' conosciuta con il nome di root. In Objective-C, e' possibile definire una propria classe radice anche se, normalmente, e' una cosa poco comune. Al contrario, quello che si fa' solitamente e' trarre vantaggio dalle classi preesistenti.
Tutte le classi definite sino ad ora nei nostri esempi, sono discendenti della classe radice chiamata Object

@interface Fraction: Object
...
...
@end



La classe Fraction e' anche definita come classe figlia o sottoclasse



Da un punto di vista di terminologia, durante il corso, parleremo di classi, classi figlie e classi padre. Oppure, indifferentemente, parleremo di classi, sottoclassi e superclassi. Entrambe le terminologie sono comunemente utilizzate e dovrebbero diventare di uso comune quando ci riferiamo alla programmazione Objective-C

Ogni qualvolta viene definita una nuova classe (diversa da quella root), vengono ereditate alcune proprieta' dalla classe padre. Ad esempio, tutte le variabili di istanza ed i metodi di istanza della classe padre sono implicitamente ereditate anche dalle classi figlie. Questo significa che le sottoclassi possono accedere a questi metodi e variabili direttamente, come se fossero parte della dichiarazione di interfaccia della sottoclasse stessa.



Creazione di un'applicazione programmatica












Riferimenti

- Mac OS X Reference Library: Objects, Classes, and Messaging
- Mac OS X Reference Library: Defining a Class
- Mac OS X Reference Library: Allocating and Initializing Objects
- Mac OS X Reference Library: Memory Management Programming Guide for Cocoa
- Programming in Objective-C: Capitolo 4
- Programming in Objective-C: Capitolo 7
- Programming in Objective-C: Capitolo 8




    lap1

downloads