Класс-объекты, одиночные классы - PullRequest
10 голосов
/ 25 декабря 2011

Я играю с метапрограммированием в ruby, и у меня есть вопрос. У меня есть класс:

class Klass
  class << self
    @x = "yeah"
  end
end
b = Klass.new
a = class << Klass; self; end
a.instance_eval "@x"           #=> yeah
Klass.instance_eval "@x"       #=> nil

Почему? В переменной a у меня есть одноэлементный класс, верно? И Klass.instance_eval exec в контексте одноэлементного класса:

Klass.instance_eval "def yeah; puts 10; end"
Klass.yeah                     #=> 10

Кроме того, Klass в интерпретаторе указывает на контекст класса, да? И a указывает на контекст одноэлементного класса? А что указывает a.class_eval и a.instance_eval? Я делаю:

a.instance_eval "def pops; puts 0; end"
a.class_eval "def popsx; puts 1; end"
a.pops                         #=> 0
a.popsx                        # FAIL
Klass.pops                     # FAIL
Klass.popsx                    #=> 1
b.pops; b.popsx                # DOUBLE FAIL

и я этого не понимаю. Спасибо!

Ответы [ 2 ]

3 голосов
/ 26 декабря 2011

Во-первых, хотя кажется, что eigentclass используется некоторыми людьми, singleton class - более распространенный термин. Класс Singleton содержит объектно-ориентированное поведение для объекта в Ruby. Вы не можете создавать другие экземпляры этого класса, кроме исходного объекта, которому принадлежит этот класс-одиночка.

Говоря об определении методов внутри различных типов eval в этой статье вводится хорошее правило для методов, определенных в instance_eval и class_eval:

Use ClassName.instance_eval to define class methods.
Use ClassName.class_eval to define instance methods.

Это в значительной степени описывает ситуацию.

Была огромная рецензия на классы, которые являются экземплярами класса Class, их одноэлементные классы, которые являются подклассами класса Class, и некоторые другие сумасшедшие вещи (не слишком связанные с проблемой). Но так как ваш вопрос может быть легко применен к обычным объектам и их классам (и это значительно упрощает объяснение), я решил удалить все это (хотя вы все еще можете видеть это в истории изменений ответа).

Давайте посмотрим на обычный класс и экземпляр этого класса и посмотрим, как все это работает:

class A; end
a = A.new

Определения методов внутри различных типов eval:

# define instance method inside class context
A.class_eval { def bar; 'bar'; end }
puts a.bar     # => bar
puts A.new.bar # => bar

# class_eval is equivalent to re-opening the class
class A
  def bar2; 'bar2'; end
end
puts a.bar2     # => bar2
puts A.new.bar2 # => bar2

Определение специфичных для объекта методов:

# define object-specific method in the context of object itself
a.instance_eval { def foo; 'foo'; end }
puts a.foo # => foo

# method definition inside instance_eval is equivalent to this
def a.foo2; 'foo2'; end
puts a.foo2 # => foo2

# no foo method here
# puts A.new.foo # => undefined method `foo' for #<A:0x8b35b20>

Давайте теперь посмотрим на одноэлементный класс объекта a:

# singleton class of a is subclass of A
p (class << a; self; end).ancestors
# => [A, Object, Kernel, BasicObject]

# define instance method inside a's singleton class context
class << a
  def foobar; 'foobar'; end;
end
puts a.foobar # => foobar

# as expected foobar is not available for other instances of class A
# because it's instance method of a's singleton class and a is the only
# instance of that class
# puts A.new.foobar # => undefined method `foobar' for #<A:0x8b35b20>

# same for equivalent class_eval version
(class << a; self; end).class_eval do
  def foobar2; 'foobar2'; end;
end
puts a.foobar2 # => foobar2

# no foobar2 here as well
# puts A.new.foobar2 # => undefined method `foobar2' for #<A:0x8b35b20>

Теперь давайте посмотрим на переменные экземпляра:

# define instance variable for object a
a.instance_eval { @x = 1 }

# we can access that @x using same instance_eval
puts a.instance_eval { @x } # => 1
# or via convenient instance_variable_get method
puts a.instance_variable_get(:@x) # => 1

А теперь для экземпляра переменных внутри class_eval:

# class_eval is instance method of Module class
# so it's not available for object a
# a.class_eval { } # => undefined method `class_eval' for #<A:0x8fbaa74>

# instance variable definition works the same inside
# class_eval and instance_eval
A.instance_eval { @y = 1 }
A.class_eval    { @z = 1 }

# both variables belong to A class itself
p A.instance_variables # => [:@y, :@z]

# instance variables can be accessed in both ways as well
puts A.instance_eval { @y } # => 1
puts A.class_eval    { @z } # => 1

# no instance_variables here
p A.new.instance_variables # => []

Теперь, если вы замените класс A на класс Class и объект a на объект Klass (что в данной конкретной ситуации является не чем иным, как экземпляром класса Class), я надеюсь, вы получите объяснение на ваши вопросы. Если у вас все еще есть вопросы, не стесняйтесь спрашивать.

3 голосов
/ 26 декабря 2011

Трудно полностью ответить на ваш вопрос (для полного объяснения модели класса Руби посмотрите на превосходную презентацию Дейва Томаса ), тем не менее:

С class_eval вы фактически определяете методы экземпляра - это как если бы вы были внутри тела класса. Например:

class Klass
end

Klass.class_eval do
  def instance_method
    puts 'instance method'
  end
end

obj = Klass.new
obj.instance_method  # instance method

С instance_eval вы фактически определяете методы класса - это как если бы вы были внутри тела синглтон-класса (eigenclass) данного объекта (не говоря уже о том, что классы тоже являются объектами в Ruby). Например:

Klass.instance_eval do
  def class_method
    puts 'class method'
  end
end

Klass.class_method  # class method

А в вашем случае:

Klass.instance_eval "@x" не работает, потому что @x не является частью Klass, это часть синглтон-класса Klass:

class Klass
  class << self
    @x = "yeah"
  end

  puts @x
end

# prints nothing

a.instance_eval "@x" работает нормально, потому что вы оцениваете "@x" в контексте одноэлементного класса a, который связан с одноэлементным классом класса Klass, в котором вы определили переменную экземпляра @x. Как одноэлементные классы могут быть взаимосвязаны, можно проиллюстрировать на этом примере:

class Foo
end

f = class << Foo; self; end
g = class << Foo; self; end

f.instance_eval "def yeah; puts 'yeah'; end"

g.yeah  # yeah

g.instance_eval "def ghee; puts 'ghee'; end"

f.ghee  # ghee

Klass.instance_eval "def yeah; puts 10; end" определяет «нормальный» метод класса. Поэтому Klass.yeah работает нормально (см. Klass.class_method в предыдущем примере).

a.instance_eval "def pops; puts 0; end" определяет метод класса для одноэлементного класса a. Следовательно, a.pops фактически означает вызов метода класса pops (опять же, как если бы вы вызывали Klass.class_method).

a.popsx не работает, потому что вам сначала нужно создать экземпляр a, чтобы иметь возможность вызывать popsx для него (но невозможно создать новый экземпляр одноэлементного класса).

Klass.pops не работает, потому что не существует метода pops, определенного в синглтон-классе Klass (pops определен в синглтон-классе a).

Klass.popsx работает, потому что с a.class_eval "def popsx; puts 1; end" вы определили метод экземпляра popsx, который затем вызываете для объекта Klass. В некотором смысле это похоже на этот пример:

class Other
end

o = Other.new

Other.class_eval "def yeah; puts 'yeah'; end"

o.yeah  # yeah

Надеюсь, это поможет.

...