Introduzione

Objective-C è il linguaggio maggiormente usato per scrivere software in ambiente Mac. Se avete già delle conoscenze sulla programmazione orientata ad oggetti e sulla sintassi del linguaggio C allora imparare Objective-C sarà un passo semplice per voi. Altrimenti sarebbe meglio approfondire prima i concetti riguardanti la OOP e il linguaggio C.

Invocare i metodi

Per cominciare più in fretta possibile, guardiamo alcuni esempi di codice. La sintassi di base per richiamare un metodo appartenente ad un oggetto è la seguente:

[object method];
[object methodWithInput:input];

Ovviamente i metodi possono restituire dei valori nel seguente modo:

output = [object methodWithOutput];
output = [object methodWithInputAndOutput:input];

E’ possibile richiamare anche dei metodi appartenenti alle classi, che tra l’altro è il modo in cui queste vengono istanziate. Nell’esempio di seguito invochiamo un metodo della classe NSString che ci restituisce una istanza della classe stessa:

id myObject = [NSString string];

Il tipo id può contenere un oggetto di qualsiasi tipo. Questo rende possibile scrivere codice in modo dinamico senza sapere a priori cosa una determinata variabile conterrà in runtime.

In questo esempio la variabile conterrà una stringa quindi possiamo anche modificare il suo tipo in NSString:

NSString* myString = [NSString string];

In questo modo ora il compilatore potrà avvisarci se cercheremo di usare un metodo su questo oggetto che non supporta la classe NSString.

Da notare che di seguito al tipo dell’oggetto c’è un asterico. Tutti gli oggetti in Objective-C sono di tipo puntatore. Per il tipo id invece, che è di default un tipo puntatore, non va specificato l’asterisco.

Messaggi annidati

Nella maggior parte dei linguaggi la chiamata di metodi o funzioni annidati appare in questo modo:

function1 (function2());

Il risultato di function2 viene passato in input a function1. In Objective-C i messaggi annidati funzionano in questo modo:

[NSString stringWithFormat:[prefs format]];

Annidare più di due messaggi in una singola istruzione va evitato perchè riduce la leggibilità del codice stesso.

Metodi con input multiplo

Alcuni metodi prendono in input più di un valore. In Objective-C, il nome di un metodo può essere scomposto in più parti. L’intestazione di un metodo con più valori in input quindi apparirà così:

- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;

Il metodo verrà poi chiamato in questo modo:

BOOL result = [myData writeToFile:@"/tmp/log.txt" atomically:NO];

Il nome del metodo sarà quindi writeToFile:atomically.

Variabili private

Tutte le variabili di istanza sono private per default in Objective-C, quindi è necessario utilizzare gli accessor per prelevare il loro valore.

Ecco la sintassi per Objective-C 1.x:

[photo setCaption:@"Day at the Beach"];
output = [photo caption];

Da notare che nella seconda riga non si sta accedendo direttamente alla variabile caption, ma si sta chiamando un metodo get con quel nome. In molti casi infatti non viene aggiunto il prefisso get per i getter in Objective-C.

Con Mac OS X 10.5 e Objective-C 2.0 è stata introdotta la sintassi puntata per i getter e i setter. Ad esempio:

photo.caption = @"Day at the Beach";
output = photo.caption;

E’ possibile utilizzare entrambi gli stili ma è preferibile sceglierne uno. Inoltre la notazione puntata dovrebbe essere utilizzata solamente per i metodi setter e getter e non per tutti gli altri metodi.

Creare gli oggetti

Ci sono due modi per creare un oggetto. Il primo è quello visto in precedenza:

NSString* myString = [NSString string];

L’altro invece, che vedremo in dettaglio più tardi, è questo:

NSString* myString = [[NSString alloc] init];

Questa è una chiamata annidata di metodi. Il primo è alloc che appartiene a NSString ed è un metodo di basso livello che riserva la memoria e istanzia l’oggetto.

Il secondo metodo invece, init, inizializza il nuovo oggetto. Di solito questo metodo è utilizzato per creare le variabili istanza della classe.

In alcuni casi è necessario utilizzare una versione con input del metodo init:

NSNumber* value = [[NSNumber alloc] initWithFloat:1.0];

Gestione della memoria

Quando si sviluppano applicazioni per OS X si può attivare il garbage collector. Questo vuol dire che non è necessario pensare alla gestione della memoria tranne in alcuni casi particolari.

Comunque non è detto che ci si troverà sempre a sviluppare applicazioni in un ambiente che supporta il garbage collector e in questi casi sarà fondamentale conoscere alcuni concetti di base.

