Compatibile con Xcode 8

Le differenze tra struct e class del linguaggio Swift

ENTRA NEL NOSTRO GRUPPO FACEBOOK

Una delle tante domande che scatta non appena entrati in contatto con la programmazione ad oggetti è: Quali sono le differenze tra struct e class del linguaggio Swift?

La confusione nasce dal fatto che sia le classi che le strutture hanno caratteristiche simili che non permettono, ad un programmatore alle prime armi, di capirne le differenze.

Infatti, per il linguaggio Swift, sia le classi che le strutture vengono considerati oggetti ed entrambe:

  1. Possono definire delle proprietà per conservare valori
  2. Possono avere dei metodi che forniscono funzionalità o che lavorano con altri oggetti
  3. Sono estensibili tramite le extension
  4. Possono implementare dei protocol 
  5. Forniscono gli init o costruttori con il quale è possibile creare gli oggetti
  6. Possono avere dei subscript per accedere ai valori delle proprietà
Allora, cos’è che differenzia una struttura da una classe?
Scopriamolo insieme!

Classi: Ereditarietà, casting e deinit

Le classi sono uno strumento potentissimo. Danno vita a quello che il mondo informatico chiama pattern di programmazione orientata agli oggetti.

Una classe (come anche una struct) è la maschera di un insieme di proprietà e metodi che sono accumunate da uno scopo. Una classe permette di astrarre la complessità di un problema mascherando, all’interno di essa, tutte le logiche ed informazioni.

Le classi però vanno oltre questo concetto aggiungendo la possibilità di ereditare gli attributi e metodi da una classe genitore. Permettendoti di estendere e diversificare un problema con una precisione che le strutture, purtroppo, non permettono.

Se questa sintassi ti è nuova, forse dovresti andare a leggere la lezione gratuita sull’ereditarietà delle classi.

In questo modo, l’ereditarietà, ti permette di lavorare su oggetti logicamente simili e praticamente accumunati dall’essere figli di una classe genitore. Cosa che comporta il poter utilizzare i metodi ed attributi del genitore e tutte le operazioni di casting:

Infine, le classi, permettono di definire un metodo di deinit che funziona esattamente all’inverso di un init. Viene invocato durante il processo di distruzione dell’oggetto.

Non entro nei dettagli perché si aprirebbe un argomento a parte che si chiama Automatic Reference Counting ma per capire un po’ di che si tratta, il deinit viene chiamato quando l’ultimo riferimento ad un oggetto viene passato a nil:

Il perché della sua esistenza è una diretta conseguenza dei prossimi paragrafi. Ad ogni modo ne parlerò in un tutorial dedicato all’argomento.

Il concetto di Puntatore o Riferimento

Figo, bello e bellissimo! Così per come ho presentato le classi, solo un pazzo si azzarderebbe ad utilizzare le struct perché risultano essere decisamente più limitate.

Allora finiamo qua?

Per arrivare al punto principale, devo farti scendere nelle viscere della programmazione.

All’interno del corso gratuito, precisamente qui, ho accennato a come vengono memorizzate le informazioni all’interno della memoria di un dispositivo.

Per farla breve, la memoria dinamica devi immaginarla come ad una tabella con due colonne. La colonna di sinistra rappresenta gli indirizzi di memoria, più o meno come un’indirizzo di una casa in una via, e la colonna di destra rappresenta il contenuto cioè chi abita in quell’indirizzo.

indirizzi di memoria

Questo vuol dire che quando vado a creare una nuova variabile:

Il sistema prenderà un indirizzo libero, per esempio il n.1, e metterà al suo interno il valore 45. Quando andrai a leggere la variabile unaVar, il sistema saprà recuperare il valore 45 perché conosce l’indirizzo di memoria in cui è stato conservato.

Quindi, le variabili (come le costanti), non sono altro che riferimenti (o puntatori) a specifici indirizzi di memoria.

Value Type

Le strutture hanno la proprietà d’essere di tipo Value Type.

Per capire il significato voglio farti un esempio:

Ipotizziamo di creare un oggetto di questa struttura, di modificarlo e di passarlo ad un’altra variabile:

Il passaggio di un oggetto, nato da una struct, da a verso b implica la creazione di un nuovo oggetto copia dell’oggetto passato.

Mi spiego meglio.

Ad a ho dato il valore 10 alla sua proprietà. Poi ho creato una var b a cui ho passato l’oggetto a. In questo passaggio il sistema ha creato un nuovo oggetto copia di a (quindi avrà lo stesso valore 10) che ha poi passato alla nuova var.

Essendo due oggetti diversi, la modifica di b non influirà sull’oggetto a perché trattasi di due oggetti diversi. Questo significa che, a livello di memoria, la variabile a e la variabile b puntano a due indirizzi differenti.

I tipi di dato fondamentali: String, Int, Double, Float, Character ecc sono strutture e quindi sono di tipo Value Type. Puoi accorgertene dal simbolo S accanto all’assistente di scrittura.

Solo per correttezza di informazioni. Non è sempre vero che una struttura viene immediatamente copiata al momento del passaggio. Per evitare problemi di sovraccarico, la copiatura in un nuovo riferimento viene effettuato solo quando viene modificato un valore dell’oggetto.

Reference Type

Le classi hanno la proprietà d’essere di tipo Reference Type.

Ipotizziamo d’avere la seguente classe e di eseguire un passaggio di un oggetto di questo tipo da una var a verso una var b:

La modifica di b.nome ha comportato pure la modifica di a.nome. Più in generale, la modifica dell’oggetto conservato in b ha avuto come conseguenza anche la modifica dell’oggetto conservato in a.

Come?

