Abbiamo già parlato altre volte dei Block qui su devAPP, ad esempio in questo articolo introduttivo o ancora in quest’altro, dove viene spiegato come usarli per creare delle animazioni sulle UIView.
Vorrei tornarne a parlarne ancora una volta perché lo ritengo un argomento molto importante, che se conosciuto a fondo può fornire un’utile strumento per risolvere diversi problemi di programmazione.
Prima di spiegare cosa sono i block e come usarli vorrei provare a fare un passo indietro per spiegare com’era il mondo prima dei block. Scopriremo che anche in questo caso gli ingegneri Apple sono stati in grado di prendere qualcosa di già esistente e portalo ad un altro livello di astrazione rendendo il loro utilizzo davvero molto semplice.
Tutti i linguaggi di programmazione permettono di dichiarare le funzioni (verrò smentito nei commenti da chissà quale strano linguaggio…) ma in alcuni casi le funzioni costituiscono anche un vero e proprio tipo di dato, come un intero, una stringa e così via. In questo caso si dice che per quel linguaggio le funzioni sono “first-class object” (wikipedia).
Questa possibilità potremmo affermare che è alla base della programmazione ad oggetti, perché come ho avuto modo di spiegare nel corso di C una classe in fondo non è altro che una struttura (nel senso di struct) con alcune variabili locali e alcune funzioni memorizzate all’interno della struttura. Se le funzioni non si potessero trattare come normali variabili sarebbe impensabile poterle inserire all’interno di una struttura.
Ma a cosa può servire poter trattare le funzioni come tipo di dato? A fare cose molto divertenti come ad esempio una funzione che prenda in input un elenco di valori e una seconda funzione e che restituisce l’elenco dei valori ai quali ha applicato singolarmente la seconda funzione:
fun map(list,f) {
for each l in list
f(l)
return list
}
Oppure la funzione potrebbe prendere in input una lista più una funzione e restiture true solo se almeno un oggetto della lista, passato alla funzione parametro, restituisce true:
fun filter(list,f) {
for each l in list
if f(l) return true;
}
Quindi avremmo un applicatore e un filtratore dinamici, il cui comportamento è variabile e dipende dalla funzione usata come parametro, insomma: una versione ridotta e semplificata del delegate design pattern.
Se volessimo realizzare, ad esempio, la funzione map in C, il codice che dovremmo utilizzare sarebbe questo:
#include
#include
int doSomeStuff(int a) {
return a * 42;
}
void map(int *list, int size, int (*f)(int)) {
int i = 0;
for (i = 0; i < size; ++i)
{
printf(" %d ", f(list[i]));
}
}
int main(int argc, char **argv){
int (*f)(int); //1
int lista[5] = {4,6,7,0,10};
f = doSomeStuff; //2
map(lista,5,f);
return 0;
}
Abbiamo la funzione da applicare (doSomeStuff) abbiamo la lista dei valori (list[5]) e come si può vedere al punto //1 ho dichiarato un puntatore ad una funzione che accetta come input un intero e che restituisce un intero.
Alla nota numero //2 il puntatore è stato assegnato alla funzione doSomeStuff, e finalmente richiamiamo la funzione map.
Se non è chiaro non preoccupatevi, uno dei motivi per cui a Cupertino hanno investito sulla creazione dei block è proprio perché l'utilizzo a basso livello dei puntatori a funzione non è proprio semplice.
I linguaggi che implementano le funzioni come firt-class object sono veramente tanti, soprattutto linguaggi più moderni come python oppure lua, spesso però in questi contesti si sente parlare di closure, (wikipedia).
Qual'è la differenza?
La differenza è molto importante soprattutto perché i blocks sono un'implementazione di closure, non di semplici puntatori a funzione.
La differenza è che quando si parla di closure la funzione puntata può accedere a tutte le variabili che erano presenti nel suo scope al momento della creazione, anche se l'oggetto che l'ha dichiarata non è più in memoria..... eh? Qui è necessario fermarsi e riflettere...
Supponiamo di avere il nostro programma scritto in python, o anche in obj-c. All'interno di una classe A dichiariamo una funzione che avrà accesso ovviamente a tutte le variabili di istanza della classe A, poi creiamo un puntatore a questa funzione e lo passiamo in giro per il programma... a questo punto anche se la classe A dovesse essere deallocata, in ogni caso la funzione potrebbe accedere alle variabili di istanza perché ne ha una sua versione "congelata". Un filo più chiaro?
Ecco quindi che se abbiamo capito questo è molto più semplice inquadrare i block nel loro reale contesto, sono una molto elegante implementazione delle clousure in obj-c.
Ma come si usano?
Per spiegare come utilizzare i block in obj-c non è necessario creare un progetto iOS, possiamo usare un semplice programma a riga di comando, quindi apriamo Xcode e dal selettore del template scegliamo Application -> command line tool.
Come si dichiara un block? Dipende ovviamente dal tipo di codice al quale dovrà puntare, ad esempio questa sintassi:
int (^f)(int);
dichiara una variabile che si chiama f e che sarà di tipo block. Il block in questione dovrà ritornare un int e prendere in input un int;
Un secondo esempio come questo:
BOOL (^f2)(int);
La variabile si chiama f2, accetta un intero come parametro e restituisce un BOOL.
Per l'inizializzazione si utilizza questa sintassi:
f = ^(int num){
return num * 2;
};
In questo caso la varibabile f punta ad un blocco di codice che accetta un intero come parametro e che ne restituisce il valore moltiplicato per due.
Va sottolineato che all'interno di questa porzione di codice possiamo accedere a tutte le variabili visibili, quindi se avessimo dichiarato una variabile fuori dal block, avremmo potuto utilizzarla anche all'interno, come in questo esempio:
int multiplier = 4;
int (^f)(int);
f = ^(int num){
return num * multiplier;
};
Poiché, come abbiamo detto, si tratta di clousure e non di puntatori a funzioni, anche se dovessimo passare la variabile f ad un'altra classe non ci sarebbero problemi per l'accesso a multiplier, perché ciasun block porta con sé una "copia" di tutte le variabili alle quali ha accesso.
Per richiamare il block si fa semplicemente così:
NSLog(@"Risultato %d",f(20));
Come si può implementare quindi in obj-c la nostra funzione map? Il concetto è del tutto simile a quanto abbiamo visto in C, cambia solo leggermente la sintassi:
/*
* La funzione accetta la solita lista,
* un intero che ne specifica la dimensione
* e una variabile di tipo block.
* Percorre la lista applicando il block all'elemento e stampando il valore
*/
void map(int *lista, int size, int (^f)(int) ){
for (int i = 0; i < size; i++) {
NSLog(@"%d", f(lista[i]));
}
}
int main(int argc, const char * argv[])
{
@autoreleasepool {
//dichiarazione e inizializzazione della lista.
int lista[5] = {1,2,3,4,5};
//variabile esterna visibile anche dentro il block.
int multiplier = 4;
//variabile di tipo block
int (^f)(int);
//assegnazione della variabile block
f = ^(int num){
return num * multiplier;
};
//invocazione della funzione map
map(lista, 5, f);
}
return 0;
}
Se siete arrivati fin qui adesso i block non dovrebbero avere più segreti, proviamo infatti ad esaminare un metodo tra i tanti che li utilizzano, ad esempio guardiamo il prototipo di:
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block
Questo metodo cosa ci dice?
E' un metodo di istaza (il -) non restituisce alcun valore (void) e accetta un parametro di tipo block, che a sua volta non restituisce nulla e accetta tre parametri, un id, un intero e un puntatore a BOOL
Se lo volessimo usare allora cosa dovremmo fare?
- Dichiarare una variabile di tipo block che accetti il giusto numero e tipo di parametri, in questo modo:
void (^p)(id, NSUInteger, BOOL *); //la variabile in questo caso si chiama p
- Assegnare la variabile ad una porzione di codice corretta:
p = ^(id obj, NSUInteger index, BOOL *stop){ NSLog(@"At index: %lu obj=%@", index, obj); };
- Invocare il metodo enumerateObjectsUsingBlock su un nostro array passando il block come parametro:
NSArray *array = [NSArray arrayWithObjects:@"ONE",@"TWO", @"THREE", nil]; [array enumerateObjectsUsingBlock:p];
Questo è ovviamente il modo più prolisso, ma che fa capire bene cosa sta succedendo, probabilmente vi capiterà più spesso di saltare la dichiarazione e di mettere il block direttamente nell'invocazione, in questo modo:
NSArray *array = [NSArray arrayWithObjects:@"ONE",@"TWO", @"THREE", nil];
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop){
NSLog(@"At index: %lu obj=%@", index, obj);
}];
Una domanda per i lettori: Perché l'ultimo parametro è un puntatore a BOOL? Date la vostra risposta nei commenti!
Prima di lasciarci un'ultima informazione molto utile quando si usano i block, abbiamo detto che tutte le variabili accessibili al block al momento della sua creazione (variabili globali, di istanza etc) sono "copiate" insieme al block in modo che anche se la sua esecuzione dovesse avvenire fuori dallo scope originale non ci sarebbero problemi di visibilità... non abbiamo specificato però che queste variabili sono accessibili in sola lettura, perché gestirne anche la scrittura sarebbe un tantino oneroso per il sistema e poi non vogliamo realmente che del codice, in giro per il programma, possa modificare tutte le nostre variabili... non sarebbe molto pulito.
Esiste un modo però per far sì che una variabile sia copiata in lettura e scrittura e consiste nell'utilizzare il modificatore di scope __block prima della dichiarazione, come in questo esempio:
__block int multiplier = 4; //la variabile adesso è modificabile anche dall'interno del block
int (^f)(int);
//assegnazione della variabile block
f = ^(int num){
multiplier = multiplier + 1;
return num * multiplier;
};
Con questo credo proprio di aver soddisfatto la vostra curiosità sui block, per domande e commenti visitate il nostro forum o utilizzate i commenti qui in basso.
Alla prossima!
One Response to “L#021 – Guida completa all’uso dei BLOCK”
24 Maggio 2012
simoneil puntatore a bool permette di modificare il valore della variabile puntata