Quando creiamo un oggetto attraverso l’allocazione manuale sarà anche necessario rilasciare l’oggetto. E’ inoltre necessario stare attenti a non cercare di rilasciare un oggetto già rilasciato in automatico perchè causeremmo il crash dell’applicazione.

Di seguito due esempi:

// string1 verrà rilasciata automaticamente
NSString* string1 = [NSString string];

// string2 viene incece rilasciata manualmente
NSString* string2 = [[NSString alloc] init];
[string2 release];

Ci sono ovviamente altre cose da sapere sulla gestione della memoria ma le vedremo dopo aver spiegato altri concetti.

Creare una Class Interface

La sintassi dell’Objective-C per creare delle nuove classi è molto semplice e si divide in due parti.

L’interfaccia della classe è situata nel file NomeDellaClasse.h e definisce le variabili d’istanza e i metodi pubblici della stessa.

L’implementazione invece viene scritta nel file NomeDellaClasse.m che contiene il codice per i metodi. Inoltre in questo file vengono definiti metodi privati che non saranno visibili all’esterno della classe.

Ecco un esempio di interfaccia di una classe. La classe si chiama Photo quindi l’interfaccia si troverà nel file Photo.h:

#import

@interface Photo : NSObject
{
NSString* caption;
NSString* photographer;
}
@end

Prima di tutto importiamo Cocoa.h per avere a disposizione le classi base del framework Cocoa. La direttiva #import garantisce che uno stesso file non venga importato più volte.

@interface serve invece ad indicare che si sta dichiarando una classe. I due punti invece specificano la superclasse che in questo caso è NSObject.

All’interno delle parentesi graffe abbiamo poi due variabili d’istanza: caption e photographer. Entrambe sono di tipo NSString ma potrebbero essere di qualsiasi altro tipo incluso id.

Infine abbiamo @end che chiude la dichiarazione della classe.

Aggiungere metodi

Ora andiamo ad aggiungere due metodi getter per le variabili.

#import

@interface Photo : NSObject
{
NSString* caption;
NSString* photographer;
}

- caption;
- photographer;

@end

Ricordatevi che in Objective-C i metodi non includono il prefisso get. Il simbolo “-” prima del metodo indica che si tratta di metodi d’istanza. Il segno “+” invece è usato per i metodi di classe.

Per default il compilatore assume che il metodo ritorni un oggetto di tipo id e che tutti i valori in input siano di tipo id. Il codice visto sopra è quindi corretto ma è sicuramente inusuale. Aggiungiamo quindi un tipo specifico come valore di ritorno dei nostri metodi.

#import

@interface Photo : NSObject
{
NSString* caption;
NSString* photographer;
}

- (NSString*) caption;
- (NSString*) photographer;

@end

Ora aggiungiamo invece i metodi setter.

#import

@interface Photo : NSObject
{
NSString* caption;
NSString* photographer;
}

- (NSString*) caption;
- (NSString*) photographer;
- (void) setCaption: (NSString*)input;
- (void) setPhotographer: (NSString*)input;

@end

Questi metodi non neccessitano di restituire un valore quindi li dichiareremo di tipo void.

Implementare l’interfaccia di una classe

Vediamo ora la parte implementativa della classe a partire dai metodi getter.

#import "Photo.h"

@implementation Photo

- (NSString*) caption
{
return caption;
}

- (NSString*) photographer
{
return photographer;
}

@end

Questa parte di codice inizia con @implementation seguito dal nome della classe e finisce con @end come per l’interfaccia. Tutti i metodi dovranno apparire tra queste due righe.

I metodi getter presenti nel codice dell’esempio saranno molto familiari a chiunque abbia mai scritto del codice. Possiamo quindi passare ai setter che necessitano di qualche spiegazione in più.

- (void) setCaption: (NSString*)input
{
[caption autorelease];
caption = [input retain];
}

- (void) setPhotographer: (NSString*)input
{
[photographer autorelease];
photographer = [input retain];
}

Ognuno dei due metodi lavora su due variabili. La prima è un rigerimento all’oggetto esistente mentre la seconda è un oggetto passato in input.

In un ambiente che supporti il garbage collector potremmo semplicemente assegnare il nuovo valore:

- (void) setCaption: (NSString*)input
{
caption = input;
}

Se però non disponiamo di un garbage collector dobbiamo rilasciare l’oggetto e possiamo farlo in due modi: con release e con autorelease. Con release l’oggetto verrà rilasciato immediatamente mentre con autorelease l’oggetto verrà rilasciato non appena il metodo corrente termina.

