Fin dall’arrivo di Swift, gli optionals sembrano aver suscitato molta confusione nelle persone impegnate nello studio di questo linguaggio. Devo ammettere che all’inizio anche io ho impiegato un po’ di tempo a mettere in ordine i miei pensieri a tal riguardo: in seguito ho capito di non aver investito abbastanza tempo nel loro apprendimento.

Il mio approccio iniziale consisteva nel tentativo di utilizzarli all’interno di un progetto in corso, ma tale metodo si rivelò errato. Punti interrogativi e ed esclamativi spuntavano ovunque e mi lasciavano perplesso sui motivi. Nonostante capissi bene o male il loro significato, molte volte non comprendevo realmente il perché delle cose che facevo o se le eseguivo o meno nel modo corretto.

Soprattutto quando si è nuovi alla programmazione, potrebbe non essere facile individuare le problematiche che gli optionals risolvono e quando sfruttarli. Oltre ciò, gli operatori degli optionals (?, ! e ??) creano confusione circa il loro diverso significato a seconda dei contesti.

Prima di tutto vedremo cosa sono gli optionals e come usarli; vedremo poi quali problemi risolvono, perché è una buona idea usarli e il modo in cui riescono a far rispettare le buone pratiche rispetto alla scrittura di codice.

Contenuti di questo articolo

  • Perché abbiamo bisogno degli optionals?
  • I valori opzionali: unwrap forzato, optional binding e nil coalescing
  • Gli optionals nelle strutture e nelle classi: optional chaining
  • Evitare le verifiche per nil: unwrap implicito

Perché abbiamo bisogno degli optionals?

In un mondo ideale, il codice funziona sempre con dati completi e ben definiti. In realtà molti bug sono dovuti proprio al fatto che molti developer scrivono codice partendo da questo presupposto. Prendiamo come esempio un comune esercizio di programmazione: una funzione per calcolare il fattoriale di un numero intero.

Il fattoriale di un numero intero è il prodotto di tutti i numeri interi positivi fino a quel dato numero. Ad esempio, il fattoriale di 5 è 1 x 2 x 3 x 4 x 5 = 120.

Scriviamo una funzione fattoriale in Swift:

Alquanto facile, ma se proviamo a calcolare il fattoriale di -3, la nostra funzione restituisce erroneamente 1. La definizione del fattoriale afferma che esso esiste solo per i numeri positivi, dunque il fattoriale di -3 non dovrebbe restituire alcun valore. In che modo possiamo rappresentare ciò?

In molti altri linguaggi vengono utilizzati valori speciali per rappresentare risultati che non esistono. In questo caso, ad esempio, potremmo restituire -1 per rappresentare l’assenza di un fattoriale per un dato numero, dal momento che il fattoriale di un numero è sempre un numero positivo, per definizione. Ciò può funzionare, ed è stata infatti la scelta effettuata per molti programmi del passato… tuttavia non si tratta della soluzione migliore.

Ma se qualcuno utilizza questa funzione, come fa a sapere che -1 vuol dire “nessun risultato”? Senza dubbio, chi non conosce la definizione di fattoriale non potrebbe mai sapere se il risultato mostrato sia corretto o errato. Potremmo scrivere della documentazione su tale funzione, ma la gente potrebbe ignorarla e continuare ad usarla in modo improprio. In tal caso, il valore errato finirebbe per diffondersi in altre parti del programma causando bug difficili da individuare.

Alcuni linguaggi usano un valore speciale, chiamato nil, per rappresentare l’assenza di un valore. In molti di questi linguaggi, tuttavia, ciò è possibile solo per il riferimento ad oggetti, lasciando fuori types semplici come i numeri interi. Per questi, ancora una volta i developer si sono dovuti affidare a valori speciali, sperando che questi non rientrino nell’insieme dei valori validi.

Swift risolve questo problema con gli optionals. Aggiungendo un ? dopo la dichiarazione di un type si stabilisce che potrebbe esservi un valore o nessun valore (in Swift è rappresentato da nil come in altri linguaggi).

Ora possiamo riscrivere la nostra funzione fattoriale in modo da poter avere la non restituzione di un valore in caso di numeri negativi:

Tale dichiarazione stabilisce in modo esplicito che il valore restituito potrebbe trattarsi di un numero primo o un valore nil. È una differenza fondamentale rispetto ad altri linguaggi che usano lo stesso valore, dove non potremo mai sapere se un valore può essere o meno nil.

È necessario considerare sempre la documentazione ufficiale, nel caso in cui esista. In Swift, al contrario, questa circostanza è resa chiara dal type, dunque sappiamo sempre quali valori potrebbero essere nil e quali invece saranno sempre validi.

