Programmering i Ruby

Den Pragmatiske Programmerers Veiledning

Forrige < Innhold ^
Neste >

Beholdere, blokker og iteratorer



En jukeboks med en sang vil antagelig ikke slå an (med unntak i noen veldig skumle buler). Derfor vil vi snart begynne å tenke på å lage en katalog med tilgjengelige sanger og en liste med sangene som venter på sin tur til å bli avspilt. Både listen og katalogen er beholdere: objekter som holder referanser til en eller flere objekter.

Både katalogene og avspillingslisten behøver endel metoder som er felles for dem begge: legg til en sang, fjern en sang, returner en liste av sanger og så videre. Avspillingslisten vil muligens kunne utføre andre oppgaver, som å legge inn reklamepauser med jevne mellomrom eller holde styr på total spilletid, men vi bekymrer oss ikke om dette i første omgang. I mellomtiden virker det hensiktsmessig å lage en generell SongList klasse, som kan spesialiseres for kataloger og avspillingslister.

Beholdere

Før vi begynner implementeringen må vi finne ut hvordan vi vil lagre listen av sanger inne i SongList objektet. Vi har tre åpenbare valg. Vi kan bruke Ruby sin tabelltype (Array), bruke Ruby sin hashtabelltype (Hash) eller lage vår egen listestruktur. Late som vi er, holder vi oss foreløpig til Array og Hash, og velger en av disse som utgangspunkt.

Tabeller

Klassen Array holder styr på en samling av objektreferanser. Hver referanse tar opp en posisjon i tabellen, identifisert med en heltallsindeks som ikke kan være negativ.

Du kan lage litterale tabeller direkte i koden eller eksplisitt lage et Array-objekt. En tabell litteral er intet mer enn en list av objekter mellom to hakeparenteser.

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

For å slå opp i en tabell kan en bruke []-operatoren. Som med de fleste operatorene i Ruby, er dette egentlig en metode (i Array klassen) og kan dermed redefineres i subklasser. Vi ser i eksempelet at indekseringen starter på null. Dersom du slår opp i en tabell med bare et heltall, returneres objektet som er på den posisjonen eller nil hvis det ikke var noe der. Negative heltall gjør at oppslaget teller bakover i fra slutten. Dette vises i figur 4.1 som følger.

Figur 4.1: Indeksering av tabeller

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

Du kan også slå opp i tabeller med to tall, [start, antall]. Dette returnerer en ny tabell som består av referansene til de antall objektene som var i den oppprinnelige tabellen fra posisjon start og utover.

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

Sist, men ikke minst, kan du slå opp i tabeller bed å bruke rekkevidder (Range), hvor start- og sluttposisjonen separeres av to eller tre punktum. Varianten med to punktum inkluderer sluttposisjonen, mens varianten med tre ikke gjør det.

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]

Operatoren [] har en tilsvarende []= operator som kan brukes for å sette elementer i tabellen. Brukes den med et enkelt heltall, blir elementet på den posisjonen byttet ut med hva enn som er på høyresiden av tilordningen. Eventuelle tomrom fylles med nil.

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]

Hvis []= mottar to tall (en startindeks og en lengde) eller en rekkevidde, da blir de angitte elementene i den opprinnelige tabellen byttet ut med det som er på høyresiden av tilordningen. Dersom lengden er null vil uttrykket på høyre side smettes inn før startposisjonen og ingen elementer blir fjernet. Hvis uttrykket på høyresiden også er en tabell, blir elementene den inneholder brukt i overskrivningen. Størrelsen på tabellen justeres automatisk dersom indeksen velger et annet antall elementer enn det som er tilgjengelig på høyresiden.

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]

Tabeller har mange andre nyttige metoder. Ved hjelp av disse kan du bruke tabeller som stakker, sett, , kølister, tohodete køer, og først-inn-først-ut lister. En komplett liste over metodene i Array finnes på side 278(??).

Hashtabeller

Hashtabeller (går også under navnene assosiative tabeller[associative arrays] eller datakataloger[dictionaries]) ligner på datatabeller i det at de er indekserte samlinger av objektreferanser.

