Přetěžování
Některé objektové jazyky nabízejí takzvané přetěžování metod. Jedná se o možnost definovat několik metod se stejným názvem, lišících se počtem a typem parametrů. U jazyka s dynamickým typováním (jakým je i Ruby) je koncept přetěžování nesmyslný. Podobného efektu lze dosáhnout zkoumáním aktuálního typu parametru za běhu programu a využitím defaultních hodnot parametrů:
class Movable def steer(s=1,d=1) # přiřadíme defaultní hodnoty parametrům if d.kind_of? String # test - je parametr řetězec? d=-1 if d=='left' # převedeme řetězec 'left' na -1 d=1 if d=='right' # a řetězec 'right' na +1 end @direction+=s*d # zatočíme ve správném směru end end a=Movable.new(10,10,5) puts a.to_s a.steer # oběma parametrům bude přiřazena defaultní hodnota puts a.to_s a.steer(5) # první parametr je zadán, druhý bude default puts a.to_s a.steer(3, -1) # druhým parametrem je číslo puts a.to_s a.steer(4, 'right') # druhým parametrem je řetězec puts a.to_s
Po spuštění uvidíme:
Movable: X=10, Y=10, M=5kg, C=1, D=0, V=0 Movable: X=10, Y=10, M=5kg, C=1, D=1, V=0 Movable: X=10, Y=10, M=5kg, C=1, D=6, V=0 Movable: X=10, Y=10, M=5kg, C=1, D=3, V=0 Movable: X=10, Y=10, M=5kg, C=1, D=7, V=0
Pomocí defaultních hodnot parametrů jsme dosáhli možnosti volání metody steer s žádným až dvěma parametry. Druhý parametr, který určuje směr zatáčení, může být navíc buď číslo (-1 – doleva, 1 doprava), nebo řetězec. O aktuálním typu parametru rozhodujeme pomocí metody kind_of?. (V uvedené definici metody steer záměrně zůstala potenciální chyba, jejíž odstranění se ponechává jako cvičení pro laskavého čtenáře.)
Obecně proměnlivý počet parametrů lze zpracovat také následující syntaktickou konstrukcí:
def vararg(*s) # 's' bude vždy pole puts s.size # velikost pole - počet parametrů end vararg # otestujeme s různými počty a typy parametrů vararg('a') vararg('a',1,2,'b')
Poslednímu parametru metody může být předřazena hvězdička. Tento parametr je pak považován za pole a případné přebytečné parametry jsou uloženy jako jeho prvky. Na výpisu vidíme:
0 1 4
Singleton
Pokud pocítíme potřebu přidat nebo předefinovat metodu jen jedné z instancí stejné třídy, vyjde nám Ruby vstříc. Umožňuje připojit k instanci anonymní třídu, ve které můžeme potřebné definice uvést. Více osvětlí krátký příklad:
a=Movable.new(0,0,10,0,5) # vytvoříme dva objekty stejné třídy b=Movable.new(0,0,10,0,6) # tento objekt by měl být "tajný" class << b # anonymní třída pro objekt 'b' def to_s # předefinuje metodu return 'Secret object...' # tajný objekt nebude poskytovat žádné údaje end end puts a.to_s # vyzkoušíme puts b.to_s
Po spuštění se ukáže, jak naše utajení funguje:
Movable: X=0, Y=0, M=10kg, C=2, D=0, V=5 Secret object...
Přidaná anonymní třída se nazývá singleton, protože má jen jednu instanci – objekt, pro který byla vytvořena. Předkem singletonu se stává původní třída objektu.
Jednalo se nám jen o předefinování jedné metody. Mohli jsme proto použít jednodušší zápis:
def b.to_s return 'Secret object...' end
Vnitřní zpracování je však stejné jako v předešlém případě. Opět se vytváří singleton.
Sebereflexe
Dynamické objektově orientované jazyky umožňují měnit program za jeho běhu. Současně vzniká potřeba zjišťovat informace o třídách, metodách a instancích, které mohou dynamicky vznikat a zanikat. Tato schopnost se obvykle označuje jako „reflection“, což bychom mohli do češtiny přeložit jako sebereflexe.
Již jsme se setkali s metodou kind_of?, která zjišťuje, zda je objekt instancí dané třídy nebo některého z jejích předků. Uveďme si rozsáhlejší příklad:
n = 1 puts n.id puts n.class puts n.class.superclass puts n.class.superclass.superclass puts n.kind_of? Fixnum puts n.kind_of? Numeric puts n.instance_of? Fixnum puts n.instance_of? Numeric
Výsledkem je:
5 11 Fixnum Integer Numeric true true true false
Vzpomínáte si na objektový model Ruby? Každá instance má svůj unikátní identifikátor přístupný voláním metody id. Každá instance také patří k nějaké třídě. Voláním metody class získáme referenci na instanci třídy Class. Každá třída je v Ruby instancí třídy Class. Ta má mimo jiné definovanou metodu superclass, která ukazuje na předka dané třídy. Na uvedeném příkladu vidíme, že pro čísla v Ruby existuje základní třída Numeric s potomkem Integer pro celá čísla. Integer má potomka Fixnum pro celá čísla, která nepřesahují velikost integeru na dané architektuře (například 32 nebo 64 bitů; pro větší čísla existuje třída Bignum).
Jak jsme dříve uvedli, metoda kind_of? zjistí, zda je objekt instancí třídy, předané jako parametr, nebo některého z jejích potomků. Metoda instance_of? je naopak specifickým dotazem na jednu třídu.
K metodě superclass existuje podobná metoda ancestors:
a = Fixnum.ancestors puts a.join(', ')
Vypíše:
Fixnum, Integer, Precision, Numeric, Comparable, Object, RW_IO_EMULATE, Kernel
Volání ancestors vrací pole se všemi předky třídy a se všemi moduly, které byly v průběhu dědění namixovány. Modul je samozřejmě také objektem – instancí třídy Module.
Podívejme se, jak získat přehled o všech instancích určité třídy v programu:
a=Movable.new(10,10,5,0,0) b=Movable.new(20,20,5,0,0) ObjectSpace.each_object(Movable) { |o| puts "#{o.id} - #{o.to_s}" }
Získáme seznam:
84006728 - Movable: X=20, Y=20, M=5kg, C=2, D=0, V=0 84008600 - Movable: X=10, Y=10, M=5kg, C=2, D=0, V=0
Pokud bychom iterátor each_object definovaný modulem ObjectSpace volali bez parametru, získali bychom seznam všech objektů existujících v dané chvíli.
Další informací, kterou můžeme o objektu získat, je přehled metod, které má definovány:
m = a.methods puts m.size
S výsledkem:
56
Metoda methods vrací pole instancí třídy Method. Relativně velký počet, který jsme obdrželi, je důsledkem dědění mnoha metod ze základní třídy Object. Na metody se můžeme dotazovat i po jedné:
puts a.respond_to? :steer puts a.respond_to? :fly
Podle očekávání kód vypíše:
true false
Vzpomeňme si ještě na postupné prohledávání stromu tříd při volání metody. Když není metoda nalezena ani ve třídě Object, volá se metoda method_missing. Tu můžeme snadno předefinovat podle potřeby:
class Numeric def method_missing(met) puts "Metoda #{met} není definována!" end end a = 5 a.isprime?
Při volání neexistující metody získáme nyní české hlášení:
Metoda isprime? není definována!
Stojí za zmínku, že jsme předefinovali metodu method_missing až ve třídě Numeric, přestože původně je definována ve třídě Object. Ruby však správně volá method_missing té třídy, jejíž neexistující metodu jsme požadovali. Pro každou třídu proto můžeme vytvořit vlastní obsluhu volání nedefinovaných metod.
Dynamické volání metod
Vzhledem k tomu, že je možné za běhu programu definovat nové metody a máme prostředky pro zjištění, na jaké metody objekt reaguje, je logickým krokem dynamické volání metod.
a=-5 puts a.send(:abs) # voláme metodu 'abs' puts a.send(:+,3) # můžeme předat i parametry s='abs' puts a.send(s.intern) # řetězec převedeme na symbol
Po spuštění získáme:
5 -2 5
Volání metody je implementací mechanismu zaslání zprávy objektu. Metoda send zprávu zasílá ještě zřetelněji. Součástí zprávy je název metody, která má být spuštěna, a parametry, které jí mají být předány. Název metody je předáván jako tzv. symbol. Symbol vytvoříme z řetězcové konstanty přidáním dvojtečky. Druhou možností je využít metody intern třídy String. Díky tomu můžeme název volané metody skutečně konstruovat až za běhu programu.
Víme, že metoda je instancí třídy Method. Následující příklad ukazuje, jak můžeme s metodami pracovat jako s běžnými objekty:
s='test' m1=s.method(:upcase) # reference na metodu 'upcase' objektu 's' m2=s.method(:index) # reference na metodu 'index' objektu 's' puts m1.type puts m1.call # zavoláme uloženou metodu puts m2.call('s') # můžeme i předávat parametry
Kód vypíše:
Method TEST 2
Univerzální mechanismus pro dynamické spouštění kódu nabízí metoda eval. Při základním způsobu použití zpracuje interpret kód předaný metodě jako parametr. K vyhodnocení dochází přesně v místě volání eval:
a=5 code='puts ' # kód určený k vykonání si můžeme libovolně zkonstruovat code+='a' eval(code) # zde spustíme vytvořený řetězec
Na výstupu se objeví:
5
Spouštění kódu získaného prakticky libovolně za běhu programu vypadá z pohledu bezpečnosti jako jako noční můra. Ruby naštěstí disponuje jednoduchým bezpečnostním modelem, který zabrání nejhoršímu. V globální konstantě $SAFE se uchovává aktuální bezpečnostní úroveň. Defaultní hodnota je 0. Nastavíme-li však vyšší hodnotu, může náš příklad dopadnout zcela jinak:
$SAFE=3 # nastavíme vyšší bezpečnostní úroveň a=5 code='puts ' # kód určený k vykonání si můžeme libovolně zkonstruovat code+='a' eval(code) # zde spustíme vytvořený řetězec
Dočkáme se výjimky:
-:5:in `eval': Insecure operation - eval (SecurityError) from -:5
Všechny objekty v Ruby mají příznak, který stanoví, zda je vhodné používat je k určitým „nebezpečným“ operacím (mezi které patří samozřejmě i vykonání obsahu řetězce interpetem). Při nastavení bezpečnostní úrovně tři a vyšší jsou všechny nově vytvořené objekty automaticky označeny jako nevyhovující. Volání eval proto způsobí výjimku.
Marshaling
Často by bylo vhodné objekty, které vznikají při běhu aplikace, uchovávat i po ukončení programu a obnovit jejich stav při dalším spuštění. Pro permanentní ukládání stavu objektů se vžil název serializace, zpopularizovaný zejména jazykem Java. Ruby nabízí obdobný mechanismus a nazývá ho marshaling.
Funkčnost je soustředěna v metodách modulu Marshal. Pro pochopení nám postačí stručný příklad s uložením jednoho objektu do souboru a jeho opětovným načtením:
a=Movable.new(10,10,5,0,0) # vytvoříme instanci třídy 'Movable' puts a.id # ověříme si identifikátor a obsah objektu puts a.to_s File.open("store", "w+") { |file| # volání otevře soubor a vykoná blok Marshal.dump(a, file) # zapíšeme objekt do souboru } file=File.open("store") # otevřeme znovu soubor b=Marshal.load(file) # načteme ze souboru objekt file.close # zavřeme soubor puts b.id # výpisem ověříme, že se jedná o jiný objekt puts b.to_s # se stejným obsahem
Podíváme se na výsledek:
84008600 Movable: X=10, Y=10, M=5kg, C=1, D=0, V=0 84003812 Movable: X=10, Y=10, M=5kg, C=1, D=0, V=0
Pokud je ukládaný objekt kontejnerem (například pole), jsou uloženy nebo načteny i všechny jeho prvky, a to rekurzivně. Toho lze využít pro ukládání celých objektových stromů.
Marshaling nabízí mnohem širší možnosti, než mohl demonstrovat náš jednoduchý příklad. Využívá jej například i knihovna, která poskytuje v Ruby efektivní rámec pro distribuované aplikace. Marshaling zde slouží k posílání objektů po síti mezi dvěma běžícími Ruby programy. Dynamické volání metod zase vytváří základ vzdáleného volání. Knihovna drb (Distributed Ruby) má jen kolem dvou stovek řádků Ruby kódu. I to dokazuje, že elegantní a robustní objektový model Ruby umožňuje schůdné budování rozsáhlých aplikací zcela v duchu OOP.
Zdroje
- Matsumoto, Y.: Ruby in a Nutshell, O'Reilly & Associates, 2001
- Thomas, D. – Hunt, A.: Programming Ruby