Błędy, o których większość programistów Swift nie wie
Opublikowany: 2022-03-11Pochodząc ze środowiska Objective-C, na początku czułem, że Swift mnie powstrzymuje. Swift nie pozwalał mi robić postępów ze względu na swoją silnie typizowaną naturę, co czasami doprowadzało mnie do szału.
W przeciwieństwie do Objective-C, Swift wymusza wiele wymagań w czasie kompilacji. Rzeczy, które są zrelaksowane w Objective-C, takie jak typ id
i niejawne konwersje, nie są rzeczą w Swift. Nawet jeśli masz Int
i Double
i chcesz je dodać, będziesz musiał jawnie przekonwertować je na jeden typ.
Ponadto opcje są podstawową częścią języka i chociaż są prostym pojęciem, przyzwyczajenie się do nich zajmuje trochę czasu.
Na początku możesz chcieć wymusić rozpakowanie wszystkiego, ale w końcu doprowadzi to do awarii. Gdy zapoznasz się z językiem, zaczniesz kochać to, że prawie nie ma błędów w czasie wykonywania, ponieważ wiele błędów jest wyłapywanych w czasie kompilacji.
Większość programistów Swift ma znaczące wcześniejsze doświadczenie z Objective-C, które między innymi może prowadzić ich do pisania kodu Swift przy użyciu tych samych praktyk, które znają w innych językach. A to może spowodować kilka poważnych błędów.
W tym artykule przedstawiamy najczęstsze błędy popełniane przez programistów Swift oraz sposoby ich unikania.
1. Opcje wymuszania rozpakowywania
Zmienna typu opcjonalnego (np. String?
) może, ale nie musi, przechowywać wartość. Gdy nie mają wartości, są równe nil
. Aby uzyskać wartość opcjonalnych, najpierw musisz je rozpakować i można to zrobić na dwa różne sposoby.
Jednym ze sposobów jest opcjonalne wiązanie za pomocą if let
lub guard let
, czyli:
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 }
Drugi to wymuszenie rozpakowania za pomocą !
operatora lub użycie niejawnie nieopakowanego typu opcjonalnego (np. String!
). Jeśli opcjonalna wartość to nil
, wymuszenie rozpakowania spowoduje błąd w czasie wykonywania i zamknięcie aplikacji. Ponadto próba uzyskania dostępu do wartości niejawnie nieopakowanej opcji spowoduje to samo.
Czasami mamy zmienne, których nie możemy (lub nie chcemy) zainicjować w inicjatorze klasy/struktury. Dlatego musimy je zadeklarować jako opcjonalne. W niektórych przypadkach zakładamy, że nie będą one nil
w niektórych częściach naszego kodu, więc wymuszamy ich rozpakowanie lub deklarujemy jako niejawnie rozpakowane opcje opcjonalne, ponieważ jest to łatwiejsze niż konieczność ciągłego wiązania opcjonalnego. Należy to robić ostrożnie.
Jest to podobne do pracy z IBOutlet
s, które są zmiennymi, które odwołują się do obiektu w końcówce lub scenorysie. Nie zostaną zainicjowane podczas inicjalizacji obiektu nadrzędnego (zwykle kontroler widoku lub niestandardowy UIView
), ale możemy być pewni, że nie będą nil
, gdy zostanie wywołane viewDidLoad
(w kontrolerze widoku) lub awakeFromNib
(w widoku), dzięki czemu możemy mieć do nich bezpieczny dostęp.
Ogólnie rzecz biorąc, najlepszym rozwiązaniem jest unikanie wymuszania rozpakowywania i używania niejawnie rozpakowanych opcji. Zawsze bierz pod uwagę, że opcjonalna może być nil
i obsługuj ją odpowiednio, używając opcjonalnego powiązania lub sprawdzając, czy nie jest nil
przed wymuszeniem rozpakowania lub uzyskaniem dostępu do zmiennej w przypadku niejawnie rozpakowanej opcjonalnej.
2. Nieznajomość pułapek silnych cykli referencyjnych
Cykl silnego odniesienia istnieje, gdy para obiektów utrzymuje silne odniesienie do siebie. Nie jest to coś nowego w Swift, ponieważ Objective-C ma ten sam problem, a doświadczeni programiści Objective-C powinni odpowiednio nim zarządzać. Ważne jest, aby zwracać uwagę na mocne odniesienia i jakie odniesienia. Dokumentacja Swift zawiera sekcję poświęconą temu tematowi.
Szczególnie ważne jest zarządzanie referencjami podczas korzystania z zamknięć. Domyślnie domknięcia (lub bloki) zachowują silne odniesienie do każdego obiektu, do którego odwołuje się wewnątrz nich. Jeśli którykolwiek z tych obiektów ma silne odniesienie do samego domknięcia, mamy silny cykl odniesienia. Niezbędne jest korzystanie z list przechwytywania, aby właściwie zarządzać sposobem przechwytywania referencji.
Jeśli istnieje możliwość, że instancja przechwycona przez blok zostanie cofnięta przed wywołaniem bloku, musisz przechwycić ją jako słabe odniesienie , co będzie opcjonalne, ponieważ może być nil
. Teraz, jeśli masz pewność, że przechwycona instancja nie zostanie cofnięta w okresie istnienia bloku, możesz ją przechwycić jako odwołanie bez właściciela . Zaletą używania unowned
zamiast weak
jest to, że odwołanie nie będzie opcjonalne i możesz użyć wartości bezpośrednio, bez konieczności jej rozwijania.
W poniższym przykładzie, który można uruchomić w Xcode Playground, klasa Container
ma tablicę i opcjonalne zamknięcie, które jest wywoływane za każdym razem, gdy zmienia się jej tablica (w tym celu używa obserwatorów właściwości). Klasa Whatever
ma instancję Container
i w swoim inicjatorze przypisuje zamknięcie do arrayDidChange
, a to zamknięcie odwołuje się do self
, tworząc w ten sposób silną relację między instancją Whatever
a zamknięciem.
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
Jeśli uruchomisz ten przykład, zauważysz, że deinit whatever
nigdy nie zostanie wydrukowane, co oznacza, że nasza instancja w
nie zostanie cofnięta z pamięci. Aby to naprawić, musimy użyć listy przechwytywania, aby nie przechwytywać mocno self
:
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
W tym przypadku możemy użyć unowned
, ponieważ self
nigdy nie będzie nil
w czasie trwania zamknięcia.
Dobrą praktyką jest prawie zawsze używanie list przechwytywania, aby uniknąć cykli referencyjnych, co zmniejszy wycieki pamięci i ostatecznie zapewni bezpieczniejszy kod.
3. Używanie self
wszędzie
W przeciwieństwie do Objective-C, w Swift nie musimy używać self
, aby uzyskać dostęp do właściwości klasy lub struktury wewnątrz metody. Musimy to zrobić tylko w zamknięciu, ponieważ musi to schwytać self
. Używanie self
tam, gdzie nie jest to wymagane, nie jest do końca błędem, działa dobrze i nie będzie żadnych błędów ani ostrzeżeń. Po co jednak pisać więcej kodu niż trzeba? Ponadto ważne jest, aby kod był spójny.
4. Nieznajomość typu swoich typów
Swift używa typów wartości i typów referencyjnych . Ponadto wystąpienia typu wartości wykazują nieco inne zachowanie wystąpień typów referencyjnych. Niewiedza, do jakiej kategorii pasuje każda z twoich instancji, spowoduje fałszywe oczekiwania co do zachowania kodu.
W większości języków zorientowanych obiektowo, kiedy tworzymy instancję klasy i przekazujemy ją innym instancjom oraz jako argument do metod, oczekujemy, że instancja ta będzie wszędzie taka sama. Oznacza to, że każda zmiana w tym zakresie zostanie odzwierciedlona wszędzie, ponieważ w rzeczywistości mamy tylko kilka odniesień do dokładnie tych samych danych. Obiekty, które wykazują to zachowanie, są typami referencyjnymi, a w języku Swift wszystkie typy zadeklarowane jako class
są typami referencyjnymi.
Następnie mamy typy wartości, które są deklarowane za pomocą struct
lub enum
. Typy wartości są kopiowane, gdy są przypisane do zmiennej lub przekazane jako argument do funkcji lub metody. Jeśli zmienisz coś w skopiowanej instancji, oryginalna instancja nie zostanie zmodyfikowana. Typy wartości są niezmienne . Jeśli przypiszesz nową wartość do właściwości wystąpienia typu wartości, takiego jak CGPoint
lub CGSize
, zostanie utworzone nowe wystąpienie ze zmianami. Dlatego możemy użyć obserwatorów właściwości na tablicy (jak w powyższym przykładzie w klasie Container
), aby powiadomić nas o zmianach. To, co faktycznie się dzieje, to tworzenie nowej tablicy ze zmianami; jest przypisywana do właściwości, a następnie wywoływana jest didSet
.

