我最近考虑了很多元编程(Metaprogramming)的问题,并希望看到更多这方面技术的例子和讲解。无论好坏,元编程已经进入Ruby社区,并成为完成各种任务和简化代码的标准方式。既然找不到这类资源,我准备抛砖引玉写一些通用Ruby技术的文章。这些内容可能对从其它语言转向Ruby或者还没有体验到Ruby元编程乐趣的程序员非常有用。
1. 使用单例类 Use the singleton-class
许多操作单个对象的方法是基于操作其单例类(singleton class),并且这样可以使元编程更简单。获得单例类的经典方法是执行如下代码:
RCR231建议这样定义Kernel#singleton_class方法:
我会在下文使用这个方法。
2. DSL的使用类方法来修改子类 Write DSL's using class-methods that rewrite subclasses
当你想创建一个DSL来定义类信息时,最常见的问题是怎样表示信息来让框架的其它部分使用。以定义一个ActiveRecord模型对象为例:
在这个例子中,令人感兴趣的是set_table_name的使用。这是怎么起作用的呢?好吧,这里涉及到一个小魔法。这是一种实现方法:
这里令人感兴趣的是define_attr_method。在这个例子中我们需要获得Product类的单例类,但又不想修改ActiveRecord::Base。通过使用单例类我们达到了这个目的。我们为原来的方法取别名,再定义新的存取器(accessor)来返回值。如果ActiveRecord需要table name就可以直接调用存取器。这种动态创建方法和存取器的技术在单例类是很常见的,特别是Rails。
3. 动态创建class和module Create classes and modules dynamically
Ruby允许你动态创建和修改class和module。你可以在没有冻结的class或module上做任何修改。特定情况下会很有用。Struct类可能是最好的例子:
这会创建一个新类,并赋给PersonVO,然后创建一个类的实例。从草稿创建新类并定义新方法也很简单:
除了Struct,还能在SOAP4R和Camping找到轻松创建类的例子。Camping尤其令人感兴趣,因为它有专门的方法创建这些类,被你的controller和view继承。Camping的许多有趣的功能都是用这种方式实现的:
这使得可以这样创建controller:
你也可以这样创建module,然后在类中包含module。
4. 使用method_missing来做有趣的事 Use method_missing to do interesting things
除了闭包(block),method_missing可能是Ruby最强大的特性,也是最容易滥用的一个。用好method_missing的话有些代码会变得超级简单,甚至是不能缺少。一个好的例子(Camping)是扩展Hash:
就可以这样使用hash:
如你所见,如果有人调用了一个hash不存在的方法,则会搜索内部集合。如果方法名以=结尾,则会赋给同名的key。
Markaby中可以找到另一个很好的method_missing技巧。以下引用的代码可以生成任何包含CSS class的XHTML标签:
会生成:
绝大多数这种功能,特别是CSS class名是通过method_missing设置了self的属性然后返回self。
5. 方法模式的调度 Dispatch on method-patterns
这对于无法预测的方法来说可以轻松的达到可扩展性。我最近创建了一个小型验证框架,核心的验证类会找出自身所有以check_开头的方法并调用,这样就可以轻松地增加新的验证:只要往类或实例中添加新方法。
这非常简单,并且难以置信的强大。可以看一下Test::Unit到处使用这种方法。
6. 替换方法 Replacing methods
有时候一个方法的实现不是你要的,或者只做了一半。标准的面向对象方法是继承并重载,再调用父类方法。仅当你有对象实例化的控制权时才有用,经常不是这种情况,继承也就没有价值。为得到同样的功能,可以重命名(alias)旧方法,并添加一个新的方法定义来调用旧方法,并确保旧方法的前后条件得到保留。
一个极端的用法是临时修改一个方法,然后再还原。例如:
这个例子展示了编写add_tracing和remove_tracing的一种典型方法。它依赖于第1条的单例类:
如果这些方法是添加到module(有一点点不同,看你能不能写出来!),你也可以在类而非实例上添加和删除tracing。
7. 使用nil类来引入空对象的重构 Use NilClass to implement the Introduce Null Object refactoring
在Fowler的重构中,“引入空对象”的重构是一个对象要么存在,要么为空时有一个预定义值。典型例子如下:
目前基于Java的重构会推荐创建一个类似于null的子类。例如NullPerson会继承Person,重载name方法总是返回"default name"。但是在Ruby中我们可以打开类,可以这样做:
8. 学习eval的不同版本 Learn the different versions of eval
Ruby有几种版本的执行方法(evaluation)。了解它们的区别和使用情景是很重要的。有eval、instance_eval、module_eval和class_eval几种。首先,class_eval是module_eval的别名。其次,eval和其他的有些不同。最重要的是eval只能够执行一个字符串,其它的可以执行block。这意味着eval是你做任何事的最后选择,它有它的用处,但绝大多数情况下应该用instance_eval和module_eval执行block。
eval会在当前环境执行字符串,除非环境已经提供绑定(binding)。(见第11条)
instance_eval会在接收者(reveiver)的上下文中执行字符串或block,没有指定的话self会作为接收者。
module_eval会在调用的module的上下文中执行字符串或block。这个比较适合在module或单例类中定义新方法。instance_eval和module_eval的主要区别在于定义的方法会放在哪里。如果你用String.instance_eval定义foo方法会得到String.foo,如果是用module_eval会得到String.new.foo。
module_eval几乎总是适用;要像对待瘟疫一样避免使用eval。遵守这些简单的规则会对你有好处。
9. 实例变量的内省 Introspect on instance variables
Rails使用了一个技巧来使controller中的实例变量也能用在view中,就是内省一个对象的实例变量。这会严重破坏封装,然而有时候确实非常顺手。可以很容易的通过instance_variables、instance_variable_get和instance_variable_set实现。要把所有实例变量从一个复制到另一个,可以这样:
10. 从block创建Proc并公开 Create Procs from blocks and send them around
把一个Proc实例化保存在变量中并公开的做法使得很多API容易使用。这是Markaby用来管理CSS class定义的一种方法。很容易把block转换成Proc:
调用也很容易:
如果要用proc来定义方法,应该用lambda来创建,就可以用return和break:
如果有block的话method_missing会调用block:
11. 用绑定(binding)来控制eval Use binding to control your evaluations
如果你确实需要用eval,你可以控制哪些变量是有效的。这时候要用kernel方法binding来获得所绑定的对象。例如:
ERb和Rails用这种技术来设置哪些实例变量是有效的。例如:
希望这些技巧和技术已经为您阐明了元编程。我并不声称自己是Ruby或者元编程方面的专家,这只是我对这个问题的一些想法。
1. 使用单例类 Use the singleton-class
许多操作单个对象的方法是基于操作其单例类(singleton class),并且这样可以使元编程更简单。获得单例类的经典方法是执行如下代码:
- sclass = (class << self; self; end)
sclass = (class << self; self; end)
RCR231建议这样定义Kernel#singleton_class方法:
- module Kernel
- def singleton_class
- class << self; self; end
- end
- end
module Kernel def singleton_class class << self; self; end end end
我会在下文使用这个方法。
2. DSL的使用类方法来修改子类 Write DSL's using class-methods that rewrite subclasses
当你想创建一个DSL来定义类信息时,最常见的问题是怎样表示信息来让框架的其它部分使用。以定义一个ActiveRecord模型对象为例:
- class Product < ActiveRecord::Base
- set_table_name 'produce'
- end
class Product < ActiveRecord::Base set_table_name 'produce' end
在这个例子中,令人感兴趣的是set_table_name的使用。这是怎么起作用的呢?好吧,这里涉及到一个小魔法。这是一种实现方法:
- module ActiveRecord
- class Base
- def self.set_table_name name
- define_attr_method :table_name, name
- end
- def self.define_attr_method(name, value)
- singleton_class.send :alias_method, "original_#{name}", name
- singleton_class.class_eval do
- define_method(name) do
- value
- end
- end
- end
- end
- end
module ActiveRecord class Base def self.set_table_name name define_attr_method :table_name, name end def self.define_attr_method(name, value) singleton_class.send :alias_method, "original_#{name}", name singleton_class.class_eval do define_method(name) do value end end end end end
这里令人感兴趣的是define_attr_method。在这个例子中我们需要获得Product类的单例类,但又不想修改ActiveRecord::Base。通过使用单例类我们达到了这个目的。我们为原来的方法取别名,再定义新的存取器(accessor)来返回值。如果ActiveRecord需要table name就可以直接调用存取器。这种动态创建方法和存取器的技术在单例类是很常见的,特别是Rails。
3. 动态创建class和module Create classes and modules dynamically
Ruby允许你动态创建和修改class和module。你可以在没有冻结的class或module上做任何修改。特定情况下会很有用。Struct类可能是最好的例子:
- PersonVO = Struct.new(:name, :phone, :email)
- p1 = PersonVO.new(:name => "Ola Bini")
PersonVO = Struct.new(:name, :phone, :email) p1 = PersonVO.new(:name => "Ola Bini")
这会创建一个新类,并赋给PersonVO,然后创建一个类的实例。从草稿创建新类并定义新方法也很简单:
- c = Class.new
- c.class_eval do
- define_method :foo do
- puts "Hello World"
- end
- end
- c.new.foo # => "Hello World"
c = Class.new c.class_eval do define_method :foo do puts "Hello World" end end c.new.foo # => "Hello World"
除了Struct,还能在SOAP4R和Camping找到轻松创建类的例子。Camping尤其令人感兴趣,因为它有专门的方法创建这些类,被你的controller和view继承。Camping的许多有趣的功能都是用这种方式实现的:
- def R(*urls); Class.new(R) { meta_def(:urls) { urls } };
- end
def R(*urls); Class.new(R) { meta_def(:urls) { urls } }; end
这使得可以这样创建controller:
- class View < R '/view/(/d+)'
- def get post_id
- end
- end
class View < R '/view/(/d+)' def get post_id end end
你也可以这样创建module,然后在类中包含module。
4. 使用method_missing来做有趣的事 Use method_missing to do interesting things
除了闭包(block),method_missing可能是Ruby最强大的特性,也是最容易滥用的一个。用好method_missing的话有些代码会变得超级简单,甚至是不能缺少。一个好的例子(Camping)是扩展Hash:
- class Hash
- def method_missing(m,*a)
- if m.to_s =~ /=$/
- self[$`] = a[0]
- elsif a.empty?
- self[m]
- else
- raise NoMethodError, "#{m}"
- end
- end
- end
class Hash def method_missing(m,*a) if m.to_s =~ /=$/ self[$`] = a[0] elsif a.empty? self[m] else raise NoMethodError, "#{m}" end end end
就可以这样使用hash:
- x = {'abc' => 123}
- x.abc # => 123
- x.foo = :baz
- x # => {'abc' => 123, 'foo' => :baz}
x = {'abc' => 123} x.abc # => 123 x.foo = :baz x # => {'abc' => 123, 'foo' => :baz}
如你所见,如果有人调用了一个hash不存在的方法,则会搜索内部集合。如果方法名以=结尾,则会赋给同名的key。
Markaby中可以找到另一个很好的method_missing技巧。以下引用的代码可以生成任何包含CSS class的XHTML标签:
- body do
- h1.header 'Blog'
- div.content do
- 'Hellu'
- end
- end
body do h1.header 'Blog' div.content do 'Hellu' end end
会生成:
- <body>
- <h1 class="header">Blog</h1>
- <div class="content">
- Hellu
- </div>
- </body>
<body> <h1 class="header">Blog</h1> <div class="content"> Hellu </div> </body>
绝大多数这种功能,特别是CSS class名是通过method_missing设置了self的属性然后返回self。
5. 方法模式的调度 Dispatch on method-patterns
这对于无法预测的方法来说可以轻松的达到可扩展性。我最近创建了一个小型验证框架,核心的验证类会找出自身所有以check_开头的方法并调用,这样就可以轻松地增加新的验证:只要往类或实例中添加新方法。
- methods.grep /^check_/ do |m|
- self.send m
- end
methods.grep /^check_/ do |m| self.send m end
这非常简单,并且难以置信的强大。可以看一下Test::Unit到处使用这种方法。
6. 替换方法 Replacing methods
有时候一个方法的实现不是你要的,或者只做了一半。标准的面向对象方法是继承并重载,再调用父类方法。仅当你有对象实例化的控制权时才有用,经常不是这种情况,继承也就没有价值。为得到同样的功能,可以重命名(alias)旧方法,并添加一个新的方法定义来调用旧方法,并确保旧方法的前后条件得到保留。
- class String
- alias_method :original_reverse, :reverse
- def reverse
- puts "reversing, please wait..." original_reverse
- end
- end
class String alias_method :original_reverse, :reverse def reverse puts "reversing, please wait..." original_reverse end end
一个极端的用法是临时修改一个方法,然后再还原。例如:
- def trace(*mths)
- add_tracing(*mths) # aliases the methods named, adding tracing
- yield
- remove_tracing(*mths) # removes the tracing aliases
- end
def trace(*mths) add_tracing(*mths) # aliases the methods named, adding tracing yield remove_tracing(*mths) # removes the tracing aliases end
这个例子展示了编写add_tracing和remove_tracing的一种典型方法。它依赖于第1条的单例类:
- class Object
- def add_tracing(*mths)
- mths.each do |m|
- singleton_class.send :alias_method, "traced_#{m}", m
- singleton_class.send :define_method, m do |*args|
- $stderr.puts "before #{m}(#{args.inspect})"
- ret = self.send("traced_#{m}", *args)
- $stderr.puts "after #{m} - #{ret.inspect}"
- ret
- end
- end
- end
- def remove_tracing(*mths)
- mths.each do |m|
- singleton_class.send :alias_method, m, "traced_#{m}"
- end
- end
- end
- "abc".add_tracing :reverse
class Object def add_tracing(*mths) mths.each do |m| singleton_class.send :alias_method, "traced_#{m}", m singleton_class.send :define_method, m do |*args| $stderr.puts "before #{m}(#{args.inspect})" ret = self.send("traced_#{m}", *args) $stderr.puts "after #{m} - #{ret.inspect}" ret end end end def remove_tracing(*mths) mths.each do |m| singleton_class.send :alias_method, m, "traced_#{m}" end end end "abc".add_tracing :reverse
如果这些方法是添加到module(有一点点不同,看你能不能写出来!),你也可以在类而非实例上添加和删除tracing。
7. 使用nil类来引入空对象的重构 Use NilClass to implement the Introduce Null Object refactoring
在Fowler的重构中,“引入空对象”的重构是一个对象要么存在,要么为空时有一个预定义值。典型例子如下:
- name = x.nil? ? "default name" : x.name
name = x.nil? ? "default name" : x.name
目前基于Java的重构会推荐创建一个类似于null的子类。例如NullPerson会继承Person,重载name方法总是返回"default name"。但是在Ruby中我们可以打开类,可以这样做:
- def nil.name; "default name"; end
- x # => nil
- name = x.name # => "default name"
def nil.name; "default name"; end x # => nil name = x.name # => "default name"
8. 学习eval的不同版本 Learn the different versions of eval
Ruby有几种版本的执行方法(evaluation)。了解它们的区别和使用情景是很重要的。有eval、instance_eval、module_eval和class_eval几种。首先,class_eval是module_eval的别名。其次,eval和其他的有些不同。最重要的是eval只能够执行一个字符串,其它的可以执行block。这意味着eval是你做任何事的最后选择,它有它的用处,但绝大多数情况下应该用instance_eval和module_eval执行block。
eval会在当前环境执行字符串,除非环境已经提供绑定(binding)。(见第11条)
instance_eval会在接收者(reveiver)的上下文中执行字符串或block,没有指定的话self会作为接收者。
module_eval会在调用的module的上下文中执行字符串或block。这个比较适合在module或单例类中定义新方法。instance_eval和module_eval的主要区别在于定义的方法会放在哪里。如果你用String.instance_eval定义foo方法会得到String.foo,如果是用module_eval会得到String.new.foo。
module_eval几乎总是适用;要像对待瘟疫一样避免使用eval。遵守这些简单的规则会对你有好处。
9. 实例变量的内省 Introspect on instance variables
Rails使用了一个技巧来使controller中的实例变量也能用在view中,就是内省一个对象的实例变量。这会严重破坏封装,然而有时候确实非常顺手。可以很容易的通过instance_variables、instance_variable_get和instance_variable_set实现。要把所有实例变量从一个复制到另一个,可以这样:
- from.instance_variables.each do |v|
- to.instance_variable_set v, from.instance_variable_get(v)
- end
from.instance_variables.each do |v| to.instance_variable_set v, from.instance_variable_get(v) end
10. 从block创建Proc并公开 Create Procs from blocks and send them around
把一个Proc实例化保存在变量中并公开的做法使得很多API容易使用。这是Markaby用来管理CSS class定义的一种方法。很容易把block转换成Proc:
- def create_proc(&p); p; end
- create_proc do
- puts "hello"
- end # => #<Proc ...>
def create_proc(&p); p; end create_proc do puts "hello" end # => #<Proc ...>
调用也很容易:
- p.call(*args)
p.call(*args)
如果要用proc来定义方法,应该用lambda来创建,就可以用return和break:
- p = lambda { puts "hoho"; return 1 }
- define_method(:a, &p)
p = lambda { puts "hoho"; return 1 } define_method(:a, &p)
如果有block的话method_missing会调用block:
- def method_missing(name, *args, &block)
- block.call(*args) if block_given?
- end
- thismethoddoesntexist("abc","cde") do |*args|
- p args
- end # => ["abc","cde"]
def method_missing(name, *args, &block) block.call(*args) if block_given? end thismethoddoesntexist("abc","cde") do |*args| p args end # => ["abc","cde"]
11. 用绑定(binding)来控制eval Use binding to control your evaluations
如果你确实需要用eval,你可以控制哪些变量是有效的。这时候要用kernel方法binding来获得所绑定的对象。例如:
- def get_b; binding; end
- foo = 13
- eval("puts foo",get_b) # => NameError: undefined local variable or method `foo' for main:Object
def get_b; binding; end foo = 13 eval("puts foo",get_b) # => NameError: undefined local variable or method `foo' for main:Object
ERb和Rails用这种技术来设置哪些实例变量是有效的。例如:
- class Holder
- def get_b; binding; end
- end
- h = Holder.new
- h.instance_variable_set "@foo", 25
- eval("@foo",h.get_b)
class Holder def get_b; binding; end end h = Holder.new h.instance_variable_set "@foo", 25 eval("@foo",h.get_b)
希望这些技巧和技术已经为您阐明了元编程。我并不声称自己是Ruby或者元编程方面的专家,这只是我对这个问题的一些想法。