|
|||
Class
, die alle diese Objekt-Dinger enthält, sowie zusätzlich
eine Liste mit Methoden und eine Referenz auf eine Superklasse (die wiederum selber eine
andere Klasse ist). Alle Methoden-Aufrufe in Ruby benennen einen Empfänger (der
defaultmäßig self
ist, das aktuelle Objekt).
Ruby findet die zu startende Methode, indem es die Liste der Methoden der
Empfänger-Klasse durchsucht. Findet es die Methode dort nicht, sucht es in der nächsten
Superklasse und dann in deren Superklasse und so weiter. Wird die Methode nicht in der
Empfänger-Klasse und auch nicht in deren Vorfahren gefunden, so ruft Ruby die
Methode method_missing
des ursprünglichen Empfängers auf.
Das war dann alles --- die ganze Erklärung. Weiter mit dem nächsten Kapitel.
``Moment noch,'' hören wir den Leser rufen, ``Ich habe doch gutes Geld für dieses
Kapitel ausgegeben. Was ist denn mit diesem ganzen anderen Kram --- Singleton-Klassen,
Klassen-Methoden und so was. Wie funktionieren die denn?''
[Wenn einer der Leser tatsächlich gutes Geld für dieses Buch ausgeben will: ich
maile ihm gerne meine Kontonummer, der Übersetzer]
Gute Frage.
lucille
referenziertes Objekt, die Klasse Guitar
dieses Objekts, und deren Oberklasse Object
.
Beachte, wie die Klassenreferenz des Objekts (klass
genannt, aus historischen
Gründen, die Andy wirklich nerven) auf das Klassen-Objekt zeigt und wie der super
-Zeiger
dieser Klasse auf die Eltern-Klasse referenziert.
lucille.play
aufrufen, geht Ruby zum Empfänger,
lucille
, und folgt der klass
-Referenz zum Klassen-Objekt
Guitar
. Dort durchsucht Ruby die Methodentabelle, findet
play
und ruft es auf.
Wenn wir stattdessen lucille.display
aufrufen, macht Ruby dasselbe, findet
aber display
nicht in der Methodentabelle der Klasse
Guitar
. Dann folgt es der super
-Referenz zu Obejct
, der
Oberklasse von Guitar
, wo es die Methode findet und ausführt.
klass
-Unterpunkte der Klassen-Objekte in Figur 19.1 nirgends hinzeigen.
Mittlerweile haben wir alle Informationen, um uns vorzustellen, wo sie hinzeigen
sollten
. Wenn man lucille.play()
schreibt, dann folgt Ruby dem
klass
-Zeiger von lucille
, um das Klassen-Objekt zu finden, in dem
es nach dieser Methode suchen soll. Was ist aber, wenn wir eine Klassen-Methode aufrufen,
wie etwa Guitar.strings(...)
? Dabei ist der Emnpfänger das Klassen-Objekt
Guitar
selber. Um also konsistent zu bleiben, müssen wir die Methoden in eine
andere Klasse stecken, die vom klass
-Zeiger von Guitar
referenziert
wird. Das wird dann Meta-Klasse
genannt. Wir bezeichnen hier die Metaklasse von
Guitar
mit Guitar'
. Aber das ist noch nicht alles. Weil Guitar
eine Unterklasse von Object
ist, muss auch seine Metaklasse Guitar'
eine Unterklasse der Metaklasse von Object
, nämlich Object'
sein.
In Figur 19.2 zeigen wir diese zusätzlichen Metaklassen.
Guitar.strings()
ausführt, dann folgt es denselben Regeln wie vorher
auch: es geht zum Empfänger, Klasse Guitar
, folgt der
klass
-Referenz zur Klasse Guitar'
und findet die Methode.
Beachte schließlich auch noch, dass sich ein ``S'' in die Flags der Klasse
Guitar'
eingeschlichen hat. Die von Ruby automatisch erzeugten
Klassen werden intern als Singleton-Klassen markiert. Singleton-Klassen werden
bei Ruby etwas anders behandelt. Von außen gesehen ist der offensichtlichste Unterschied,
dass sie einfach unsichtbar sind: sie werden niemals zurückgeliefert von Methoden wie
Module#ancestors
oder ObjectSpace::each_object
.
String
-Objekte. Mit einem
von ihnen verbinden wir dann eine anonyme Klasse und überschreiben dabei eine der Methoden
der Basis-Klasse dieses Objekts und fügen eine weitere Methode hinzu.
a = "hello" |
||
b = a.dup |
||
|
||
class <<a |
||
def to_s |
||
"The value is '#{self}'" |
||
end |
||
def twoTimes |
||
self + self |
||
end |
||
end |
||
|
||
a.to_s |
» | "The value is 'hello'" |
a.twoTimes |
» | "hellohello" |
b.to_s |
» | "hello" |
class <<
obj''-Notation,
die im Wesentlichen sagt: ``baue eine neue Klasse nur für dieses obj-Objekt.''
Wir hätten das auch so schreiben können:
a = "hello" |
||
b = a.dup |
||
def a.to_s |
||
"The value is '#{self}'" |
||
end |
||
def a.twoTimes |
||
self + self |
||
end |
||
|
||
a.to_s |
» | "The value is 'hello'" |
a.twoTimes |
» | "hellohello" |
b.to_s |
» | "hello" |
a
'' hinzugefügt. Das gibt uns einen zeimlich genauen Hinweis darauf, wie
das in Ruby implementiert ist: Eine Singleton-Klasse wird erzeugt und als a
's
direkte Klasse eingefügt. a
's original Klasse, String
,
wird dabei zu der Oberklasse dieses Singletons. Die Vorher- Nachher-Bilder dazu werden
in Figur 19.3 gezeigt.
Ruby führt mit diesen Singleton-Klassen noch eine kleine Optimierung durch. Wenn der
klass
-Zeiger eines Objekts bereits auf eine Singleton-Klasse zeigt, dann wird
keine neue erzeugt. Das heißt für das vorherige Beispiel, dass die erste der beiden
Methoden-Definitionen eine Singleton-Klasse erzeugt, die zweite fügt ihr einfach nur eine
Methode hinzu.
module SillyModule |
||
def hello |
||
"Hello." |
||
end |
||
end |
||
class SillyClass |
||
include SillyModule |
||
end |
||
s = SillyClass.new |
||
s.hello |
» | "Hello." |
module SillyModule |
||
def hello |
||
"Hi, there!" |
||
end |
||
end |
||
s.hello |
» | "Hi, there!" |
class <<obj
'' erzeugen kann, kann man auch
ein Modul zu einem Objekt hinzu mischen mit
Object#extend
.
Beispiel:
module Humor |
||
def tickle |
||
"hee, hee!" |
||
end |
||
end |
||
|
||
a = "Grouchy" |
||
a.extend Humor |
||
a.tickle |
» | "hee, hee!" |
extend
einen interessanten Trick durchführen.
Wenn man es innerhalb einer Klassen-Definition benutzt, werden die Modul-Methoden zu
Klassen-Methoden.
module Humor |
||
def tickle |
||
"hee, hee!" |
||
end |
||
end |
||
|
||
class Grouchy |
||
include Humor |
||
extend Humor |
||
end |
||
|
||
Grouchy.tickle |
» | "hee, hee!" |
a = Grouchy.new |
||
a.tickle |
» | "hee, hee!" |
extend
äquivalent ist zu
self.extend
. Die Methode wird also zu self
hinzu gefügt und das ist
innerhalb einer Klassen-Definition eben die Klasse selber.
class MediaPlayer include Tracing if $DEBUGGING if ::EXPORT_VERSION def decrypt(stream) raise "Decryption not available" end else def decrypt(stream) # ... end end end |
self
muss irgendwohin zeigen. Sehen wir uns an, wohin.
class Test puts "Type of self = #{self.type}" puts "Name of self = #{self.name}" end |
Type of self = Class Name of self = Test |
class Test def Test.sayHello puts "Hello from #{name}" end sayHello end |
Hello from Test |
Test.sayHello
und rufen sie im Rumpf der Klassen-Definition
auf. Innerhalb von sayHello
rufen wir name
auf, eine Instanz-Methode
der Klasse Module
. Weil Module
ein Vorfahr von
Class
ist, können seine Instanz-Methoden ohne expliziten Empfänger
innerhalb einer Klassen-Definition aufgerufen werden.
Tatsächlich sind viele Anweisungen, die man während der Definition einer Klasse oder eines
Moduls benutzt, etwa alias_method
, attr
und
public
ganz einfach nur Methoden aus der Klasse Module
.
Dies eröffnet uns einige interessante Möglichkeiten --- man kann die Funktionalität von
Klassen- und Modul-Definitionen mit Ruby-Code erweitern. Schauen wir uns ein paar Beispiele
an.
Als erstes Beispiel werden wir die Möglichkeit einer einfachen Dokumentation für Module und
Klassen hinzufügen. Damit können wir einen String mit von uns geschriebenen Klassen und
Modulen verbinden, auf den während des Programmablaufs zugegriffen werden kann.
Wir benutzen eine einfache Syntax.
class Example doc "This is a sample documentation string" # .. rest of class end |
doc
für jede Klasse und jedes Modul verfügbar machen, also muss
daraus eine Instanz-Methode der Klasse Module
werden.
class Module @@docs = Hash.new(nil) def doc(str) @@docs[self.name] = str end def Module::doc(aClass) # If we're passed a class or module, convert to string # ('<=' for classes checks for same class or subtype) aClass = aClass.name if aClass.type <= Module @@docs[aClass] || "No documentation for #{aClass}" end end class Example doc "This is a sample documentation string" # .. rest of class end module Another doc <<-edoc And this is a documentation string in a module edoc # rest of module end puts Module::doc(Example) puts Module::doc("Another") |
This is a sample documentation string And this is a documentation string in a module |
date
-Modul
von Tadayoshi Funaba (Beschreibung ab Seite 443). Wir haben zu Beispiel eine Klasse, die einen
Basiswert repräsentiert (in diesem Fall ein Datum). Diese Klasse besitzt nun viele
Attribute, die denselben Wert auf unterschiedliche Weise darstellen: als Nummer des
Julianischen Tages, als [Jahr, Monat, Tag]-Tripel und so weiter. Jeder Wert repräsentiert
dasselbe Datum und benötigt eventuell eine recht komplexe Berechnung. Deshalb mochten wir
jedes Attribut nur einmal berechnen, wenn das erste Mal darauf zugegriffen wird.
Nach üblicher Vorgehensweise macht man das mit einem Test für jede Zugriffs-Variable:
class ExampleDate def initialize(dayNumber) @dayNumber = dayNumber end def asDayNumber @dayNumber end def asString unless @string # complex calculation @string = result end @string end def asYMD unless @ymd # another calculation @ymd = [ y, m, d ] end @ymd end # ... end |
once
-Modifikator von Eiffel sehr ähnlich. Wir
möchten etwas schreiben können, das etwa so aussieht:
class ExampleDate def asDayNumber @dayNumber end def asString # complex calculation end def asYMD # another calculation [ y, m, d ] end once :asString, :asYMD end |
once
als Anweisung benutzen, indem wir es als Klassen-Methode von
ExampleDate
schreiben,
aber wie sollte es intern aussehen? Der Trick ist, dass es die Methoden, deren Namen
es übergeben bekommt, überschreibt. Für jede Methode erzeugt es einen Alias für den
Original-Code und erzeugt dann eine neue Methode mit demselben Namen. Diese neue Methode
macht zwei Dinge. Als erstes ruft sie die Original-Methode auf (über den Alias) und
speichert den Rückgabewert in einer Instanz-Variablen. Als zweites defniniert sie sich
selber um, so dass bei weiteren Afurufen nur noch der Wert der Instanz-Variablen
zurück gegeben wird. Dies ist Tadayoshi Funabas Code, nur ein wenig reformatiert.
def ExampleDate.once(*ids) for id in ids module_eval <<-"end_eval" alias_method :__#{id.to_i}__, #{id.inspect} def #{id.id2name}(*args, &block) def self.#{id.id2name}(*args, &block) @__#{id.to_i}__ end @__#{id.to_i}__ = __#{id.to_i}__(*args, &block) end end_eval end end |
module_eval
, um einen Code-Block im Kontext des aufrufenden
Moduls (bzw. hier der aufrufenden Klasse) auszuführen. Die Original-Methode wird umbenannt
in __nnn__, wobei der nnn-Anteil die Integer-Repräsentation der
Symbol-Id des Methoden-Namens ist. Der Code nutzt denselben Namen für die speichernde
Instanz-Variable. Der Großteil des Codes ist eine Methode, die sich dynamisch redefiniert.
Beachte, dass diese Redefinition die Tatsache nutzt, dass Methoden eingeschachtelte
Singleton-Methoden enthalten dürfen, ein raffinierter Trick.
Wenn du diesern Code verstehst, bist du schon weit vorangeschritten auf dem Weg zu
wahrer Meisterschaft in Ruby.
Wir können es aber noch weiter treiben. Sieh dir das date
-Modul an, da sieht die
Methode once
noch etwas anders aus.
class Date class << self def once(*ids) # ... end end # ... end |
class << self
''. Damit wird eine Klasse definiert basierend auf
dem Objekt self
, und self
ist gerade das Klassen-Objekt für
Date
. Das Ergebnis? Jede Methode, die in der inneren Klassen-Definition
geschrieben wird, ist automatisch eine Klassen-Methode von Date
.
Diese once
-Eigenschaft ist allgemein einsetzbar --- das sollte mit
jeder Klasse funktionieren. Wenn man once
zu einer privaten Methode der Klasse
Module
machen würde, wäre es in jeder Ruby-Klasse verfügbar.
Class
Objekt selber. Wenn man etwa
String.new("gumby")
schreibt, sendet man die Nachricht new
an das
Objekt, das die Klasse String
ist. Aber woher weiß Ruby das?
Schließlich sollte der Empfänger einer Nachricht eine Objekt-Referenz sein, was bedeutet, dass
es irgendwo eine Konstante namens ``String'' geben muss, die eine Referenz auf das
String
-Objekt enthält.
[Das ist eine Konstante, keine Variable, schließlich fängt ``String''
mit einem Großbuchstaben an.] Und tatsächlich, genau das passiert. Alle eingebauten
und alle selbst definierten Klassen besitzen eine dazu gehörende globale Konstante mit
dem selben Namen wie die Klasse. Das ist sowohl einfach als auch raffiniert. Die
Raffinesse kommt daher, dass es tatsächlich zwei Dinge namens (zum Beispiel)
String
im System gibt, zum einen die Konstante, die ein Objekt
der Klasse String
referenziert, und zum anderen das Objekt selber.
Die Tatsache, dass Klassen-Namen einfach nur Konstanten sind, bedeutet, dass man Klassen
wie jedes andere Ruby-Objekt auch behandeln kann: man kann sie kopieren, sie an Methoden
übergeben und sie in Ausdrücken benutzen.
def factory(klass, *args) |
||
klass.new(*args) |
||
end |
||
|
||
factory(String, "Hello") |
» | "Hello" |
factory(Dir, ".") |
» | #<Dir:0x4018d1a8> |
|
||
flag = true |
||
(flag ? Array : Hash)[1, 2, 3, 4] |
» | [1, 2, 3, 4] |
flag = false |
||
(flag ? Array : Hash)[1, 2, 3, 4] |
» | {1=>2, 3=>4} |
puts "Hello, World" |
"Hello, World"
einen Ruby-String
erzeugt, da ist also schon mal ein Objekt. Wir wissen auch, dass der nicht ergänzte
Methoden-Aufruf puts
tatsächlich das selbe ist wie self.puts
. Aber was
ist nun ``self''?
self.type |
» | Object |
Object
sind, können wir alle Methoden von Object
benutzen (inklusive die eingemischten von Kernel
) in der Form wie eine
Funktion. Das erklärt auch, warum wir Kernel
-Methoden wie
puts
im Top-Level (und natürlich auch sonst überall) aufrufen können: diese
Methoden sind Teil jeden Objekts.
class Base def aMethod puts "Got here" end private :aMethod end class Derived1 < Base public :aMethod end class Derived2 < Base end |
aMethod
in Instanzen der Klasse
Derived1
aufrufen, aber nicht über Instanzen von
Base
oder
Derived2
.
Wie bringt Ruby nun dieses Kunststück fertig, dass eine Methode zwei verschiedene
Sichtbarkeiten zeigt? Um es einfach zu sagen, Ruby mogelt.
Wenn eine Unterklasse die Sichtbarkeit einer Methode in einem Elternteil ändert, fügt
Ruby in Wirklichkeit eine versteckte Proxy-Methode in der Unterklasse ein, die die
Original-Methode mit super
aufruft. Die Sichtbarkeit dieses Proxy wird dann
auf die gewünschte Art gesetzt. Das bedeutet, dass der Code:
class Derived1 < Base public :aMethod end |
class Derived1 < Base def aMethod(*args) super end public :aMethod end |
super
kann auf die Eltern-Methode unabhängig von deren
Sichtbarkeit zugegriffen werden, so dass diese Umschreibung es der Unterklasse erlaubt, die
Sichtbarkeitsregeln der Elternklasse zu überschreiben. Ganz schön schaurig, nicht wahr?
Object#freeze
.
Ein eingefrorenes Objekt kann nicht verändert werden: man kann seine Instanz-Variablen nicht
ändern (weder direkt noch indirekt), man kann keine Singleton-Methoden hinzufügen, und im Falle
einer Klasse oder eines Moduls kann man keine Methoden hinzufügen, entfernen oder ändern.
Einmal eingefroren gibt es kein Zurück: es gibt kein
Object#thaw
.
Man kann den Zustand eines Objekt prüfen mit
Object#frozen?
.
Was passiert, wenn man ein eingefrorenes Objekt kopiert? Das kommt auf die benutzte
Methode an. Wenn man die clone
-Methode eines Objekts benutzt, wird der
gesamte Objekt-Zustand (inklusive ob es eingefroren ist) in das neue Objekt kopiert.
Andererseits kopiert dup
üblicherweise nur den Inhalt eines Objekts --- das
neue Objekt erbt nicht den eingefrorenen Zustand.
str1 = "hello" |
||
str1.freeze |
» | "hello" |
str1.frozen? |
» | true |
str2 = str1.clone |
||
str2.frozen? |
» | true |
str3 = str1.dup |
||
str3.frozen? |
» | false |