Zaawansowane typy danych

Teraz, gdy znasz już proste typy danych oraz konstrukcje dające pełną programistyczną moc, czas zapoznać się z bardziej złożonymi typami: tablicą i słownikiem.

Tablica (Array)

Dotychczas, w naszych programach mieliśmy, co najwyżej kilka zmiennych. W prawdziwym życiu programiści muszą borykać się z przypadkami, kiedy ilości danych "idą w terabajty". Wyobraź sobie sytuację, w której chcesz wpisać do programu średnią, dobową temperaturę wszystkich dni w minionym roku. Tworzenie osobnej zmiennej dla każdego dnia nie jest dobrym pomysłem (choćby z powodu czytelności kodu). Właśnie w takich sytuacjach przydają się tablice. Tablica to obiekt, który w uporządkowany sposób “przechowuje” inne obiekty. Przykład:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0> a = [1, "word", 10, nil]
=> [1, "word", 10, nil]
irb(main):002:0>

Składnia jest bardzo intuicyjna. Tablicę tworzymy poprzez wpisanie oddzielonych przecinkami elementów w nawiasy kwadratowe. Aby otrzymać element (i go zmodyfikować) o indeksie n, z tablicy a, należy wpisać a[n] (pamiętaj, że programiści numerują od zera, a nie od jedynki ;)):

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0> a = [1, "word", 10, nil]
=> [1, "word", 10, nil]
irb(main):002:0> a[2]
=> 10
irb(main):003:0>a[0]+=100
=> 101

Aby otrzymać zakres elementów możemy użyć… zakresu ;) W ten sam sposób możemy uzyskać część napisu:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0> a = [1, "word", 10, nil]
=> [1, "word", 10, nil]
irb(main):002:0> a[2]
=> 10
irb(main):003:0>a[0]+=100
=> 101
irb(main):004:0> a[1..3]
=> ["word", 10, nil]
irb(main):005:0>"Dog, cat and hamster"[0..7]
=> "Dog, cat"

Aby dodać element na końcu tablicy powinniśmy użyć metody push, lub operatora <<, który de facto jest skróconą formą zapisu tej metody. Jeśli chcesz umieścić element w konkretnym miejscu tablicy - użyj metody insert:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0> a = [1, "word", 10, nil]
=> [1, "word", 10, nil]
irb(main):002:0> a[2]
=> 10
irb(main):003:0>a[0]+=100
=> 101
irb(main):004:0> a[1..3]
=> ["word", 10, nil]
irb(main):005:0>"Dog, cat and hamster"[0..7]
=> "Dog, cat"
irb(main):006:0> a.push(96)
=> [101, "word", 10, nil, 96]
irb(main):007:0> a << "a"
=> [101, "word", 10, nil, 96, "a"]
irb(main):008:0> a.insert(2, nil)
=> [101, "word", nil, 10, nil, 96, "a"]

Analogicznie (przy usuwaniu) do metod push i insert działają pop oraz delete_at:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0> arr = [1,2,3,4,5,6,7,8,9]
=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
irb(main):002:0> arr.pop
=> 9
irb(main):003:0> arr.delete_at(3)
=> 4

Zauważ, że metody push, << oraz insert zwracają całą liste, natomiast pop oraz delete_at tylko usunięty element.

Słownik (Hash)

Słowniki, nazywane są też tablicami asocjacyjnymi. Działają bardzo podobnie do zwykłych tablic, ale wartości mogą być indeksowane dowolnym typem obiektu (w przypadku tablic są to liczby całkowite). Innymi słowy, słowniki to zbiory par klucz=>wartość. Zauważ, że o ile wartości mogą się powtarzać, klucz musi być jednoznaczny. Poniżej kilka równoważnych sposobów inicjalizacji słownika:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0> h1 = {"a"=>1, "b"=>2, "c"=>4, "d"=>4}
=> {"a"=>1, "b"=>2, "c"=>4, "d"=>4}
irb(main):002:0> h2 = Hash["a"=>1, "b"=>2, "c"=>4, "d"=>4]
=> {"a"=>1, "b"=>2, "c"=>4, "d"=>4}
irb(main):003:0> h3 = Hash["a", 1, "b", 2, "c", 4, "d", 4]
=> {"a"=>1, "b"=>2, "c"=>4, "d"=>4}

