self, Classes Singleton, e Por Que Métodos de Classe Não Existem de Verdade
Objective
self é uma variável interna somente leitura que controla exatamente duas
coisas: de onde uma @var nua é lida e onde é escrita, e quem recebe uma
chamada de método escrita sem receptor explícito. Todo objeto também pode
ter uma classe singleton: uma classe anônima inserida diretamente acima
dele, guardando métodos que pertencem só àquele objeto. Junte esses dois
fatos e "métodos de classe" deixam de ser um recurso especial: dentro de
class Foo, self é o objeto classe Foo, então def self.bar é só um
método singleton definido em um objeto particular que por acaso é uma
classe.
Use Cases
- Depurar a surpresa clássica de
class Foo; @count = 0; end, onde a ivar é invisível de todo método de instância porque mora no objeto classe. - Escrever configuração de nível de classe para uma gem ou objeto de serviço
(
class << self; attr_accessor :logger; end) em vez de recorrer a globais. - Anexar comportamento a exatamente um objeto (um stub em um teste, um
handler de callback avulso) com
def obj.methodouobj.extend(SomeModule). - Explicar por que métodos de classe são herdados por subclasses sem ninguém escrever código para fazer isso acontecer.
- Construir classes em tempo de execução com
Struct.new,Data.define, ouClass.newe atribuí-las a constantes, fazer subclasse delas, ou reabri-las com um bloco.
Deep Dive
O que self de fato controla
plaintextclass Counter def initialize = @count = 0 def increment @count += 1 # @count is looked up on self, the Counter instance report # no receiver: sent to self self # the object the method was called on end private def report = puts("count is now #{@count}") end c = Counter.new c.increment # prints "count is now 1"
Chamar c.increment define self como c durante o método, e restaura o
self anterior quando retorna. Essa única regra explica os dois
comportamentos acima: @count resolve contra as próprias variáveis de
instância de c, e a chamada sem receptor para report é enviada para c
(que também é o motivo de um método private poder ser chamado dessa forma;
não há receptor explícito).
A armadilha do corpo de classe: self é o objeto classe
Dentro de um corpo de classe ou módulo, fora de qualquer definição de
método, self é o próprio objeto classe. Então uma atribuição ali cria uma
variável de instância na classe, não em instâncias futuras:
plaintextclass Config @setting = "class-level" def setting = @setting # self here is a Config *instance* def self.setting = @setting # self here is the Config *class* end Config.new.setting # => nil Config.setting # => "class-level"
Os dois métodos contêm o texto-fonte idêntico @setting, e leem duas
variáveis diferentes, porque rodam com dois valores diferentes de self. O
nil de Config.new.setting não é um erro; uma variável de instância não
definida simplesmente lê como nil, o que é o motivo de esse bug ser
silencioso em vez de barulhento.
Métodos singleton e a classe singleton
def obj.method diz ao Ruby para criar uma classe anônima específica para
obj (a classe singleton, também chamada eigenclass), colocar o método
ali, e tornar a classe original de obj a superclasse dessa classe
singleton:
plaintextgreeting = "hello" def greeting.shout = upcase + "!" greeting.shout # => "HELLO!" "hello".shout # NoMethodError, a different String object greeting.singleton_class # => #<Class:#<String:0x000000010a3c4d20>> greeting.singleton_class.superclass # => String greeting.singleton_methods # => [:shout]
class << obj é a sintaxe alternativa para abrir essa mesma classe
singleton, e é a que se usa ao definir vários métodos de uma vez. Dentro
desse bloco, self é a própria classe singleton:
plaintextclass << greeting def whisper = downcase def shout = upcase + "!!!" end
Isso também é o que extend faz por baixo dos panos:
obj.extend(SomeModule) mistura o módulo na classe singleton de obj, o
que é o motivo de os métodos do módulo ficarem disponíveis naquele único
objeto e em nenhum outro lugar.
"Métodos de classe" são métodos singleton no objeto classe
Como self dentro de class Foo é o objeto Foo, essas três definições
são a mesma coisa escrita de três formas:
plaintextclass Registry def self.register(x) = entries << x # most common end def Registry.register(x) = entries << x # explicit receiver, identical result class Registry class << self def register(x) = entries << x # opening the singleton class directly end end
Não existe nenhum armazenamento separado de "método de classe" em lugar
nenhum do interpretador; as três colocam register na tabela de métodos de
Registry.singleton_class. E como o Ruby mantém a hierarquia de classes
singleton paralela à hierarquia de classes normal, a herança de método de
classe surge de graça:
plaintextclass Base def self.describe = "I am #{name}" end class Child < Base; end Child.describe # => "I am Child" Child.singleton_class.superclass == Base.singleton_class # => true
Child.describe é encontrado pela busca de lookup comum, só que começando a
partir da classe singleton de Child em vez de a partir de Child. Note
que self dentro de describe é Child quando chamado dessa forma, que é
o que faz name retornar "Child".
O idioma class << self; attr_accessor; end
Como attr_accessor define métodos em qualquer que seja self no momento,
chamá-lo dentro de class << self os define na classe singleton, produzindo
getters e setters para variáveis de instância de nível de classe:
plaintextclass HttpClient class << self attr_accessor :base_url, :timeout end self.timeout = 5 def initialize(url: self.class.base_url) = @url = url end HttpClient.base_url = "https://api.example.com" HttpClient.base_url # => "https://api.example.com" HttpClient.timeout # => 5
Escrito como um attr_accessor :base_url simples no corpo da classe, você
teria acessores em nível de instância em vez disso; mesmo método, self
diferente, lugar diferente onde os métodos caem.
Visibilidade de um método herdado
Mudar a visibilidade de um método herdado em uma subclasse funciona sem tocar no pai:
plaintextclass Parent private def secret = "shh" end class Child < Parent public :secret # now callable with an explicit receiver on Child instances end
O Ruby lida com isso inserindo um método proxy oculto em Child que
simplesmente chama super com a nova visibilidade. Parent#secret em si
fica intocado, então outras subclasses (e instâncias de Parent) mantêm a
visibilidade original.
Classes são objetos, então qualquer coisa que retorne uma classe funciona
Struct.new, Data.define, e Class.new são chamadas de método comuns que
retornam objetos classe de verdade. Em qualquer lugar onde o Ruby espera
uma classe, uma expressão que avalia para uma é igualmente válida:
plaintext# Assigned to a constant, the class picks up the constant's name Person = Struct.new(:name, :address) do def to_s = "#{name} of #{address}" end # Used directly as a superclass class Employee < Struct.new(:name, :salary) def annual = salary * 12 end # Immutable value object (Ruby 3.2+) Point = Data.define(:x, :y) p1 = Point.new(x: 1, y: 2) p2 = p1.with(y: 9) # => #<data Point x=1, y=9>, a new instance p1.to_h # => {x: 1, y: 2} # Fully dynamic audit_log = Class.new(Employee) do def self.kind = "audit" end audit_log.name # => nil, it's anonymous AuditLog = audit_log AuditLog.name # => "AuditLog", naming happens on constant assignment
Uma classe anônima fica sem nome até ser atribuída a uma constante, momento
em que ela adota permanentemente o nome daquela constante. Se você quer um
nome legível para depuração sem introduzir uma constante,
Module#set_temporary_name (Ruby 3.3+) dá um que uma atribuição de
constante posterior ainda pode sobrescrever.
⚠️ Livro vs. hoje (Ruby 3.4+): o livro imprime hashes da forma antiga, então os exemplos mostram a saída de
to_hcomo{:x=>1, :y=>2}. O Ruby 3.4 mudouHash#inspectpara renderizar chaves symbol na forma abreviada, então a mesma chamada agora imprime{x: 1, y: 2}. Só a exibição mudou (o hash é idêntico), mas isso vai morder qualquer teste que faça asserção sobre a saída deinspectou sobre uma string de hash interpolada.
Trade-offs
- Uma variável de instância de nível de classe não é herdada, mesmo que o
método de classe que a lê seja.
@settingmora em um objeto classe específico, então uma subclasse começa comnilpara ela:
Essa é exatamente a lacuna que oplaintextclass Base @format = :json def self.format = @format end class Child < Base; end Base.format # => :json Child.format # => nil, Child is a different object with its own ivarsclass_attributedo Rails existe para preencher; em Ruby puro você ou aceita isso ou escreve um hookinheritedpara copiar o valor para baixo. privatese comporta diferente para métodos de classe do que para métodos de instância. Umprivatenu no corpo da classe não tem efeito em métodosdef self.definidos depois dele; você precisa deprivate_class_method :name, ou umprivatedentro de um blococlass << self, que é mais um motivo para preferir a formaclass << selfquando vários métodos de classe estão envolvidos.- Métodos singleton por objeto tornam objetos não uniformes, e isso tem
custos reais. São invisíveis ao ler a definição da classe, não aparecem
em
SomeClass.instance_methods, e derrotam o cache de método baseado em classe do interpretador para aquele objeto. Também simplesmente não existem para alguns valores: objetos imediatos e objetos congelados os rejeitam.
Reserve-os para casos genuinamente avulsos (stubs de teste, uma única instância configurada); quando vários objetos precisam do comportamento, um módulo ou uma subclasse é mais claro.plaintextn = 42 def n.double = self * 2 # TypeError: can't define singleton s = "frozen".freeze def s.shout = upcase # TypeError: can't define singleton class << selfé poderoso, mas opaco para iniciantes. Para um único método de classe,def self.foocomunica a intenção sem exigir nenhum vocabulário de metaprogramação; a sintaxe de classe singleton se justifica quando você precisa de controle de visibilidade,attr_accessor, ou um bloco de várias definições.