元编程就是编写能为你编写代码的代码。但那不就是代码生成器所做的事情吗,就像是 rails gem,或者 yeoman?或者甚至是字节码编译器?
是的,但元编程一般指的是 Ruby 中的另外一些东西 。 ruby 中的元编程指的是能为你动态地编写代码的代码 。是在运行时发生的。 Ruby 是动态元编程的首要语言,因为它采用了 类型推断 并且是强 发射 的 – 相比现有的其它语言处在一个更高的级别。这能让你做一些像使用几行代码就可以加入大量功能这样事情,真的很酷,但是有一个问题:一个不小心,在你提升很多东西的同时,得到却是几乎无法阅读的代码。这件事情的意思,用 Uncle Ben 的话说就是 :
“能力越大,责任也就越大.”
Uncle Ben 说这话的时候,并不是在谈论现实生活中的什么东西,他讲的是元编程。
假设你想创建一个方法,功能是接受一个字符串参数,只保留字符串中数字和字母。
def to_alphanumeric(s) s.gsub(/[^/w/s]/, '') end puts to_alphanumeric("A&^ar$o%n&* (is&*&))) t&*(*he B0&*S**^S)") # => "Aaron is the B0SS"
这样就完成了功能,但是好像不“面向对象”,下面我们这样来修改。
class String def to_alphanumeric gsub(/[^/w/s]/, '') end end puts "A&^ar$o%n&* (is&*&))) t&*(*he B0&*S**^S)".to_alphanumeric # => "Aaron is the B0SS"
在 Ruby 中,即使你不是类的最初的定义者,你也可以像上面的例子一样向一个已经存在的类中加入你想要扩展的功能(string 是 Ruby 的默认字符串类)。但是,在开放类中会有问题,看下面的代码。
class Array def replace(original, replacement) self.map {|e| e == original ? replacement : e } end end puts ['x', 'y', 'z'].replace('x', 'a') # => a, y, z
我们写一个叫 Array#replace 的方法,带两个参数,第一个是你想在数组中被替换的值,第二个是替换的最终值。
代码运行正常。为什么有问题?因为 Array#replace 已经存在了,功能是用一个作为参数传入的数组替换当前数组的全部内容。我们写的方法直接覆盖了原来的方法,这不太好。因为可能我们的本意并没有想覆盖原有的功能。
在 Ruby 里这种修改类的处理方法叫 monkeypatching。可能这种做法无可厚非,但是你一定要明确自己在做什么。
在更深入了解之前,我们需要先讨论一下 Ruby 对象模型。
这看起来像一个复杂的图表,但是它清晰的表示出了 Ruby 中对象,类,模块之间的联系。有三个要点。
实例化的对象 (obj1,obj2,obj3) 是 MyClass 类的对象。
MyClass 是属于 Class 的对象(这在 Ruby 里也是对象的意思,我知道,这让你很费解)。
MyClass 是 Class 的类对象,同时它也继承自 Object。
我们还会在第二部分引用这个,现在我们继续看继承链。
下面这张图要容易理解一点,只涉及继承和模块包含关系。
当你调用一个方法时,Ruby 向右进入的接收者类,然后向上遍历继承链,直到找到被调用方法或者抵达尽头。在上图中,对象 b 是 Book 类的示例,Book 类包含两个模块: Printable 和 Document 。Book 继承自 Object 类,这是 Ruby 中几乎一切事物的基类。Object 包含一个模块 Kernel 。最后,Object 又继承于 BasicObject 类——Ruby 中一切对象的绝对父类。
现在我们稍微了解了点这两个重要的主题——即 Ruby 对象模型和继承链——是时候来点代码了。
在 Ruby 中,可以动态的创建方法,也可以动态的调用方法。甚至可以调用一个并不存在的方法,也不会抛出异常。
方法 1:动态定义方法
为什么会有动态定义方法的需求?可能是为了减少重复的代码,也可能是为了加上更 cool 的功能。 ActivateRecord (Rails 工程的默认 ORM 工具)大量的使用了这个特性。看下这个例子。
class Book < ActiveRecord::Base end b = Book.new b.title
如果你熟悉 ActivateRecord,那你会感觉这和普通的 ORM 没有什么区别。即使我们没有在 Book 类中定义 title 属性,我们假定 Book 类是一个 Book 数据库表的一个 ORM 包装器,并且 title 就是这个表里的字段,那么我们就能得到由 b 表示的那个数据库里的 title 所指定的那一列。
通常在这个类调用 tilte 方法会引发 NoMethodError 错误 - 但是 ActiveRecord 会动态添加这些方法,就和我们手动添加的一样。ActiveRecord 的源代码就是如何把元编程运用到极致的一个很好的例子。
让我们试一下元编程,首先创建一些方法
def foo puts "foo was called" end def baz puts "baz was called" end def bar puts "bar was called" end foo baz bar # => foo was called # => baz was called # => bar was called
看到这些重复的代码了吗? 让我们用元编程来改进一下
%w(foo baz bar).each do |s| define_method(s) do puts "#{s} was called" end end foo baz bar # => foo was called # => baz was called # => bar was called
上边这段代码做的是动态定义了方法 foo, baz 和 bar, 然后去调用它们。 Module#define_method 这个方法我个人经常使用它,它非常非常有用。这里有个我在之前写过的 gem 模块中使用它的 例子 。
你可以看到我们在这节省了多少代码 - 尤其是当我们编写真正的方法时。但是-他的作用大于他带来的代码复杂度吗? 这要由你自己决定了。
这里有一个既可以通过方法名字符串也可以通过到达名称符号调用方法的例子。
%w(test1 test2 test3 test4 test5).each do |s| define_method(s) do puts "#{s} was called" end end # New Code (1..5).each { |n| send("test#{n}") } # => test1 was called # => test2 was called # => test3 was called # => test4 was called # => test5 was called
Object#send 方法就是我们来演示如何动态调用的方法示例。通过数字 1 到 5 依次产生 5 个函数,并且通过当前变量的值来确定函数的名字。
由于在 Ruby 里所有的对象都继承自 Object 类,所以你可以在任意对象上调用 send 方法来访问这个对象的其他方法或者属性,如下,
class OKCRB def is_boss? puts "true" end end okcrb = OKCRB.new okcrb.send("is_boss?") # => true
这个方法的能力(访问其他对象属性方法的能力)取决于你在调用方法的时候的作用域情况-通常都是基于一个变量值。Object 也允许你调用私有方法,但是如果你本意不想这么做就一定要注意。如果可以,使用 Object#public_send 这种方式,作用与直接调用相同,但是会限制访问私有成员和私有方法。
如果我们尝试执行这样的代码会发生什么?
class Book end b = Book.new b.read
我们将会得到一个 无此方法的错误 ,因为 Book 实例不知道如果处理 read 这个方法。但是它也并不是不能处理。让我们来引入 method_missing 方法。
class Book def method_missing(method, *args, █) puts "You called: #{method}(#{args.join(', ')})" puts "(You also passed it a block)" if block_given? end end b = Book.new b.read b.read('a', 'b') { "foo" } # => You called: read() # => You called read(a, b) # => (You also passed it a block)
BasicObject#method_missing 为你提供了可以创建一个在无此方法错误事件(但在之前是会有错误)前被自动触发调用的处理器的途径。随后你需要提供你希望调用的方法的名称,以及其相应的参数作为 method_missing 的参数,和它的代码块。在那里,你可以做任何你想做的事。
这样的用法看起来很酷,但谨慎使用它除非你有一个明确的好理由,因为:
由于你颤倒了继承链,使得需要花费大量的时间才能命中 method_missing 处理器
如果你不够仔细,你就要忍受很多意料之外的错误。用户会丢失很多意外的错误,因为那样的话会调用默认的 method_missing 处理器
以上,就是我们在第一部分打算覆盖的全部内容。我们回顾一下,有:开放类、Ruby的对象模型、继承链、动态定义方法、动态调用方法,以及魔法方法,但在第二部分中我们将会触及到更多,包括有:Scopes、动态定义类、闭包(块、Procs和Lambda表达式)、各种执行(instance_eval,class_eval,和 eval)、编写多功能模块。
然而,我们不会讲到 单例方法和特征类 。这些内容涵盖了 Ruby 元编程中很好的方面,但在我看来他们也是最令人困惑、最难掌控的内容,而且我也从来没有遇到过使用他们会让我的代码变得更好的场景。所以我把他们完全忽略了,但如果你有兴趣想学习更多的话,有大量关于这些方面的论文可参考。
再次感谢阅读到最后——并且敬请关注 Ruby 元编程:第二部分。