V tomto článku se vrátíme k reprezentaci dat v Ruby a podíváme se na dvě nejčastěji používané složitější datové struktury. Na přetřes přijde pole a hash. Struktury s těmito něbo podobnými názvy jsou implementovány ve většině moderních programovacích jazyků, ale v jejich přesné definici bývají rozdíly.

Stručně řečeno, pole v Ruby je reprezentováno třídou Array a je to uspořádaný seznam odkazů na objekty. Každá pozice v seznamu je označena pořadovým číslem. Číslování začíná od nuly. Velikost pole se automaticky mění operacemi, které do něj přidávají nebo odebírají prvky. To je složitý popis vcelku jednoduché věci, kterou si raději rovnou předveďme na příkladu.

irb(main):001:0> a = [‚a‘, ‚b‘, ‚c‘]
=> [„a“, „b“, „c“]
irb(main):002:0> a.size
=> 3
irb(main):003:0> a[1]
=> „b“
irb(main):004:0> a[-1]
=> „c“
irb(main):005:0> a[0..1]
=> [„a“, „b“]
irb(main):006:0> a << ‚d‘
=> [„a“, „b“, „c“, „d“]
irb(main):007:0> b = []
=> []
irb(main):008:0> c = Array.new
=> []
irb(main):009:0> a.class
=> Array
irb(main):010:0> b.class
=> Array
irb(main):011:0> c.class
=> Array
irb(main):012:0> [‚a‘, 1, ‚b‘, 2.0]
=> [„a“, 1, „b“, 2.0]
irb(main):013:0> [[1, 2], [3, 4]]
=> [[1, 2], [3, 4]]

V kódu můžeme pole vytvořit buď pomocí literálu [] nebo voláním metody new třídy Array. Na prvním řádku příkladu je forma s literálem, ve kterém uvnitř vytvoříme tři řetězcové objekty a posléze pole, které obsahuje odkazy na ně. Odkaz na pole samotné je v proměnné a. Následně voláme metodu size, která určí počet prvků. Podobně jako v řetězci lze použitím indexovacího operátoru přistupovat k jednotlivýcm znakům, můžeme v poli přistupovat k jeho prvkům. To platí včetně indexování zápornými čísly od konce pole nebo indexování pomocí rozsahů, které vrací příslušnou část pole. Pomocí operátoru << můžeme přidat další prvek na konec pole. Následují dva alternativní způsoby vytvoření prázdného pole pomocí literálu a pomocí volání metody new.

Na konci příkladu je ukázáno, že v rámci pole lze „míchat jablka s hruškami“. Pole v Ruby může obsahovat odkazy na objekty různých i navzájem odlišných tříd. Je také možné vytvářet libovolně vnořená pole, což je vlastně jen speciálním případem předchozí věty, kdy prvek pole je odkaz na objekt třídy Array.

Třída Array má spoustu metod. Pro začátek se podívejme na ty, které fungují jako operátory.

irb(main):001:0> a = [1, 3, 5]
=> [1, 3, 5]
irb(main):002:0> b = [2, 4, 6]
=> [2, 4, 6]
irb(main):003:0> c = [2, 3, 4]
=> [2, 3, 4]
irb(main):004:0> a + b
=> [1, 3, 5, 2, 4, 6]
irb(main):005:0> c – b
=> [3]
irb(main):006:0> a * 3
=> [1, 3, 5, 1, 3, 5, 1, 3, 5]
irb(main):007:0> a & c
=> [3]
irb(main):008:0> a | c
=> [1, 3, 5, 2, 4]
irb(main):009:0> a == b
=> false
irb(main):010:0> a == [1, 3, 5]
=> true

