Ruby 元编程 进一步理解 metaclass

skpark1987 · 2015年05月22日 · 最后由 iaiae 回复于 2015年06月21日 · 4980 次阅读
本帖已被管理员设置为精华贴

原文链接:Seeing Metaclasses Clearly 如果你刚开始使用元编程,并且想要使用它的话,下面的四个方法或许会给你一些帮助。

class Object
   # 扩展Object类,添加metaclass方法,返回meta-class
   def metaclass; class << self; self; end; end
   def meta_eval &blk; metaclass.instance_eval &blk; end

   # 添加方法到meta-class
   def meta_def name, &blk
     meta_eval { define_method name, &blk }
   end

   # 类里创建实例方法
   def class_def name, &blk
     class_eval { define_method name, &blk }
   end
 end

我将把这些文件保存到 metaid.rb的文件,为了使用 metaclass 更方便,我准备创建一个库。让我们开始讨论 metaclass 吧,我建议你最好在电脑里也保存一份metaid.rb。 花点时间把这篇文章里的代码都执行一遍,你会发现对元编程的理解更加深刻了。

关于 Class

好吧,什么是 Class? 让我们创建一个简单的对象了解一下。

class MailTruck  #Truck意思是卡车
  attr_accessor :driver, :route
  def initialize( driver, route )
    @driver, @route = driver, route
  end
end

m = MailTruck.new( "Harold", ['12 Corrigan Way', '23 Antler Ave'] ) 
#=> <MailTruck:0x007fda0094ebc0 @driver="Harold", @route=["12 Corrigan Way", "23 Antler Ave"]> 
m.class
#=> MailTruck

对象是保存变量的存储器。MailTruck 是一个对象,当实例化后,会有@driver@route变量。当然它也可以持有其他任何类型的变量。

m.instance_variable_set( "@speed", 45 )
m.driver
#=> Harold
m.instance_variables
#=> [:@driver, :@route, :@speed] 

好吧,@driver实例变量有一个访问控制器。当 Ruby 在 MailTruck 类的定义中看到attr_accessor :driver的时候,你可以获取ReaderWriter方法。方法 driver以及 driver= 。 这些方法保存在类中。因此实例变量保存在对象里而访问控制方法(Reader 和 Writer)保存在类定义里。他们是两个完全不同的地方。 这是一个很重要的知识:对象不会保存方法,只有类才能保存

类是对象

好吧,但是类是对象,不是吗?我的意思是 Ruby 中的任何东西都是对象,因此类和对象都是对象才对。是什么东西导致他们一样? 当然,类是对象。你可以在类中运行所有在该类的对象里可以运行的方法。运行下例,他们都会有 ID 及符号表。

m.object_id
MailTruck.object_id

但是我之前告诉过你:类保存方法。他们是不同的。现在我知道你已经有些糊涂了,“如果类是对象,对象的创建是基于类,这样的话不是导致无线循环了吗?” 不,没有。这点我不是很愿意跟你说,但是类不是真正的对象。从 Ruby 的源代码,我们看到:

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;
};

注意!一个类它含有 m_tbl(一个符号表用来保存方法) 和一个父类 (指向父类)。 但是让我考考你。作为 Ruby 程序员,一个类是一个对象。因为它满足两个重要的条件:你可以在类中保存实力变量及它可以从它是从 Object 类而来。就这么简单。

o = Object.new
#=> <Object:0x007fda0185f2b8>
o.class
#=> Object
Class.superclass.superclass
#=> Object
Object.class
#=> Class
Object.superclass
#=> BasicObject

Object 类处于很顶层的类当只有在其他地方找不到方法时才会加入进来。

Metaclass 到底是什么

我们可以假设metaclass为“一个可以定义 class 的 class”。尽管这个定义在 Ruby 中不怎么行得通。“一个可以定义 class 的 class”,其实就是一个类。

class Class
  def attr_abort( *args )
    abort "Please no more attributes today."
  end
end

class MyNewClass
  attr_abort :id, :diagram, :telegram
end

会打印“Please no more attributes today.”。attr_abort 方法可以在定义中被使用。 你常常在 Ruby 中定义,再定义类。它不是 meta,它仅仅是代码中的一部分。类持有方法。你还觉得它复杂吗? 既然之前的定义不再有效了,我认为 Ruby 的metaclass可以被理解为“一个类,通过这个类对象可以重新定义自己”。

对象是否需要 metaclass

对象是无法持有方法的。大部分对象没必要保存方法。 但是有些时候你希望一个对象能够保存一些方法。你没法做到这些。但 Matz 提供给我们 metaclass,它足够有些可以实现这些功能。 在 YAML库中,当一个对象输出时你可以自定义属性。

require 'yaml'
class << m
  def to_yaml_properties
    ['@driver', '@route']
  end
end

YAML::dump m
#=> --- !ruby/object:MailTruck
#=> driver: Harold
#=> route:
#=>   - 12 Corrigan Way
#=>   - 23 Antler Ave

当你想要在不影响其他对象的前提下 dump 某个对象为指定的 YAML 风格时这是很方便的。上例中,只有 m 变量中的对象才会以属性的顺序进行输出。 MailTruck 类的其他对象还是按照 YAML 库所选择的顺序进行输出。有 时候我们想要以指定的格式格式显示字符串而不更改 String 类 (如果更改 String 类,会影响你的所有代码)。 因此 m 变量中的对象拥有它自己的to_yaml_properties 方法。它保存在 metaclass。metaclass 为对象保存方法并且在继承链中紧挨着对象。 我们还可以用以下几种方式添加to_yaml_properties**方法。

