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.
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 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.
SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure and A/B test your entire paywall UI without any code changes or app updates.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.