Význam je následovný jednotlivých operátorů pro pole je následovný:

  • + spojí dvě pole tak, že výsledné pole obsahuje prvky obou spojených polí včetně případných duplicit (jednoduše řečeno připojí druhé pole na konec prvního),
  • – vytvoří pole, které obsahuje prvky, které jsou v prvním zdrojovém poli a zároveň nejsou v druhém zdrojovém poli,
  • * vytvoří pole opakovaným přidáváním zdrojového pole nakonec sebe sama, přičemž celkový počet opakování je dán druhým operandem (první operand je pole, druhý celé číslo),
  • & vytvoří pole, které obsahuje prvky, které jsou obsaženy v prvním zdrojovém poli a zároveň i ve druhém zdrojovém poli (průnik),
  • | spojí dvě pole tak, že výsledné pole obsahuje prvky obou spojených polí s vyloučením duplicit (sjednocení),
  • == je relační operátor posuzující rovnost dvou polí, přičemž ta se rovnají právě když se rovnají postupně všechny jejich navzájem si odpovídající prvky.

Důležité je samozřejmě také mít přehled o způsobech, jak pracovat s jednotlivými prvky pole. Tuto problematiku jsem již nakousli výše, ale máme ještě další možnosti. Začněme opět příkladem.

irb(main):001:0> a = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> a.shift
=> 1
irb(main):003:0> a
=> [2, 3, 4]
irb(main):004:0> a.unshift(1)
=> [1, 2, 3, 4]
irb(main):005:0> a.pop
=> 4
irb(main):006:0> a
=> [1, 2, 3]
irb(main):007:0> a.push(4)
=> [1, 2, 3, 4]
irb(main):008:0> a.first
=> 1
irb(main):009:0> a.last
=> 4
irb(main):010:0> a
=> [1, 2, 3, 4]
irb(main):011:0> a[1] = ‚b‘
=> „b“
irb(main):012:0> a
=> [1, „b“, 3, 4]
irb(main):013:0> a.join(‚, ‚)
=> „1, b, 3, 4“

K dispozici máme několik metod, které nám umožňují pracovat se začátkem a koncem pole:

  • shift – odebere první prvek pole (prvek je návratovou hodnotou metody)
  • unshift – vloží prvek na první místo pole, stávající prvky se posunou o jednu pozici „nahoru“
  • pop – odebere poslední prvek pole (prvek je návratovou hodnotou metody)
  • push – vloží prvek na konec pole (stejně jako operátor <<)
  • first – vrátí první prvek pole a pole ponechá beze změny (stejné jako [0])
  • last – vrátí poslední prvek pole a pole ponechá beze změny (stejné jako [-1])

Díky těmto metodám můžeme třídu Array snadno použít pro reprezentaci datových struktur typu fronta (FIFO), zásobník (LIFO) nebo různých kombinací. V příkladu bylo dále ukázáno přiřazení do prvku pole a metoda join, která převádí pole na řetězec vytvořený textovými reprezentacemi prvků oddělenými zadaným oddělovačem.

Za zmínku stojí na první pohled triviální fakt, který ale může být zdrojem nepříjmených chyb. Pole uchovává jen odkazy (či reference) na objekty. Pokud se objekt nějakým způsobem změní (například přistoupením přes odkaz uložený v jiné proměnné), změní se tím zároveň prvek pole.

irb(main):001:0> x = ‚abc‘
=> „abc“
irb(main):002:0> a = Array.new(5, x)
=> [„abc“, „abc“, „abc“, „abc“, „abc“]
irb(main):003:0> x.object_id
=> 21390450
irb(main):004:0> a[0].object_id
=> 21390450
irb(main):005:0> x.upcase!
=> „ABC“
irb(main):006:0> a
=> [„ABC“, „ABC“, „ABC“, „ABC“, „ABC“]

V příkladu je ukázán extrémní případ, kdy jsou při volání metody new použity parametry znamenající, že nově vytvořené pole má být inicializováno pěti prvky s hodnotou x. Ve skutečnosti je však pokaždé uložen odkaz na jediný objekt. Když tento objekt pak změníme metodou upcase! změní se „všechny“ prvky pole. Mimochodem vykřičníkem jsou v Ruby označeny metody, které mění příjemce. Metoda stejného jména bez vykřičníku má ovykle stejný účel, ale vrací odkaz nově vytvořený objekt a příjemce ponechává beze změny.

