NEW: Get your ticket for Hacking with Swift Live 2019! >>

< Previous: Kontrola dostępu   Next: Domknięcie (closure) >

Polimorfizm i Konwersja typu

Ponieważ klasy mogą dziedziczyć po sobie ( np. CountrySinger może dziedziczyć po Singer) to znaczy, że jedna klasa zawiera w sobie drugą: Klasa B posaida to wszystko co A i jeszcze trochę. To znaczy, że możemy traktować B jako typ B lub typ A w zależności od potrzeb.

To skomplikowane? Spróbujmy więc kodem:

class Album {
    var name: String

    init(name: String) {
        self.name = name
    }
}

class StudioAlbum: Album {
    var studio: String

    init(name: String, studio: String) {
        self.studio = studio
        super.init(name: name)
    }
}

class LiveAlbum: Album {
    var location: String

    init(name: String, location: String) {
        self.location = location
        super.init(name: name)
    }
}

Zdefiniowaliśmy trzy klasy: album, album studyjny i live album, z czego te dwa ostatnie dziedziczą po Album. Ponieważ każda instancja LiveAlbum dziedziczy po album, to może być traktowana jako Album lub LiveAlbum - należy do obu typów jednocześnie. To znaczy, że jest polimorficzna, co przekłada się na kod. Np:

var taylorSwift = StudioAlbum(name: "Taylor Swift", studio: "The Castles Studios")
var fearless = StudioAlbum(name: "Speak Now", studio: "Aimeeland Studio")
var iTunesLive = LiveAlbum(name: "iTunes Live from SoHo", location: "New York")

var allAlbums: [Album] = [taylorSwift, fearless, iTunesLive]

Stworzyliśmy tablicę przetrzymującą albumy. W środku znajdują się dwa albumy studyjne i jeden live album. Swift rozpoznajde to, ponieważ wszystkie te albumy dziedziczą po klasie Album, więc mogą dzielić tą samą tablicę.

Możemy zrobić krok dalej, aby zademonstrować więcej możliwości poliformizmu. Dodajmy metodę getPerformance() do wszystkich trzech klas:

class Album {
    var name: String

    init(name: String) {
        self.name = name
    }

    func getPerformance() -> String {
        return "The album \(name) sold lots"
    }
}

class StudioAlbum: Album {
    var studio: String

    init(name: String, studio: String) {
        self.studio = studio
        super.init(name: name)
    }

    override func getPerformance() -> String {
        return "The studio album \(name) sold lots"
    }
}

class LiveAlbum: Album {
    var location: String

    init(name: String, location: String) {
        self.location = location
        super.init(name: name)
    }

    override func getPerformance() -> String {
        return "The live album \(name) sold lots"
    }
}

Metoda getPerformance() istnieje w klasie Album, lecz obie klasy dziedziczące ją nadpisują. Kiedy tworzymy tablicę Albumów, tak naprawdę zapełniamy są podklasami albumu LiveAlbum i StudioAlbum. Ich istnienie w tablicy jest zupełnie w porządku, ponieważ obie dziedziczą po Album, ale nie tracą oryginalnej klasy. Więc możemy zapisać, że:

var taylorSwift = StudioAlbum(name: "Taylor Swift", studio: "The Castles Studios")
var fearless = StudioAlbum(name: "Speak Now", studio: "Aimeeland Studio")
var iTunesLive = LiveAlbum(name: "iTunes Live from SoHo", location: "New York")

var allAlbums: [Album] = [taylorSwift, fearless, iTunesLive]

for album in allAlbums {
    print(album.getPerformance())
}

To automatycznie nadpisze wersje funkcji getPerformance() w zależności od subklasy. To jest polimorfizm: obiekt, który może działać zarówno jako klasa bazowa jak i klasa dziedzicząca w tym samym momencie.

Konwersja typu

Często obiekty będą pokazywać się jako obiekty pewnego typu, ale Ty mimo wszystko będziesz wiedzieć, że są innego. Niestety Swift nie wie tyle co Ty i nie pozwoli Ci zbudować kodu. Istnieje jednak rozwiązanie, które nazywamy konwersją typu, czyli konwersją obiektu jednego typu w inny.

Jeśli zastanawiasz się, dlaczego może być to potrzebne, spójrz na przypadek poniżej:

for album in allAlbums {
    print(album.getPerformance())
}

To pętla z poprzedniego przykładu. Tablica allAlbums przetrzymuje typ Album, ale my wiemy przecież, że tak naprawdę zawiera w sobie podklasy StudioAlbum czy LiveAlbum. Swift tego nie wie. Dlatego jeśli spróbujesz napisać print(album.studio) to Swift nie pozwoli Ci zbudować kodu, ponieważ tylko typ StudioAlbum posiada tą właściwosć.

Konwersja typu w Swift pojawia się w trzech formach. Najczęściej spotkasz ją jako as? i as! zwane częściej opcjonalnym rzutowaniem w dół (optional downcasting) i wymuszonym rzutowaniem w dół (forced downcasting). Ten pierwszy mówi "Myślę, że konwersja powinna się udać, ale jest szansa, że się nie powiedzie", a drugi "Wiem, że konwersja się uda, a jeśli nie to możesz crash'ować appkę"

Notka: Kiedy mówię "konwersja" nie mam na myśli żadnej zmiany obiektu. Zmienia się tylko sposób, w jaki Swift będzie ten obiekt traktować - mówisz mu, że obiekt, który brał wcześniej za typ A jest typem E.