Aby uzyskać element zapisany pod danym kluczem (i go zmodyfikować), należy użyć składni analogicznej do tej z tablic:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0>  h = {"a"=>1, "b"=>nil, "c"=>123}
=> {"a"=>1, "b"=>nil, "c"=>123}
irb(main):002:0> h["a"]
=> 1
irb(main):003:0> h["b"]=1000
=> 1000
irb(main):004:0> h["d"]
=> nil
irb(main):005:0> h
=> {"a"=>1, "b"=>1000, "c"=>123}

Zauważ, że, gdy “poprosimy” słownik o wartość pod kluczem, którego w nim nie ma, zwrócony będzie nil (analogiczne zachowanie występuje w tablicach).

Aby dodać parę klucz=>wartość do słownika należy użyć poniższej składni:

 

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0>  h = {"a"=>1, "b"=>2, "c"=>3
=> {"a"=>1, "b"=>2, "c"=>3}
irb(main):002:0>h["z"] = 1.11
=> 1.11
irb(main):003:0> h
=> {"a"=>1, "b"=>2, "c"=>3, "z"=>1.11}

Jeśli nie należysz do strachliwych spróbuj zastosować/zaadoptować tą składnię dla tablic ;)

Do usuwania elementów ze słownika służy metoda delete:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0>  h = {"a"=>1, "b"=>2, "c"=>3
=> {"a"=>1, "b"=>2, "c"=>3}
irb(main):002:0>h["z"] = 1.11
=> 1.11
irb(main):003:0> h
=> {"a"=>1, "b"=>2, "c"=>3, "z"=>1.11}
irb(main):004:0> h.delete("a")
=> 1
irb(main):005:0> h
=> {"a"=>1, "c"=>123, "z"=>1.11}

Symbol

Symbol to typ służący do reprezentowania nazw. Najczęściej wykorzystuje się je jako klucze w słownikach, gdyż są bardziej wydajne (dzięki nim programy działają szybciej) niż napisy.

Tworzymy je za pomocą dwukropka i następujących po nim liter i cyfr:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0> s1 = :dog 
=> :dog
irb(main):002:0> h1 = {:one => 1, :two => 2}
=> {:one=>1, :two=>2}
irb(main):003:0> h2 = {one: 1, two: 2}
=> {:one=>1, :two=>2}

Zauważ, że jeśli używamy symboli jako kluczy słownika możemy użyć specjalnej skróconej formy inicjalizacji słownika, gdzie dwukropek należący do symbolu przestawiamy na jego koniec. Pozwala to pomijać znak => (tzw. hash rocket).

Iterowanie po tablicach i słownikach

Tablice i słowniki pozwalają na wykonywanie pętli po ich kolejnych elementach (tak jak zakresy). Poniżej przykładowy program z użyciem pętli for:

Iterating.rb
a = [1,3,7,"end"]
h = {a: 1, b:"B", c:8}
for i in a
  puts i
end
for i,j in h
  puts i
  puts j
end
jan@Stacja-Linux ~/Pulpit $ ruby Iterating.rb 
1
3
7
end
a
1
b
B
c
8

Zauważ, że iterując po słowniku potrzebne są dwie zmienne (w naszym wypadku i oraz j) na które przypisywane będą kolejne elementy, gdyż element słownika to para klucz=>wartość.

Tablice tablic

Elementem tablicy może być dowolny obiekt, w tym... tablica. Zagnieżdżone tablice do dosyć naturalna reprezentacja macierzy (dwu- i więcej wymiarowych). Poniżej przykład inicjalizacji takiej macierzy oraz wypisanie jej elementu (zauważ jakie to proste i eleganckie ;)):

