NEW: Learn SwiftUI with my free YouTube video series! >>

< Previous: Switch case   Next: Wiązanie typów opcjonalnych (z ang. Optional chaining) >

Typy opcjonalne

Język Swift jest bardzo bezpiecznym językiem. Mam przez to na myśli, że bardzo stara się, aby Twój kod nie przestał działać w najmniej oczekiwanym momencie.

Jednym z najczęstszych powodów, przez które kod przestaje działać jest moment gdy próbuje użyć złych lub brakujących danych. Dla przykładu, spójrz na funkcję poniżej:

func getHaterStatus() -> String {
    return "Hate"
}

Ta funkcja nie przyjmuje żadnych parametrów i zwraca ciąg znaków: "Hate". Co jeśli dzisiejszy dzień jest wyjątkowo słoneczny i hejterzy nie mają ochoty na nienawiść? Może wtedy chcielibyśmy zwrócić pustą wartość?

Teraz możesz pomyśleć, gdy mowa o ciągach znaków, że zwrócenie pustego ciągu byłoby dobrą reprezentacją pustej wartości, i w niektórych przypadkach rzeczywiście tak jest, ale co gdy zwracana wartość będzie liczbą? Czy 0 to "pusta wartość"? Czy pustą wartością jest -1?

Zanim zaczniesz tworzyć zmyślone zasady dla samego siebie, język Swift ma na ten problem rozwiązanie - typy opcjonalne. Wartość opcjonalna to taka, która może mieć wartość - lub nie. Większość ludzi ma problem ze zrozumieniem typów opcjonalnych, i nie ma w tym nic złego - spróbuję wyjaśnić to pojęcie na parę sposobów, mając nadzieję, że któryś z nich będzie dla Ciebie zrozumiały.

Wyobraź sobie ankietę, w której pytasz: "W skali od 1 do 5, jak bardzo niesamowita jest Taylor Swift?" - jak na to pytanie odpowie ktoś kto nigdy o niej nie słyszał? 1 byłoby krzywdzącą oceną a 5 z kolei, zawyżoną gdy ta osoba nie ma pojęcia kim jest Taylor Swift. Rozwiązaniem są typy opcjonalne: "Nie chcę oddać żadnej oceny."

Gdy użyliśmy -> String, oznajmiliśmy "to na pewno zwróci ciąg znaków", co z kolei oznacza, że ta funkcja nie może zwrócić żadnej wartości. Czyni to tę funkcję bezpieczną, rozumiejąc przez to, że zawsze dostaniemy wartość, którą możemy użyć jako ciąg znaków. Jeśli chcemy zakomunikować językowi Swift, że ta funkcja może zwrócić wartość, ale nie musi, robimy to w taki sposób:

func getHaterStatus() -> String? {
    return "Hate"
}

Zauważ dodatkowy znak zapytania: String? oznacza "opcjonalny ciąg znaków." Aktualnie, w naszym przypadku dalej za każdym razem zwracamy "Hate", ale teraz zmodyfikujmy tę funkcję: jeśli pogoda jest słoneczna, hejterzy zaczynają nowy rozdział w swoim życiu i zaprzestają życia w nienawiści, nie zwracamy żadnej wartości. W języku Swift "brak wartości" ma specjalną nazwę: nil.

Zmień funkcję w taki sposób:

func getHaterStatus(weather: String) -> String? {
    if weather == "sunny" {
        return nil
    } else {
        return "Hate"
    }
}

Teraz funkcja przyjmuje jeden parametr (pogodę) i zwraca jeden ciąg znaków (stan nienawiści), ale zwracana wartość może istnieć lub nie - być nilem. W naszym przypadku oznacza to, że możemy dostać ciąg znaków albo nil.

Ważne do zapamiętania: język Swift chce aby Twój kod był bardzo bezpieczny i próba użycia wartości nil jest złym pomysłem. Taka próba może zatrzymać egzekucję Twoje kodu, może zepsuć logikę Twojej aplikacji, albo może spowodować zaprezentowanie złej rzeczy na interfejsie użytkownika.

Spróbujmy teraz dodać poniższe linie kodu do playgroundu:

var status: String
status = getHaterStatus(weather: "rainy")

Pierwsza linia tworzy zmienną typu String a druga przypisuje do niej wartość zwracaną z funkcji getHaterStatus() - dzisiejsza pogoda jest deszczowa, więc hejterzy na pewno są pełni nienawiści.

