Willkommen zu einer neuen Episode von Ruby Magic! In diesem Monat dreht sich alles um Metaklassen, ein Thema, das durch eine Diskussion zwischen zwei Entwicklern (Hi Maud!).
Durch die Untersuchung von Metaklassen lernen wir, wie Klassen- und Instanzmethoden in Ruby funktionieren. Entdecken Sie dabei den Unterschied zwischen der Definition einer Methode durch Übergeben eines expliziten „definee“ und der Verwendung von class << self
oder instance_eval
. Los geht’s!
Klasseninstanzen und Instanzmethoden
Um zu verstehen, warum Metaklassen in Ruby verwendet werden, untersuchen wir zunächst die Unterschiede zwischen Instanz- und Klassenmethoden.
In Ruby ist eine Klasse ein Objekt, das einen Blueprint zum Erstellen anderer Objekte definiert. Klassen definieren, welche Methoden für eine Instanz dieser Klasse verfügbar sind.
Wenn Sie eine Methode in einer Klasse definieren, wird eine Instanzmethode für diese Klasse erstellt. Jede zukünftige Instanz dieser Klasse wird diese Methode zur Verfügung haben.
1 2 3 4 5 6 7 8 9 10 11 12 |
class User def initialize(name) @name = name end def name @name end end user = User.new('Thijs') user.name # => "Thijs" |
In diesem Beispiel erstellen wir eine Klasse mit dem Namen User
mit einer Instanzmethode mit dem Namen #name
, die den Namen des Benutzers zurückgibt. Mit der Klasse erstellen wir dann eine Klasseninstanz und speichern sie in einer Variablen mit dem Namen user
. Da user
eine Instanz der Klasse User
ist, steht die Methode #name
zur Verfügung.
Eine Klasse speichert ihre Instanzmethoden in ihrer Methodentabelle. Jede Instanz dieser Klasse verweist auf die Methodentabelle ihrer Klasse, um Zugriff auf ihre Instanzmethoden zu erhalten.
Klassenobjekte
Eine Klassenmethode ist eine Methode, die direkt in der Klasse aufgerufen werden kann, ohne zuerst eine Instanz erstellen zu müssen. Eine Klassenmethode wird erstellt, indem ihrem Namen bei der Definition self.
vorangestellt wird.
Eine Klasse ist selbst ein Objekt. Eine Konstante bezieht sich auf das Klassenobjekt, sodass Klassenmethoden, die darauf definiert sind, von überall in der Anwendung aufgerufen werden können.
1 2 3 4 5 6 7 8 9 |
class User # ... def self.all end end User.all # => |
Methoden, die mit einem self.
-Präfix definiert sind, werden nicht zur Methodentabelle der Klasse hinzugefügt. Sie werden stattdessen zur Metaklasse der Klasse hinzugefügt.
Metaklassen
Abgesehen von einer Klasse hat jedes Objekt in Ruby eine versteckte Metaklasse. Metaklassen sind Singletons, dh sie gehören zu einem einzelnen Objekt. Wenn Sie mehrere Instanzen einer Klasse erstellen, verwenden sie dieselbe Klasse, haben jedoch alle separate Metaklassen.
1 2 3 4 5 6 7 8 9 |
thijs, robert, tom = User.all thijs.class # => User robert.class # => User tom.class # => User thijs.singleton_class # => #<Class:#<User:0x00007fb71a9a2cb0>> robert.singleton_class # => #<Class:#<User:0x00007fb71a9a2c60>> tom.singleton_class # => #<Class:#<User:0x00007fb71a9a2c10>> |
In diesem Beispiel sehen wir, dass, obwohl jedes der Objekte die Klasse User
hat, ihre Singleton-Klassen unterschiedliche Objekt-IDs haben, was bedeutet, dass sie separate Objekte sind.
Durch den Zugriff auf eine Metaklasse ermöglicht Ruby das Hinzufügen von Methoden direkt zu vorhandenen Objekten. Dadurch wird der Klasse des Objekts keine neue Methode hinzugefügt.
1 2 3 4 5 6 7 8 |
robert = User.new("Robert") def robert.last_name "Beekman" end robert.last_name # => "Beekman" User.new("Tom").last_name # => NoMethodError (undefined method `last_name' for #<User:0x00007fe1cb116408>) |
In diesem Beispiel fügen wir dem in der Variablen robert
gespeicherten Benutzer eine #last_name
hinzu. Obwohl robert
eine Instanz von User
ist, haben alle neu erstellten Instanzen von User
keinen Zugriff auf die Methode #last_name
, da sie nur in der Metaklasse von robert
vorhanden ist.
Was ist Selbst?
Wenn Sie eine Methode definieren und einen Empfänger übergeben, wird die neue Methode der Metaklasse des Empfängers hinzugefügt, anstatt sie der Methodentabelle der Klasse hinzuzufügen.
1 2 3 4 5 |
tom = User.new("Tom") def tom.last_name "de Bruijn" end |
Im obigen Beispiel haben wir #last_name
direkt zum Objekt tom
hinzugefügt, indem wir bei der Definition der Methode tom
als Empfänger übergeben haben.
So funktioniert es auch für Klassenmethoden.
1 2 3 4 5 6 7 |
class User # ... def self.all end end |
Hier übergeben wir beim Erstellen der Methode .all
explizit self
als Empfänger. In einer Klassendefinition bezieht sich self
auf die Klasse (in diesem FallUser
), sodass die .all
-Methode zur Metaklasse von User
hinzugefügt wird.
Da User
ein Objekt ist, das in einer Konstanten gespeichert ist, greifen wir jedes Mal auf dasselbe Objekt — und dieselbe Metaklasse — zu, wenn wir darauf verweisen.
Öffnen der Metaklasse
Wir haben gelernt, dass Klassenmethoden Methoden in der Metaklasse des Klassenobjekts sind. Wenn wir das wissen, werden wir uns einige andere Techniken zum Erstellen von Klassenmethoden ansehen, die Sie vielleicht schon einmal gesehen haben.
class << self
Obwohl es etwas aus der Mode gekommen ist, verwenden einige Bibliotheken class << self
, um Klassenmethoden zu definieren. Dieser Syntaxtrick öffnet die Metaklasse der aktuellen Klasse und interagiert direkt mit ihr.
1 2 3 4 5 6 7 8 9 10 11 |
class User class << self self # => #<Class:User> def all end end end User.all # => |
In diesem Beispiel wird eine Klassenmethode mit dem Namen User.all
erstellt, indem der Metaklasse User
eine Methode hinzugefügt wird. Anstatt explizit einen Empfänger für die Methode zu übergeben, wie wir zuvor gesehen haben, setzen wir self
auf die Metaklasse von User
anstelle von User
selbst.
Wie wir bereits gelernt haben, wird jede Methodendefinition ohne expliziten Empfänger als Instanzmethode der aktuellen Klasse hinzugefügt. Innerhalb des Blocks ist die aktuelle Klasse die Metaklasse (#<Class:User>
) von User
.
instance_eval
Eine andere Option ist die Verwendung von instance_eval
, was dasselbe mit einem großen Unterschied tut. Obwohl die Metaklasse der Klasse die im Block definierten Methoden empfängt, bleibt self
ein Verweis auf die Hauptklasse.
1 2 3 4 5 6 7 8 9 10 11 |
class User instance_eval do self # => User def all end end end User.all # => |
In diesem Beispiel definieren wir eine Instanzmethode für die Metaklasse von User
wie zuvor, aber self
zeigt immer noch auf User
. Obwohl es normalerweise auf dasselbe Objekt zeigt, können „default definee“ und self
auf verschiedene Objekte zeigen.
Was wir gelernt haben
Wir haben gelernt, dass Klassen die einzigen Objekte sind, die Methoden haben können, und dass Instanzmethoden tatsächlich Methoden in der Metaklasse eines Objekts sind. Wir wissen, dass class << self
einfach self
vertauscht, damit Sie Methoden für die Metaklasse definieren können, und wir wissen, dass instance_eval
größtenteils dasselbe tut (jedoch ohne self
zu berühren).
Obwohl Sie nicht explizit mit Metaklassen arbeiten, verwendet Ruby sie ausgiebig unter der Haube. Zu wissen, was passiert, wenn Sie eine Methode definieren, kann Ihnen helfen zu verstehen, warum sich Ruby so verhält (und warum Sie Klassenmethoden mit self.
voranstellen müssen).
Danke fürs Lesen. Wenn Ihnen das, was Sie gelesen haben, gefallen hat, können Sie Ruby Magic abonnieren, um eine E-Mail zu erhalten, wenn wir etwa einmal im Monat einen neuen Artikel veröffentlichen.