|
|||
Forrige < |
Innhold ^
|
Neste >
|
SongList
klasse, som kan spesialiseres for kataloger og avspillingslister.
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.
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"]
|
[]
-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
|
[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]
|
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]
|
[]
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] |
[]=
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] |
=>
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}
|
Hash
-klassen begynner på side 317(??).
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.
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 |
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 |
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 |
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)) |
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
|
[]
, 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 |
list[0]
|
» |
Song: title1--artist1 (1)
|
list[2]
|
» |
Song: title3--artist3 (3)
|
list[9]
|
» |
nil
|
[]
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 |
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 |
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 |
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).
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" } |
Hello Hello Hello |
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, " " } |
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 |
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
|
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 } |
1 3 5 |
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"]
|
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 |
This is line one This is line two This is line three And so on... |
inject
metoden.
sumOfValues "Smalltalk metode" ^self values inject: 0 into: [ :sum :element | sum + element value] |
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
|
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 |
This is line one This is line two This is line three And so on... |
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 |
bStart = Button.new("Start") bPause = Button.new("Pause") # ... |
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 |
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 } |
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 "
|
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 >
|