Ruby 里的元编程

关于元编程

Wikipedia 上关于元编程的定义说元编程就是将程序作为数据进行处理。“用程序来处理程序”,这就是“元”的来源了,这本身是一个容易产生混淆的地方,就像“用语言来描述语言”一样,数学上的许多悖论就来自于此呢。幸好我们用的编程语言比自然语言要简单许多,并且都有严格的定义规范,有兴趣的人可以尝试在自己喜欢的编程语言里面构造一下 “This statement is false” 这个经典悖论。

回到元编程,程序处理程序可以分为“处理其他程序”和“处理自己”,对于前者,有我们熟悉的 lex 和 lacc 作为例子。而对于后者,如果再细分,可以分为“宏扩展”、“源代码生成”以及“运行时动态修改”等几种。

宏扩展从最简单的 C 语言的宏到复杂的 Lisp 的宏系统,甚至 C++ 的“模板元编程”也可以包含在这一类里面,我在这里对它们进行了一些介绍。

源代码生成则主要是利用编程语言的 eval 功能,对生成出来的源代码(除了在 Lisp 这样的语言里面以外,通常是以字符串的方式)进行求值。有一类有趣的程序 quine ,它们运行的结果就是把自己的源代码原封不动地打印出来,通常要证明你精通某一门语言,为它写一个 quine 是绝佳的选择了,这里有我搜集的一些相关资料。

最后是运行时修改,像 Lisp 、Python 以及 Ruby 之类的语言允许在运行时直接修改程序自身的结构,而不用再经过“生成源代码”这一层。当然对源代码进行 eval 的方法也许是最灵活的,但是它的效率却不怎么样,所以相来说并不是特别流行。这里主要介绍的是这种方式的元编程在 Ruby 里面的应用,如果对元编程的其他方面感兴趣,前面的几个链接应该都是很好的去处。

类与对象

Ruby 是一门完全面向对象的语言,在 Ruby 里面,所有的东西都是对象。对象是什么?对象是类的一个实例。那么类又是什么?按照前面的说法,一切都是对象,那么类也应该是一个对象喽!没错,类也是一个对象。按照前面的说法,对象应该是某一个类的一个实例,这也没错,一个特定的类就是“类(Class)”这个类的一个实例。已经开始拗口了吧?因为已经开始接触到“元”了,也就是所谓的用自己来描述自己,而描述一个类的类,通常叫做“元类(metaclass)”,不过在 Ruby 里面,元类和普通类其实是同一个东西。事实上, Ruby 还是一个单根的面向对象语言,所有的类都继承自 Object 类,包括 Class 类。运行一下下面的单元测试代码就可以验证这一点(完整的代码可以在末尾找到下载链接)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'test/unit'
 
class MetaTest < Test::Unit::TestCase
  def test_class
    str = String.new
 
    # str is an instance of the class String
    assert_equal String, str.class
    # the class String is an instance of the class Class
    assert_equal Class, str.class.class
    # the class Class is an instance of the class Class itself,
    # so metaclass is just normal class
    assert_equal Class, str.class.class.class
    # the class Class is also derived from the class Object
    assert_equal Object, str.class.class.superclass.superclass
  end
 
end

和其他面向对象语言一样。Ruby 里面对象保存成员变量的值,但是并不保存成员函数(方法),方法是放在对象所属的类里面。事实上,由于对象里面不会保存函数,Ruby 的底层实现里面类这种对象和普通对象还是有一定的区别的:

321
322
323
324
325
326
327
328
329
330
331
struct RObject {
    struct RBasic basic;
    struct st_table *iv_tbl;
};
 
struct RClass {
    struct RBasic basic;
    struct st_table *iv_tbl;
    struct st_table *m_tbl;
    VALUE super;
};

只是我们在 Ruby 里面看起来它们是一样的,或者说,类的类型派生自对象的类型,加了一个放函数的表和一个指向父类的域。

另外,还有一类方法叫做“类方法”,它不能引用到对象的成员变量,例如:

class Foo
  def Foo.func1
 
  end
 
  def Foo.func2
 
  end
end

而在类的环境里面 self 是指向类自己的引用,因此也可以用 self 代替代码中的 Foo ,像这样:

def self.func1

另外,Ruby 还允许另外一种写法:

class Foo
  class << self
    def func1
 
    end
 
    def func2
 
    end
  end
end

事实上,在类方法里面不能引用到对象的成员变量的原因很简单,因为类方法的 self 引用到特定的类,而成员方法引用到特定的对象。看下面的测试代码:

  def test_class_method
    # foo is an instance of class Foo
    foo = Foo.new
    # klass is the class of the object foo, i.e. Foo
    klass = foo.class
    assert_equal Foo, klass
 
    # class method of object foo is just instance method
    # of object klass(which is the class of foo)
    klass.set(10)
    assert_equal 10, klass.get
 
    # because get is not instance method of object foo
    assert_raises(NoMethodError) { foo.get }
  end

但是在 Foo 这个对象的类里面并不能找到 get 这个方法,如果说 getFoo 的成员方法的话,那么它应该包含在 Foo.class.instance_methods 里面才对。不过这里其实另有乾坤,关系到一个 singleton class ,我们将在后面再回到这个问题上来。

Ruby 里面的元编程

一个比较常见的例子当属 define_method 。Ruby 里面有 attr_accessor 方法,例如:

class Foo
  attr_accessor :foo, :bar
end

Foo 这个类就有了 foofoo=barbar= 这些方法,用于读取和设置 foobar 两个属性。使用起来很方便,其实一点也不神奇,用 define_method 我们立马就可以写一个,由于 Ruby 的类可以随时重新打开添加东西,我们直接修改 Class 类:

class Class
  def prop_accessor(*props)
    props.each do |prop|
      define_method prop do
        instance_variable_get "@#{prop}"
      end
      define_method "#{prop}=" do |val|
        instance_variable_set "@#{prop}", val
      end
    end
  end
end

在定义 Foo 的时候 self 是引用到 Foo (这是 Class 这个类的一个实例),因此可以使用 Class 类的成员方法,所以可以像前面的例子里面使用 attr_accessor 那样来使用这里定义的 prop_accessor ,效果是一样的。

对象真的不能持有自己的方法吗?

是的,对象不能持有自己的方法,在前面看到 Ruby 的实现代码里面 RObject 这个结构里面根本没有用于存放方法的表。但是我真的想让对象有它自己的方法,要怎么办呢?只有 RClass 结构能保存方法列表,于是就让 RObject 保存一个自己的 RClass 类就好了。这个类通常叫做“singleton class”,甚至也有叫“metaclass”的,但是通常我们认为“metaclass”是用于定义类的类,容易造成混淆,Matz 正在考虑在以后的版本里面把这个类叫做“eigenclass”,那么我们这里就称它为“eigenclass”好了。一个对象(例如 foo)的 eigenclass 与它所属的类(可以通过 foo.class 获得)有几点不同:

  • eigenclass 是对象所独有的,属于同一个类的不同对象拥有不同的 eigenclass 。
  • Ruby 目前没有提供像 foo.class 这样的方法来直接获得这个 eigenclass ,不过幸运的是我们可以很轻松地实现这个方法。
  • Ruby 会优先从 eigenclass 里面查找方法,其次才是对象所属的类以及各个祖先类。

虽然没有内置的轻松获取 eigenclass 的方法,但是将方法存放到 eigenclass 却是非常简单的,事实上语法我们已经很熟悉了。

  def test_singleton_method
    foo = Foo.new
 
    # now we put the singleton_func in the eigenclass of foo
    def foo.singleton_func
      self
    end
 
    # singleton_func exists in foo, and self refer to foo
    assert_equal foo, foo.singleton_func
    # singleton_func is not available in other instance of class Foo
    assert_raise(NoMethodError) { Foo.new.singleton_func }
  end

另外,还可以这样写

class << foo
  def singleton_func
    self
  end
end

是不是觉得似曾相识?没错,就是前面定义类方法的时候用的语法。可是那里点好前面是一个类,这里是一个对象,不是吗?没错!可是别忘了在 Ruby 里面,类也是一个对象!好了,让我们用 eigenclass 和 singleton_method 来解释一下前面的类方法的代码。

def Foo.func1

这是在 Foo 这个对象的 eigenclass 里面放入了 func1 这个方法。这也是我们在 Foo.class 里面找不到 func1 的原因了,因为它根本不在那里。现在我们已经迫不及待地想要取出 eigenclass 来看看了,怎么能容忍它偷偷地躲在那里呢?下面我们来给 Ruby 里面所有类的元祖 Object 类开开刀:

class Object
  def eigenclass
    class << self
      self
    end
  end
end

这么简单几行代码就 OK 了,在 eigenclass 方法内,打开 self(这里引用到当前的对象) 的 eigenclass ,打开以后里面那个 self 就引用到我们要的那个 eigenclass 了,注意最后一行是作为返回值的,这样我们就成功取出了 eigenclass ,赶紧测试一下!

  def test_eigenclass
    foo = Foo.new
 
    def foo.singleton_func
      self
    end
 
    # eigenclass is also an instance of the class Class
    assert_equal Class, foo.eigenclass.class
    # singleton_func is put as an instance method in the eigenclass
    assert foo.eigenclass.instance_methods.include?('singleton_func')
    # so here is the magic of the so called class method
    assert Foo.eigenclass.instance_methods.include?('get')
  end

回到类方法的问题上。我们进行如下定义以方便描述

foo = Foo.new
klass = foo.class
metaclass = klass.class
eigenclass = klass.eigenclass
  • foo 的成员方法很简单,它是存放在 klass 中的方法。也就是 klass 作为一个类所拥有的方法,调用 klassinstance_methods 就可以得到这些方法。
  • foo 的类方法,它是 klass 的成员方法,但是并不是像普通的成员方法那样存放在 metaclass 中,而是存放于 klass 的 singleton class eigenclass 中。

define_singleton_method

singleton method 并不是 Ruby 独有的,在 CLOSDylan 等语言里面也有,而像 SelfNewtonScript 之类的基于原型的语言甚至只有 singleton method 。Ruby 里面采用这种方式漂亮地实现了“类方法”。遗憾的是,目前的 Ruby 里面并没有提供一个和 define_method 同样功能的用于定义类方法的方法,我们只好自己写一个了。

class Object
  def define_singleton_method name, &body
    eigenclass.send :define_method, name, &body
  end
end

由于 define_method 是私有方法,因此我们需要使用 send 来调用。或者也可以使用 instance_eval 的方式来实现。另外,在类里面使用的时候,我们可以用一个更明确的名字:

class Class
  alias define_class_method define_singleton_method
end

Dwemthy’s Array 是一个很有趣的 Ruby 元编程的例子。仅用了几十行代码就写好了一个 RPG 游戏。在这里我们来把它的 Creature 类的 traits 类方法简化一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Creature
  def self.traits(*arr)
    return @traits if arr.empty?
 
    attr_accessor *arr
 
    arr.each do |trait|
      define_class_method trait do |val|
        @traits ||= { }
        @traits[trait] = val
      end
      define_method :initialize do
        self.class.traits.each do |k, v|
          instance_variable_set("@#{k}", v)
        end
      end
    end
  end
end

参考文献

附件

Testcase

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值