Dawno dawno temu w pewnej wiosce żył sobie pewien Smok. Smok był bardzo zły i zionął ogniem. Ogień był gorący, więc parzył wszystkich innych mieszkańców wioski, którzy przypadkiem przebywali w pobliżu, zwłaszcza gdy próbowali się z nim przywitać. Mieszkańcy wytypowali pewnego kucharza, aby ten smoka pokonał. Szewc przygotował więc stado owiec nadzianych siarką. Gdy je zobaczył, zrobił się bardzo łakomy. A gdy smok zjadł tej siarki 10 kilogramów, zrobił się bardzo zmęczony. A gdy zjadł jej 20 kilo, padł i już na zawsze dał spokój mieszkańcom wioski, którzy od tej pory co jakiś czas przychodzili do jego jaskini i sprawdzali, czy przywitanie jest wciąż bezpieczne.
Twoim zadaniem z tej części będzie zaprogramowanie takiej historii. Co więcej zaprogramowanie jej w sposób obiektowy! [1]
RZUT OKA NA SWIFT
Zanim zaczniemy cokolwiek pisać, musimy dowiedzieć się jak w Swift należy definiować enumeracje i struktury , które przydadzą się w dalszych rozważaniach.
Enumeracje, struktury i klasy są to jedne z bardziej zaawansowanych struktury danych. Najważniejszą ich cechą jest to, że pozwalają wykorzystywać się jako typy zmiennych, co daje niesamowite możliwości programistyczne.
ENUMERACJE
Enumeracje są kojarzone z wartościami. Pozwalają nam na dokładne określenie wartości, które może przyjmować pewna zmienna.
W przypadku zadania, które poznaliśmy na początku enumerację będziemy mogli wykorzystać do określenia stanu naszego smoka. I tak powinniśmy zdefiniować kilka możliwych wartości tego stanu: zly, lakomy, zmeczony i wykonczony.
A czemu nie użyć zmiennej typu Int? Zapytasz… Wystarczyłoby przecież przypisać wartościom odpowiednie stany a potem ustawiać te wartości zmiennej… A ja zapytam: a co będzie, jak się pomylisz i przypiszesz gdzieś tej zmiennej złą wartość, która nie oznacza żadnego stanu smoka?
var stan: Int = 0
stan = 0 // smok jest Zly
stan = 1 // smok jest Lakomy
stan = 2 // smok jest Zmeczony
stan = 3 // smok jest Wykonczony
//...
stan = 234 // a to co?.
Kompilator w powyższym przypadku nie poinformuje nas o błędnym ostatnim wierszu.
Wróćmy więc do enumeracji. Zobaczmy jak będzie wyglądać jej definicja.
enum Stan{
case Zly, Lakomy, Zmeczony, Wykonczony
}
Jak widać całość deklaracji enumeracji sprowadza się do wymienienia kilku etykiet. Etykiety będą w rzeczywistości odpowiadać wartościom typu Int. Działa to więc tak, jakbyśmy zamiast tych etykiet używali rzeczywiście wartości całkowitych. Użycie enumeracji pozwoli jednak zapobiec sytuacji złego przypisania wartości zmiennej stan.
var stan: Stan = .Zly
stan = Stan.Zly // smok jest Zly
stan = Stan.Lakomy // smok jest Lakomy
stan = Stan.Zmeczony // smok jest Zmeczony
stan = Stan.Wykonczony // smok jest Wykonczony
//...
stan = 234 // A to?
Tym razem ostatni błędny wiersz zostanie wychwycony podczas kompilacji.
Podczas kompilacji kompilator wie, że zmienna stan ma typ określony na Stan. Pozwala to zastosować skrócony zapis wyboru etykiety enumeracji. Można pominąć jej nazwę.
var stan: Stan = .Zly
stan = .Zly // smok jest Zly
stan = .Lakomy // smok jest Lakomy
stan = .Zmeczony // smok jest Zmeczony
stan = .Wykonczony // smok jest Wykonczony
Jak kontrolować jednak dobrze działanie zmiennej, która może przyjmować kilka wartości. Otóż mamy możliwość zaimplementowania wewnątrz enumeracji również metody[2], która zmieni nam stan z aktualnego na jakiś inny. I tak przykładowa jej implementacja może wyglądać tak:
enum Stan{
case Zly, Lakomy, Zmeczony, Wykonczony
mutating func next(){
switch self{
case .Zly: self = .Lakomy
case .Lakomy: self = .Zmeczony
case .Zmeczony: self = .Wykonczony
case .Wykonczony: self = .Wykonczony
}
}
}
Fakt, że metoda będzie zmieniała wartość obiektu enumeracji, na którym będzie uruchomiona wymaga jej oznaczenia słowem mutating. Teraz, żeby zmienić stan na kolejny, wystarczy jedna linia kodu
stan.next()
Narzędzie kontroli stanów już więc mamy.
STRUKTURY Zastanawiając się nad istotą Owcy możemy dojść do wniosku, że ilość siarki, jaką pomieszczą różne owce może być różna, dlatego powinniśmy w jakiś sposób zdefiniować parametry owcy. Do tego posłużą nam struktury[3].
Struktura w Swift pozwala na:
- Definiowanie pól składowych.
- Definiowanie funkcji.
- Definiowanie subscript aby można było wykorzystać taką syntaktykę jak w tablicach [4].
- Tworzenie rozszerzeń, aby zmienić ich funkcjonalność bez modyfikacji kodu źródłowego [5].
- Implementację zgodności z protokołami opisującymi pewne funkcjonalności. [6].
Zdefiniujmy więc strukturę, która będzie opisywać charakterystyczne cechy owcy.
struct Owca{
var siarka:Int
}
Tak zdefiniowana struktura pozwala nam na deklarację zmiennej i inicjalizację jej odpowiednimi danymi. Stwórzmy więc Owcę, która pomieściła w sobie 2 kilogramy siarki.
var owca = Owca(siarka: 2)
Po prawej stronie operatora przypisania (=) widzimy wywołanie inicjalizatora, który w przypadku Struktur jest generowany automatycznie. Inicjalizator jest konstruktorem, który pozwala na nadanie polom struktury wartości początkowych. Sam anazwa inicjalizator wyraźniej od konstruktra sugeruje, że nie konstruuje, tylko “dekoruje” już skonstruowane w pamięci obiekty, nadając im wartości początkowe.
Pójdźmy o krok dalej i stwórzmy tablicę, zawierający stado owiec.
var stado = [Owca(siarka:2),
Owca(siarka:1),
Owca(siarka:1),
Owca(siarka:2),
Owca(siarka:3),
Owca(siarka:2),
Owca(siarka:1),
Owca(siarka:2),
Owca(siarka:1),
Owca(siarka:2),
Owca(siarka:1),
Owca(siarka:2)]
Ta ilość owiec nie wystarczy aby nakarmić smoka, postaraj się więc wygenerować więcej.
Pora zająć się smokiem.
Smoka reprezentować nam będzie obiekt typu Smok. A typ Smok, to oczywiście będzie kolejna struktura.
Zacznijmy od najprostszego:
struct Smok{
}
Zgodnie z naszą bajką, smok miał podczas powitania miał przypalić trochę mieszkańca.
Stąd potrzeba zaimplementowania w klasie Smok funkcji powitaj.
struct Smok{
func powitaj(mieszkanca m: Mieszkaniec){
print("S: Arrrrr")
m.przypal()
}
}
Jednocześnie powinniśmy uzupełnić naszą bajkę o strukturę, której isntancje będą mieszkańcami. Tutaj musimy również stworzyć funkcję powitaj, która spróbuje skontaktować się ze smokiem, oraz funkcję przypal, która na odpowiedź smoka zareaguje.
struct Mieszkaniec{
func powitaj(smoka s: Smok){
print("M: Witaj smoku ")
s.powitaj(mieszkanca: self)
}
func przypal(){
print("M: Auuuu ")
}
}
Możemy teraz przetestować nasz kod, który pozwoli już na prostą interakcję obiektów obu typów.
var smok = Smok()
var mieszkaniec = Mieszkaniec()
mieszkaniec.powitaj(smoka: smok)
Efektem działania tego kodu będzie tekst, który pojawi się w konsoli:
M: Witaj smoku
S: Arrrrr
M: Auuuu
Pozostało dodać jeszcze możliwość zapamiętania stanu smoka. W przypadku podejścia obiektowego, musimy pamiętać, że stan obiektu jest jego wewnętrzną sprawą. Więc powienien być zapamiętany gdzieś wewnątrz struktury Smok. W końcu on sam będzie najlepiej wiedział jak się czuje. Tutaj wykorzystamy możliwość dodania do struktury Smok pola stan, którego typ określimy na Stan[7]:
class Smok{
var stan = Stan.Zly
func powitaj(m: Mieszkaniec){
print("S: Arrrrr ")
m.przypal()
}
}
Pierwszy etap jest więc za nami. W kolejnej części rozbudujemy strukturę Smoka, zaimplementujemy strukturę Owcy i dokończymy naszą bajkę.
Dla tych, którzy nie wiedzą, co to jest sposób obiektowy: nie martwcie się, wszystko Wam wyjaśnię. … czyli funkcji W zasadzie powinniśmy tutaj użyć klas, bo struktury mają pewną wadę, która sprawia, że program zachowuje się trochę inaczzej niż w prawdziwym świecie, ale o tym w części #6. To omówimy kiedy indziej. To też omówimy kiedy indziej. I to też. …czyli wykorzystujemy enumerację z początku lekcji.