FIGYELEM! Ez a dokumentum kizárólag az ELTE IK hallgatók számára oktatási célra készült! Félkész munka, dolgozunk rajta! Terjeszteni, felhasználni máshol a szerzők engedélye nélkül tilos!

Konténerek, blokkok és iterátorok


Egy egy számot tartalmazó zenegép valószínűleg nem lesz túl népszerű. (Kivéve esetleg egy-két nagyon ijesztő bárt...) Szóval lassan elkezdhetünk azon gondolkodni, hogyan vezessünk egy katalógust az rendelkezésre álló számokról, valamint egy listát a lejátszásra váró számokról. Mindkettő egy tároló, azaz olyan objektumok, amelyek más objektumra mutató referenciákat tartalmaznak.

Mind a katalógus, mind a számlista hasonló műveletekkel rendelkezik: dal hozzáadása, dal törlése, dalok listájának visszaadása, és hasonlók. A számlista valószínűleg további feladatokat is el kell lásson, mint például hirdetés beszúrása, vagy a lejátszási idő nyomon követése, de ezekkel majd később foglalkozunk. Ezek alapján jó ötletnek tűnik hát egyfajta általános SongList osztály megalapozása, amelyet majd specializálhatunk katalógussá, valamint számlistává.

Konténerek

Mielőtt hozzálátnánk az implementációhoz, ki kell gondolnunk, hogyan szeretnénk tárolni a dalokat az egyes SongList objektumokban. Három kézenfekvő megoldásunk van. Használhatjuk a Ruby tömb (Array) típusát, a Ruby hash tábla (Hash) típusát, vagy létrehozhatunk saját konténert. Mivel lusták vagyunk, egyelőre nézzük meg a tömböket és hash táblákat, és válasszuk ki az egyiket a saját osztályunk számára.

Tömbök

Az Array osztály objektumok referenciájának csoportját tárolja. Minden objektumra mutató referencia egy helyet foglal el a tömbben, amelyet egy nemnegatív egész jelöl.

Létrehozhatunk tömböt literál használatával, vagy explicit megadhatjuk az új tömb objektumot. A literál tömb egyszerűen objektumok listája, ahol az objektumokat vesszővel választjuk el.

a = [ 3.14159, "pie", 99 ]
a.class
»    Array
a.length
»    3
a[0]
»    3.14159
a[1]
»    "pie"
a[2]
»    99
a[3]
»    nil
b = Array.new
b.class
»    Array
b.length
»    0
b[0] ="second"
b[1] = "array"
b
»    ["second", "array"]

A tömböket a [] operátor használatával indexeljük. Mint a legtöbb Ruby operátor, ez valójában egy Array osztálybeli metódus, amit az alosztályokban akár felül is definiálhatunk. Mint a példa is mutatja, a tömböket 0-tól indexeljük. Indexelj egy tömböt egész számmal, és az visszaadja az adott pozíción található objektumot, vagy nil-t, ha nincs ott semmi. Indexelj egy tömböt negatív egésszel, és az a végéről visszafelé kezd el számolni.

a = [ 1, 3, 5, 7, 9 ]
a[-1]
»  9
a[-2]
»  7
a[-99]
»  nil

Tömböt indexelhetünk továbbá egy számpárral a következő módon: [index, darab]. Ez egy, a darab darabszámú, indextől kezdődő referenciákat tartalmazó új tömböt ad vissza.

a = [ 1, 3, 5, 7, 9 ]
a[1, 3]
»  [3, 5, 7]
a[3, 1]
»  [7]
a[-3, 2]
»  [5, 7]

Végül indexelhetünk egy intervallum megadásával is, ahol a kezdő- és végpontokat 2, vagy 3 ponttal választunk el. A 2 pontos változat tartalmazza végpontot, míg a 3 pontos nem.

a = [ 1, 3, 5, 7, 9 ]
a[1..3]
»  [3, 5, 7]
a[1...3]
»  [3, 5]
a[3..3]
»  [7]
a[-3..-1]
»  [5, 7, 9]

A [] operátorhoz kapcsolódik a []= operátor, amely lehetőséget ad a tömb elemeinek értékadására. Egy egyszerű egészet használva indexként az adott elem lecserélődik, bármi is álljon az értékadás jobb oldalán. Minden létrejövő üres hely nil-lel töltődik ki.