jan@Stacja-Linux ~ $ irb
irb(main):001:0> matrix = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
=> [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
irb(main):002:0> matrix[2][0]
=> 9

Przegląd najważniejszych metod (i operatorów) dla list i słowników.

Metody wymienione wcześniej w rozdziale nie są wypisane poniżej.

Tablice

Metoda Opis
count(n) Zwraca ilość elementów w tablicy (jeśli nie podamy argumentu n) lub ilość elementów równych n w tablicy.
+ Zwraca konkatenację podanych tablic.
& Zwraca część wspólną podanych tablic.
clear Usuwa wszystkie elementy z listy.
index(obj) Zwraca indeks pierwszego elementu równego obj w tablicy lub nil jeśli takowego w niej nie ma.
reverse Zwraca tablicę z odwróconą kolejnością elementów.
sort Zwraca tablicę z posortowanymi elementami.
uniq Zwraca tablicę z usuniętymi powtórzeniami elementów.

Poniżej praktyczne spojrzenie na powyższe metody:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0>  a = [1,2,3,4,5,6,7,8,9]
=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
irb(main):002:0>  a.count 8
=> 1
irb(main):003:0>  a.count 11
=> 0
irb(main):004:0>  a+a
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 9]
irb(main):005:0> a&[0,5,10]
=> [5]
irb(main):006:0> a.index 2
=> 1
irb(main):007:0> a.index 11
=> nil
irb(main):008:0> a.reverse
=> [9, 8, 7, 6, 5, 4, 3, 2, 1]
irb(main):009:0> a.sort
=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
irb(main):010:0> [9,3,2,9,2,3,4,5,1,2,3,1].sort
=> [1, 1, 2, 2, 2, 3, 3, 3, 4, 5, 9, 9]
irb(main):011:0> ["aba","aca","a","b"].sort
=> ["a", "aba", "aca", "b"]
irb(main):012:0> [1,1,1,1,1,2,2,2,2,1,1,1,2,2,2,3,4,5,6,5,4,3,4,5,6].uniq
=> [1, 2, 3, 4, 5, 6]
irb(main):013:0> a.clear
=> []
irb(main):014:0> a
=> []

Słowniki

Metoda Opis
length Zwraca liczbę elementów w słowniku.
has_key? Zwraca true, jeśli słownik zawiera podany klucz  i false w przeciwnym przypadku.
has_value? Zraca true, jeśli słownik zawiera podaną wartość i false w przeciwnym przypadku.
clear Usuwa wszystkie elementy ze słownika.
invert Zwraca słownik, w którym wartości podanego słownika są kluczami i odwrotnie.
merge(other_hash) Zwraca słownik będący połączeniem dwu podanych słowników.

Poniżej powyższe metody w przykładach:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0> h = {a: 1, b: 2, c: 3}
=> {:a=>1, :b=>2, :c=>3}
irb(main):002:0> h.length
=> 3
irb(main):003:0> h.has_key? :a
=> true
irb(main):004:0> h.has_key? :q
=> false
irb(main):005:0> h.has_value? :a
=> false
irb(main):006:0> h.has_value? 1
=> true
irb(main):007:0> h.merge({d: 4, e: 5})
=> {:a=>1, :b=>2, :c=>3, :d=>4, :e=>5}
irb(main):008:0> h.invert
=> {1=>:a, 2=>:b, 3=>:c}
irb(main):009:0> h.clear
=> {}
irb(main):010:0>

Po co komu podłoga?

Kilka rozdziałów temu mówiliśmy sobie o dziwnym zachowaniu Ruby’iego przy dzieleniu liczb całkowitych:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0> 9/5
=> 2

Poniżej krótki przykład pokazujący, że czasem jest to bardzo użyteczny feature:

jan@Stacja-Linux ~/Pulpit $ irb
irb(main):001:0>  a = [1,2,3,4,5,6,7,8,9]
=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
irb(main):002:0>  a[a.length/2]
=> 5

 

Dla ciekawych świata:
Kompletny przegląd metod (i garść informacji) dla tablic, słowników i symboli.