Ten kod nie zadziała, ponieważ zdeklarowaliśmy, że status jest typu String, który wymaga wartości, a funkcja getHaterStatus() niekoniecznie musi takową zwrócić - zwraca opcjonalny ciąg znaków. Powiedzieliśmy, że w zmiennej status na pewno znajdzie się ciąg znaków, natomiast funkcja getHaterStatus() może zwrócić pustą wartość. Język Swift nie pozwoli Ci na popełnienie tego błędu, co jest bardzo pomocne, ponieważ zapobiega w ten sposób dużej ilości powszechnych błędów.

Aby naprawić ten problem, musimy zmienić typ zmiennej status na String? lub usunąc adnotację typu całkowicie i pozwolić językowi Swift wywnioskować go samodzielnie. Pierwsze rozwiązanie wygląda tak:

var status: String?
status = getHaterStatus(weather: "rainy")

Drugie, natomiast wygląda tak:

var status = getHaterStatus(weather: "rainy")

Obojętnie, które rozwiązanie wybierzesz, ta wartość może tam być lub nie i Swift domyślnie nie pozwoli Ci jej użyć w niebezpieczny sposób. Dla przykładu, wyobraź sobie taką funkcję:

func takeHaterAction(status: String) {
    if status == "Hate" {
        print("Hating")
    }
}

Przyjmuje ona ciąg znaków i prezentuje wiadomość w zależności od zawartości tego ciągu. Ta funkcja przyjmuje wartość String a nie wartość String? - nie możesz podstawić tutaj typu opcjonalnego, funkcja wymaga istniejącego ciągu znaków, co oznacza, że nie możemy jej wywołać używając zmiennej status.

Język swift oferuje dwa rozwiązania dla tego problemu. Oba są używane, ale jedno jest bardziej preferowane ponad drugie. Pierwsze nazywamy rozpakowaniem typu opcjonalnego (z ang. optional unwrapping), i jest wykonywane we wnętrzu instrukcji warunkowej używając specjalnej składni. Podczas tego procesu dzieją się dwie rzeczy: sprawdzenie czy typ opcjonalny posiada wartość, i jeśli tak, zostaje on rozpakowany do nieopcjonalnego typu, a następnie wykonywany jest blok kodu.

Składnia wygląda następująco:

if let unwrappedStatus = status {
    // unwrappedStatus contains a non-optional value!
} else {
    // in case you want an else block, here you go…
}

Wyrażenie if let sprawdza i rozpakowuje typ opcjonalny w jednej zwięzłej linii, przez co jest bardzo często używane. Używając tego sposobu możemy bezpiecznie rozpakować i zwrócić wartość funkcji getHaterStatus() i być pewni, że wywołujemy funkcję takeHaterAction() z poprawnym i istniejącym ciągiem znaków. Poniżej kompletny kod:

func getHaterStatus(weather: String) -> String? {
    if weather == "sunny" {
        return nil
    } else {
        return "Hate"
    }
}

func takeHaterAction(status: String) {
    if status == "Hate" {
        print("Hating")
    }
}

if let haterStatus = getHaterStatus(weather: "rainy") {
    takeHaterAction(status: haterStatus)
}

Jeśli zrozumiałeś pojęcie rozpakowania typu opcjonalnego, jesteś gotowy do przejścia do sekcji "Wymuszone rozpakowanie typów opcjonanych (z ang. Force unwrapping optionals)" Jeśli dalej nie jesteś do końca pewny czy dobrze rozumiesz typy opcjonalne, kontynuuj poniżej.

Ok, jeśli nadal to czytasz to znaczy, że albo wyjaśnienie powyżej nie miało żadnego sensu, albo mniej więcej rozumiesz, ale przydałoby się bardziej ugruntować Twoją wiedzę. Typy opcjonalne w Swift'cie są używane bardzo często, więc zrozumienie ich jest naprawdę kluczowe. Postaram się wytłumaczyć je ponownie, tym razem w inny sposób. Mam nadzieję, że to pomoże!

Spójrzmy na nową funkcję:

func yearAlbumReleased(name: String) -> Int {
    if name == "Taylor Swift" { return 2006 }
    if name == "Fearless" { return 2008 }
    if name == "Speak Now" { return 2010 }
    if name == "Red" { return 2012 }
    if name == "1989" { return 2014 }

    return 0
}

Przyjmuje ona nazwę albumu Taylor Swift i zwraca rok, w którym został wydany. Jeśli jednak, wywołamy ją z nazwą albumu "Lantern", ponieważ pomylimy Taylor Swift z Hudsonem Mohawke (co jest bardzo prawdopodobne, prawda?) wtedy zwróci ona 0 ponieważ, nie jest to jeden z albumów Taylor Swift.

