Gli errori che la maggior parte degli sviluppatori Swift non sa di fare

Pubblicato: 2022-03-11

Venendo da un background di Objective-C, all'inizio, mi sentivo come se Swift mi stesse trattenendo. Swift non mi permetteva di fare progressi a causa della sua natura fortemente tipizzata, che a volte era irritante.

A differenza di Objective-C, Swift applica molti requisiti in fase di compilazione. Le cose che sono rilassate in Objective-C, come il tipo id e le conversioni implicite, non sono una cosa in Swift. Anche se hai un Int e un Double e vuoi sommarli, dovrai convertirli in un unico tipo in modo esplicito.

Inoltre, gli optional sono una parte fondamentale del linguaggio e, anche se sono un concetto semplice, ci vuole del tempo per abituarsi.

All'inizio, potresti voler forzare lo scarto di tutto, ma ciò alla fine porterà a arresti anomali. Man mano che acquisisci familiarità con il linguaggio, inizi ad amare il modo in cui non hai quasi errori di runtime poiché molti errori vengono rilevati in fase di compilazione.

La maggior parte dei programmatori Swift ha una significativa esperienza precedente con Objective-C, che, tra le altre cose, potrebbe portarli a scrivere codice Swift usando le stesse pratiche con cui hanno familiarità in altri linguaggi. E questo può causare alcuni brutti errori.

In questo articolo, delineiamo gli errori più comuni commessi dagli sviluppatori Swift e i modi per evitarli.

Non commettere errori: le migliori pratiche di Objective-C non sono le migliori pratiche di Swift.
Twitta

1. Opzionali per l'annullamento dell'imballaggio forzato

Una variabile di tipo facoltativo (ad es String? ) potrebbe contenere o meno un valore. Quando non mantengono un valore, sono uguali a nil . Per ottenere il valore di un optional bisogna prima scartarli , e questo può essere realizzato in due modi diversi.

Un modo è l'associazione facoltativa usando if let o guard let , ovvero:

 var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }

Il secondo è forzare lo scarto usando il ! operatore o utilizzando un tipo facoltativo implicitamente annullato (ad es String! ). Se l'opzione facoltativa è nil , forzare un'annullamento del wrapping causerà un errore di runtime e chiuderà l'applicazione. Inoltre, il tentativo di accedere al valore di un optional annullato in modo implicito causerà lo stesso.

A volte abbiamo variabili che non possiamo (o non vogliamo) inizializzare nell'inizializzatore di classe/struct. Pertanto, dobbiamo dichiararli come optional. In alcuni casi, assumiamo che non saranno nil in alcune parti del nostro codice, quindi forziamo lo scarto o li dichiariamo come optional implicitamente scartati perché è più facile che dover eseguire sempre il binding facoltativo. Questo dovrebbe essere fatto con cura.

Questo è simile a lavorare con IBOutlet s, che sono variabili che fanno riferimento a un oggetto in un pennino o storyboard. Non verranno inizializzati all'inizializzazione dell'oggetto padre (di solito un controller di visualizzazione o UIView personalizzato), ma possiamo essere sicuri che non saranno nil quando viene viewDidLoad (in un controller di visualizzazione) o awakeFromNib (in una visualizzazione), e così possiamo accedervi in ​​sicurezza.

In generale, la best practice consiste nell'evitare di forzare lo scarto e di usare gli optional implicitamente scartati. Considera sempre che l'opzionale potrebbe essere nil e gestiscilo in modo appropriato utilizzando l'associazione opzionale o controllando se non è nil prima di forzare uno scarto o accedere alla variabile in caso di un optional annullato implicitamente.

2. Non conoscere le insidie ​​dei forti cicli di riferimento

Un forte ciclo di riferimento esiste quando una coppia di oggetti mantiene un forte riferimento l'uno all'altro. Questo non è qualcosa di nuovo per Swift, dal momento che Objective-C ha lo stesso problema e ci si aspetta che gli sviluppatori di Objective-C esperti lo gestiscano correttamente. È importante prestare attenzione ai riferimenti forti ea cosa fa riferimento a cosa. La documentazione di Swift ha una sezione dedicata a questo argomento.

È particolarmente importante gestire i riferimenti quando si utilizzano le chiusure. Per impostazione predefinita, le chiusure (o blocchi), mantengono un forte riferimento a ogni oggetto a cui si fa riferimento al loro interno. Se uno di questi oggetti ha un forte riferimento alla chiusura stessa, abbiamo un forte ciclo di riferimento. È necessario utilizzare gli elenchi di acquisizione per gestire correttamente la modalità di acquisizione dei riferimenti.