a = [ 1, 3, 5, 7, 9 ]
»  [1, 3, 5, 7, 9]
a[1] = 'bat'
»  [1, "bat", 5, 7, 9]
a[-3] = 'cat'
»  [1, "bat", "cat", 7, 9]
a[3] = [ 9, 8 ]
»  [1, "bat", "cat", [9, 8], 9]
a[6] = 99
»  [1, "bat", "cat", [9, 8], 9, nil, 99]

Ha az []= operátornak átadott index két egész, vagy egy intervallum, akkor a meghatározott indexeken szereplő objektumok lecserélődnek, bármi is szerepeljen az értékadás jobb oldalán. Ha a hossz 0, akkor a jobb oldalon álló érték beszúródik a kezdőpozíció elé; ekkor semmilyen elem nem kerül eltávolításra. Ha a jobb oldal egy tömb, akkor elemei akkor annak elemei felhasználódnak a cserénél. A tömb mérete automatikusan beállítódik,

a = [ 1, 3, 5, 7, 9 ]
»  [1, 3, 5, 7, 9]
a[2, 2] = 'cat'
»  [1, 3, "cat", 9]
a[2, 0] = 'dog'
»  [1, 3, "dog", "cat", 9]
a[1, 1] = [ 9, 8, 7 ]
»  [1, 9, 8, 7, "dog", "cat", 9]
a[0..3] = []
»  ["dog", "cat", 9]
a[5] = 99
»  ["dog", "cat", 9, nil, nil, 99]

A tömböknek számos egyéb beépített metódusa van. Ezek használatával szimulálhatunk vermet, halmazt, sort, valamint fifo-t. A tömbök metódusainak részletes leírását lásd a megfelelő oldalon.

Hash táblák

A hasítókat olykor asszociatív tömbnek, vagy szótárnak is nevezik. Abban hasonlítanak a tömbökhöz, hogy objektumok referenciáinak gyűjteményével indexelhetők.

Míg azonban a tömböket egészekkel indexeljük, a hasítókat bármilyen típussal indexelhetjük: stringekkel, reguláris kifejezésekkel, és így tovább. Egy érték hasítóban való tárolásához két értéket kell megadnunk: egy kulcsot, és magát a tárolandó értéket. Egymást követve visszakaphatjuk az értéket a megfelelő kulcs használatával. Egy hasítóban bármilyen típusú objektum tárolható. A következő példákban hasító felsorolásokat alkalmazunk, azaz kulcs => érték párokat vesszővel elválasztva.

h = { 'dog' => 'canine', 'cat' => 'feline', 'donkey' => 'asinine' }
h.length
»  3
h['dog']
» 'canine'
h['cow']    = 'bovine'
h[12]      = 'dodecine'
h['cat']    = 99
h
»  {"cow"=>"bovine", "cat"=>99,
    12=>"dodecine", "donkey"=>"asinine", "dog"=>"canine"}

A hash tábláknak van egy nagy előnye a tömbökkel szemben: bármilyen objektum használható indexelésre. Habár van egy nagy hátrányuk is: az elemeik nem rendezettek, így hash táblát nehezen használhatnánk verem vagy sor szimulálására.

Észre fogjuk venni, hogy a hash táblák a legáltalánosabban használt adatszerkezetek a Ruby-ban. A Hash osztály által nyújtott metódusokat lásd a megfelelő oldalon.

SongList konténer implementálása

Eme a kis kitérő után készen is állunk a SongList osztály implementálására. Nézzük csak, milyen alapmetódusokra van szükségünk?

append(aSong) » lista
   Az adott dal listához való hozzáfűzése.

deleteFirst() » aSong
   A listában szereplő első dal eltávolítása, visszaadva az adott dalt.

deleteLast() » aSong
   A listában szereplő utolsó dal eltávolítása, visszaadva az adott dalt.

[egyIndex] » aSong

   Az egyIndex által azonosított dal visszaadása, ahol az index lehet egész, vagy a szám címe.

Ezek a metódusok ötletet adhatnak az implementációhoz. Az a képesség, ogy egy dalt hozzáfűzünk a lista végéhez, valamint az, hogy a végéről, és az elejéről is tudunk törölni, a kétirányú listák felé terelik gondolatainkat. Ezt pedig könnyen implementálhatjuk az Array osztály segítségével. Hasonlóképp, az egésszel indexelést is támogatják a tömbök.