Il metodo autorelease è più sicuro nei metodi setter. Potremmo infatti accidentalmente rilasciare un oggetto che avremmo poi dovuto restituire.

Probabilmente ora sarete un po confusi ma andando avanti tutto sarà più chiaro. Non è necessario aver capito tutto sino ad ora.

Il metodo init

Per la nostra classe potremmo prevedere un metodo init che imposti i valori iniziali alle variabili d’istanza:

- (id) init
{
if ( self = [super init] )
{
[self setCaption:@"Default Caption"];
[self setPhotographer:@"Default Photographer"];
}

return self;
}

L’unica istruzione che potrebbe richiedere delle spiegazioni in questo codice è la seconda nella quale viene assegnato a self il risultato di [super init].

Questo serve solamente a chiedere alla superclasse di effettuare le inizializzazioni necessarie. L’if è necessario per controllare che l’inizializzazione sia avvenuta con successo.

Il metodo dealloc

Il metodo dealloc è chiamato quando un oggetto è rimosso dalla memoria. Questo è sicuramente il momento migliore per rilasciare tutte le variabili d’istanza presenti nella classe:

- (void) dealloc
{
[caption release];
[photographer release];
[super dealloc];
}

Nelle prime due istruzioni richiamiamo i metodi release delle due variabili. In questo caso possiamo utilizzare release invece che autorelease per quadagnare un po di velocità.

L’ultima riga è invece molto importante. Questa infatti chiede alla superclasse di effettuare le deallocazioni del caso. Se non chiamiamo questo metodo l’oggetto non potrà essere rilasciato.

Se è attivo il garbage collector non sarà richiamato il metodo dealloc ma il metodo finalize.

Ulteriori cenni alla gestione della memoria

Il sistema di gestione della memoria di Objective-C è basato sul conteggio dei riferimenti agli oggetti.

In parole semplici effettuiamo una alloc su un oggetto, poi probabilmente effettueremo una retain e successivamente una release per ogni chiamata ad alloc o retain effettuata. Quindi se abbiamo effettuato una sola alloc e una sola retain dovremo effettuare due release.

Probabilmente una immagine potrà chiarire il concetto:

Questa è la teoria alla base del reference counting. Nella pratica però ci sono solamente due ragioni per le quali creare un oggetto:

  1. Per conservarlo in una variabile d’istanza
  2. Per usarlo temporaneamente all’interno di una funzione

Nella maggior parte dei casi i setter per una variabile d’istanza dovrebbero effettuare una release del vecchio oggetto e ritornarne uno nuovo. Bisogna quindi assicurarsi di rilasciarlo in dealloc.

Quindi in conclusione l’unica cosa che il programmatore deve fare è gestire i riferimenti locali all’interno delle funzioni. E la regola è questa: se abbiamo creato un oggetto con alloc o copy allora dobbiamo effettuare una release o una autorelease alla fine della funzione, altrimento non c’è nient’altro da fare.

Questo è il primo caso:

- (void) setTotalAmount: (NSNumber*)input
{
[totalAmount autorelease];
totalAmount = [input retain];
}

- (void) dealloc
{
[totalAmount release];
[super dealloc];
}

Questo è invece il secondo caso:

NSNumber* value1 = [[NSNumber alloc] initWithFloat:8.75];
NSNumber* value2 = [NSNumber numberWithFloat:14.78];

// rilasciamo solamente value1 e non value2
[value1 release];

Ed ecco invece una combinazione dei due casi precedenti:

NSNumber* value1 = [[NSNumber alloc] initWithFloat:8.75];
[self setTotal:value1];

NSNumber* value2 = [NSNumber numberWithFloat:14.78];
[self setTotal:value2];

[value1 release];

Se avete capito questi concetti, avete chiaro il 90% di quello che c’è da sapere sulla gestione della memoria in Objective-C.

NSLog

Loggare messaggi in console con Objective-C è molto semplice. Abbiamo infatti a disposizione la funzione NSLog() che è molto simile alla printf() del c eccetto per il token %@ che permette di stampare oggetti.

NSLog(@"The current date and time is: %@", [NSDate date]);

Come accennato prima è possibile stampare degli oggetti. NSLog chiama il metodo description dell’oggetto e stampa l’NSString ritornata. Questo metodo può essere anche sovrascritto nelle nostre classi per restituire una stringa personalizzata.

Le proprietà

Quando abbiamo scritto i metodi per accedere a caption e author avrete sicuramente notato che il codice usato è ripetitivo e può essere generalizzato.