Se esiste la possibilità che l'istanza catturata dal blocco venga deallocata prima che il blocco venga chiamato, è necessario acquisirla come riferimento debole , che sarà facoltativo poiché può essere nil . Ora, se sei sicuro che l'istanza acquisita non verrà deallocata durante la vita del blocco, puoi acquisirla come riferimento non proprietario . Il vantaggio dell'utilizzo di unowned invece di weak è che il riferimento non sarà un optional e puoi utilizzare il valore direttamente senza la necessità di scartarlo.

Nell'esempio seguente, che puoi eseguire in Xcode Playground, la classe Container ha un array e una chiusura facoltativa che viene invocata ogni volta che il suo array cambia (usa gli osservatori di proprietà per farlo). La classe Whatever ha un'istanza Container e, nel suo inizializzatore, assegna una chiusura a arrayDidChange e questa chiusura fa riferimento a self , creando così una forte relazione tra l'istanza Whatever e la chiusura.

 struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil

Se esegui questo esempio, noterai che deinit whatever non viene mai stampata, il che significa che la nostra istanza w non viene deallocata dalla memoria. Per risolvere questo problema, dobbiamo utilizzare un elenco di acquisizione per non acquisire self in modo forte:

 struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil

In questo caso, possiamo usare unowned , perché self non sarà mai nil durante la durata della chiusura.

È buona norma utilizzare quasi sempre elenchi di acquisizione per evitare cicli di riferimento, che ridurranno le perdite di memoria e alla fine un codice più sicuro.

3. Usando self Ovunque

A differenza di Objective-C, con Swift, non ci viene richiesto di usare self per accedere alle proprietà di una classe o di una struttura all'interno di un metodo. Siamo tenuti a farlo solo in una chiusura perché ha bisogno di catturare self . Usare self dove non è richiesto non è esattamente un errore, funziona bene e non ci saranno né errori né avvisi. Tuttavia, perché scrivere più codice del necessario? Inoltre, è importante mantenere il codice coerente.

4. Non conoscere il tipo dei tuoi tipi

Swift utilizza tipi di valore e tipi di riferimento . Inoltre, le istanze di un tipo di valore mostrano un comportamento leggermente diverso delle istanze dei tipi di riferimento. Non sapere in quale categoria rientra ciascuna delle tue istanze causerà false aspettative sul comportamento del codice.

Nei linguaggi più orientati agli oggetti, quando creiamo un'istanza di una classe e la passiamo ad altre istanze e come argomento ai metodi, ci aspettiamo che questa istanza sia la stessa ovunque. Ciò significa che qualsiasi modifica ad esso si rifletterà ovunque, perché in effetti, ciò che abbiamo sono solo un mucchio di riferimenti agli stessi identici dati. Gli oggetti che mostrano questo comportamento sono tipi di riferimento e in Swift tutti i tipi dichiarati come class sono tipi di riferimento.

Successivamente, abbiamo tipi di valore dichiarati utilizzando struct o enum . I tipi di valore vengono copiati quando vengono assegnati a una variabile o passati come argomento a una funzione oa un metodo. Se modifichi qualcosa nell'istanza copiata, quella originale non verrà modificata. I tipi di valore sono immutabili . Se assegni un nuovo valore a una proprietà di un'istanza di un tipo di valore, come CGPoint o CGSize , viene creata una nuova istanza con le modifiche. Ecco perché possiamo utilizzare gli osservatori di proprietà su un array (come nell'esempio sopra nella classe Container ) per notificarci le modifiche. Ciò che sta effettivamente accadendo è che viene creato un nuovo array con le modifiche; viene assegnato alla proprietà e quindi didSet viene richiamato.

Pertanto, se non sai che l'oggetto con cui hai a che fare è di un tipo di riferimento o valore, le tue aspettative su ciò che farà il tuo codice potrebbero essere del tutto sbagliate.

5. Non utilizzare il pieno potenziale di enumerazioni

Quando si parla di enum, generalmente si pensa all'enumerazione C di base, che è solo un elenco di costanti correlate che sono sottostanti interi. In Swift, gli enum sono molto più potenti. Ad esempio, puoi allegare un valore a ogni caso di enumerazione. Le enumerazioni hanno anche metodi e proprietà di sola lettura/calcolate che possono essere utilizzate per arricchire ogni caso con ulteriori informazioni e dettagli.

La documentazione ufficiale sulle enumerazioni è molto intuitiva e la documentazione sulla gestione degli errori presenta alcuni casi d'uso per la potenza extra delle enumerazioni in Swift. Inoltre, dai un'occhiata alla vasta esplorazione delle enumerazioni in Swift per imparare praticamente tutto ciò che puoi fare con esse.

6. Non utilizzare le funzioni funzionali

La Swift Standard Library fornisce molti metodi che sono fondamentali nella programmazione funzionale e ci consentono di fare molto con una sola riga di codice, come mappare, ridurre e filtrare, tra gli altri.

Esaminiamo alcuni esempi.

Supponiamo che tu debba calcolare l'altezza di una vista tabella. Dato che hai una sottoclasse UITableViewCell come la seguente:

 class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }

Si consideri che abbiamo una matrice di istanze del modello modelArray ; possiamo calcolare l'altezza della vista tabella con una riga di codice:

 let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)

La map genererà un array di CGFloat , contenente l'altezza di ciascuna cella, e la reduce le sommerà.

Se vuoi rimuovere elementi da un array, potresti finire per fare quanto segue:

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }

Questo esempio non sembra elegante, né molto efficiente poiché chiamiamo indexOf per ogni elemento. Considera il seguente esempio:

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }

Ora il codice è più efficiente, ma può essere ulteriormente migliorato utilizzando il filter :

 var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)

L'esempio successivo illustra come rimuovere tutte le viste secondarie di un UIView che soddisfano determinati criteri, ad esempio la cornice che interseca un rettangolo particolare. Puoi usare qualcosa come:

 for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }

Dobbiamo stare attenti, però, perché potresti essere tentato di concatenare un paio di chiamate a questi metodi per creare filtri e trasformazioni fantasiosi, che potrebbero finire con una riga di codice spaghetti illeggibile.

7. Rimanere nella zona di comfort e non provare la programmazione orientata al protocollo

Si dice che Swift sia il primo linguaggio di programmazione orientato al protocollo , come menzionato nella sessione Programmazione orientata al protocollo del WWDC nella sessione Swift. Fondamentalmente, ciò significa che possiamo modellare i nostri programmi attorno ai protocolli e aggiungere comportamenti ai tipi semplicemente conformandoli ai protocolli ed estendendoli. Ad esempio, dato che abbiamo un protocollo Shape , possiamo estendere CollectionType (che è conforme a tipi come Array , Set , Dictionary ) e aggiungere un metodo che calcola l'area totale tenendo conto delle intersezioni

 protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }

L'istruzione where Generator.Element: Shape è un vincolo che afferma che i metodi nell'estensione saranno disponibili solo in istanze di tipi conformi a CollectionType , che contiene elementi di tipi conformi a Shape . Ad esempio, questi metodi possono essere richiamati su un'istanza di Array<Shape> , ma non su un'istanza di Array<String> . Se abbiamo una classe Polygon conforme al protocollo Shape , quei metodi saranno disponibili anche per un'istanza di Array<Polygon> .

Con le estensioni del protocollo, puoi dare un'implementazione predefinita ai metodi dichiarati nel protocollo, che saranno quindi disponibili in tutti i tipi conformi a quel protocollo senza dover apportare modifiche a quei tipi (classi, strutture o enumerazioni). Questo viene fatto ampiamente in tutta la Swift Standard Library, ad esempio, la map e la reduce sono definite in un'estensione di CollectionType e questa stessa implementazione è condivisa da tipi come Array e Dictionary senza alcun codice aggiuntivo.

Questo comportamento è simile ai mixin di altri linguaggi, come Ruby o Python. Semplicemente conformandosi a un protocollo con implementazioni di metodi predefinite, aggiungi funzionalità al tuo tipo.

La programmazione orientata al protocollo potrebbe sembrare piuttosto imbarazzante e non molto utile a prima vista, il che potrebbe farti ignorare e non provarci nemmeno. Questo post fornisce una buona conoscenza dell'utilizzo della programmazione orientata al protocollo nelle applicazioni reali.

Come abbiamo imparato, Swift non è un linguaggio giocattolo

Swift è stato inizialmente accolto con molto scetticismo; la gente sembrava pensare che Apple avrebbe sostituito Objective-C con un linguaggio giocattolo per bambini o con qualcosa per i non programmatori. Tuttavia, Swift ha dimostrato di essere un linguaggio serio e potente che rende la programmazione molto piacevole. Poiché è fortemente digitato, è difficile commettere errori e, in quanto tale, è difficile elencare gli errori che puoi fare con la lingua.

Quando ti abituerai a Swift e tornerai a Objective-C, noterai la differenza. Ti mancheranno le belle funzionalità offerte da Swift e dovrai scrivere codice noioso in Objective-C per ottenere lo stesso effetto. Altre volte, dovrai affrontare errori di runtime che Swift avrebbe rilevato durante la compilazione. È un ottimo aggiornamento per i programmatori Apple e c'è ancora molto altro in arrivo man mano che il linguaggio matura.

Correlati: una guida per sviluppatori iOS: da Objective-C a Learning Swift