Mens du slår opp i en tabell med et heltallsindeks, kan du bruke alle slags objekter som indeks på en hash: strenger, regulære uttrykk og så videre. Når du lagrer en verdi i en hashtabell angir du to objekter---en nøkkel og en verdi. Etterpå kan du hente tilbake verdien ved å slå opp i hashtabellen med samme nøkkelen. Verdiene i en Hash kan være en hvilken som helst type objekt. Det neste eksempelet bruker Hash-litteraler: en liste av nøkkel => verdi par mellom klammeparenteser.

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

Sammenlignet med tabeller, har hashtabeller den store fordel at alle typer objekter kan brukes som indeks. Men det medfølger en stor bakdel, da elementene ikke er ordnet, som gjør at man ikke enkelt kan bruke hashtabeller til å implementere stakker eller køer.

Du vil etterhvert merke at hashtabeller er en av de vanligste datastrukturene i Ruby. En komplett liste av alle metodene implementert i Hash-klassen begynner på side 317(??).

Implementering av en SongList beholder

Etter den lille avsporingen innom tabeller og hashtabeller, er vi nå rede til å begynne implementeringen av jukeboksens SongList. La oss finne ut hvilke grunnleggende metoder vi trenger i vår SongList. Underveis vil flere metoder komme til, men foreløpig vil dette holde.

append( aSong ) » list
Legg sangen til listen.
deleteFirst() » aSong
Fjern den første sangen fra listen og returner sangen.
deleteLast() » aSong
Fjern den siste sangen fra listen og returner sangen.
[ anIndex ] » aSong
Returner den sangen som er identifisert av anIndex, som kan være en heltallsindeks eller en sangtittel.

Denne listen gir oss noen tips angående implementeringen. Mulighetene for å legge til sanger på slutten og fjerne dem fra begynnelsen og slutten, antyder en dobbel-hodet kø, som vi vet vi kan implementere med en Array. Videre er også muligheten for å hente ut en sang utifra en heltallsindeks også støttet av tabeller.

På den andre siden har vi også behov for å hente fram sanger basert på tittel, hvilket antyder en hashtabell, med tittelen som nøkkel og selve sangen som verdi. Ville det vært mulig å bruke en hashtabell? Det er ikke umulig, men heller ikke problemfritt. For det først er en hashtabell uordnet, så vi ville sannsynligvis trenge en tabell i tillegg for å holde styr på listen. Et større problem er at en hashtabell ikke støtter flere nøkler for en og samme verdi. Det ville blitt problematisk for avspillingslisten vår, hvor den samme sangen kan være ført opp for avspilling flere ganger. Derfor holder vi oss til tabeller foreløpig, og søker etter titler ved behov. Dersom dette blir en ytelsesmessig flaskehals, kan vi alltids legge til oppslag via hashtabell senere.

Vi starter vår klasse med den grunnleggende initialize-metoden, som lager det Array-objektet vi vil benytte for som beholder for sangene og lagrer en referanse til dette objektet i instansvariabelen @songs.

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

Metoden SongList#append legger den angitte sangen til slutten av @songs tabellen. Den returnerer også self, som er en referanse til det gjeldende SongList objektet. Dette er en nyttig konvensjon som lar oss lenke sammen flere kall til append metoden i en lang kjede. Vi ser et eksempel på dette litt senere.

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

I neste omgang legger vi til metodene deleteFirst og deleteLast, og implementerer dem på en enkel måte ved å benytte Array#shift og Array#pop .

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

Når vi har kommet så langt, er det på tide å teste litt. Først vil vi legge fire sanger inn i listen. For å vise frem kunnskapene våre, vil vi benytte oss av at append returnerer SongList-objektet slik at vi kan kjede metodekallene sammen.

list = SongList.new
list.
  append(Song.new('title1', 'artist1', 1)).
  append(Song.new('title2', 'artist2', 2)).
  append(Song.new('title3', 'artist3', 3)).
  append(Song.new('title4', 'artist4', 4))

Deretter sjekker vi at sletting av sanger fra starten og slutten av listen fungerer som forventet, og at nil returneres når listen har blitt helt tom.

list.deleteFirst » Song: title1--artist1 (1)
list.deleteFirst » Song: title2--artist2 (2)
list.deleteLast » Song: title4--artist4 (4)
list.deleteLast » Song: title3--artist3 (3)
list.deleteLast » nil