Ale czy 0 w tym przypadku ma sens? Oczywiście, jeśli album był wydany w 0 roku naszej ery gdy cesarz Oktawian August panował w Rzymie, 0 mogłoby mieć sens, ale w tym momencie jest po prostu mylące - ludzie muszą być świadomi, że 0 oznacza "nierozpoznane"

O wiele lepszym pomysłem jest przepisanie tej funkcji w taki sposób aby zwracała liczbę całkowitą (gdy rok został znaleziony) albo nil (gdy nie został znalesiony), co ułatwiają nam typy opcjonalne. Poniżej nowa wersja funkcji:

func yearAlbumReleased(name: String) -> Int? {
    if name == "Taylor Swift" { return 2006 }
    if name == "Fearless" { return 2008 }
    if name == "Speak Now" { return 2010 }
    if name == "Red" { return 2012 }
    if name == "1989" { return 2014 }

    return nil
}

Teraz gdy zwracany jest nil, należy rozpakować wynik używając if let, ponieważ musimy sprawdzić czy wartość istnieje.

Jeśli zrozumiałeś pojęcie rozpakowania typu opcjonalnego, jesteś gotowy do przejścia do sekcji "Wymuszone rozpakowanie typów opcjonanych (z ang. Force unwrapping optionals)" Jeśli dalej nie jesteś do końca pewny czy dobrze rozumiesz typy opcjonalne, kontynuuj poniżej.

Ok, jeśli to czytasz to znaczy, że masz problem ze zrozumieniem koncepcji typów opcjonalnych, więc spróbuję ją wytłumaczyć jeszcze raz.

Spójrzmy na tę tablicę imion:

var items = ["James", "John", "Sally"]

Jeśli chcielibyśmy napisać funkcję, która iterowałaby po tej tablicy i zwracała nam indeks wybranego imienia, mogłaby wyglądać tak:

func position(of string: String, in array: [String]) -> Int {
    for i in 0 ..< array.count {
        if array[i] == string {
            return i
        }
    }

    return 0
}

Iteruje one po wszystkich elementach tablicy, zwraca indeks elementu jeśli go znajdzie, w innym przypadku zwraca 0.

Teraz spróbuj wykonać te cztery linie kodu:

let jamesPosition = position(of: "James", in: items)
let johnPosition = position(of: "John", in: items)
let sallyPosition = position(of: "Sally", in: items)
let bobPosition = position(of: "Bob", in: items)

Wypiszą kolejno 0, 1, 2, 0 - indeksy imion James i Bob są takie same, pomimo tego, że jeden istnieje a drugi nie. To dlatego, że użyłem 0, żeby znaczyło "nie znaleziono." Szybkim rozwiązaniem byłoby użycie -1 zamiast 0, ale to nie rozwiązuje całkowicie problemu, ponieważ dalej musisz pamiętać jaki numer oznacza "nie znaleziono."

Dobrym rozwiązaniem w tym przypadku jest typ opcjonalny: zwrócić liczbę całkowitą jeśli odpowiednie imię zostało znalezione, nil w przeciwnym wypadku. Tak naprawdę, to jest podejście jakiego używa wbudowana w język Swift metoda "znajdź w tablicy": someArray.index(of: someValue).

Kiedy pracujesz z wartościami, które "mogą istnieć albo nie", język Swift wymusza na Tobie rozpakowanie ich przez użyciem, aby potwierdzić czy wartość rzeczywiście istnieje. To właśnie robi wyrażenie if let: jeśli typ opcjonalny posiada wartość - rozpakuj ją i użyj, w przeciwnym wypadku nic nie rób. Nie będziesz mógł użyć "prawdopodobnie pustej" wartości przez przypadek, ponieważ język Swift Ci na to nie pozwoli.

Jeśli nadal nie jesteś pewny jak działają typy opcjonalne, to najlepszym wyjściem będzie zapytanie mnie o nie na serwisie Twitter a ja spróbuję Ci pomóc. Możesz mnie znaleźć tutaj: @twostraws.

Wymuszone rozpakowanie typów opcjonanych (z ang. Force unwrapping optionals)

Jęzuk Swift pozwala Ci przeforsować jego zabezpieczenia używając znaku interpunkcyjnego" !. Jeśli wiesz, że typ opcjonalny na pewno posiada wartość możesz wymusić na nim rozpakowanie umieszcając za nim wykrzyknik.

Bądź jednak ostrożny: jeśli spróbujesz tego na zmiennej opcjonalnej, która nie ma wartości, Twój kod zatrzyma egzekucję