Le proprietà sono una caratteristica di Objective-C che permette di generare automaticamente i metodi per l’accesso alle variabili e che forniscono anche altri benefici. Modifichiamo quindi la classe Photo per l’utilizzo delle proprietà.

Ecco come appariva il codice in precedenza:

#import

@interface Photo : NSObject
{
NSString* caption;
NSString* photographer;
}

- (NSString*) caption;
- (NSString*) photographer;
- (void) setCaption: (NSString*)input;
- (void) setPhotographer: (NSString*)input;

@end

Ed ecco come appare ora:

#import

@interface Photo : NSObject
{
NSString* caption;
NSString* photographer;
}

@property (retain) NSString* caption;
@property (retain) NSString* photographer;

@end

La direttiva @property dichiara le proprietà. Il retain racchiuso nelle parentesi invece indica che i setter devono restituire il valore in input. Il resto della dichiarazione delle proprietà invece indica il nome e il tipo delle proprietà.

Vediamo invece come dorvà essere scritto il file di implementazione:

#import "Photo.h"

@implementation Photo

@synthesize caption;
@synthesize photographer;

- (void) dealloc
{
[caption release];
[photographer release];
[super dealloc];
}

@end

La direttiva @synthesize genera automaticamente getter e setter per noi quindi dovremo solamente implementare il metodo dealloc.

I metodi per l’accesso alle variabili vengono generati solamente se non sono già esistenti, quindi possiamo anche specificare la direttiva @synthesize per una proprietà, per poi implementarla a mano. Il compilatore completerà l’implementazione con i soli metodi mancanti.

Ci sono altre opzioni nell’uso delle proprietà ma non fanno parte dello scopo di questo tutorial.

L’oggetto nil

In Objective-C l’oggetto nil è l’ecquivalente di NULL per gli altri linguaggi. La differenza sta nel fatto che su possono chiamare metodi su oggetti nil senza causare crash o sollevare eccezioni.

Questa tecnica è usta nei frameworks in diverse occasioni.

Possiamo anche utilizzarla per migliorare il nostro metodo dealloc:

- (void) dealloc
{
self.caption = nil;
self.photographer = nil;
[super dealloc];
}

Questo è possibile perchè quando impostiamo una variabile a nil il setter ritorna come valore nil e rilascia il vecchio valore. Questo approccio a volte è preferibile perchè non c’è possibilità per la variabile rilasciata di puntare ad un indirizzo casuale che appartiene ad un altro oggetto.

Notate che nel codice precedente viene usato self il che vuol dire che il setter sarà chiamato automaticamente.

Se avessimo scritto il codice in questo modo invece avremmo avuto un errore:

// sbagliato. provoca una violazione di memoria
// usate self.caption per passare attraverso il setter
caption = nil;

Le categorie

Le categorie sono una delle funzioni di Objective-C più utili. Essenzialmente una categoria permette di aggiungere metodi ad una classe preesistente senza ereditarla.

Questo è particolarmente utile perchè offre la possibilità di aggiungere metodi ad oggetti compilati. Se ad esempio volessimo aggiungere un metodo ad una istanza della classe NSString possiamo farlo aggiungendo una categoria.

Per esempio supponiamo di voler aggiungere un metodo ad NSString per determinare se contiene un URL. Ecco il codice:

#import

@interface NSString (Utilities)
- (BOOL) isURL;
@end

Come potete vedere è molto simile alla dichiarazione di una classe. Le differenze sono nel fatto che non c’è una superclasse e al suo posto c’è il nome della categoria tra parentesi.

Ecco invece l’implementazione. Tenete presente però che questo non è il modo migliore per riconoscere un URL! Quello che importa è spiegare il concetto di categoria:

#import "NSString-Utilities.h"

@implementation NSString (Utilities)

- (BOOL) isURL
{
if ([self hasPrefix:@"http://"])
return YES;
else
return NO;
}

@end

Ora potremo usare questo metodo su una qualsiasi NSString. Il codice seguente stamperà “string1 è una stringa” in console:

NSString* string1 = @"http://pixar.com/";
NSString* string2 = @"Pixar";

if ([string1 isURL])
NSLog (@"string1 is a URL");

if ( [string2 isURL] )
NSLog (@"string2 is a URL");

A differenza delle sottoclassi, le categorie non possono aggiungere variabili d’istanza. E’ invece possibile sovrascrivere metodi già esistenti nella classe, anche se questo deve essere fatto con molta attenzione.

Ricordate quando fate delle modifiche ad una classe usando una categoria che queste saranno applicate a tutte le istanze di quella classe nell’applicazione.

Conclusioni

Questa guida è una mia traduzione dell’articolo originale in lingua inglese.