Tak więc, jeśli nie wiesz, że obiekt, z którym masz do czynienia, jest obiektem typu referencyjnego lub wartościowego, twoje oczekiwania dotyczące tego, co zrobi twój kod, mogą być całkowicie błędne.
5. Niewykorzystywanie pełnego potencjału wyliczeń
Kiedy mówimy o wyliczeniach, zwykle myślimy o podstawowym wyliczeniu C, które jest po prostu listą powiązanych stałych, które są liczbami całkowitymi poniżej. W Swift wyliczenia są znacznie potężniejsze. Na przykład możesz dołączyć wartość do każdego przypadku wyliczenia. Wyliczenia mają również metody i właściwości tylko do odczytu/obliczone, których można użyć do wzbogacenia każdego przypadku o więcej informacji i szczegółów.
Oficjalna dokumentacja wyliczeń jest bardzo intuicyjna, a dokumentacja obsługi błędów przedstawia kilka przypadków użycia dodatkowej mocy wyliczeń w Swift. Zapoznaj się również z obszerną eksploracją wyliczeń w Swift, aby dowiedzieć się prawie wszystkiego, co możesz z nimi zrobić.
6. Nieużywanie funkcji funkcjonalnych
Biblioteka standardowa Swift zapewnia wiele metod, które są fundamentalne w programowaniu funkcjonalnym i pozwalają nam zrobić wiele za pomocą tylko jednej linii kodu, między innymi mapowania, redukcji i filtrowania.
Przeanalizujmy kilka przykładów.
Powiedzmy, że musisz obliczyć wysokość widoku tabeli. Biorąc pod uwagę, że masz podklasę UITableViewCell
, taką jak następująca:
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 }
Rozważmy, mamy tablicę instancji modelu modelArray
; możemy obliczyć wysokość widoku tabeli za pomocą jednego wiersza kodu:
let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)
map
wygeneruje tablicę CGFloat
, zawierającą wysokość każdej komórki, a reduce
doda je.
Jeśli chcesz usunąć elementy z tablicy, możesz wykonać następujące czynności:
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) } }
Ten przykład nie wygląda ani elegancko, ani zbyt wydajnie, ponieważ wywołujemy indexOf
dla każdego elementu. Rozważmy następujący przykład:
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) } }
Teraz kod jest bardziej wydajny, ale można go dodatkowo ulepszyć za pomocą 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)
Następny przykład ilustruje, jak usunąć wszystkie widoki podrzędne UIView
, które spełniają określone kryteria, takie jak ramka przecinająca określony prostokąt. Możesz użyć czegoś takiego:
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() }
Musimy jednak być ostrożni, ponieważ możesz ulec pokusie, aby połączyć kilka wywołań tych metod, aby stworzyć fantazyjne filtrowanie i przekształcanie, co może skończyć się jednym wierszem nieczytelnego kodu spaghetti.
7. Pozostawanie w strefie komfortu i nie próbowanie programowania zorientowanego na protokół
Swift jest uważany za pierwszy język programowania zorientowany na protokół , jak wspomniano w sesji WWDC Protocol-Oriented Programming in Swift. Zasadniczo oznacza to, że możemy modelować nasze programy wokół protokołów i dodawać zachowania do typów, po prostu dostosowując się do protokołów i rozszerzając je. Na przykład, mając protokół Shape
, możemy rozszerzyć CollectionType
(który jest zgodny z typami takimi jak Array
, Set
, Dictionary
) i dodać do niego metodę obliczającą łączną powierzchnię uwzględniającą przecięcia
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 { /*___*/ } }
Instrukcja where Generator.Element: Shape
jest ograniczeniem, które stwierdza, że metody w rozszerzeniu będą dostępne tylko w wystąpieniach typów, które są zgodne z CollectionType
, które zawierają elementy typów, które są zgodne z Shape
. Na przykład te metody można wywoływać w wystąpieniu Array<Shape>
, ale nie w wystąpieniu Array<String>
. Jeśli mamy klasę Polygon
, która jest zgodna z protokołem Shape
, to te metody będą również dostępne dla instancji Array<Polygon>
.
Dzięki rozszerzeniom protokołu można nadać domyślną implementację metodom zadeklarowanym w protokole, które będą następnie dostępne we wszystkich typach zgodnych z tym protokołem bez konieczności wprowadzania jakichkolwiek zmian w tych typach (klasach, strukturach lub wyliczeniach). Odbywa się to szeroko w całej bibliotece standardowej Swift, na przykład map
i reduce
są zdefiniowane w rozszerzeniu CollectionType
, a ta sama implementacja jest współdzielona przez typy takie jak Array
i Dictionary
bez dodatkowego kodu.
To zachowanie jest podobne do domieszek z innych języków, takich jak Ruby czy Python. Po prostu dostosowując się do protokołu z domyślnymi implementacjami metod, dodajesz funkcjonalność do swojego typu.
Programowanie zorientowane na protokół może na pierwszy rzut oka wyglądać dość niezręcznie i niezbyt przydatne, co może sprawić, że zignorujesz je, a nawet nie spróbujesz. Ten post daje dobre pojęcie o używaniu programowania zorientowanego na protokoły w rzeczywistych aplikacjach.
Jak się dowiedzieliśmy, szybki nie jest językiem zabawek
Swift był początkowo przyjmowany z dużym sceptycyzmem; ludzie wydawali się myśleć, że Apple zamierza zastąpić Objective-C językiem zabawek dla dzieci lub czymś dla nie-programistów. Jednak Swift okazał się poważnym i potężnym językiem, który sprawia, że programowanie jest bardzo przyjemne. Ponieważ jest mocno napisany, trudno jest popełniać błędy, a zatem trudno jest wymienić błędy, które można popełnić w języku.
Kiedy przyzwyczaisz się do Szybkiego i wrócisz do celu C, zauważysz różnicę. Będziesz tęsknił za fajnymi funkcjami, które oferuje Swift i będziesz musiał pisać żmudny kod w Objective-C, aby osiągnąć ten sam efekt. Innym razem napotkasz błędy uruchomieniowe, które Swift wykryłby podczas kompilacji. To świetna aktualizacja dla programistów Apple, a wraz z dojrzewaniem języka jest jeszcze wiele do zrobienia.