Die Fehler, von denen die meisten Swift-Entwickler nicht wissen, dass sie sie machen

Veröffentlicht: 2022-03-11

Da ich aus einem Objective-C-Hintergrund komme, hatte ich am Anfang das Gefühl, dass Swift mich zurückhielt. Swift ließ mich wegen seiner stark typisierten Natur nicht vorankommen, was manchmal ärgerlich war.

Im Gegensatz zu Objective-C erzwingt Swift viele Anforderungen zur Kompilierzeit. Dinge, die in Objective-C gelockert sind, wie der id -Typ und implizite Konvertierungen, sind in Swift nicht vorhanden. Selbst wenn Sie ein Int und ein Double haben und diese addieren möchten, müssen Sie sie explizit in einen einzigen Typ konvertieren.

Außerdem sind Optionals ein grundlegender Bestandteil der Sprache, und obwohl es sich um ein einfaches Konzept handelt, dauert es einige Zeit, sich an sie zu gewöhnen.

Am Anfang möchten Sie vielleicht alles erzwingen, aber das wird schließlich zu Abstürzen führen. Wenn Sie sich mit der Sprache vertraut machen, beginnen Sie zu lieben, dass Sie kaum Laufzeitfehler haben, da viele Fehler zur Kompilierzeit abgefangen werden.

Die meisten Swift-Programmierer haben umfangreiche Vorerfahrungen mit Objective-C, was sie unter anderem dazu veranlassen könnte, Swift-Code mit den gleichen Praktiken zu schreiben, mit denen sie in anderen Sprachen vertraut sind. Und das kann einige schlimme Fehler verursachen.

In diesem Artikel skizzieren wir die häufigsten Fehler, die Swift-Entwickler machen, und Möglichkeiten, sie zu vermeiden.

Machen Sie keinen Fehler – Best Practices von Objective-C sind keine Best Practices von Swift.
Twittern

1. Optionales Entpacken erzwingen

Eine Variable eines optionalen Typs (z. B. String? ) kann einen Wert enthalten oder auch nicht. Wenn sie keinen Wert haben, sind sie gleich nil . Um den Wert einer Option zu erhalten, müssen Sie sie zuerst auspacken , und das kann auf zwei verschiedene Arten erfolgen.

Eine Möglichkeit ist die optionale Bindung mit einem if let oder einem guard let , das heißt:

 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 }

Zweitens erzwingen Sie das Auspacken mit dem ! -Operator oder die Verwendung eines implizit ausgepackten optionalen Typs (z. B. String! ). Wenn die Option nil ist, führt das Erzwingen eines Unwrap zu einem Laufzeitfehler und beendet die Anwendung. Darüber hinaus wird der Versuch, auf den Wert einer implizit ausgepackten Option zuzugreifen, dasselbe bewirken.

Wir haben manchmal Variablen, die wir im Klassen-/Struktur-Initialisierer nicht initialisieren können (oder wollen). Daher müssen wir sie als optional deklarieren. In einigen Fällen gehen wir davon aus, dass sie in bestimmten Teilen unseres Codes nicht nil sein werden, also erzwingen wir das Auspacken oder deklarieren sie als implizit ausgepackte Optionals, weil das einfacher ist, als die ganze Zeit optionale Bindungen durchführen zu müssen. Dies sollte mit Sorgfalt geschehen.

Dies ähnelt der Arbeit mit IBOutlet s, bei denen es sich um Variablen handelt, die auf ein Objekt in einem Nib oder Storyboard verweisen. Sie werden bei der Initialisierung des übergeordneten Objekts nicht initialisiert (normalerweise ein View-Controller oder ein benutzerdefiniertes UIView ), aber wir können sicher sein, dass sie nicht nil sind, wenn viewDidLoad (in einem View-Controller) oder awakeFromNib (in einer Ansicht) aufgerufen wird. und so können wir sicher darauf zugreifen.

