Greșelile pe care cei mai mulți dezvoltatori rapidi nu știu că le fac

Publicat: 2022-03-11

Venind dintr-un fundal Objective-C, la început, am simțit că Swift mă reține. Swift nu îmi permitea să fac progrese din cauza naturii sale puternic tipărite, care obișnuia să fie enervantă uneori.

Spre deosebire de Objective-C, Swift impune multe cerințe la momentul compilării. Lucrurile care sunt relaxate în Objective-C, cum ar fi tipul de id și conversiile implicite, nu sunt un lucru în Swift. Chiar dacă aveți un Int și un Double și doriți să le adăugați, va trebui să le convertiți într-un singur tip în mod explicit.

De asemenea, opționalele sunt o parte fundamentală a limbajului și, deși sunt un concept simplu, este nevoie de ceva timp pentru a te obișnui cu ele.

La început, s-ar putea să doriți să forțați să desfaceți totul, dar asta va duce în cele din urmă la blocări. Pe măsură ce te familiarizezi cu limbajul, începi să-ți placă cum nu ai erori de rulare, deoarece multe greșeli sunt prinse în timpul compilării.

Majoritatea programatorilor Swift au o experiență anterioară semnificativă cu Objective-C, ceea ce, printre altele, i-ar putea determina să scrie cod Swift folosind aceleași practici cu care sunt familiarizați în alte limbi. Și asta poate provoca unele greșeli rele.

În acest articol, prezentăm cele mai frecvente greșeli pe care le fac dezvoltatorii Swift și modalități de a le evita.

Nu faceți greșeli - Cele mai bune practici Objective-C nu sunt cele mai bune practici Swift.
Tweet

1. Opționale de despachetare forțată

O variabilă de tip opțional (de exemplu String? ) poate sau nu să dețină o valoare. Când nu dețin o valoare, sunt egale cu nil . Pentru a obține valoarea unui opțional, mai întâi trebuie să le desfaceți , iar asta se poate face în două moduri diferite.

O modalitate este legarea opțională folosind un if let sau un guard let , adică:

 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 }

În al doilea rând este forțarea despachetului folosind ! operator, sau folosind un tip opțional neîncărcat implicit (de ex String! ). Dacă opționalul este nil , forțarea unei despachetari va cauza o eroare de rulare și va închide aplicația. În plus, încercarea de a accesa valoarea unei opționale implicite dezpachete va provoca același lucru.

Uneori avem variabile pe care nu le putem (sau nu dorim) să le inițializam în inițializatorul class/struct. Astfel, trebuie să le declarăm opționale. În unele cazuri, presupunem că acestea nu vor fi nil în anumite părți ale codului nostru, așa că le forțăm despachetarea sau le declarăm ca opționale implicit dezvăluite, deoarece este mai ușor decât a trebui să facem legături opționale tot timpul. Acest lucru ar trebui făcut cu grijă.

Acest lucru este similar cu lucrul cu IBOutlet uri, care sunt variabile care fac referire la un obiect într-un nib sau un storyboard. Ele nu vor fi inițializate la inițializarea obiectului părinte (de obicei, un controler de vizualizare sau UIView personalizat), dar putem fi siguri că nu vor fi nil atunci când se viewDidLoad (într-un controler de vizualizare) sau awakeFromNib (într-o vizualizare), și astfel le putem accesa în siguranță.

În general, cea mai bună practică este de a evita forțarea despachetului și folosirea opționale implicite despachetate. Luați în considerare întotdeauna opționalul ar putea fi nil și gestionați-l în mod corespunzător fie utilizând legarea opțională, fie verificând dacă nu este nil înainte de a forța o despachetare, sau accesând variabila în cazul unui opțional despachetat implicit.

2. Necunoașterea capcanelor unor cicluri de referință puternice

Un ciclu de referință puternic există atunci când o pereche de obiecte păstrează o referință puternică unul la celălalt. Acest lucru nu este ceva nou pentru Swift, deoarece Objective-C are aceeași problemă și se așteaptă ca dezvoltatorii experimentați Objective-C să gestioneze corect acest lucru. Este important să acordați atenție referințelor puternice și la ce referințe. Documentația Swift are o secțiune dedicată acestui subiect.

Este deosebit de important să vă gestionați referințele atunci când utilizați închideri. În mod implicit, închiderile (sau blocurile) păstrează o referință puternică la fiecare obiect care este referit în interiorul lor. Dacă oricare dintre aceste obiecte are o referință puternică la închiderea în sine, avem un ciclu de referință puternic. Este necesar să utilizați listele de captură pentru a gestiona corect modul în care sunt capturate referințele dvs.