Reference Type significa che il passaggio di un oggetto, nato da una classe, verso una var/let implica il solo passaggio del riferimento a quell’oggetto.

Questo vuol dire che, l’oggetto contenuto in b e quello contenuto in a, sono lo stesso identico oggetto. A livello di memoria, la var a e la var b puntano esattamente allo stesso identico indirizzo di memoria.

Conseguenze

Vediamo alcuni problemi dall’utilizzo di una o dell’altra soluzione.

No alle struct come contenitori di grandi informazioni

Il passaggio di strutture dovresti aver capito che crea delle copie quando vengono passate da un punto all’altro. Questo, però, non significa che le copie precedenti vengano sempre distrutte automaticamente.

Di conseguenza, con le struct, potresti andar in contro a problemi di sovraccarico della memoria o di rallentamento in caso di strutture molto grandi.

Per questo motivo, se hai notato,  gli oggetti del framework Cocoa Touch per la gestione di file, che potrebbero avere grandi dimensioni, vengono gestiti tramite classi. In questo modo, durante il passaggio da un punto all’altro dell’applicazione, verrà sempre passato il riferimento.

Attenzione alle classi ed al reference counting

All’interno del nostro dispositivo esiste un modulo che si chiama ARC o Automatic Reference Counting che gestisce e tiene traccia di tutti i passaggi di riferimento di un oggetto, nato da una classe, ad un altro oggetto.

In pratica è un contatore che viene incrementato ad ogni passaggio:

Dato che viene passato il riferimento, sia che sono lo stesso oggetto. Questo ci ti può portare a credere che la distruzione della var b comporti anche la distruzione dell’oggetto a lui collegato.

Nessun deinit viene chiamato. Perché?

Perché la distruzione dell’oggetto può essere invocato solamente quando tutte le variabili che puntano a quell’oggetto vengono distrutte.

Esistono dei metodi per la gestione di questa singolarità ma ne parlerò in un topic dedicato. Così su due piedi, comunque, dovrebbe cominciare a farti venire qualche sospetto.

Immagina di passare un oggetto X, come riferimento ad un altro oggetto Y, in un punto indefinito dell’applicazione (per esempio da un ViewControllerA ad un ViewControllerB). L’ARC di X verrebbe incrementato. Ora immagina che l’oggetto a cui hai passato X, cioè Y, non venga mai distrutto perché Y ha anche altre variabili che puntano a lui.

Quello che si verrebbe a creare è un insieme di oggetti non più utilizzati (memory leaks) e, che rimarrebbero in memoria in un limbo, in attesa della liberazione che potrà avvenire solo al kill dell’applicazione.

Questi oggetti non distrutti porteranno irrimediabilmente al riempimento della memoria.

N.B. L’aumento di memoria di un’applicazione in maniera continua non è obbligatoriamente frutto di un memory leaks. Esistono degli strumenti appositi per l’analisi di questo problema.

Attenzione alla combinazione fatale tra le struct

Le struct sono indubbiamente più semplici e non causano i problemi di reference counting dato che vengono sempre copiate. Quindi potresti essere indotto a pensare di creare solo strutture.

Facciamo un esempio di un probabile problema.

Abbiamo un Hotel con una Room che ha come proprietà la possibilità di essere occupata o menoSia la Room che l’Hotel sono strutture:

Ora immagina di passare l’oggetto contenuto nella var sheraton ad un altro e di modificare da questa nuova var la stanza. Un possibile esempio potrebbe essere quello del passaggio di questo oggetto da un ViewController ad un altro.

La modifica del nuovo oggetto non comporterà la modifica del precedente e si creerà una situazione di disomogeneità dei dati prodotti.

La stanza è occupata o no?

Problema facilmente risolvibile con una combinazione tra struct e class:

In questo modo, l’hotel, che serve solo da contenitore può essere strutturato come una semplice struct. Mentre tutti i suoi dati passibili a modifica e non direttamente inglobati nella struttura, come class.

Considerazioni

Quindi struct o class?

Come si può ben capire, non esiste un regola generale. Tutto dipende dalla tipologia di problema che hai davanti.

Entrambe le soluzioni comunque portano alla risoluzione del problema. Una volta conosciuti i segreti e come gestirli si tratta di una semplice implementazione corretta.

– Peppe, io ho sempre programmato all’oscuro di quest’argomento e non ho mai avuto problemi!
– Hai perfettamente ragione!

Oggi, in un mercato sempre più in competizione e di fretta, si tende a sottovalutare l’aspetto tecnico delle soluzioni adottate. Gli hardware su cui i software girano sono talmente evoluti ed espansi che l’eventuale problema incorre o si presenta solo in situazioni complesse o stravaganti (per esempio in ambiente multithread).

Quindi il programmatore medio tralascia tutti questi particolari e sviluppa come il cuore lo comanda, il più in fretta possibile.

Alla fine, se difficilmente queste grane si presenteranno, tutto si può ricondurre al: quali sono le differenze tra un Programmatore ed un programmatore?

Come ho già spiegato nel tutorial sulla legge di Demetra, spetta a te scegliere in quale categoria collocarti.

Una cosa comunque è certa.

Se vuoi fare il salto di qualità devi cominciare a ragionare sulle scelte che prendi. Giuste o sbagliate che siano, devi provare a dare una motivazione sul perché stai scegliendo una class/struct o stai scrivendo un codice di 1k righe.

Se ti limiterai a scrivere codice, al primo progetto impegnativo o al primo colloquio con un’azienda “seria” finirai per scontrarti con qualcosa più grande di te.

Buona programmazione!

Start typing and press Enter to search

prinicpio-di-singola-responsabilit