Im Allgemeinen besteht die beste Vorgehensweise darin, das Erzwingen des Auspackens zu vermeiden und implizit ausgepackte Optionen zu verwenden. Denken Sie immer daran, dass die Option nil sein könnte, und behandeln Sie sie entsprechend, indem Sie entweder eine optionale Bindung verwenden oder prüfen, ob sie nicht nil ist, bevor Sie ein Auspacken erzwingen, oder auf die Variable zugreifen, falls eine implizit ausgepackte Option vorhanden ist.

2. Fallstricke starker Referenzzyklen nicht kennen

Ein starker Referenzzyklus liegt vor, wenn ein Objektpaar einen starken Bezug zueinander behält. Dies ist nichts Neues für Swift, da Objective-C das gleiche Problem hat und von erfahrenen Objective-C-Entwicklern erwartet wird, dass sie dies ordnungsgemäß handhaben. Es ist wichtig, auf starke Referenzen zu achten und darauf, was auf was verweist. Die Swift-Dokumentation enthält einen Abschnitt, der diesem Thema gewidmet ist.

Es ist besonders wichtig, Ihre Referenzen zu verwalten, wenn Sie Closures verwenden. Closures (oder Blöcke) behalten standardmäßig einen starken Verweis auf jedes Objekt, das in ihnen referenziert wird. Wenn eines dieser Objekte einen starken Bezug zum Abschluss selbst hat, haben wir einen starken Bezugszyklus. Es ist notwendig, Erfassungslisten zu verwenden, um die Erfassung Ihrer Referenzen richtig zu verwalten.

Wenn die Möglichkeit besteht, dass die vom Block erfasste Instanz freigegeben wird, bevor der Block aufgerufen wird, müssen Sie sie als schwache Referenz erfassen, die optional ist, da sie nil sein kann. Wenn Sie nun sicher sind, dass die erfasste Instanz während der Lebensdauer des Blocks nicht freigegeben wird, können Sie sie als unbesessene Referenz erfassen . Der Vorteil der Verwendung von unowned anstelle von weak besteht darin, dass die Referenz nicht optional ist und Sie den Wert direkt verwenden können, ohne ihn entpacken zu müssen.

Im folgenden Beispiel, das Sie im Xcode Playground ausführen können, verfügt die Container -Klasse über ein Array und eine optionale Closure, die aufgerufen wird, wenn sich ihr Array ändert (sie verwendet dazu Eigenschaftsbeobachter). Die Klasse Whatever hat eine Container -Instanz und weist in ihrem Initialisierer arrayDidChange eine Closure zu, und diese Closure verweist auf self , wodurch eine starke Beziehung zwischen der Whatever -Instanz und der Closure entsteht.

 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

Wenn Sie dieses Beispiel ausführen, werden Sie feststellen, dass deinit whatever nie gedruckt wird, nicht ausgibt, was bedeutet, dass unsere Instanz w nicht aus dem Speicher freigegeben wird. Um dies zu beheben, müssen wir eine Capture-Liste verwenden, um self nicht stark zu erfassen:

 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 diesem Fall können wir unowned verwenden, da self während der Lebensdauer des Abschlusses niemals nil sein wird.

Es hat sich bewährt, fast immer Capture-Listen zu verwenden, um Referenzzyklen zu vermeiden, was Speicherlecks reduziert und am Ende einen sichereren Code ergibt.

3. Überall self verwenden

Anders als in Objective-C müssen wir mit Swift nicht self verwenden, um auf die Eigenschaften einer Klasse oder Struktur innerhalb einer Methode zuzugreifen. Wir müssen dies nur bei einer Schließung tun, weil sie sich self erfassen muss. Die Verwendung von self , wo es nicht erforderlich ist, ist nicht gerade ein Fehler, es funktioniert gut, und es gibt keine Fehler und keine Warnungen. Warum aber mehr Code schreiben als nötig? Außerdem ist es wichtig, Ihren Code konsistent zu halten.

4. Den Typ deiner Typen nicht zu kennen

Swift verwendet Werttypen und Referenztypen . Darüber hinaus zeigen Instanzen eines Werttyps ein etwas anderes Verhalten als Instanzen von Referenztypen. Wenn Sie nicht wissen, in welche Kategorie jede Ihrer Instanzen passt, werden falsche Erwartungen an das Verhalten des Codes geweckt.