Pojďme se teď podívat na třídu Hash. Opět se jedná o složitější datovou strukturu, která uchovává odkazy na jiné objekty. Na rozdíl od pole není hash uspořádaným seznamem (prvky nemají žádné definované pořadí), ale na druhou stranu lze prvky indexovat prakticky libovolným objektem (oproti celému číslu u pole). Na index (či klíč) třídy Hash je kladen jediný požadavek: aby tento objekt měl definovánu metodu hash vracející konstantní číselnou „hash“ hodnotu. Na hash se dá pohlížet jako na neuspořádaný seznam dvojic klíč/hodnota, ve kterém lze snadno vyhledávat podle klíče. Někdy se tato datová struktura také označuje jako slovník nebo rejstřík.

irb(main):001:0> h = { ‚a‘ => 1, ‚b‘ => 2 }
=> {„a“=>1, „b“=>2}
irb(main):002:0> h[‚a‘]
=> 1
irb(main):003:0> h[‚c‘]
=> nil
irb(main):004:0> h.size
=> 2
irb(main):005:0> h.keys
=> [„a“, „b“]
irb(main):006:0> h.values
=> [1, 2]
irb(main):007:0> h.has_key?(‚a‘)
=> true
irb(main):008:0> h.has_key?(‚A‘)
=> false
irb(main):009:0> h.has_value?(1)
=> true

Hash lze vytvořit pomocí literálu, ve kterém jsou postupně definovány dvojice klíč/hodnota. Uzavřen je ve složených závorkách. Alternativně lze opět volat metodu new třídy Hash. Té lze zadat jako parametr hodnotu, která se má vracet jako implicitní hodnota pro nedefinované klíče. Pokud implicitní hodnotu nezvolíme, je to nil, jak je v příkladu vidět. Dále je ukázáno několik metod, které zjišťují velikost hashe (na počet párů klíč/hodnota) a vracejí pole klíčů a pole metod a nebo zjišťují, zda je v hashi obsažen určitý klíč nebo určitá hodnota.

Výše definovaný jednoduchý hash využíval jako klíče řetězce, což je častá situace i ve skutečných programech. Pokud je pro nás řetězec užitečný jen jako jméno či nálepka, je vhodné využít příbuzné třídy Symbol. Symbol získáme zápisem řetězce, kterému předřadíme dvojtečku nebo voláním metody to_sym třídy String.

irb(main):001:0> h = Hash.new
=> {}
irb(main):002:0> h[:jedna] = 1
=> 1
irb(main):003:0> ‚deset‘.to_sym
=> :deset

Hlavní rozdíl v případě použití symbolu oproti řetězci je úspora paměti. Pokud v kódu vytvoříme mnoho objektů třídy String, tak i v případě, že jsou tvořeny pomocí stejného literálu, bude každý řetězec vytvořen v paměti znovu. Oproti tomu interpret Ruby udržuje jen jednu tabulku symbolů a další použití symbolu definovaného stejným literálem se odkazuje na stále stejný symbol v tabulce. Používání symbolů jako klíčů hashe je proto v Ruby prakticky idiomem.

Na rozdíl od pole nemá hash žádné metody, které by se tvářily jako operátory, kromě relačního operátoru ==. Platí, že dva hashe se rovnají v případě, že mají stejný počet párů klíč/hodnota a že se rovnají všechny tyto páry.

Příště se podíváme, jak jednoduchý a univerzální mechanismus nám Ruby nabízí pro práci s prvky složitějších datových struktur, když přes ně potřebujeme procházet nebo filtrovat či transformovat jejich prvky.

Žádný příspěvek v diskuzi

Odpovědět