Una piccola nota. Come suggerito da un lettore, la funzione fattoriale dovrebbe verificare il suo input attraverso una asserzione al fine di assicurarsi che un valore negativo non passi mai. Questo perché il fattoriale è indefinito per i parametri negativi, quindi non dovrebbe accettarli e lasciare la verifica dell’input al chiamante.

I valori opzionali: unwrap forzato, optional binding e nil coalescing

Optional chaining sulle proprietà

In Swift i developer non sono gli unici a sapere quali valori sono opzionali. Anche il compilatore ne è al corrente e può eseguire una serie di verifiche solitamente impossibili in altri linguaggi. È sempre positivo delegare al compilatore quante più verifiche possibili, liberando così i nostri pensieri.

Proviamo ad usare la nostra nuova funzione fattoriale:

Sfortunatamente non va a buon fine. Il compilatore sembra pensare a qualcosa… ma perché non possiamo semplicemente aggiungere questi due numeri? Dopotutto, siamo sicuri che il fattoriale di 3 è 6, quindi l’operazione dovrebbe essere possibile.

Il problema qui sta nel fatto che stiamo provando ad aggiungere ciò che per il compilatore sono due type distinti. Il risultato della funzione fattoriale non è di type Int? e per il compilatore si tratta di due type differenti che dunque non possono essere sommati tra loro. Vedremo in seguito perché ciò è vero. Per ora è utile pensare che un numero non può essere sommato a qualcosa che potrebbe o non potrebbe essere un numero. Per eseguire questa addizione, dobbiamo trasformare il valore opzionale in un valore non opzionale: lo facciamo con l’operatore ! dell’unwrap forzato.

Il significato dell’operatore del forced unwrapping è: “So che questo opzionale ha un valore quindi, per favore, usalo”. In ogni caso c’è un problema: mentre in questo semplice esempio ne abbiamo la certezza, gran parte delle volte non possiamo sapere se un opzionale avrà o meno un valore. Usare il forced unwrapping su un valore nil causerà un’eccezione del tempo di esecuzione ed un crash. Dobbiamo assicurarci che l’opzionale abbia un valore con un if statement prima di eseguire l’unwrap:

Le dichiarazioni if e while supportano l’optional binding, il quale permette di verificare un opzionale e di estrarre il suo valore in una singola azione. In questo modo l’unwrap forzato dell’opzionale all’interno del corpo della dichiarazione non è più necessario. Ciò torna particolarmente utile se un opzionale viene utilizzato più di una volta.

A volte, quando un opzionale non ha valore, vogliamo stabilirne uno di default. Possiamo fare ciò attraverso un if statement o, più rapidamente, con un operatore ternario:

L’espressione tra parentesi significa: “Se fact ha un valore, usalo. Altrimenti usa 0”. Tenete conto che in tal caso il punto interrogativo non è un’operazione opzionale ma è parte della sintassi dell’operatore ternario. L’operatore nil coalescing è un modo per accorciare l’espressione:

Gli optionals nelle strutture e nelle classi: optional chaining

Gli opzionali non sono usati solo come valori di ritorno nelle funzione, ma possono inoltre intervenire per le variabili e per le proprietà di strutture e classi. Li usiamo per indicare una variabile o proprietà che non contiene nessun valore. Ad esempio, creiamo una struttura al fine di conservare informazioni su una persona per una libreria cittadina e associamo alla persona una card che registrerà l’elenco dei libri presi in prestito.

La proprietà libraryCard è un optional, dato che non tutti sono registrati al sistema della libreria. Ora vogliamo stampare il numero di libri che una persona ha preso in prestito in passato. Per ottenere la proprietà di numberOfBorrowedBooks dobbiamo tuttavia passare per la proprietà opzionale di libraryCard della classe Person, che potrebbe risultare nil. Possiamo ovviamente utilizzare un optional binding su libraryCard e, se non fosse nil, accedere alla proprietà numberOfBorrowedBooks del valore unwrapped.

Swift dispone però di un meccanismo alternativo che ci consente di andare direttamente attraverso l’optional al valore che vogliamo, senza utilizzare l’optional binding. Questo metodo è chiamato optional chaining e si esegue con un ? dopo la optional property:

In questo caso, il ? ha un significato diverso rispetto all’uso visto in precedenza, ossia dopo il type. Quest’ultimo vuol significare che “questo valore potrebbe non esistere”. Posto alla fine di una variabile o di una proprietà, il ? assume il significato di “se ha un valore, accede a quest’altra proprietà, altrimenti restituisce nil”. Quindi, se libraryCard non è nil, accediamo direttamente alla sua proprietà numberOfBorrowedBooks. Altra cosa è l’uso del !, che forzerebbe l’unwrap causando un crash in caso di valore nil.