Żeby zobrazować działający przykład, spójrz na kod poniżej:

func yearAlbumReleased(name: String) -> Int? {
    if name == "Taylor Swift" { return 2006 }
    if name == "Fearless" { return 2008 }
    if name == "Speak Now" { return 2010 }
    if name == "Red" { return 2012 }
    if name == "1989" { return 2014 }

    return nil
}

var year = yearAlbumReleased(name: "Red")

if year == nil {
    print("There was an error")
} else {
    print("It was released in \(year)")
}

Znajduje on rok, w którym album został wydany. Jeśli taki album nie może zostać znaleziony, zmienna year ustawiana jest na nil, i pokazywana jest wiadomość z błędem. Jeśli wszystko pójdzie dobrze, pokazywany jest rok wydania.

Ale czy na pewno? yearAlbumReleased() zwraca opcjonalną liczbę całkowitą, a kod powyżej nie używa if let aby rozpakować typ opcjonalny. W rezultacie, pokaże wiadomość: "It was release in Optional(2012)" - nie do końca to czego chcieliśmy!

Na ten moment, w naszym kodzie już znajduje się sprawdzenie, czy typ opcjonalny zawiera poprawną wartość, więc bezcelowym byłoby umieszczać tam jeszcze jeden if let aby go rozpakować. Język Swift oferuje rozwiązanie dla tej sytuacji: zamień drugie wyrażenie print() na poniższe:

print("It was released in \(year!)")

Zauważ wykrzyknik: oznacza on "Jestem pewny, że typ opcjonalny zawiera wartość, więc wymuś na nim rozpakowanie."

Niejawnie rozpakowane typy opcjonalne (z ang. Implicitly unwrapped optionals)

Możesz również użyć wykrzyknika aby niejawnie rozpakowywać typy opcjonalne, i tutaj wiele ludzi zaczyna się gubić, więc proszę przeczytaj poniższe akapity uważnie!

  • Zwykła zmienna musi zawierać wartość. Przykład: String musi zawierać ciąg znaków, nawet jeśli ten ciąg jest pusty, np. "". Nie może być nilem.
  • Opcjonalna zmienna może zawierać wartość, ale nie musi. Musi zostać rozpakowana przed jej użyciem. Przykład: String? może zawierać ciąg znaków lub nie - może zawierać nil. Jedyny sposób, żeby się przekonać to jego rozpakowanie.
  • Niejawnie rozpakowany typ opcjonalny może zawierać wartość, ale nie musi. W przeciwieństwie do typu opcjonalnego jednak, nie musi być rozpakowany przed użyciem. Język Swift nie wykona sprawdzenia za Ciebie, więc musisz być szczególnie ostrożny. Przykład: String! może zawierać ciąg znaków lub być nilem. Odpowiedzialność leży po Twojej stronie, żeby poprawnie go użyć. Niejawnie rozpakowany typ opcjonalny jest podobny do zwykłego typu opcjonalnego z tą różnicą, że język Swift pozwoli Ci na dostanie się do jego wartości bezpośrednio bez rozpakowania. Jeśli spróbujesz to zrobić, znaczy to, ze jesteś pewny, że znajduje się tam wartość, ale jeśli okaże się, że nie - Twoja aplikacja przestanie działać.

Najczęściej z niejawnie rozpakowanymi typami opcjonalnymi będziesz spotykać się przy pracy z elementami interfejsu użytkownika w klasach UIKit na systemach iOS lub klasach AppKit na systemach macOS. Te elementy muszą być zdeklarowane z góry, ale nie możesz ich użyć przez ich stworzeniem - a Apple lubi tworzyć elementy interfejsu użytkownika w możliwie najpóźniejszym momencie aby uniknąć niepotrzebnej pracy. Każdorazowe rozpakowanie wartości, które na pewno istnieją byłoby bardzo irytujące, więc od początku są niejawnie rozpakowane.

Nie martw sie jeśli masz trudności ze zrozumieniem niejawnie rozpakowanych typów opcjonalnych - zrozumienie przyjdzie gdy trochę dłużej popracujesz z językiem Swift.

LEARN SWIFTUI FOR FREE I wrote a massive, free SwiftUI tutorial collection, and also have a growing list of free SwiftUI tutorials on YouTube – get started today!

< Previous: Switch case   Next: Wiązanie typów opcjonalnych (z ang. Optional chaining) >
MASTER SWIFT NOW
Buy Testing Swift Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let me know!

Click here to visit the Hacking with Swift store >>