原文链接: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 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的时候,你可以获取Reader和Writer方法。方法 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为“一个可以定义 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可以被理解为“一个类,通过这个类对象可以重新定义自己”。
对象是无法持有方法的。大部分对象没必要保存方法。 但是有些时候你希望一个对象能够保存一些方法。你没法做到这些。但 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 没有任何影响。 我重申之前声明的类及基于类的创建。
ruby
class MailTruck
@trucks = []
def MailTruck.add( truck )
@trucks << truck
end
end
为什么不直接使用类变量
ruby
class MailTruck
@@trucks = []
def MailTruck.add( truck )
@@trucks << truck
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!"
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
这个看起来很简答,但很有用。你可以很简单的写出类方法,实现添加类方法到派生类。