Wenn wir in den meisten objektorientierten Sprachen eine Instanz einer Klasse erstellen und sie an andere Instanzen und als Argument an Methoden weitergeben, erwarten wir, dass diese Instanz überall gleich ist. Das bedeutet, dass jede Änderung überall widergespiegelt wird, denn tatsächlich haben wir nur eine Reihe von Verweisen auf genau dieselben Daten. Objekte, die dieses Verhalten aufweisen, sind Referenztypen, und in Swift sind alle als class deklarierten Typen Referenztypen.

Als nächstes haben wir Werttypen, die mit struct oder enum deklariert werden. Werttypen werden kopiert, wenn sie einer Variablen zugewiesen oder als Argument an eine Funktion oder Methode übergeben werden. Wenn Sie etwas in der kopierten Instanz ändern, wird die ursprüngliche nicht geändert. Werttypen sind unveränderlich . Wenn Sie einer Eigenschaft einer Instanz eines Werttyps wie CGPoint oder CGSize einen neuen Wert zuweisen, wird eine neue Instanz mit den Änderungen erstellt. Aus diesem Grund können wir Eigenschaftsbeobachter für ein Array verwenden (wie im obigen Beispiel in der Container -Klasse), um uns über Änderungen zu informieren. Was tatsächlich passiert, ist, dass ein neues Array mit den Änderungen erstellt wird; es wird der Eigenschaft zugewiesen, und dann wird didSet aufgerufen.

Wenn Sie also nicht wissen, dass das Objekt, mit dem Sie es zu tun haben, ein Referenz- oder Werttyp ist, könnten Ihre Erwartungen darüber, was Ihr Code tun wird, völlig falsch sein.

5. Nicht das volle Potenzial von Enums nutzen

Wenn wir über Enumerationen sprechen, denken wir im Allgemeinen an die grundlegende C-Enumeration, die nur eine Liste verwandter Konstanten ist, die darunter Integer sind. In Swift sind Aufzählungen viel leistungsfähiger. Beispielsweise können Sie jedem Aufzählungsfall einen Wert zuweisen. Aufzählungen haben auch Methoden und schreibgeschützte/berechnete Eigenschaften, die verwendet werden können, um jeden Fall mit weiteren Informationen und Details anzureichern.

Die offizielle Dokumentation zu Enums ist sehr intuitiv, und die Fehlerbehandlungsdokumentation präsentiert einige Anwendungsfälle für die zusätzliche Leistung von Enums in Swift. Sehen Sie sich auch die folgende ausführliche Untersuchung von Enums in Swift an, um so ziemlich alles zu erfahren, was Sie damit machen können.

6. Keine Verwendung von Funktionsmerkmalen

Die Swift-Standardbibliothek bietet viele Methoden, die für die funktionale Programmierung grundlegend sind und es uns ermöglichen, mit nur einer Codezeile viel zu tun, z. B. unter anderem zuordnen, reduzieren und filtern.

Lassen Sie uns einige Beispiele untersuchen.

Angenommen, Sie müssen die Höhe einer Tabellenansicht berechnen. Vorausgesetzt, Sie haben eine UITableViewCell Unterklasse wie die folgende:

 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 }

Stellen Sie sich vor, wir haben ein Array von Modellinstanzen modelArray ; Wir können die Höhe der Tabellenansicht mit einer Codezeile berechnen:

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

Die map gibt ein Array von CGFloat aus, das die Höhe jeder Zelle enthält, und das reduce addiert sie.

Wenn Sie Elemente aus einem Array entfernen möchten, können Sie Folgendes tun:

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

Dieses Beispiel sieht weder elegant noch sehr effizient aus, da wir indexOf für jedes Element aufrufen. Betrachten Sie das folgende Beispiel:

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

Jetzt ist der Code effizienter, kann aber durch die Verwendung des filter weiter verbessert werden:

 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)

Das nächste Beispiel veranschaulicht, wie Sie alle Unteransichten einer UIView entfernen können, die bestimmte Kriterien erfüllen, z. B. den Rahmen, der ein bestimmtes Rechteck schneidet. Sie können etwas verwenden wie:

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