Dacă există posibilitatea ca instanța capturată de bloc să fie dealocată înainte ca blocul să fie apelat, trebuie să o capturați ca referință slabă , care va fi opțională deoarece poate fi nil . Acum, dacă sunteți sigur că instanța capturată nu va fi dealocată pe durata de viață a blocului, o puteți captura ca referință neproprietă . Avantajul utilizării unowned în loc de weak este că referința nu va fi opțională și puteți folosi valoarea direct, fără a fi nevoie să o despachetați.

În exemplul următor, pe care îl puteți rula în Xcode Playground, clasa Container are o matrice și o închidere opțională care este invocată ori de câte ori matricea sa se modifică (folosește observatori de proprietate pentru a face acest lucru). Clasa Whatever are o instanță Container și, în inițializatorul său, atribuie o închidere la arrayDidChange și această închidere se referă la self , creând astfel o relație puternică între instanța Whatever și închidere.

 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

Dacă rulați acest exemplu, veți observa că deinit whatever nu este niciodată tipărit, ceea ce înseamnă că instanța noastră w nu este dealocată din memorie. Pentru a remedia acest lucru, trebuie să folosim o listă de captură pentru a nu self captura puternic:

 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

În acest caz, putem folosi unowned , deoarece self nu va fi niciodată nil pe durata de viață a închiderii.

Este o practică bună să folosiți aproape întotdeauna listele de captură pentru a evita ciclurile de referință, ceea ce va reduce pierderile de memorie și, în final, un cod mai sigur.

3. Folosirea self peste tot

Spre deosebire de Objective-C, cu Swift, nu ni se cere să folosim self pentru a accesa proprietățile unei clase sau ale unei structuri în interiorul unei metode. Ni se cere să facem acest lucru doar într-o închidere, deoarece trebuie să se capteze pe self . Folosirea self acolo unde nu este necesar nu este tocmai o greșeală, funcționează foarte bine și nu vor exista erori și avertismente. Totuși, de ce să scrieți mai mult cod decât trebuie? De asemenea, este important să păstrați codul consecvent.

4. Nu cunoașteți tipul tipurilor dvs

Swift utilizează tipuri de valori și tipuri de referință . Mai mult, instanțele unui tip de valoare prezintă un comportament ușor diferit al instanțelor tipurilor de referință. Neștiind în ce categorie se încadrează fiecare dintre instanțele dvs. va cauza așteptări false cu privire la comportamentul codului.

În cele mai multe limbaje orientate obiect, când creăm o instanță a unei clase și o transmitem altor instanțe și ca argument pentru metode, ne așteptăm ca această instanță să fie aceeași peste tot. Asta înseamnă că orice modificare a acesteia se va reflecta peste tot, pentru că, de fapt, ceea ce avem sunt doar o grămadă de referințe la exact aceleași date. Obiectele care prezintă acest comportament sunt tipuri de referință, iar în Swift, toate tipurile declarate ca class sunt tipuri de referință.

În continuare, avem tipuri de valori care sunt declarate folosind struct sau enum . Tipurile de valori sunt copiate atunci când sunt atribuite unei variabile sau transmise ca argument unei funcții sau metode. Dacă modificați ceva în instanța copiată, cea originală nu va fi modificată. Tipurile de valori sunt imuabile . Dacă atribuiți o nouă valoare unei proprietăți a unei instanțe de tip valoare, cum ar fi CGPoint sau CGSize , o nouă instanță este creată cu modificările. De aceea putem folosi observatori de proprietăți pe o matrice (ca în exemplul de mai sus în clasa Container ) pentru a ne notifica modificările. Ceea ce se întâmplă de fapt, este că o nouă matrice este creată cu modificările; este atribuit proprietății și apoi didSet este invocat.

Astfel, dacă nu știți că obiectul cu care aveți de-a face este de tip referință sau valoare, așteptările dvs. cu privire la ceea ce va face codul dvs. ar putea fi complet greșite.

5. Nu se utilizează întregul potențial al enumerarilor

Când vorbim despre enumerari, ne gândim în general la enumerarea C de bază, care este doar o listă de constante înrudite care sunt numere întregi dedesubt. În Swift, enumerațiile sunt mult mai puternice. De exemplu, puteți atașa o valoare fiecărui caz de enumerare. Enumerările au, de asemenea, metode și proprietăți numai pentru citire/calculate care pot fi folosite pentru a îmbogăți fiecare caz cu mai multe informații și detalii.

Documentația oficială despre enumerari este foarte intuitivă, iar documentația de gestionare a erorilor prezintă câteva cazuri de utilizare pentru puterea suplimentară a enumerarilor în Swift. De asemenea, verificați în urma explorării extensive a enumerarilor în Swift pentru a afla aproape tot ce puteți face cu ele.

6. Nu se utilizează caracteristici funcționale

