Benvenuti in un nuovo episodio di Ruby Magic! L’edizione di questo mese è interamente dedicata ai metaclassi, un argomento scatenato da una discussione tra due sviluppatori (Hi Maud!).
Esaminando le metaclassi, impareremo come funzionano i metodi di classe e istanza in Ruby. Lungo la strada, scopri la differenza tra la definizione di un metodo passando un “definee” esplicito e l’utilizzo di class << self
o instance_eval
. Andiamo!
Istanze di classe e metodi di istanza
Per capire perché i metaclassi vengono utilizzati in Ruby, inizieremo esaminando quali sono le differenze tra i metodi di istanza e di classe.
In Ruby, una classe è un oggetto che definisce un progetto per creare altri oggetti. Le classi definiscono quali metodi sono disponibili su qualsiasi istanza di quella classe.
La definizione di un metodo all’interno di una classe crea un metodo di istanza su quella classe. Qualsiasi istanza futura di quella classe avrà quel metodo disponibile.
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 questo esempio, creiamo una classe denominata User
, con un metodo di istanza denominato #name
che restituisce il nome dell’utente. Usando la classe, creiamo quindi un’istanza di classe e la memorizziamo in una variabile denominata user
. Poiché user
è un’istanza della classe User
, è disponibile il metodo #name
.
Una classe memorizza i suoi metodi di istanza nella sua tabella dei metodi. Qualsiasi istanza di quella classe si riferisce alla tabella dei metodi della sua classe per ottenere l’accesso ai suoi metodi di istanza.
Oggetti di classe
Un metodo di classe è un metodo che può essere chiamato direttamente sulla classe senza dover creare prima un’istanza. Un metodo di classe viene creato prefissando il suo nome con self.
quando lo si definisce.
Una classe è essa stessa un oggetto. Una costante si riferisce all’oggetto class, quindi i metodi di classe definiti su di esso possono essere chiamati da qualsiasi punto dell’applicazione.
1 2 3 4 5 6 7 8 9 |
class User # ... def self.all end end User.all # => |
I metodi definiti con un prefisso self.
-non vengono aggiunti alla tabella dei metodi della classe. Sono invece aggiunti alla classe ‘ metaclass.
Metaclassi
A parte una classe, ogni oggetto in Ruby ha una metaclasse nascosta. Le metaclassi sono singleton, nel senso che appartengono a un singolo oggetto. Se crei più istanze di una classe, condivideranno la stessa classe, ma avranno tutte metaclassi separate.
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 questo esempio, vediamo che sebbene ciascuno degli oggetti abbia la classe User
, le loro classi singleton hanno ID oggetto diversi, il che significa che sono oggetti separati.
Avendo accesso a una metaclasse, Ruby consente di aggiungere metodi direttamente agli oggetti esistenti. In questo modo non verrà aggiunto un nuovo metodo alla classe dell’oggetto.
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 questo esempio, aggiungiamo un #last_name
all’utente memorizzato nella variabile robert
. Sebbene robert
sia un’istanza di User
, qualsiasi istanza appena creata di User
non avrà accesso al metodo #last_name
, poiché esiste solo sulla metaclasse di robert
.
Che cosa è sé?
Quando si definisce un metodo e si passa un ricevitore, il nuovo metodo viene aggiunto alla metaclasse del ricevitore, invece di aggiungerlo alla tabella dei metodi della classe.
1 2 3 4 5 |
tom = User.new("Tom") def tom.last_name "de Bruijn" end |
Nell’esempio sopra, abbiamo aggiunto #last_name
direttamente sull’oggetto tom
, passando tom
come ricevitore quando si definisce il metodo.
Questo è anche il modo in cui funziona per i metodi di classe.
1 2 3 4 5 6 7 |
class User # ... def self.all end end |
Qui, passiamo esplicitamente self
come ricevitore quando si crea il metodo .all
. In una definizione di classe, self
si riferisce alla classe (User
in questo caso), quindi il metodo .all
viene aggiunto alla metaclasse di User
.
Poiché User
è un oggetto memorizzato in una costante, accederemo allo stesso oggetto—e alla stessa metaclasse—ogni volta che lo facciamo riferimento.
Aprendo la Metaclasse
Abbiamo imparato che i metodi di classe sono metodi nella metaclasse dell’oggetto di classe. Sapendo questo, vedremo alcune altre tecniche di creazione di metodi di classe che potresti aver visto prima.
class <<self
Sebbene sia un po ‘ fuori moda, alcune librerie usano class << self
per definire i metodi di classe. Questo trucco di sintassi apre la metaclasse della classe corrente e interagisce direttamente con essa.
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 # => |
Questo esempio crea un metodo di classe denominato User.all
aggiungendo un metodo alla metaclasse di User
. Invece di passare esplicitamente un ricevitore per il metodo come abbiamo visto in precedenza, abbiamo impostato self
su User
la metaclasse invece di User
stessa.
Come abbiamo appreso prima, qualsiasi definizione di metodo senza un ricevitore esplicito viene aggiunta come metodo di istanza della classe corrente. All’interno del blocco, la classe corrente è la metaclasse di User
(#<Class:User>
).
instance_eval
Un’altra opzione è usare instance_eval
, che fa la stessa cosa con una grande differenza. Sebbene la metaclasse della classe riceva i metodi definiti nel blocco, self
rimane un riferimento alla classe principale.
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 questo esempio, definiamo un metodo di istanza sulla metaclasse di User
proprio come prima, ma self
punta ancora a User
. Sebbene di solito punti allo stesso oggetto, “default definee” e self
possono puntare a oggetti diversi.
Cosa abbiamo imparato
Abbiamo imparato che le classi sono gli unici oggetti che possono avere metodi e che i metodi di istanza sono in realtà metodi sulla metaclasse di un oggetto. Sappiamo che class << self
semplicemente scambia self
per consentire di definire metodi sulla metaclasse, e sappiamo che instance_eval
fa per lo più la stessa cosa (ma senza toccare self
).
Sebbene non lavorerai esplicitamente con metaclassi, Ruby li usa ampiamente sotto il cofano. Sapere cosa succede quando definisci un metodo può aiutarti a capire perché Ruby si comporta come fa (e perché devi prefisso metodi di classe con self.
).
Grazie per la lettura. Se ti è piaciuto quello che hai letto, ti piacerebbe iscriverti a Ruby Magic per ricevere una e-mail quando pubblichiamo un nuovo articolo circa una volta al mese.