Do tej pory definiowane przez nas klasy miały konkretny cel zgrupowania pewnych metod albo, szerzej, operowania konkretnym obiektem, na przykład dźwiękiem. Pierwotnie jednak ich cel był zupełnie inny.
Od samego początku istnienia komputerów pojawiały się pewne dane powtarzalne dla licznych elementów. Wyobraźmy sobie, że chcemy zapisać dane o jakimś człowieku, będą to imię, nazwisko oraz data urodzenia. Normalnie sięgnęlibyśmy po kilka zmiennych, nazwijmy je roboczo “firstname”, “surname” oraz “birthdate”. Na tym etapie wszystko działa. Gdybyśmy jednak chcieli zapisać tak dane o setce osób, musielibyśmy sięgnąć po tablice – osobne tablice dla imion, nazwisk… Usuwając daną osobę z naszej bazy musielibyśmy pamiętać o usuwaniu jej z każdej jednej tablicy. Co więcej, dodanie nowego atrybutu oznaczałoby dodawanie kolejnych tablic.
Tak pojawiły się jeszcze w latach 50-tych zeszłego wieku struktury, czyli sposoby na grupowanie licznych danych w jednym obiekcie. Oczywiście ścisły recenzent tego kursu powiedziałby, że wtedy o żadnych obiektach mowy jeszcze nie było, a struktury w programowaniu strukturalnym to tylko pewna ilość uporządkowanej pamięci, ale nie bądźmy tacy czepliwi i przełóżmy całe zagadnienie od razu na Rubiego.
Z biegiem czasu programiści wymyślili klasy, a co za tym idzie struktury i klasy nieco się wymieszały swą ideą. Wciąż w niektórych językach się je rozróżnia, ale w zdecydowanej większości współczesnych języków programowania struktura i klasa są definiowane dokładnie tak samo, z resztą dość powszechnym zjawiskiem jest, że w miarę rozwijania oprogramowania coś, co kilka wersji temu było strukturą, teraz już jest klasą.
Stwórzmy więc człowieka
class Person
attr_accessor :firstname, :surname, :birthdate
end
pr=Person.new
pr.firstname = "Jan"
pr.surname = "Kowalski"
pr.birthdate = Time.local(2000, 1, 1)
alert("#{pr.firstname} #{pr.surname} was born in #{pr.birthdate.year}")
Definiujemy tu w znany nam już sposób klasę o nazwie “Person”. Dalej znajduje się dość dziwna instrukcja “attr_accessor”. Jak ona działa? Tak naprawdę jest to skrót, dokładnie to samo moglibyśmy zapisać tak:
class Person
def firstname
return @firstname
end
def firstname=(val)
@firstname=val
end
def surname
return @surname
end
def surname=(val)
@surname=val
end
def birthdate
return @birthdate
end
def birthdate=(val)
@birthdate=val
end
end
Innymi słowy, ten piękny skrótowiec skraca 18 linijek do jednej.
“attr_accessor” oznacza tyle, co “utwórz atrybut o danej nazwie” – nazwę poprzedzamy dwukropkiem, podobnie jak w przechwytywaniu zdarzeń, np. “:nazwa_zmiennej”. Od tej pory obiekty naszej klasy pozwalają w innych miejscach kodu pobierać i zmieniać wartość zmiennej “@nazwa_zmiennej” poprzez użycie konstrukcji “obiekt.nazwa_zmiennej”. Brzmi to dość skomplikowanie, ale mam nadzieję, że przykład wszystko wyjaśnia. Prócz skrótowca “attr_accessor” do dyspozycji mamy jeszcze dwa: “attr_reader”, który definiuje jedynie możliwość odczytu danej zmiennej (dość przydatny, gdy chcemy uniemożliwić jej edycję lub napisać własną funkcję do jej zmiany) oraz “attr_writer”, który analogicznie umożliwia jedynie zmianę wartości zmiennej, to rzadko pożądane zachowanie, ale czasem się przydaje.
Wartości początkowe
Kiedy stworzymy naszego człowieka, jego imię to nil, nazwisko to nil, co gorsze data urodzenia także jest nil, a więc próba wywołania atrybutu “birthdate.year” skończy się pięknym błędem. Warto byłoby się upewnić, że nasz człek dostanie jakieś wartości domyślne. Rozwiązaniem jest po prostu napisanie konstruktora.
class Person
attr_accessor :firstname, :surname, :birthdate
def initialize
@firstname, @surname = "No", "Name"
@birthdate = Time.now
end
end
W ten sposób po stworzeniu, nasz człowiek nazywa się “No Name” i urodził się, no, właśnie teraz. Tu taka wskazówka, gdy chcemy szybko zdefiniować kilka zmiennych, możemy zrobić to kolejnym jednoliniowcem:
a, b, c = 1, 2, 3
alert(a)
alert(b)
alert(c)
Jak widać, w ten sposób szybko przypisaliśmy wartości “a=1”, “b=2” oraz “c=3”.
Jeszcze bardziej skróćmy nasz kod
Czy można jeszcze bardziej uprościć nasz kod? No oczywiście! Skoro konstruktor może przyjmować argumenty, skorzystajmy tu z tego!
class Person
attr_accessor :firstname, :surname, :birthdate
def initialize(firstname="No", surname="Name", birthdate=Time.local(1990, 1, 1))
@firstname, @surname, @birthdate = firstname, surname, birthdate
end
end
pr=Person.new("Jan", "Kowalski", Time.local(2000, 1, 1))
alert("#{pr.firstname} #{pr.surname} was born in #{pr.birthdate.year}")
9 linijek, lepiej chyba już nie będzie!
Jak widzimy, pozwoliliśmy tu, by to konstruktor od razu tworzył naszego człowieka. To dziwne “równa się” (=) w liście parametrów tworzy domyślną wartość, która zostanie wykorzystana, gdybyśmy nie przekazali żadnych argumentów. Tak więc, jeśli programista wpisze po prostu “Person.new”, uzyska człowieka “No Name” urodzonego 1 stycznia 1990.
Ze struktury do klasy
Na początku pisałem, że granica między klasą a strukturą stała się bardzo płynna. Przecież stworzony obiekt bez wątpienia jest strukturą, nie ma żadnych funkcji powodujących inne działanie. Ale chętnie kilka pododajemy.
Zastanówmy się, jakie informacje będą nam zwykle o człowieku potrzebne. Jedną z nich może być pełne imię i nazwisko, drugą wiek. Zamiast zawsze je liczyć, możemy przecież nieco zmienić definicję naszej klasy.
class Person
attr_accessor :firstname, :surname, :birthdate
def initialize(firstname="No", surname="Name", birthdate=Time.local(1990, 1, 1))
@firstname, @surname, @birthdate = firstname, surname, birthdate
end
def fullname
return @firstname+" "+@surname
end
def age
now = Time.now
return now.year-@birthdate.year - ((now.month>@birthdate.month || (now.month==@birthdate.month && now.day>=@birthdate.day))?0:1)
end
end
Mamy teraz dwie metody, które nam zwracają pełne imię i nazwisko oraz wiek danej osoby. Ten potworek z obliczania wieku może wyglądać straszliwie, ale dość łatwo go wyjaśnić. Odejmujemy od obecnego roku rok urodzenia naszego człowieka. To jednak nie jest wszystko, przecież mógł się on urodzić w grudniu, a teraz może być styczeń. Sprawdzamy więc jeszcze, czy nie wystąpiła taka sytuacja, porównujemy odpowiednio miesiąc i dzień urodzenia. Jeśli jeszcze urodziny tego człowieka nie wypadły, od wieku odejmujemy jeszcze jeden, w przeciwnym wypadku na tym kończymy.
Otwieramy szkołę
Skoro możemy tworzyć młodych ludzi, stwórzmy tym młodym ludziom naturalne zbiorowisko, czyli szkołę. Chcemy w naszym programie zapisać informację o wszystkich uczniach danej szkoły. Można to uzyskać na wiele, bardzo wiele sposobów. Ten, który dziś omówimy, wcale nie musi być najlepszy, zależnie od celów i działania aplikacji, dużo lepszą metodą może się po prostu okazać uwzględnienie atrybutu klasy w klasie ucznia albo stworzenie wielkiej klasy do obsługi szkoły.
Do definicji uczniów wykorzystamy wcześniej napisaną klasę… [1] Chcemy jednak zapisać także informację o tym, do jakiej klasy należy student. Tu się przydadzą dość ciekawe obiekty, jakimi są “hasze” (inaczej także nazywane “tablicami haszowymi” bądź “słownikami”).
Załóżmy na razie, że nasza szkoła jest dość mała, mamy dwie klasy, a w każdej po dwóch uczniów.
class Person
attr_accessor :firstname, :surname, :birthdate
def initialize(firstname="No", surname="Name", birthdate=Time.local(1990, 1, 1))
@firstname, @surname, @birthdate = firstname, surname, birthdate
end
def fullname
return @firstname+" "+@surname
end
def age
now = Time.now
return now.year-@birthdate.year - ((now.month>@birthdate.month || (now.month==@birthdate.month && now.day>=@birthdate.day))?0:1)
end
end
students = {
'1A' => [
Person.new("Michał", "Jabłon", Time.local(2013, 5, 8)),
Person.new("Anna", "Zając", Time.local(2013, 6, 14))
],
'1B' => [
Person.new("Wincenty", "Pirowski", Time.local(2013, 1, 11)),
Person.new("Dorota", "Szkud", Time.local(2013, 2, 23))
]
}
Hasze pod wieloma względami przypominają zwykłe tablice, mają jednak pewną różnicę. W tradycyjnej tablicy elementy są indeksowane numerami od zera, w haszach za indeks elementu (tzw. klucz) może służyć absolutnie cokolwiek: jakiś łańcuch znaków, liczba czy nawet obiekt. Tu mamy hasz, który posiada dwie tablice, odpowiednio pod kluczami “1A” oraz “1B”. Zobaczmy
students['1A'][0]
: #=> #<EltenAPI::Common::Console::Person:0x11b89d50 @surname="Jabłon", @firstname="Michał", @birthdate=Tue May 08 00:00:00 środkowoeuropejski czas letni 2013>
Wygenerujmy sobie szkołę
Napiszmy teraz program, który tworzy szkołę z szesnastoma klasami (1A, 1B, 2A, 2B i tak dalej). Do każdej klasy trafi między 20, a 30 uczniów.
firstnames = ["Maja", "Agata", "Wojciech", "Gracjan", "Zuzanna", "Aleksander", "Eliza", "Michał", "Dariusz", "Monika", "Zofia", "Weronika", "Lucjan", "Jakub", "Jacek", "Maria", "Małgorzata", "Cecylia", "Dorota", "Agnieszka", "Łukasz", "Piotr", "Paweł", "Alicja", "Janina", "Andrzej", "Paulina", "Kacper", "Barbara", "Stefan", "Bartłomiej", "Urszula", "Jadwiga", "Natalia"]
surnames = ["Andruszkiewicz", "Pepka", "Skowron", "Metyk", "Wróbel", "Swat", "Olcha", "Gołąbek", "Małysik", "Rusałek", "Gilec", "Tolman", "Wichoń", "Kasprzak", "Wilk", "Dominiak", "Pinko", "Machul", "Złotko", "Kołacz", "Dudek", "Kaczmarek", "Cieślak", "Jarosz", "Sowa", "Świątek", "Sobczyk", "Rybak", "Kwiecień"]
students={}
for i in 1..8
for l in ["A", "B"]
className = i.to_s+l
students[className] = []
(20+rand(11)).times {
student = Person.new(firstnames[rand(firstnames.size)], surnames[rand(surnames.size)], Time.local(2013+i, rand(12)+1, rand(28)+1))
students[className].push(student)
}
end
end
for k,v in students
alert("#{k}: #{v.map{|s|s.fullname}.join(", ")}.")
end
Napisaliśmy 19 dość skomplikowanych linijek, a więc czas na nieco wyjaśnień. Najpierw tworzymy tablicę z przykładowymi imionami i nazwiskami. Następnie w pętli dla klas od 1 do 8 osadzamy drugą pętlę, dla podziału na A i B. W każdej z nich losujemy ilość przebiegów (20..30). Tak oto mamy potrójnie zagnieżdżoną pętlę. Teraz tworzymy ucznia, przypisując mu losowe imię i nazwisko, a także losową datę urodzenia z właściwego mu rocznika. By nie komplikować programu jeszcze bardziej, datę urodzenia losujemy tylko do 28 dnia miesiąca. Wskazówka! Obrana metodyka generowania dat urodzin uczniów miała być prosta, ale by równo i sprawiedliwie wylosować daty z całego roku, wystarczy wylosować każdemu uczniowi sekundę narodzin z zakresu czasu Unixa od 1 stycznia do 31 grudnia danego roku. Warto zauważyć tu metodę “push”, która pozwala na dostawianie elementu na koniec istniejącej tablicy.
Potem dzieje się coś dziwnego, piszemy dość złożoną pętlę “for”. Nie ma tu jednak niczego zadziwiającego, po prostu do zmiennej “k” trafia wartość klucza (tu nazwa klasy), a do “v” jego wartość (tu tablica uczniów). Na koniec wypowiadamy listę uczniów danej klasy, ale… Zaraz… Coś tu bardzo dziwnego się dzieje.
Mapowanie tablic
Mapowanie to dość przydatna funkcja podczas szybkiego operowania na tablicy. Pozwala ona wygenerować nową tablicę na podstawie wszystkich elementów starej. Jakiś przykład:
[1, 2, 3, 4, 5].map{|a|a*2}
: #=> [2, 4, 6, 8, 10]
W tym wypadku utworzyliśmy nową tablicę, podwajając każdy element starej. Tylko tyle i aż tyle.
Dziennik się rozsypał
Dyrektor naszej placówki spisywał dziennik uczniów, ale porozsypywały mu się kartki i dopiero po pewnym czasie zauważył, że zapisał ich niealfabetycznie. Na szczęście dziennik był elektroniczny, a więc szybko zadzwonił do informatyka z prośbą o naprawienie problemu. Sortowanie danych to całkiem złożone zagadnienie. Istnieje wiele algorytmów, a matematycy prześcigają się w tworzeniu kolejnych, wydajniejszych od starych. Na szczęście w wypadku Rubiego nie musimy się tym przejmować, bo zrobi to za nas. Do sortowania tablicy służy funkcja o nazwie… nie zgadniecie… “sort”.
[3, 4, 1, 5, 2].sort
: #=> [1, 2, 3, 4, 5]
W ten sposób można jednak sortować tylko dane, które komputer umie posortować: liczby, napisy… Skąd ma jednak wiedzieć, jak posortować nasze klasy? Oczywiście dałoby mu się to powiedzieć poprzez zdefiniowanie operatora porównania, ale to nieco bardziej złożone, a więc zacznijmy od czegoś prostego, mianowicie funkcji “sort_by”. Funkcja ta sortuje tablicę wg wybranego kryterium, o tak:
[3, 4, 1, 5, 2].sort_by{|a|a*-1}
: #=> [5, 4, 3, 2, 1]
Otrzymaliśmy wartość odwrotną, ponieważ nakazaliśmy programowi sortować wedle liczb przeciwnych do elementów tablicy.
Teraz możemy więc poprawnie posortować naszych uczniów, zmieniamy nieco instrukcję ich tworzenia.
for i in 1..8
for l in ["A", "B"]
className = i.to_s+l
students[className] = []
(20+rand(11)).times {
student = Person.new(firstnames[rand(firstnames.size)], surnames[rand(surnames.size)], Time.local(2013+i, rand(12)+1, rand(28)+1))
students[className].push(student)
}
students[className] = students[className].sort_by{|s|s.surname+" "+s.firstname}
end
end
Sortowanie nietypowe, czyli odkryjmy koło na nowo
Jak widać, nasze tablice się grzecznie sortują, ale co tam się dzieje, że się sortują? Wewnętrznie, dobierając odpowiedni algorytm, interpreter porównuje różne wartości i ustawia je w odpowiedniej kolejności. W tym celu wykorzystywany jest operator porównania, który zapisujemy jako “<=>”. Operator ten zwraca trzy wartości: “-1”, gdy pierwsza wartość poprzedza drugą, “0”, gdy się równają i “1”, gdy druga poprzedza pierwszą. Przykład:
0<=>1
: #=> -1
1<=>1
: #=> 0
1<=>0
: #=> 1
Tak więc w praktyce polecenie:
students[className] = students[className].sort_by{|s|s.surname+" "+s.firstname}
Możemy zastąpić, wymuszając nasze własne wyniki porównań, o tak:
students[className] = students[className].sort{|a,b|(a.surname+" "+a.firstname) <=> (b.surname+" "+b.firstname)}
W tym wypadku głupie, ale czasem przydatne. Przykładowo zamieniając a i b, możemy wymusić dość wydajne sortowanie odwrotne.
Policzmy dziewczęta
Ostatnie, co dzisiaj spróbujemy napisać, to licznik dziewcząt. Na razie nie zapisujemy informacji o płci uczniów, zróbmy jednak dość uproszczone założenie, że imiona dziewcząt kończą się literą “a”. W tym celu dopiszmy do naszej klasy “Person” dwie metody:
def female?
return @firstname[-1..-1]=="a"
end
def male?
return !female?
end
W tablicach indeksy poniżej zera oznaczają dany element od końca, tak więc “-1” to ostatni element, “-2” przedostatni i tak dalej. Składnia “@firstname [-1..-1]” zwraca ostatni znak imienia, uwaga, składnia “@firstname [-1]” zwróciłaby jego kod ASCII, na czym nam nie zależy. Stworzyliśmy więc dwie metody: “female?” sprawdzającą, czy uczeń jest uczennicą i “male?”, która zwraca odwrotność powyższej.
Teoretycznie moglibyśmy znów napisać pętlę po haszu “students”, ale są prostsze metody. Z pomocą przychodzi nam dość użyteczna metoda “flatten”.
[[1, 2], 3, [4, 5], 6].flatten
: #=> [1, 2, 3, 4, 5, 6]
Metoda “flatten” rozkłada wszystkie podtablice w tablicy do jednej. Potrzeba nam jeszcze wiedzy, że odpowiednio atrybuty “keys” i “values” w haszu zwracają tablicę kluczy i wartości, by napisać nasz piękny licznik:
girls=0
for student in students.values.flatten
girls+=1 if student.female?
end
alert("There are #{girls} girls in this school.")
Podsumowanie
Ufff, dzisiaj znów wiele teorii, przerzuciliśmy struktury, tablice i hasze na lewo i prawo… Poniżej prezentuję kompletny program zliczający uczniów i uczennice, to jest 51 wywalczonych dziś linijek.
class Person
attr_accessor :firstname, :surname, :birthdate
def initialize(firstname="No", surname="Name", birthdate=Time.local(1990, 1, 1))
@firstname, @surname, @birthdate = firstname, surname, birthdate
end
def fullname
return @firstname+" "+@surname
end
def age
now = Time.now
return now.year-@birthdate.year - ((now.month>@birthdate.month || (now.month==@birthdate.month && now.day>=@birthdate.day))?0:1)
end
def female?
return @firstname[-1..-1]=="a"
end
def male?
return !female?
end
end
firstnames = ["Maja", "Agata", "Wojciech", "Gracjan", "Zuzanna", "Aleksander", "Eliza", "Michał", "Dariusz", "Monika", "Zofia", "Weronika", "Lucjan", "Jakub", "Jacek", "Maria", "Małgorzata", "Cecylia", "Dorota", "Agnieszka", "Łukasz", "Piotr", "Paweł", "Alicja", "Janina", "Andrzej", "Paulina", "Kacper", "Barbara", "Stefan", "Bartłomiej", "Urszula", "Jadwiga", "Natalia"]
surnames = ["Andruszkiewicz", "Pepka", "Skowron", "Metyk", "Wróbel", "Swat", "Olcha", "Gołąbek", "Małysik", "Rusałek", "Gilec", "Tolman", "Wichoń", "Kasprzak", "Wilk", "Dominiak", "Pinko", "Machul", "Złotko", "Kołacz", "Dudek", "Kaczmarek", "Cieślak", "Jarosz", "Sowa", "Świątek", "Sobczyk", "Rybak", "Kwiecień"]
students = {}
for i in 1..8
for l in ["A", "B"]
className = i.to_s+l
students[className] = []
(20+rand(11)).times {
student = Person.new(firstnames[rand(firstnames.size)], surnames[rand(surnames.size)], Time.local(2013+i, rand(12)+1, rand(28)+1))
students[className].push(student)
}
students[className] = students[className].sort_by{|s|s.surname+" "+s.firstname}
end
end
girls=0
boys=0
for student in students.values.flatten
if student.female?
girls+=1
else
boys+=1
end
end
alert("There are #{girls} girls and #{boys} boys in this school.")
alert("List of students:")
for k,v in students
alert("#{k}: #{v.map{|s|s.fullname}.join(", ")}.")
end
Przypisy
- Klasa w szkole, klasa w programie, wiem, może mylić…
can i do like that?
thehash={"a"=>{"b"=>{"c"=>1,"d"=>2,"e"=>5}}}
alert(thehash["a"]["b"]["e"]) #=>5
so it will be correct like this
class person
attr_accessor :name, :sername: :birthdate, :gender, :location and so on
what… the female function is easy but not usefull. emagine if you have jenifer, ann, kate, liz and other females. this function will return that they are males, because they simply don’t end in letter a! whaaaaaaaat! and there are russian male names nikita, sasha, yasha, luka that are mails. and the stupid function will count them as, ges what… females!
Dlaczego uproszczooa? W Polskimchyba tylko jedno imie nie kończy się na A, żeńskie oczywiście.