Tag Archives: Metaprogramming

隐式上下文 `cref’

不要盲目地认为当前的 `self’ 同时也决定了常量的上下文。Ruby 有三个隐式上下文:`self’、`klass’ 以及 `cref’。`self’ 和 `klass’,用一句话总结就是:`self’ 决定了调用未指定接收者的方法时的默认接收者以及如何解析当前作用域下的变量,而 `klass’ 决定了当前定义的方法属于哪个类或模块。而最后一个 `cref’,则是决定了当前的常量作用域,也就是在引用常量时决定在哪个类或模块查找,定义常量时决定它最终归属于哪个类或模块。再换句话说,就是决定了当前的常量命名空间。

先来看一个例子:

X = Class.new {}

这段代码中,Class.new 的块开启了一个新的作用域作为正在分配的 Class 对象的主体。很多人在这里就想当然地认为这个块相当于 class X … end,在块中所写的代码会和 class X … end 之间的代码有相同的效果,而这是错误的。在这个块中,只有 `self’ 和 `klass’ 这两个隐式上下文被设为了正在分配的这个 Class 对象本身,但 `cref’ 没有变。

X = Class.new do
  p self
  def foo
    p :foo
  end
end
 
x = X.new 
x.foo

这段代码输出了 Class 对象本身和 `:foo’,就是因为 `self’ 和 `klass’ 都指向了这个 Class 对象本身。然而,当你这样做的时候:

X = Class.new { Y = 1 } 
p X::Y

解释器抛出一个警告:

1.rb:5: warning: toplevel constant Y referenced by X::Y

这个警告是提醒用户,Y 是顶层常量,不需要用 X 来修饰它[1],直接引用 Y 就可以了。

很明显,这是因为当前的常量上下文没有因为程序执行进入了 Class.new 的块而改变,所以在块内部定义常量的时候,`cref’ 仍然是顶层。Object#instance_eval 和 Module#class_eval 影响的也只有 `self’ 和 `klass’,但不会改变 `cref’。那么什么才会使得 `cref’ 改变?只有语法上的 class … end 和 module … end 定义。

class X
  Y = 1
end

p X::Y         # => 1

那么如何不在不建立 class … end、module … end 结构的情况下显式指定常量归属?直接通过 `::’ 运算符修饰在某些场合下好使,但这在像 X = Class.new { … } 这样的场合下是不会成功的,因为在块执行的时候 X 还没有定义[2]。同时,这种方法是对 `X’ 这个符号做了硬性编码,但有时我们可能会需要让这个命名空间本身是不定的(变量),比如在 Class.new 的块中定义属于正在分配的这个 Class 对象的常量。这时,有一对很好用的方法就是你的朋友:Module#const_get 和 Module#const_set。

X = Class.new { const_set :Y, 1 }

p X::Y           # => 1
p X.const_get :Y # => 1

由于 const_get 和 const_set 的接收者就是它们操作常量时所使用的命名空间,动态性就实现了。

[1] 用 X 修饰常量则表示常量定义在 X 这个命名空间中。
[2] Class.new { … } 连块一起,整个返回后才会把返回值赋给 X,在此之前 X 都会保持未定义状态。

被调用者获取调用者绑定

在一个方法(被调用者)中如果想要获取调用者的绑定,最简单有效的方法自然是设置一个参数让调用者将其绑定传递过来。

def foo(binding)
  eval("local_variables", binding)
end

bar = 1

p foo(binding) # => [:bar]

然而这种途径会使得每次调用方法都多出一个额外的参数,有时可能会使方法调用显得不太美观。这时,我们可以采用传递块的方式来实现相同的功能——让调用者在调用被调用者时附带一个空的块,并让被调用者将其转换为一个 Proc 对象,然后通过 Proc#binding 获取到调用者的绑定。当然,如果被调用者本身就需要一个块,自然就不用专门在调用时附带一个 dummy 的块了。

def foo(&block)
  eval("local_variables", block.binding)
end

bar = 1

p foo {} # => [:bar]

可是这种方法仍然修改了调用方法的形式。难道在调用者不传递任何信息的情况下被调用者就无法获取调用者的绑定了吗?在 Ruby 语言层,似乎并没有现成的接口提供这样的功能,所以我们只能另辟蹊径。自行创建一个类似于调用栈的“绑定栈”来跟踪不同调用者的绑定就是一个可行的办法,虽然它带来了一定的开销。

class Binding
  @@call_binding_stack = [ nil, TOPLEVEL_BINDING ]
  class << self
    def caller_binding
      @@call_binding_stack[-2]
    end
    def push_caller_binding(binding)
      @@call_binding_stack.push(binding)
    end
    def pop_caller_binding
      @@call_binding_stack.pop
    end
  end
end

set_trace_func lambda { |event, file, line, id, binding, klass|
  return if klass == Binding
  case event
  when 'call'
    Binding.push_caller_binding(binding)
  when 'return'
    Binding.pop_caller_binding
  end
}

def foo
  eval("local_variables", Binding.caller_binding)
end

bar = 0

p foo # => [:bar]

Kernel#set_trace_func 可以让我们安装一个钩子,让解释器在所有方法被调用和返回时都回调我们的钩子过程,也就是这里的 lambda 闭包。方法调用时,我们就将当前的绑定压入绑定栈;方法返回时,我们就出栈。绑定栈初始时有两个元素,一个是 nil,这是因为顶层没有调用者;一个是 TOPLEVEL_BINDING,这是因为在顶层调用的方法,调用者自然是顶层本身。栈的顶部永远是当前(被调用者)的绑定,所以为了获取到调用者的绑定,我们需要返回顶部下面的第一个元素。

运行时移除方法

`Module#undef_method’ 是一个值得注意的方法。它的作用是将一个方法从方法分派的任务中移除,使得调用该方法者收到一个 `NoMethodError’ 异常(即 `obj.respond_to? :method => true’)。然而,`undef_method’ 并非是把方法彻底从类结构中移除了,它仅仅是将以符号表示的方法名所标识的方法标记为“未定义”,使得解释器在通过方法分派、反射等机制寻找这个方法时会立即抛出异常。这就逻辑蕴含了另一个事实:哪怕基类或是 mixed-in 模块有同名的方法,在派生类的同名方法被 `undefine_method’ 处理后,也不会尝试进行向上搜寻的方法分派。