Biblioteca Swift Standard oferă multe metode care sunt fundamentale în programarea funcțională și ne permit să facem multe cu o singură linie de cod, cum ar fi maparea, reducerea și filtrarea, printre altele.

Să examinăm câteva exemple.

Să spunem, trebuie să calculați înălțimea unei vederi de tabel. Dat fiind că aveți o subclasă UITableViewCell , cum ar fi următoarea:

 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 }

Luați în considerare, avem o serie de instanțe model modelArray ; putem calcula înălțimea vederii tabelului cu o singură linie de cod:

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

map va scoate o matrice de CGFloat , care conține înălțimea fiecărei celule, iar reduce le va adăuga.

Dacă doriți să eliminați elemente dintr-o matrice, puteți ajunge să faceți următoarele:

 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) } }

Acest exemplu nu pare elegant și nici foarte eficient, deoarece apelăm indexOf pentru fiecare articol. Luați în considerare următorul exemplu:

 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) } }

Acum, codul este mai eficient, dar poate fi îmbunătățit în continuare folosind 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)

Următorul exemplu ilustrează cum puteți elimina toate subvizualizările unui UIView care îndeplinesc anumite criterii, cum ar fi cadrul care intersectează un anumit dreptunghi. Puteți folosi ceva de genul:

 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() }

Trebuie să fim atenți, totuși, pentru că ați putea fi tentat să înlănțuiți câteva apeluri la aceste metode pentru a crea filtrare și transformare fantezie, care poate ajunge la o linie de cod spaghetti necitit.

7. Rămâneți în zona de confort și nu încercați programarea orientată pe protocol

Swift se pretinde a fi primul limbaj de programare orientat pe protocol , așa cum se menționează în sesiunea de programare orientată pe protocol WWDC în sesiunea Swift. Practic, asta înseamnă că ne putem modela programele în jurul protocoalelor și putem adăuga comportament la tipuri pur și simplu conformându-ne protocoalelor și extinzându-le. De exemplu, având în vedere că avem un protocol Shape , putem extinde CollectionType (care este conformat cu tipuri precum Array , Set , Dictionary ) și adăugați o metodă care calculează suprafața totală pentru intersecții

 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 { /*___*/ } }

Declarația where Generator.Element: Shape este o constrângere care afirmă metodele din extensie va fi disponibilă numai în cazurile de tipuri care sunt conforme cu CollectionType , care conține elemente de tipuri care sunt conforme cu Shape . De exemplu, aceste metode pot fi invocate pe o instanță a lui Array<Shape> , dar nu și pe o instanță a lui Array<String> . Dacă avem o clasă Polygon care se conformează protocolului Shape , atunci acele metode vor fi disponibile și pentru o instanță de Array<Polygon> .

Cu extensiile de protocol, puteți da o implementare implicită metodelor declarate în protocol, care vor fi apoi disponibile în toate tipurile care se conformează protocolului respectiv, fără a fi nevoie să faceți modificări la acele tipuri (clase, structuri sau enumerari). Acest lucru se face pe scară largă în întreaga bibliotecă standard Swift, de exemplu, map și reduce sunt definite într-o extensie a CollectionType și aceeași implementare este partajată de tipuri precum Array și Dictionary fără niciun cod suplimentar.

Acest comportament este similar cu mixin -urile din alte limbi, cum ar fi Ruby sau Python. Prin pur și simplu conformarea unui protocol cu ​​implementări implicite de metodă, adăugați funcționalitate tipului dvs.

Programarea orientată pe protocol ar putea părea destul de incomodă și nu foarte utilă la prima vedere, ceea ce te-ar putea face să o ignori și să nu-i dai nici măcar o șansă. Această postare oferă o bună înțelegere a utilizării programării orientate pe protocol în aplicații reale.

După cum am învățat, Swift nu este un limbaj de jucărie

Swift a fost primit initial cu mult scepticism; oamenii păreau să creadă că Apple va înlocui Objective-C cu un limbaj de jucărie pentru copii sau cu ceva pentru non-programatori. Cu toate acestea, Swift s-a dovedit a fi un limbaj serios și puternic care face programarea foarte plăcută. Deoarece este tastată puternic, este greu să faceți greșeli și, ca atare, este dificil să enumerați greșelile pe care le puteți face cu limba.

Când te obișnuiești cu Swift și te întorci la Objective-C, vei observa diferența. Veți pierde funcțiile frumoase oferite de Swift și va trebui să scrieți cod obositor în Objective-C pentru a obține același efect. Alteori, te vei confrunta cu erori de rulare pe care Swift le-ar fi detectat în timpul compilării. Este un upgrade grozav pentru programatorii Apple și mai sunt multe de urmat pe măsură ce limbajul se maturizează.

Înrudit: Ghidul dezvoltatorului iOS: de la Objective-C la Learning Swift