def m.to_yaml_properties
  ['@driver', '@route']
end

如果你载入这篇文章的头部提供的metaid.rb代码的话,试试以下代码:

m.metaclass
#=> #<Class:#<MailTruck:0x81cfb94>>
m.metaclass.class
#=> Class
m.metaclass.superclass
#=> #<Class:MailTruck>
m.metaclass.instance_methods
#=> [..., "to_yaml_properties", ...]
m.singleton_methods
#=> ["to_yaml_properties"]

当你使用 ** class << m ** 语法的时候,你正在打开metaclass。Ruby 会调用这些虚拟类 (virtual class)。注意 m.metaclass的结果。一个类附加到一个对象:

#=> #<Class:#<MailTruck:0x81cfb94>>

当一个对象在附加的 metaclass 中找到方法时,这些方法被引用为对象的单数方法 (singleton method,中文名称自己理解就行,好像没有标准翻译) 而不是类的 metaclass 的实例方法。并且因为只有一个 metaclass 附加到对象,它被称呼为单数方法。 当你使用 metaclass 的方法时,很容易找到 metaclass。一般,你可以使用 ** class << self; self; end **来获取 metaclass。但这个更加简单。 metaclass 是否需要 metaclass?

m.metaclass.metaclass
#=> #<Class:#<Class:#<MailTruck:0x81cfb94>>>
m.metaclass.metaclass.metaclass
#=> #<Class:#<Class:#<Class:#<MailTruck:0x81cfb94>>>>

试试这些我们创建的无聊的方法。那么我们是否该使用 metaclass 的 metaclass? 好吧,我们使用普通的 metaclass 做同样的事情。一个普通的 metaclass 可以持有某个对象的方法。因此 metaclass 的 metaclasss 持有该 metaclass 的方法? 当然,它就是一个对象! 问题是 metaclass 的 metaclass 对于我们来说不是很有用。只有你想深入了解的时候才会使用到它,我们不在这里花太多时间。

m.meta_eval do
  self.meta_eval do
    self.meta_eval do
      def ribbit; "*ribbit*"; end
    end
  end
end

m.metaclass.metaclass.metaclass.singleton_methods
#=> ["class_def", "metaclass", "constants", "meta_def",
     "attr_test", "nesting", "ribbit"]

metaclass 只有当做单层使用的时候才有用。你想给某个对象一些方法。或,你也可以指定的类拥有 metaclass。除了这些你也可以保存方法到隐蔽的 metaclass,隐蔽到无人可以获取它。或许某一天你会这么做。谁知道呢。 有一个很重要的点:metaclass。是的,当你给某个对象创建 metaclass 的时候,在对象的继承链响应之前,截掉方法的调用。但是它不意味着继承受到进一步 metaclass 的影响。当你创建 metaclass 的 metaclass 时,对对象最开始引用的 metaclass 没有任何影响。 我重申之前声明的类及基于类的创建。

  1. 类是对象。这意味着它可以持有变量。
  2. metaclass 可以持有实例方法。当它被添加到对象时,这些方法会变成单数方法。这些方法会截掉方法的调用。 你之前是否在类中使用过实例变量?不是类方法,而是类本身。 ruby class MailTruck @trucks = [] def MailTruck.add( truck ) @trucks << truck end end 为什么不直接使用类变量 ruby class MailTruck @@trucks = [] def MailTruck.add( truck ) @@trucks << truck end end 他们工作的完全一样,我的意思是没关系,不是吗? 下面有两个原因你需要使用类变量而不是实例变量:
  3. 类变量有两个@@符号,易于辨认
  4. 类变量在需要的时候可以被实例方法引用。 ruby class MailTruck @@trucks = [] def MailTruck.add( truck ) @@trucks << truck end def say_hi puts "Hi, I'm one of #{@@trucks.length} trucks!" end end 但是下面的无法执行 ruby class MailTruck @trucks = [] def MailTruck.add( truck ) @trucks << truck end def say_hi puts "Hi, I'm one of #{@trucks.length} trucks!" #在这里@trucks为nil end end 那么实例变量有什么好呢?多浪费空间!我再也不使用了!(是的,当碰到上面的情形时使用类变量) 让我再指出上例中有 metaclass 出现,因为每个类方法都保存在 metaclass 中。 你也可以使用self: ruby class MailTruck def self.add( truck ) @@trucks << truck end end 或 singleton 语法 ruby class MailTruck class << self def add( truck ) @@trucks << truck end end end ruby class MailTruck def self.company( name ) meta_def :company do; name; end end end 上面的代码很简单,但很实用。一个新的 company 类方法添加到了 MailTruck,该类方法可以被使用在类定义中。 ruby class HappyTruck < MailTruck company "Happy's -- We Bring the Mail, and That's It!" end 好吧,上面的代码执行了 HappyTruck 的 company 方法,参数为它的口号。在这里 meta_def都做了什么? meta 的威力在这里显现出来了。 meta_def添加了一个叫 company 的方法到 metaclass 中。这里的奇妙之处为该方法时添加到了派生类HappyTruck,而不是MailTruck 这个看起来很简答,但很有用。你可以很简单的写出类方法,实现添加类方法到派生类。

原网站上那个两周课程看上去还不错,谢谢分享。

看懂了,但是还不懂什么时候会用到~谢谢楼主分享

需要 登录 后方可回复, 如果你还没有账号请 注册新账号