Habár képesnek kell lennünk a dal címe alapján keresni, ami hasítók használatát sugallja a címet használva kulcsként, és a dalt értékként. Használhatnánk hasítót? Valószínűleg igen, de vannak problémák. Először is: a hasítók nem rendezettek, szóval szükségünk lenne egy másodlagos tömbre, amellyel nyomon követhetjük a listában a sorrendiséget. Egy nagyobb probléma: a hasítóknál nem tartozhat több kulcs ugyanahhoz az értékhez. Ez nálunk nagy gondot okozna, ugyanis egy szám nem szerepelhetne többször a lejátszási listában. Maradjunk tehát a tömböknél, cím szerint pedig akkor keresünk, ha szükség van rá. Ha ez teljesítménycsökkenéshez vezet, még mindig ráérünk később egy hasító-alapú keresést megvalósítani.

Osztályunkat az alap initialize metódus megvalósításával kezdjük, amely létrehozza az Array típusú objektumot a dalok tárolásához, és elrakja annak referenciáját a @songs példányváltozóba.

class SongList
  def initialize
    @songs = Array.new
  end
end

A SongList#append metódus hozzáfűzi a megadott dalt a @songs tömb végéhez, és visszaadja önmagát (self), azaz az aktuális SongList objektumra mutató referenciát. Ez egy hasznos konvenció, ugyanis lehetővé teszi számunkra, hogy hozzáfűzési láncokat (lista.append('...').append('...')) hozzunk létre. Erre később hozunk példát.

  class SongList
    def append(aSong)
      @songs.push(aSong)
      self
    end
  end

Ezután hozzátesszük a deleteFirst és deleteLast metódusokat, egyszerűen az Array#shift és az Array#pop metódusok használatával.

  class SongList
    def deleteFirst
      @songs.shift
    end
    def deleteLast
      @songs.pop
    end
  end

Itt az ideje egy gyors próbának. Először is, hozzáadunk 4 dalt a listához. Csak a bemutató kedvéért, felhasználjuk azt, hogy az append metódus a SongList objektumot adja vissza.

  list = SongList.new
  list.
    append(Song.new('Cím1', 'Előadó1', 1)).
    append(Song.new('Cím2', 'Előadó2', 2)).
    append(Song.new('Cím3', 'Előadó3', 3)).
    append(Song.new('Cím4', 'Előadó4', 4))

Ezután ellenőrizzük, hogy a lista végéről és elejéről való törlés helyesen működik, és nil-t kapunk vissza, ha a lista üres.

  list.deleteFirst
»  Song: Cím1--Előadó1 (1)
  list.deleteFirst
»  Song: Cím2--Előadó2 (2)
  list.deleteLast
»  Song: Cím4--Előadó4 (4)
  list.deleteLast
»  Song: Cím3--Előadó3 (3)
  list.deleteLast
»  nil