`Module#undef_method’ 还有一个家族成员——`Module#remove_method’。它彻底从当前模块(即 `remove_method’ 的接收者。一个 `Module’ 的实例)结构中移除一个方法的引用。如此一来,方法分派函数在派生类中找不到一个方法,就会尝试在基类中寻找。

class Base
  def bar
    p :'base bar'
  end
end

class Derivation < Base
  def bar
    p :'derivation bar'
  end
end

d = Derivation.new

class Derivation
  remove_method :bar
end

d.bar                   # => :"base bar"

class Derivation
  undef_method :bar
end

d.bar                   # => NoMethodError

可见 `remove_method’ 后仍然能找到基类的 bar 方法,但 `undef_method’ 后却不能。

再演示另外一个和 eigenclass (Ruby 单例类)有关的例子:

obj = Object.new

def obj.foo
  p self
end
 
p obj.respond_to? :foo  # => true

class << obj
  undef_method :foo
end

p obj.respond_to? :foo  # => false

Marshal.dump(obj)       # => error

可以发现,仅仅是 `undef_method’ 并不会移除 eigenclass 中的 eigenmethod(单例方法),所以它不能被 `Marshal’ 模块 dump。当我们把 `undef_method’ 替换为 `remove_method’ 后,就可以 dump 了。