Dal momento che l’accesso alla proprietà numberOfBorrowedBooks potrebbe non andare a buon fine, il valore restituito dall’optional chaining è un opzionale anche se la proprietà originale non lo è. In questo caso, la stessa proprietà ha type Int ma il valore restituito da me.libraryCard?.numberOfBorrowedBooks ha type Int?.

Inoltre è possibile tentare l’impostazione di una proprietà attraverso l’optional chaining. Mentre normalmente un’assegnazione non ha un return type, tentare il settaggio di una proprietà attraverso un optional chaining restituisce un valore di tipo Void?. Ciò permette di verificare se l’assegnazione è andata a buon fine:

Optional chaining sui metodi

L’optional chaining può essere utilizzato anche per chiamare metodi su un opzionale. Estendiamo ora le classi per includere un elenco di libri presi in prestito:

Come nel caso dell’assegnazione, il metodo addBook ha un Void return type, ma con l’optional chaining diventerà Void?, consentendoci di verificare il risultato del metodo chiamato.

Livelli multipli di chaining

L’optional chaining può essere concatenata per livelli multipli di optionals. Aggiungiamo una proprietà a LibraryCard per recuperare l’ultimo libro preso in prestito da una persona:

Ora possiamo utilizzare livelli multipli di chaining per estrarre il titolo del l’ultimo libro preso in prestito da John:

Come abbiamo visto, anche se il type restituito da una proprietà o da un method non è opzionale, l’optional chaining lo renderà tale. Se il type è già opzionale, invece, resterà com’è. In questo caso abbiamo due livelli di chaining, ma il type di ritorno è ancora String? e non String?? (se vi state chiedendo se ciò sia possibile, la risposta è sì – vedremo più avanti).

Infine, l’optional chaining può essere utilizzato anche sui pedici. Tutto quel che è stato detto per le proprietà e per i metodi si applica allo stesso modo.

Optional chaining sui requisiti per protocollo opzionale

Fino ad ora abbiamo visto l’uso degli opzionali per elementi che potrebbero non restituire un valore. L’optional chaining può essere usata anche per proprietà e metodi che non potrebbero non esistere. È il caso dei requisiti per il protocollo opzionale.

Quando si definisce un protocollo è possibile specificare alcuni metodi o proprietà come opzionali. Ciò vuol dire che le classi che si conformano al protocollo non devono necessariamente implementarli. Quindi, quando si accede a queste proprietà o metodi di un protocol type, non si può avere la certezza che una classe conforme abbia un implementazione per loro. Per questa ragione è richiesto l’uso dell’optional chaining.

Supponiamo di lavorare ad un gioco RPG che include dei maghi. Un mago a volte può disporre di una creatura amica (familiar), che può essere un semplice animale o un essere magico e potente. Alcune creature amiche possono offrire al mago un incantesimo extra da utilizzare. Possiamo esprimere ciò con un protocollo:

Nota: il protocollo è contrassegnato con la keyword @obcj perché è l’unico modo per dichiarare metodi e proprietà opzionali nei protocolli dell’attuale implementazione di Swift. La classe Spell eredita da NSObject invece di essere una pura classe Swift, perché il valore di ritorno dei requisiti per il protocollo opzionale necessita di essere compatibile con Objective-C).

Ora possiamo implementare la classe Wizard con un metodo che restituisce gli incantesimi disponibili:

Qui l’optional chaining è utilizzato in due modi diversi. Il primo l’abbiamo già visto: il mago potrebbe non disporre di una creatura amica, quindi la proprietà è un opzionale. Il secondo, invece, verifica se il metodo familiarSpell è presente. Da notare che ? è posizionato prima delle parentesi della chiamata del metodo e non dopo. L’ultimo verificherebbe se il valore di ritorno esisteva, mentre in questo caso verifichiamo l’esistenza del metodo stesso.

Evitare le verifiche per nil: unwrap implicito

Gli opzionali aggiungono al nostro codice una serie di punti interrogativi e di esclamazione, if statement e optional binding, i quali finiscono per rendere la lettura più difficile. Questo è uno dei compromessi degli optionals.

Ci sono alcuni casi specifici, tuttavia, in cui sappiamo che dopo una proprietà opzionale ci sarà un valore e che non sarà mai più nil. In questi casi non sono necessarie tutte le verifiche e neanche l’unwrap forzato, dal momento che l’opzionale da un certo punto avrà sempre un valore, rendono non più propriamente opzionale.