Eddig jó. A következő metódusunk a [], amely lehetővé teszi az elemek indexek általi elérését. Ha az index egy szám (amit az Object#kind_of? metódussal ellenőrzünk), egyszerűen visszaadjuk az adott indexen található elemet.

  class SongList
    def [](key)
      if key.kind_of?(Integer)
        @songs[key]
      else
        # ...
      end
    end
  end

Ismét tesztelhetünk!

  list[0]
»  Song: Cím1--Előadó1 (1)
  list[2]
»  Song: Cím3--Előadó3 (3)
  list[9]
»  nil

Most biztosítanunk kell a dalok cím alapján való kikeresését. Ez magában foglalja majd a dalok címének egyenkénti megvizsgálását. Ehhez vessünk egy pillantást a Ruby egyik legfontosabb jellemzőire: az iterátorokra.

Blokkok és iterátorok

Következő problémánk tehát a SongList osztály [] metódusának implementálása, amely egy stringet kap, és kikeresi az adott című dalt. Előregondolva: van egy dalokat tartalmazó tömbünk, szóval csak végig kell szaladnunk egyesével az elemeken, és keressük az egyezést.

class SongList
  def [](key)
    if key.kind_of?(Integer)
      return @songs[key]
    else
      for i in 0...@songs.length
        return @songs[i] if key == @songs[i].name
      end
    end
    return nil
  end
end

Ez működik, és ismerős is lehet a megoldás: egy for ciklus végigiterál a tömbön. Mi lehet természetesebb?

Hát kiderül, hogy igenis van természetesebb. Bizonyos szempontból a for ciklusunk kicsit túl indiszkréten bánik a tömbünkkel. Lekérdezi a hosszát, majd egyesével kiszedi az értékeket a találatig. Miért nem kérjük meg a tömböt, hogy futtasson le egy tesztet ő maga minden elemén? Pont erre szolgál az Array osztály find metódusa.

class SongList
  def [](key)
    if key.kind_of?(Integer)
      result = @songs[key]
    else
      result = @songs.find { |aSong| key == aSong.name }
    end
    return result
  end
end

Használhatnánk az elágazást utasításként, ezáltal rövidíthetjük a kódot.

class SongList
  def [](key)
    return @songs[key] if key.kind_of?(Integer)
    return @songs.find { |aSong| aSong.name == key }
  end
end

A find metódus egy iterátor, azaz egy olyan metódus, amely ismétlődő blokkot tartalmaz. Az iterátorok és programblokkok a Ruby érdekes szolgáltatásai közé tartoznak, vegyük tehát őket kicsit szemügyre, és közben kiderítjük, mit is csinál pontosan a most implementált operátorunk.

Iterátorok implementálása

A Ruby iterátor szimplán egy metódus, amely képes programblokk hívására. Első ránézésre a Ruby blokkjai olyanok, mint a C, Java, vagy Perl blokkok. Sajnos a szemünk most becsap. A Ruby blokkja valóban a csoportosítás egy módja, csak nem a megszokott módja.

Először is: egy blokk csak egy metódushívással szomszédosan fordulhat elő, azaz a blokkot a metódushívás utolsó paraméterének sorába írjuk. Másodszor: a blokk kódja nem kerül végrehajtásra első találkozáskor. Ehelyett a Ruby megjegyzi, hogy a blokk milyen körülmények között jelent meg (lokális változók, az aktuális objektum, és így tovább), és belép a metódusba. És itt kezdődik a varázslat!

A metóduson belül a blokkot ugyanúgy meghívhatjuk, mintha ő maga is egy metódus lenne. Ezt a yield utasítással tehetjük meg. Valahányszor kiadjuk a yield parancsot, a blokkban található kód futtatásra kerül. Amikor a blokk véget ér, az irányítás visszatér közvetlen a yield utasítás utáni részhez. (****) Kezdjük egy triviális példával.

def threeTimes
  yield
  yield
  yield
end
threeTimes { puts "Hello" }

Hatása:

Hello
Hello
Hello

A blokk (kapcsos-zárójelek közé írt kód) a threTimes metódus meghívásával kötődik össze. Ebben a metódusban, a yield egymás után háromszor kerül meghívásra, minden alkalommal meghívva a blokkban található kódot, és kedves üdvözlőszöveg jelenik meg a képernyőn. Ami a blokkokat érdekessé teszi az az, hogy paramétereket adhatsz át nekik, és értékeket kaphatsz vissza tőlük. Például, írhatnánk egy egyszerű függvényt, amely egy adott értékig megadja a Fibonacci - sorozat tagjait. (Az általános Fibonacci - sorozat egészeket tartalmaz, két darab 1-essel kezdődik, és minden következő tag az előző kettő összege. Néha rendezési algoritmusoknál alkalmazzák, valamint természeti jelenségek analizálásában is hasznosnak bizonyult.)

def fibUpTo(max)
  i1, i2 = 1, 1
# szimultán értékadás
  while i1 <= max
    yield i1
    i1, i2 = i2, i1+i2
  end
end
fibUpTo(1000) { |f| print f, " " }

Eredménye:

1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

Ebben a példában a yield utasításnak van egy paramétere. Ez az érték adódik át a yield-hez kapcsolódó blokknak. A blokk definíciójában az argumentumlista függőleges oszlopok között jelenik meg. Ebben a példányban az f változó kapja a yield-nek átadott értéket, tehát a blokk kiírja a sorozat növekvő tagjait. Habár általában egy értéked adunk át egy blokknak, ez nem alapkövetelmény. Egy blokk akárhány argumentumot kaphat. Mi történik, ha egy blokk különböző számú paraméterrel rendelkezik, mint ahányat a yield kap? Megdöbbentő véletlen útján a párhuzamos értékadásokra vonatkozó szabály lép életbe (egy kis csavarral: a yield-nek átadott paraméterek egy tömbbé konvertálódnak, ha a blokknak csak egy paramétere van).

Egy blokk paraméterei lehetnék létező lokális változók; ez esetben a változó új értéke tárolásra kerül a blokk befejezése után. Ez váratlan viselkedést eredményezhet, ám egy kis teljesítménynövekedést is elérhetünk már meglévő változók használatával.

Egy blokk értéket is adhat vissza a metódusnak. A blokk utolsó kiértékelt kifejezésének értéke visszaadódik a metódusnak, mint a yield értéke. Így működik az Array#find metódus. (A find metódust tulajdonképp az Enumerable (felsorolható) modul definiálja, amely az Array osztályba belekeverődött (******). Az implementációja a következő példához hasonlítana.

class Array
  def find
    for i in 0...size
      value = self[i]
      return value if yield(value)
    end
    return nil
  end
end
[1, 3, 5, 7, 9].find {|v| v*v > 30 }
»  7

Ez növekvő elemeket ad át a blokk tömbjének. Ha a blokk true értéked ad vissza, a metódus visszaadja a kapcsolódó elemet. Ez a példa is mutatja az iterátorok ezen megközelítésének előnyeit. Az Array osztály azt teszi, amihez legjobban ért: hozzáfér a tömbök elemeihez, és hagyja, hogy az alkalmazás a konkrét problémával foglalkozzon (ebben az esetben egy olyan bejegyzést kell találni, amely megfelel bizonyos matematikai kritériumoknak).

Bizonyos iterátorok megszokottak sok Ruby osztályban. A find-ot már megvizsgáltuk. Két másik ilyen az each, és a collect. Az each valószínűleg a legegyszerűbb iterátor: mindössze az adatainak növekvő elemeit termeli.

[ 1, 3, 5 ].each { |i| puts i }

Eredménye:

1
3
5

Az each iterátor speciális szerepet kap a Ruby nyelvben: őt használjuk a nyelv alap for ciklusának implementálására is, és még sok egyéb szolgáltatást is nyújthat osztályunknak.

A másik mindennapos iterátor a collect, amely az adatainak elemeit egyesesével átadja a blokknak. A blokk által visszaadott adatokat egy új tömbben kapjuk vissza. Például:

["H", "A", "L"].collect { |x| x.succ }
»  ["I", "B", "M"]

Ruby vs. Java és C++

Szánjunk egy kis időt arra, hogy összehasonlítjuk a Ruby, a Java és C++ iterátor-szemléletét. A Ruby-féle megközelítésben az iterátor egy metódus, mint bármely más, amely yield-et hív minden alkalommal, amikor új értéket generál. Ami az iterátort használja pedig nem más, mint egy - ezen metódussal összekapcsolt - blokknyi kód. Nincs szükség segédosztályok generálására a léptetés állapotainak nyilvántartására - mint ahogy ez a Java és a C++ teszi. Ebben, mint ahogy sok másban is, a Ruby egy átlátszó nyelv. Amikor Ruby programot írunk, elég a feladatra koncentrálnunk, és nem kell a nyelv támogatására felesleges energiát pazarolni.

Az iterátorok használata nem korlátozódik tömbökben és hasítókban található adatok elérésére. Ahogy a Fibonacci-számokat előállító példában láttuk, sz iterátor képes származtatott érték előállítására. Ezt a képességet használják a Ruby input/output osztályai, amelyek egy sorozatosan sorokat, vagy I/O folyamokat előállító iterátor felüleletet valósítanak meg.

f = File.open("testfile")
f.each do |line|
  print line
end
f.close

Eredménye:

Ez az első sor...
Ez a második...
Ez a harmadik...
És így tovább...

Nézzünk meg egy másik iterátor megvalósítást. A Smalltalk nyelv szintén támogatja az adatok gyűjteményei fölötti iterátor létrehozását. Ha arra kérünk egy Smalltalk programozót, hogy összegezzék egy tömb elemeinek értékét, akkor valószínűleg az inject függvényt használnák.

  sumOfValues              "Smalltalk metódus"
    ^self values
      inject: 0
      into: [ :sum :element | sum + element value]

Így működik az inject. Amikor a kapcsolódó blokk meghívásra kerül, a sum megkapja az inject paraméterét (lejen esetben 0-át), és az element a tömb első elemére állítódik. A második és a soron következő hivások alkalmával a sum a blokk által az előző híváskor visszaadott értéket kapja. Így a sum használható futó összeg tárolására. Az inject végső értéke a blokk utolsó hívásakor visszaadott érték lesz.

A Ruby-ban nincs inject metódus, de könnyű írni egyet. Ez esetben hozzáadjuk azt az Array osztályhoz, majd megnézzük, hogyan tehetnénk általánosabban használhatóvá.

class Array
  def inject(n)
    each { |value| n = yield(n, value) }
    n
  end
  def sum
    inject(0) { |n, value| n + value }
  end
  def product
    inject(1) { |n, value| n * value }
  end
end
[ 1, 2, 3, 4, 5 ].sum
»  15
[ 1, 2, 3, 4, 5 ].product
»  120

Habár a blokkok gyakran egy iterátor tárgyai, más módon is felhasználhatjuk őket. Nézzünk erre néhány példát.

< Előző oldalKövetkező oldal >