在方法被标记为未定义或彻底移除之前通过反射获取的该方法的 `Method’ 对象,在之后仍然能正常运作:

class Klass
  def foo
    p :'Klass#foo'
  end
end

obj = Klass.new

def obj.bar
  p :'obj.bar'
end

m_foo_unbound = Klass.instance_method :foo
m_foo = obj.method :foo
m_bar = obj.method :bar

class Klass
  remove_method :foo
end

class << obj
  remove_method :bar
end

p obj.respond_to? :foo        # => false
p obj.respond_to? :bar        # => false

m_foo_unbound.bind(obj).call  # => :"Klass#foo"
m_foo.call                    # => :"Klass#foo"
m_bar.call                    # => :"obj.bar"

这同时也演示了方法的绑定和方法分派相互独立的特性。

最后,Ruby 还提供了一个语法糖 `undef’ 以更简洁的语法(后面可以跟上一个纯粹的标识符,而无须符号或是其它字面值,类似与 `alias’ 这个语法糖)调用了和 Module#undef_method 相同的实现。`undef’ 是 Ruby 的关键字。

self 和 klass

这里有一篇 Yuki Sonoda (园田裕贵)的博文:http://yugui.jp/articles/846
其中介绍了三种隐式的上下文:self,klass,常量定义点(这个是我从英语直接翻译过来的)。下面的概念是直接从该博文中摘抄下来的,如果你能很自在地读英语,那推荐看原文,其中包含了例子代码。

所谓 “klass”,是指定义方法(不包括单例方法的定义)的默认(隐式)类,Yuki 称之为 “default definee”(默认被定义者)。比如,在任意处写下一个方法的定义(def foo; end),实际上是定义了某个类的实例方法,这个类就是这里说的 “klass”。Ruby 总是会保存这个类的一个引用,但官方的实现中并没有直接的方法获取这个类。

在 Ruby 顶层,定义一个方法,其 default definee 是 Object 类型。这个可以参考:https://szsu.wordpress.com/2009/11/07/top_level_object_kernel/

当你使用 class 的语法时,在 class … end 内部会产生一个新的上下文,这时 self 和 default definee 都会指向这个类本身。所以在内部定义的方法就是该类的实例方法,这个大家都知道。

如果你已经在一个方法主体中,那么 self 是这个方法的接收者,而 default definee 则是语法上的方法定义外部的最近的那个类。

class Klass
  def foo
    def bar
    end
  end
end

Klass.new.foo

p Klass.instance_method :bar # => #<UnboundMethod: Klass#bar>

这里 foo 也是 Klass 的实例方法。

当然,方法的定义还可以出现在其它地方,但无论在哪儿,都会有一个 default definee。比如 Yuki 给出的这个例子就是在处理方法参数默认值的时候定义的方法,其 default definee 仍然是语法上的外部的 Bar 类:

class Bar; end
$bar = Bar.new
class Baz
  def $bar.hoge(a = (def fuga; end))
    def piyo; end
  end
end
$bar.hoge

Baz.instance_method(:fuga)      # => #<UnboundMethod: Baz#fuga>
Baz.instance_method(:piyo)      # => #<UnboundMethod: Baz#piyo>

$bar.method(:fuga)              # raises a NameError "undefined method `fuga' for class `Bar'"
$bar.method(:piyo)              # raises a NameError "undefined method `piyo' for class `Bar'"

一句话:class 的定义改变了 default definee,而方法定义不会改变。

instance_eval 和 class_eval 的区别,也是在于对 default definee 的处理上。由于 class_eval 是 Module 类的实例方法,所以下面进行测试使用的接收者是一个兼容 Module 类型的实例(其运行时类型是 Class 类型),否则 class_eval 这个方法压根儿没法用。另:module_eval 只是 class_eval 的一个别名,但如果按照 Ruby 编程的习俗,我们通常会对一个可能是 Module 类型的对象则使用 module_eval,而其余场合则使用 class_eval。

其实你需要记住的就一句话:在 instance_eval 的块内部,self 会指向 instance_eval 的接收者,而 default definee 会成为接收者的 eigenclass(关于 eigenclass 可以参考 73 楼);在 class_eval 的块内部,self 和 default definee 都会成为 class_eval 的接收者。

class Klass
end

Klass.instance_eval do
  def class_method
    p :class_method
  end
  def self.another_class_method
    p :another_class_method
  end
end

Klass.class_eval do
  def instance_method
    p :instance_method
  end
  def self.yet_another_class_method
    p :yet_another_class_method
  end
end

k = Klass.new
k.instance_method               # => :instance_method
Klass.class_method              # => :class_method
Klass.another_class_method      # => :another_class_method
Klass.yet_another_class_method  # => :yet_another_class_method

我们一步一步地来看。

首先,我们在 instance_eval 的块内部定义了一个普通方法 class_method,这是实例方法的定义方式。什么?你问我为什么把它命名为“类方法”(class_method)?别忘了之前的那句加粗的话。在 instance_eval 的块内部,default definee 会成为 instance_eval 的接收者的 eigenclass。此处,instance_eval 的接收者是 Klass,所以我们定义的 class_method 方法实际上就成为了 Klass 的 eigenclass 的实例方法,这用以前的术语来说,就是定义了 Klass 这个对象(Klass 是 Class 类型)的单例方法(singleton method)。而我们知道,Class 对象的单例方法,实际上就是传统意义上的“类方法”,也就类似于 Java、C# 中的类静态方法。所以此处的 class_method 就成为了 Klass 类的类方法,在最后的输出语句中,可以直接通过 Klass 这个 Class 对象来调用 class_method 方法,而不需要 Klass 的实例。

之后我们又定义了一个 another_class_method,这次是用定义单例方法的方式定义的,因为在方法名前多了一个接收者 self。之前说了 instance_eval 的块内部的 self 会指向 instance_eval 的接收者,也就是这里的 Klass,所以 self 实际上就是 Klass 这个 Class 对象。我们对 Klass 这个对象定义单例方法,那就相当于定义了 Klass 的 eigenclass 的实例方法,所以和上面定义 class_method 的上下文没有区别,都是 Klass 的 eigenclass,故而他们都是 Klass 的类方法。

接着我们进入了 class_eval,然后定义了一个 instance_method。再次回顾之前被加粗的那句话,我们就知道,class_eval 的块的内部的 default definee 会成为 class_eval 的接收者,在这里就是 Klass 本身,所以我们定义的 instance_method 就成为了 Klass 的实例方法。在最后的输出语句中,可以看到通过 Klass 的实例 k 调用 instance_method。

最后我们又通过单例方法定义的形式定义了一个 yet_another_class_method 方法。class_eval 的块内部的 self 仍然是指向 class_eval 的接收者,也就还是 Klass 本身,所以 self 仍然是指向 Klass。如此一来,这个方法定义又成了给 Klass 这个对象定义单例方法,所以和 class_method、another_class_method 一样都是 Klass 的类方法,故而命名为 yet_another_class_method。

所以,千万不要被 instance_eval 和 class_eval 的名字迷惑。在 Module#instance_eval 中定义的普通方法,反而会成为该 Module 的类方法;在 Module#class_eval 中定义的普通方法,反而会成为该 Module 的实例方法。

特征类

obj = Object.new
class << obj
end

你知道这段代码中的 class 是什么吗?这段代码中的 class 定义的实际上就是 obj 这个对象的“单例类”。本帖主要是针对对 Ruby 单例类已经有了正确概念的朋友,否则可能不知所云。

Ruby 社区就 Ruby 的“单例类”这个术语的命名曾有过许多争议。至今,Ruby 的创造者 Matz 也没有宣布一个正式的、官方的名称。在大部分书籍,包括《The Ruby Programming Language》中,都使用了“单例类”(Singleton class)这个术语,故而“单例类”也就成了 Ruby 的“单例类”概念的 de facto(约定俗成的)命名。除了“单例类”这个命名以外,主要还有另外两种命名:“元类”(Metaclass)和“特征类”(Eigenclass)。

“元类”实际上在更早的时候就出现了。Smalltalk、Java、C#、Python 中都有被称为“元类”的概念。然而,这些语言中的元类和 Ruby 的“单例类”颇有不同。传统意义上的元类,说白了,就是类的类,是用来描述一种类的类型,其实例就是一个类。这相当于 Ruby 中的 Class 类——所有对象的类的类型都是这个 Class 类的实例。这当然和“单例类”风马牛不相及,所以以“元类”来命名 Ruby “单例类”的概念并不合适。

“特征类”是一个很好的命名。这个“特征”和线性代数中“特征值”、“特征向量”的“特征”是一个意思。“特征值”、“特征向量”的名称是由德国数学家 David Hilbert (大卫·希尔伯特)在 1904 年使用并得以流传的,德语单词“Eigen”本意为“自己的”。Eigenclass,也就意味着“自己的类”,用于 Ruby 的“单例类”概念之上十分贴切,因为“单例类”实际上就是一个对象独有的类。

我个人认为使用最广泛的“单例类”这个名称并不好,因为这很容易和著名的单例类设计模式混淆。单例类设计模式是在设计类的时候保证其实例只有一个,而 Ruby 的“单例类”却是先有一个可有多个实例的类型,比如本帖开头的 Object 类型,而后对其某个实例进行“单例的类”(及其方法)的创建。这么一想,是不是觉得“特征类”更加贴切了(特别是对于专精于线性代数的你)?

Eigenclass 这个术语应该算是 Ruby 社员的发明创造。上文提到的“特征类”是我直接从线性代数中 eigenvalue、eigenvector 取来的,也并非是正式的、官方的翻译。就目前来说,使用 eigenclass 的英语原型是最不会引起歧义的。

关于 eigenclass 的命名,更详细的讨论参见:http://www.ruby-forum.com/topic/571597