Så langt ser alt greitt ut. Neste metode vi må implementere er [], som henter fram elementer utifra en indeks. Hvis indeksen er et tall (som vi vil sjekke med Object#kind_of? metoden), returnerer vi bare elementet på den posisjonen.

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

Enda mer triviell testing.

list[0] » Song: title1--artist1 (1)
list[2] » Song: title3--artist3 (3)
list[9] » nil

Nå trenger vi å legge til funksjonaliteten slik at vi kan hente ut en sang når vi kun vet tittelen. Dette gjør at vi må gå igjennom alle sangene i listen og sjekke titlene opp mot den ønskede tittelen. For å få dette til, trenger vi å titte nærmere på en av de mest praktiske fasilitetene i Ruby først: iteratorer.

Blokker og iteratorer

Problemet vi står ovenfor nå, er å implementere metoden [] i SongList som tar en streng og søker etter en sang med den tittelen. Dette virker enkelt nok: vi har en tabell med sanger, så vi går bare igjennom tabellen, et element om gangen, og ser om vi har funnet tittelen vi søker etter.

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

Det fungerer, ser kjent ut og føler komfortabelt: en for-løkke som itererer over en tabell. Det er da helt naturlig, ikke sant?

Det viser seg at det finnes en måte å gjøre det på som er mer naturlig. Løkken vår er litt for tett innpå tabellen; den spør om en lengde for så å hente ut verdier til den finner det den leter etter. Hvorfor kan vi ikke bare be tabellen kjøre en test mot hvert enkelt element? Jo, vi kan det. Faktisk er det akkurat det som find metoden i Array gjør.

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

Vi kunne også ha brukt if som en setningsmodifikator for å forkorte koden enda litt.

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

Metoden find er en iterator---en metode som gjentar en kodeblokk. Iteratorer og kodeblokker er blant de mest interessante fasilitetene i Ruby, så la oss ta oss tid til å bli kjent med dem (og mens vi gjør det vil vi finne ut akkurat hva den kodelinjen i [] metoden egentlig gjør).

Implementering av iteratorer

En iterator i Ruby er ikke noe mer en en metode som kan kalle en kodeblokk. Ved første øyekast, minner en Ruby blokk veldig om en blokk i C, Java eller Perl. Skinnet bedrar, dessverre---en blokk i Ruby er en måte å gruppere programsetninger, men på en litt ukonvensjonell måte.

For det første, så kan en blokk (vanligvis) bare eksistere i kildekoden ved siden av et metodekall; blokken starter på samme linje som metodens siste argument. For det andre, så kjøres ikke koden i blokken når programmet kommer til den. Derimot så husker Ruby konteksten som blokken dukket opp i (slik som lokale variable, det gjeldende objekt og så videre) og går deretter inn i metoden. Og så starter Ruby sin magi.

Fra innsiden av metoden, kan blokken kalles nesten som om den selv var en metode, ved å bruke yield setningen. Hver gang en yield blir utført, kjører den koden i blokken. Når blokken er ferdig, returneres kontrollflyten tilbake til rett etter yield. [Programmeringsspråk-fantaster vil bli glad for å vite at nøkkelordet yield ble valg for å gjenspeile funksjonen av samme navn i Liskovs språk CLU, et språk som er over tyve år gammelt og fremdeles inneholder egenskaper som ikke har blitt bredt utnyttet av de som ikke har tatt "CLU-et".] La oss begynne med et enkelt eksempel.

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

Blokken (koden mellom klammeparentesene) assosieres med kallet til metoden threeTimes. Inne i denne metoden kalles yield tre ganger etter hverandre. Hver gang kjøres koden som er i blokken, og en hilsen skrives ut. Men det som gjør blokker interessante, er at du kan gi parametre inn til dem, samt motta returverdier fra dem. For eksempel kunne vi skrevet en enkel funksjon som returnerer elementer i Fibonacci-serien opp til en gitt verdi. [Den grunnleggende Fibonacci-serien er en sekvens av heltall, som starter med to et-tall, hvor hvert enkelt av de resterende elementene er lik summen av de to forrige elementene. Serien blir noen ganger brukt i sorteringsalgoritmer og i analyser av naturlige fenomener. ]

def fibUpTo(max)
  i1, i2 = 1, 1        # parallell tilordning
  while i1 <= max
    yield i1
    i1, i2 = i2, i1+i2
  end
end
fibUpTo(1000) { |f| print f, " " }
produserer:
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

I dette eksempelet har yield-setningen en parameter. Denne verdien sendes inn til den assosierte blokken. I blokkdefinisjonen er argumentlisten mellom to vertikale streker. I dette eksempelet mottar variabelen f den verdien som ble sendt til yield, slik at blokken skriver ut påfølgende elementer av serien. (Dette eksempelent viser også parallell tilordning. Vi kommer tilbake til dette på side 75) Selv om det er ganske vanlig å sende bare en verdi til en blokk, er dette ingen begrensning; en blokk kan ha forskjellige antall parametre. Hva skjer hvis en blokk har et annet antall argumenter enn det som gis til yield-setningen? Forbløffende nok viser det seg at samme reglene som vi diskuterte under parallell tilordning viser seg igjen (med en liten forskjell: flere argumenter til en yield konverteres til en tabell dersom blokken kun tar ett argument).

Argumentvariablene til en blokk kan være de samme som eksisterende lokale variable. Hvis så, vil den nye verdien til variabelen være tilgjengelig etter at blokken er ferdig. Dette kan føre til overraskende oppførsel, men samtidig er det muligheter for å øke ytelsen med å bruke variabler som allerede eksisterer. [For mer informasjon om dette og andre potensielle feller, se liste som begynner på side 129; mer informasjon om ytelse begynner på side 130 ]

En blokk kan som sagt også returnere en verdi tilbake til metoden. Verdien til det siste uttrykket som blir evaluert i blokken, sendes tilbake til metoden som resultat fra kallet til yield. Dette benytter find metoden i Array seg av. [Implementasjonen av find metoden er egentlig definert i modulen Enumerable, som har blitt mikset inn i Array klassen.] Implementasjonen kunne sett omtrent slik ut:

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

Dette sender påfølgende elementer i tabellen til den assosierte blokken. Hvis blokken returnerer true, returnerer metoden det elementet den er kommet til. Hvis ingen elementer fører til at blokken returnerer true, returnerer metoden nil. Eksempelet viser fordelen med denne måten å gjøre iterering. Array klassen gjør det den er best til, som er å romstere i og hente frem elementer fra tabeller, og overlater til applikasjonskoden å fokusere på sin oppgave (som her er å finne et element som oppfyller et matematisk kriterium).

Noen iteratorer er felles for flere typer samlinger i Ruby. Vi har allerede sett find iteratoren. To andre er each og collect. Iteratoren each er sannsynligvis den enkleste---den gjør ikke noe mer enn å gi alle elementene sine til blokken i tur og orden.

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

Iteratoren each har en spesiell betydning i Ruby; på side 87 vil vi beskrive hvordan den er basisen for språkets for-løkke og på side 104 viser vi hvordan det å definere en each metode kan gi deg mye ekstra funksjonalitet gratis.

En annen iterator som ofte blir brukt, er collect. Den tar hvert element fra samlingen og sender det til blokken, for deretter å fylle opp en ny tabell med resultatene fra blokken.

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

Ruby sammenlignet med C++ og Java

La oss bruke litt tid på å sammenligne hvordan man bruker iteratorer i Ruby i forhold til C++ og Java. I Ruby er iteratoren bare en metode, som alle andre, som kaller yield hver gang den genererer en ny verdi. Det som bruker iteratoren er bare en kodeblokk assosiert med metodekallet. Man trenger ikke å skrive hjelpeklasser for å holde på iteratorens tilstand, slik som man må i Java og C++. Dette, som mange andre ting, gjør Ruby til et veldig transparent språk. Når du skriver et program i Ruby, kan du konsentrere deg om jobben du vil ha gjort, og mindre på å bygge opp stillaskode for å støtte selve språket i sitt arbeid.

Iteratorer trenger ikke begrense seg til å hente ut eksisterende data i tabeller og hashtabeller. Som vi så i eksempelet med Fibonacci-serien, kan en iterator returnere verdier den utleder eller regner seg fram til. Dette benytter Ruby sine innput/utput klasser seg av, som har iterator-grensesnitt som returnerer påfølgende linjer (eller tegn) i en innput/utput-strøm.

f = File.open("testfile")
f.each do |line|
  print line
end
f.close
produserer:
This is line one
This is line two
This is line three
And so on...

La oss se nærmere på en iterator-implementasjon til. Språket Smalltalk støtter også iteratorer som arbeider på samlinger. Hvis du ber noen Smalltalk-programmerere om å summere elementene i en tabell, er det ikke usannsynlig at de benytter seg av inject metoden.

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

Metoden inject fungerer som følger. Den første gangen den tilknyttede blokken blir kalt, blir sum satt til verdien som inject metoden mottok som parameter (null i dette tilfellet), og element er satt til det første elementet i tabellen. Andre og påfølgende kall til blokken, blir sum satt til den verdien som blokken returnerte på forrige kall. På denne måten kan man kumulativt regne ut totalen i sum. Den siste verdien fra inject metoden er verdien blokken returnerte siste gangen den ble kalt.

Ruby har ingen innebygd inject metode, men det er ikke vanskelig å lage vår egen. I dette tilfellet legger vi den til klassen Array, men senere på side 102(??) vil vi vise hvordan man kan gjøre metoden generelt tilgjengelig.

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

Selv om blokker ofte brukes med iteratorer, har de også andre bruksområder. La oss se på et par.

Transaksjoner ved hjelp av blokker

Blokker kan brukes for å definere en bit med kode som må bli kjørt under en eller annen form for transaksjonskontroll. For eksempel, så åpner man ofte en fil, gjør noe med innholdet, og ønsker deretter å forsikre seg at filen lukkes når man er ferdig. Selv om du kan gjøre dette på egenhånd, på en konvensjonell måte, finnes det argumenter for å gjøre filen ansvarlig for å lukke seg selv. Dette kan vi gjøre med blokker. En naiv implementering (uten feilhåndtering) kan se slik ut.

class File
  def File.openAndProcess(*args)
    f = File.open(*args)
    yield f
    f.close()
  end
end

File.openAndProcess("testfile", "r") do |aFile|   print while aFile.gets end
produserer:
This is line one
This is line two
This is line three
And so on...

Dette lille eksempelet illustrerer flere teknikker. Metoden openAndProcess er en klassemetode---den kan kalles uavhengig av et faktisk File-objekt. Vi ønsker at den skal ta de samme argumentene som den vanlige File.open metoden, men vi bekymrer oss egentlig ikke i det hele tatt om hva disse argumentene er. Istedet spesifiserer vi argumentene som *args, som betyr ``samle de faktiske parametrene som ble sendt til metoden inn i en tabell.'' Vi kaller dernest File.open og sender med *args som parameter. Dette ekspanderer tabellen tilbake til individuelle parametre. Nettoresultatet er at openAndProcess blir gjennomsiktig i at hva enn parametre den mottar, så blir de sendt videre til File.open .

Når filen så har blitt åpnet, kaller openAndProcess blokken ved å bruke yield, med det åpne filobjektet som parameter til blokken. Når blokken så avsluttes, lukkes filen. På denne måten har ansvarligheten for å lukke en åpnet fil blitt flyttet i fra brukeren til filobjektene selv.

Sist, men ikke minst, bruker dette eksempelet do...end for å definere en blokk. Den eneste forskjellen mellom denne notasjonsformen og bruk av klammeparenteser, er presedens: do...end binder lavere enn ``{...}''. Vi forklarer hvilke effekter dette har på side 236.

Denne teknikken hvor filene holder styr på seg selv, er så nyttig at klassen File-klassen i Ruby støtter den direkte. Hvis et kall til File.open har en blokk tilknyttet, da vil den blokken bli kalt med filobjektet som argument, og filen vil bli lukket når blokken er ferdig. Dette er interessant, da det betyr at File.open kan oppføre seg på to forskjellige måter: når den blir kalt med en blokk, utfører den blokken og lukker filen. Når den kalles uten en blokk, returnerer den bare filobjektet. Dette gjøres mulig av metoden Kernel::block_given? , som svarer true hvis en blokk er tilknyttet det metodekallet vi er i. Med å bruke den, kunne du implementert File.open (nok en gang uten feilhåndtering) slik som følgende.

class File
  def File.myOpen(*args)
    aFile = File.new(*args)
    # Hvis der er en blokk, send inn filen til den og
    # lukk filen når blokken returnerer
    if block_given?
      yield aFile
      aFile.close
      aFile = nil
    end
    return aFile
  end
end

Blokker kan være tillukkinger

La oss komme tilbake til jukeboksen vår for et øyeblikk (du husker den, ikke sant?). På et eller annet tidspunkt vil vi arbeide med koden som håndterer brukergrensesnittet---knappene som folk trykker for å velge sanger og kontrollere jukeboksen. Vi trenger å koble disse knappene opp til handlinger: trykk STOP og musikken stopper opp. Det viser seg at Ruby sine blokker utgjør en lett tilgjengelig løsning for dette. La oss begynne med antagelsen at folkene som lagde maskinvaren implementerte en Ruby utvidelse (extension) som gir oss en grunnleggende knappeklasse. (Vi går inn på utvidelser av Ruby på side 171.)

bStart = Button.new("Start")
bPause = Button.new("Pause")
# ...

Hva skjer når brukeren trykker på en av knappene våre? I Button-klassen har maskinvarefolka gjort det slik at en tilbakekallsmetode, buttonPressed, vil bli kalt. Den åpenbare måten å legge til funksjonalitet til disse knappene er å lage subklasser av Button, hvor hver av subklassene implementerer det vi vil skal skje i buttonPressed-metoden.

class StartButton < Button
  def initialize
    super("Start")       # kall initialize-metoden til Button
  end
  def buttonPressed
    # gjør det som må til for å starte avspilling...
  end
end

bStart = StartButton.new

Det er to problemer med denne fremgangsmåten. For det første, vil det føre til en stor mengde subklasser. Hvis grensesnittet til Button endrer seg, kan det føre til mye vedlikeholdsarbeid. For det andre, så er handlingene som skjer når en knapp trykkes, uttrykket på feil nivå; de er ikke en del av knappen, men en egenskap av den automatiske platespilleren som bruker disse knappene. Vi kan løse begge disse problemene med å bruke blokker.

class JukeboxButton < Button
  def initialize(label, &action)
    super(label)
    @action = action
  end
  def buttonPressed
    @action.call(self)
  end
end

bStart = JukeboxButton.new("Start") { songList.start } bPause = JukeboxButton.new("Pause") { songList.pause }

Hemmeligheten til alt dette er den andre parameteren til JukeboxButton#initialize. Hvis den siste parameteren i en metodedefinisjon er prefikset med et og-tegn (for eksempel &action), tar Ruby og ser etter en kodeblokk når metoden kalles. Den blokken blir konvertert til et Proc-objekt og tilknyttet parameteren. Du kan da håndtere parameteren som enhver variabel. I vårt eksempel, legger vi den i instansvariabelen @action. Når tilbakekallsmetoden buttonPressed kalles, bruker vi Proc#call for å kjøre koden i blokken.

Så hva har vi egentlig når vi har et Proc-objekt? Interessant nok er det mer en bare en kodebit. Til en blokk (og dermed også et Proc-objekt) er all omliggende kontekst som blokken ble definert i: verdien til self og metodene, variablene og konstantene i skopet. En del av Ruby sin magi er at blokken kan bruke alt av denne skopinformasjonen selv om miljøet blokken ble definert i, hadde forduftet. I andre språk, kalles denne egenskapen for tillukking (eller closure).

La oss se på et oppkonstruert eksempel. Her brukes metoden proc til å konvertere en blokk til et Proc-objekt.

def nTimes(aThing)
  return proc { |n| aThing * n }
end
p1 = nTimes(23)
p1.call(3) » 69
p1.call(4) » 92
p2 = nTimes("Hello ")
p2.call(3) » "Hello Hello Hello "

Metoden nTimes returnerer et Proc-objekt som tar tak i metodens parameter aThing. Selv om denne parameteren er utenfor skopen innen blokken kalles, forblir parameteren tilgjengelig for blokken.

( In progress translation to Norwegian by NorwayRUG. $Revision: 1.25 $ )


Forrige < Innhold ^
Neste >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide".
Translation to norwegian by Norway Ruby User Group.
Copyright for the english original authored by David Thomas and Andrew Hunt:
Copyright © 2001 Addison Wesley Longman, Inc.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).

(Please note that the license for the original has changed from the above. The above is the license of the original version that was used as a foundation for the translation efforts.)

Copyright for the norwegian translation:
Copyright © 2002 Norway Ruby User Group.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).
Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.
Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.