Wir müssen jedoch vorsichtig sein, denn Sie könnten versucht sein, ein paar Aufrufe an diese Methoden zu verketten, um ausgefallene Filter und Transformationen zu erstellen, was zu einer Zeile unlesbaren Spaghetti-Codes führen kann.

7. In der Komfortzone bleiben und keine protokollorientierte Programmierung versuchen

Swift soll die erste protokollorientierte Programmiersprache sein , wie in der WWDC Protocol-Oriented Programming in Swift Session erwähnt. Im Grunde bedeutet dies, dass wir unsere Programme rund um Protokolle modellieren und Verhaltenstypen zu Typen hinzufügen können, indem wir einfach Protokolle anpassen und sie erweitern. Wenn wir beispielsweise ein Shape -Protokoll haben, können wir CollectionType (das durch Typen wie Array , Set , Dictionary konform ist) erweitern und ihm eine Methode hinzufügen, die die Gesamtfläche unter Berücksichtigung von Schnittpunkten berechnet

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

Die Anweisung where Generator.Element: Shape ist eine Einschränkung, die angibt, dass die Methoden in der Erweiterung nur in Instanzen von Typen verfügbar sind, die CollectionType entsprechen, die Elemente von Typen enthält, die Shape entsprechen. Diese Methoden können beispielsweise für eine Instanz von Array<Shape> aufgerufen werden, jedoch nicht für eine Instanz von Array<String> . Wenn wir eine Polygon -Klasse haben, die dem Shape -Protokoll entspricht, sind diese Methoden auch für eine Instanz von Array<Polygon> verfügbar.

Mit Protokollerweiterungen können Sie im Protokoll deklarierten Methoden eine Standardimplementierung geben, die dann in allen Typen verfügbar ist, die diesem Protokoll entsprechen, ohne dass Änderungen an diesen Typen (Klassen, Strukturen oder Aufzählungen) vorgenommen werden müssen. Dies geschieht umfassend in der gesamten Swift-Standardbibliothek, z. B. werden die map und reduce in einer Erweiterung von CollectionType definiert, und dieselbe Implementierung wird von Typen wie Array und Dictionary ohne zusätzlichen Code geteilt.

Dieses Verhalten ähnelt Mixins aus anderen Sprachen wie Ruby oder Python. Indem Sie sich einfach an ein Protokoll mit Standardmethodenimplementierungen anpassen, fügen Sie Ihrem Typ Funktionalität hinzu.

Protokollorientiertes Programmieren mag auf den ersten Blick ziemlich umständlich und nicht sehr nützlich aussehen, was dazu führen könnte, dass Sie es ignorieren und es nicht einmal versuchen. Dieser Beitrag vermittelt einen guten Überblick über die Verwendung protokollorientierter Programmierung in realen Anwendungen.

Wie wir gelernt haben, ist Swift keine Spielzeugsprache

Swift wurde zunächst mit viel Skepsis aufgenommen; Die Leute schienen zu glauben, dass Apple Objective-C durch eine Spielzeugsprache für Kinder oder etwas für Nicht-Programmierer ersetzen würde. Swift hat sich jedoch als seriöse und mächtige Sprache erwiesen, die das Programmieren sehr angenehm macht. Da es stark typisiert ist, ist es schwierig, Fehler zu machen, und daher ist es schwierig, Fehler aufzulisten, die Sie mit der Sprache machen können.

Wenn Sie sich an Swift gewöhnen und zu Objective-C zurückkehren, werden Sie den Unterschied bemerken. Sie werden nette Funktionen verpassen, die Swift anbietet, und müssen mühsamen Code in Objective-C schreiben, um den gleichen Effekt zu erzielen. In anderen Fällen treten Laufzeitfehler auf, die Swift während der Kompilierung abgefangen hätte. Es ist ein großartiges Upgrade für Apple-Programmierer, und es wird noch viel mehr kommen, wenn die Sprache reift.

Siehe auch: Ein iOS-Entwicklerhandbuch: Von Objective-C zu Learning Swift