Znak zapytania i wykrzyknić mogą dawać Ci wskazówkę o co tu chodzi, ponieważ jest to bardzo podobne do opcjonalnych wartości. Na przykład jeśli zapiszemy:

for album in allAlbums {
    let studioAlbum = album as? StudioAlbum
}

Swift sprawi, że stała studioAlbum będzie posiadać typ StudioAlbum?. To znaczy opcjonalny album studyjny. Konwersja mogła zadziałać - w tym wypadku otrzymasz album studyjny lub mogła zawieść - wtedy wartością tej stałej będzie nil.

Najczęściej używany jest z if let, które automatycznie odpakowuje opcjonalną wartość. Na przykład:

for album in allAlbums {
    print(album.getPerformance())

    if let studioAlbum = album as? StudioAlbum {
        print(studioAlbum.studio)
    } else if let liveAlbum = album as? LiveAlbum {
        print(liveAlbum.location)
    }
}

Ta pętla przejdzie przez każdy album w tablicy i wyprintuje jego opis, ponieważ jest to wspólne dla wszystkich wartości dziedziczących po Album. Następnie sprawdzi czy może przekonwertować album na typ StudioAlbum, a jeśli mu się uda wyprintuje jego studio. Ostatecznie sprawdzi ten sam warunek dla typu LiveAlbum

Wymuszone rzutowanie stosujemy wtedy, gdy jesteśmy w stu procentach pewni, że dany typ może być traktowany jako inny. Jeśli się mylimy - program się wysypie i powstanie crash. Wymuszone rzutowanie nie zwraca opcjonalnego typu, ponieważ zaświadczamy, że konwersja na pewno się uda. Jeśli nie - to znaczy, że kod jest źle napisany.

Aby to zademonstrować w sposób, który nie spowoduje crasha usuńmy live album z naszej tablicy. Pozostaną tam tylko albumy studyjne:

var taylorSwift = StudioAlbum(name: "Taylor Swift", studio: "The Castles Studios")
var fearless = StudioAlbum(name: "Speak Now", studio: "Aimeeland Studio")

var allAlbums: [Album] = [taylorSwift, fearless]

for album in allAlbums {
    let studioAlbum = album as! StudioAlbum
    print(studioAlbum.studio)
}

W tym oczywistym przypadku, moglibyśmy po prostu zmienić tablicę allAlbums tak, aby przyjmowała wartości [StudioAlbum]. Mimo wszystko przykład ten pokazuje jak działa rzutowanie i nie crash'uje aplikacji, ponieważ korzysta z prawidłowych założeń

Swift pozwala rzutować jako część pętli, co w tym przypadku jest bardziej wydajne. Jeśli jednak wolelibyśmy wymusić rzutowanie na poziomie tablicy moglibyśmy zapisać to tak:

for album in allAlbums as! [StudioAlbum] {
    print(album.studio)
}

Dzięki temu nie musimy rzutować każdego elementu w pętli, ponieważ dzieje się to jeszcze na jej początku. Znów - lepiej, zmienić typ wartości przyjmowanych do tablicy na StudioAlbums. W przeciwnym wypadku w razie pomyłki może wystąpić crash.

Swift pozwala także na opcjonalne rzutowanie na poziomie tablicy, jednak jest trochę bardziej skompikowane, ponieważ wymaga od Ciebie użycia operatora koalescencyjnego nil, aby zawsze zapewnić wartość w petli. Na przykład:

for album in allAlbums as? [LiveAlbum] ?? [LiveAlbum]() {
    print(album.location)
}

Co znaczy "Spróbuj przekonwertować allAlbums, tak aby stał się tablicą obiektów typu LiveAlbum, a jeśli Ci się nie uda, to stwórz pustą tablicę tego typu i użyj jej" - czyli w skrócie nie rób nic

Konwersja powszechnych typów z inicjalizacją

Konwersja typu jest użyteczna, gdy wiesz więcej niż Swift, np. kiedy masz obiekt typu A, a Swift myśli, że to typ B. Jednakże konwersja jest użyteczna tylko wtedy, kiedy wiesz co robisz. Nie możesz wymusić konwersji z typu A na Z jeśli te nie mają ze sobą nic wspólnego.

Na przykład jeśli posiadasz liczbę całkowita o nazwie number, nie możesz przekonwertować jej na String'a:

let number = 5
let text = number as! String

Nie możesz przekonwertować liczby całkowitej na tekst, ponieważ to dwa zupełnie różne typy. Natomiast możesz stworzyć nową zmienną typu String przy pomocy liczby cakowitej. Różnica jest subtelną - to będzie nowa wartość, a nie inna interpretacja starej.

W kodzie wyglądałoby to mniej więcej tak:

let number = 5
let text = String(number)
print(text)

To działa tylko w przypadku pewnych typów wbudowanych do języka Swift. Możesz w ten sposób zmieniać liczby całkowite w te zmiennoprzecinkowe i na odwrót. Nie możesz jednak stworzyć dwóch struktów i magicznie konwertować jeden typ w drugi - to działanie, które musisz napisać samemu.

HACKING WITH SWIFT LIVE This July is a new two-day event where you'll be inspired by great speakers on day one then learn all the amazing new features from WWDC on day two – click here for more information and tickets.

< Previous: Kontrola dostępu   Next: Domknięcie (closure) >
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 >>