Per questi casi possiamo dichiarare un opzionale come implicitly unwrapped. Facciamo ciò mettendo un ! dopo il suo type, al posto di un punto interrogativo. Ciò permetterà all’opzionale di iniziare con un valore nil quando un valore valido non è ancora disponibile, mentre ci consente di usarlo come se non fosse un opzionale (senza operatori speciali e verifiche). Ci sono alcuni casi in cui ciò è necessario.

Unowned references nei cicli di riferimento

Quando due oggetti hanno un riferimento reciproco, dobbiamo evitare di creare cicli di riferimento forti sotto ARC, altrimenti i due oggetti non saranno mai rimossi dalla memoria. Ci sono tre casi specifici in cui questi cicli possono esistere.

Nel primo caso, i due oggetti possono entrambi avere un riferimento nil reciproco. Tale circostanza viene risolta dichiarando entrambi i riferimenti come opzionali e impostando uno dei due come weak. Ad esempio, un oggetto Person e un oggetto Apartment possono disporre di un ciclo di riferimento, ma possono esistere indipendentemente dall’altro.

Nel secondo caso, uno dei due riferimenti necessita sempre di un valore, dato che l’esistenza dell’oggetto a cui ci si riferisce dipende sull’esistenza di un altro oggetto. Dal momento che questa proprietà non è un optional, non può essere impostato come weak, perché questo tipo di riferimenti sono impostati su nil da ARC quando il referenced object viene rimosso dalla memoria.

D’altro lato, neanche il riferimento nell’altro oggetto può essere debole: è necessario mantenere l’oggetto dipendente nella memoria. Questo caso è risolto in Swift rendendo il primo riferimento opzionale e segnando il secondo come unowned, facendo sì che si comporti come una weak reference dal punto di vista dell’ARC, senza essere impostato su nil. Per esempio, il cliente di una banca potrebbe avere o meno una carta di credito; e la carta di credito necessita sempre di essere associata ad un cliente.

Nel rimanente caso, entrambi i riferimenti necessitano sempre di un valore. In tale situazione, uno dei due oggetti viene creato nell’initializer dell’altro, che passa esso stesso come parametro all’initializer del secondo. Ciò garantisce che entrambi i riferimenti siano inizializzati nel modo appropriato. Qui c’è però un problema: Swift non permette auto-riferimenti finché l’oggetto non è completamente inizializzato.

Per essere completamente inizializzato, tuttavia il primo oggetto deve creare una istanza del secondo, passando come parametro, che è proibito. È qui che gli opzionali implicitly unwrapped arrivano a salvarci. Il primo riferimento è reso implicitly unwrapped optional, permettendo così di essere inizializzato a nil temporaneamente, mentre la seconda istanza viene creata.

Inizializzazione di IBOutlets

Altro scenario comune per gli implicitly unwrapped optionals sono gli outlets verso gli oggetti provenienti da una storyboard o da un file xib. Quando le views o i view controller vengono inizializzati fa un file interface builder, i loro outlets non possono essere ancora collegati. Verranno collegati solo dopo l’inizializzazione; nonostante ciò, il collegamento di tali outlets è garantito. Ecco perché gli IBOutlets sono sempre dichiarati come implicitly unwrapped optionals.

Inizializzatori fallibili

Se l’inizializzazione di un oggetto non va a buon fine, dovrebbe immediatamente essere restituito nil. Per definizione, però, non tutte le proprietà saranno state inizializzate da quel punto e Swift non consente ad un init method di restituire un valore finché tutte le proprietà non saranno inizializzate. Gli implicitly unwrapped optionals risolvono questo problema.

Una nota sugli opzionali implicitly unwrapped

Gli implicitly unwrapped optionals sono pericolosi e dovrebbero essere utilizzati solo quando si è sicuri di quanto si compie. Il motivo è che loro bypassano tutte le verifiche del compiler per avere modo di generare un’eccezione di runtime. Come detto, sono intesi per casi specifici in cui si è certi che il valore esisterà sempre dopo l’inizializzazione, senza richiedergli l’unwrap ogni volta.

Non è sempre facile da sapere, mentre più semplice è cadere nella tentazione di usare gli implicitly unwrapped optionals per casi che non rientrano in questa regola, dal momento che appaiono più pratici. Fare ciò, tuttavia, elimina l’intero scopo degli opzionali. Nel caso di dubbi, usate un valore non opzionale quando possibile, oppure un opzionale normale.

***

Autore: Matteo Manferdini
Articolo originale: The Complete Guide to Understanding Swift Optionals

NO COMMENTS

LEAVE A REPLY