第二十六章 反射, ObjectSpace, 和分布式Ruby(3-1)

 

动态语言如Ruby的众多优点的一个是可以内省从程序本身的内部检查程序的状况。Java中称这个特征为反射,但Ruby的能力要远远超出Java


单词“反射”请求在镜像中查看自己—perhaps investigating the relentless spread of that bald spot on the top of one’s head. That’s a pretty apt analogy: 我们使用反射来检查通常我们不能看到那部分程序。

In this deeply introspective mood, while we are contemplating our navels and burning incense (being careful not to swap the two tasks), what can we learn about our program? 我们可以现

它包含什么,

类的层次,

对象的属性和方法,及

方法的信息。

有这个信息武器,我们可以查看特定对象并确定它们的哪个方法在运行时被调用即使当我们在写代码对象的类并不存在。我们也可在程序运行时来修改它。

听起来恐怖吗?不是这样的。事实上,这些反射能力让我们可以做些有用的事。本章稍后,我们讨论分布式Rubymarshaling,两个基本的反射技术,它让我们可向全球发送对象。

 

观察对象

你能得到遍历你程序内存活对象的能力吗?我们能!Ruby可让你用ObjectSpace.each_object完成这个技巧。我们可以使用它。

例如,在所有类型为Numeric的对象上进行迭代,你可以写成这样。

a = 102.7

b = 95.1

ObjectSpace.each_object(Numeric) {|x| p x }

produces:

95.1

102.7

2.71828182845905

3.14159265358979

2.22044604925031e16

1.79769313486232e+308

2.2250738585072e308

嗨,这些额外的数字从哪来的?在我们的程序中没有定义它们。如果你查观466页和519页,你将看到Float类为最大和最小的浮点数定义常量,两个浮点之间有很小的区别。Math模块为e和π定义了常量 。因此我们检查系统内所有存活对象时,这些就会出现。

让我们用不同的数字来做个同样的例子。

a = 102

b = 95

ObjectSpace.each_object(Numeric) {|x| p x }

produces:

2.71828182845905

3.14159265358979

2.22044604925031e16

1.79769313486232e+308

2.2250738585072e308

我们创建的两个Fixnum对象都没显示出来。这是因为ObjectSpace不认识带有直接值的对象:FixnumSymboltruefalse,和nil

 

观察对象的内部

一旦你找到感兴趣的对象,你可以试着找出它能做什么。不像静态语言,变量的类型由它的类决定,和它支持的方法,Ruby支持自由对象。你其实不需要明确地告诉一个对象能做什么,直到你查看了它。我们在349页的接口类型一章中谈到了这点。

例如,我们可以获得一个对象听响应的全部方法列表。

r = 1..10 # 创建一个Range 对象

list = r.methods

list.length => 68

list[0..3] => ["collect", "to_a", "instance_eval", "all?"]

或者,我们可以检查一个对象是否支持一个特定的方法。

r.respond_to?("frozen?") => true

r.respond_to?(:has_key?) => false

"me".respond_to?("==") => true

我们可以确定我们对象的类,和它的唯一的ID并可测试它与其它类的关系。

num = 1

num.id => 3

num.class => Fixnum

num.kind_of? Fixnum => true

num.kind_of? Numeric => true

num.instance_of? Fixnum => true

num.instance_of? Numeric => false

 

观察类

了解对象是反射的一部分,但是要得到一个完整的图画,你也需要能够观察类它们包含的方法和常量。

观察类层次很容易。你可以使用Class#superclass得到任何特定类的双亲。对于类和模块,Module#ancestors列出超类和混插的模块两者。

klass = Fixnum

begin

print klass

klass = klass.superclass

print " < " if klass

end while klass

puts

p Fixnum.ancestors

produces:

Fixnum < Integer < Numeric < Object

[Fixnum, Integer, Precision, Numeric, Comparable, Object, Kernel]

如果你想构建一个完整的类层次,只要对系统内每个类运行这个代码。我们可以使用ObjectSpace来在所有对象上迭代。

ObjectSpace.each_object(Class) do |klass|

# ...

end

 

观察类的内部

我们可以在一个特定对象中找到很多方法和常量。而不只是检查看对象是否对一个给定的消息有响应,我们可以按访问级别来请求方法,并且也可只请求单态方法。我们也可以观察对象的常量,本地,和实例变量。

class Demo

@@var = 99

CONST = 1.23

private

def private_method

end

protected

def protected_method

end

public

def public_method

@inst = 1

i = 1

j = 2

local_variables

end

def Demo.class_method

end

end

Demo.private_instance_methods(false) => ["private_method"]

Demo.protected_instance_methods(false) => ["protected_method"]

Demo.public_instance_methods(false) => ["public_method"]

Demo.singleton_methods(false) => ["class_method"]

Demo.class_variables => ["@@var"]

Demo.constants Demo.

superclass.constants => ["CONST"]

demo = Demo.new

demo.instance_variables => []

# Get 'public_method' to return its local variables

# and set an instance variable

demo.public_method => ["i", "j"]

demo.instance_variables => ["@inst"]

Module.constants可以返回一个模块的所有有效的常量,包括模块超类常量。我们现在对这些不感兴趣,所以从我们的列表中去掉它们。

你可能对前面例子中的所有false参数感到奇怪。在Ruby1.8中这些反射方法将缺省地递归调用它们父类,它们的双亲,等等。传递false可停止这种递归。

给出方法名字的列表,我们现在可以试着去调用它们。幸运地,这在Ruby中很容易。

 

动态地调用方法

CJava程序员通常写某种分派表格来查找它们自己:基于命令调用的函数。考虑典型的C语言中,你必须将一个字符串翻译成一个函数指针。

typedef struct {

char *name;

void (*fptr)();

} Tuple;

Tuple list[]= { { "play", fptr_play }, { "stop", fptr_stop },

{ "record", fptr_record }, { 0, 0 }, };

...

void dispatch(char *cmd) {

int i = 0;

for (; list[i].name; i++) {

if (strncmp(list[i].name,cmd,strlen(cmd)) == 0) {

list[i].fptr();

return;

}

}

/* not found */

}

Ruby,你只一行代码就可做到这些。把你的命令行函数放到一个类中,创建那个类一个实例(我们称它为命令),然后请求那个对象运行一个调用与命令字答串相同名字的方法。

commands.send(command_string)

顺便说一句,它不是加强的C版本它是动态的。Ruby版本会在运行时很容易地找到被添加的方法。

你不必须为send写具体的命令类:它在所有对象上工作。

"John Coltrane".send(:length) => 13

"Miles Davis".send("sub", /iles/, '.') => "M. Davis"

动态地调用方法的另一个途径是使用Method对象。Method对象很像个Proc对象:它代表一组代码和它运行的上下文环境。这种情况中,方法的代码体,和上下文环境是个对象,这个对象创建了那个方法。一旦我们有了Method对象,我们可某些时候通过发送消息call来运行它。

trane = "John Coltrane".method(:length)

miles = "Miles Davis".method("sub")

trane.call => 13

miles.call(/iles/, '.') => "M. Davis"

你可以传递任何其它对象给Method对象,当你调用Method#call时,方法被运行,就好像是你在原始的对象上进行的调用。它像C风格的函数指针,但是它是完全的面向对象的风格。

你也可以带迭代器使用Method对象。

def double(a)

2*a

end

mObj = method(:double)

[ 1, 3, 5, 7 ].collect(&mObj) ! [2, 6, 10, 14]

Method对象被绑定在一个特定对象上。你可以创建去绑定方法(当然是UnboundMethod)然后随后绑定它们到一或多个对象。binding创建一个新的Method对象。被看做别名,unbound方法被引用给它们被创建时的方法的定义。

unbound_length = String.instance_method(:length)

class String

def length

99

end

end

str = "cat"

str.length => 99

bound_length = unbound_length.bind(str)

bound_length.call => 3

有三个好处,这也是另一种动态调用方法的途径。eval方法(和它变体class_evalmodule_eval,和instance_eval)将解析和运行Ruby源代码中合法的任意字符串。

trane = %q{"John Coltrane".length}

miles = %q{"Miles Davis".sub(/iles/, '.')}

eval trane => 13

eval miles => "M. Davis"

当使用eval时,它可以帮助明确应该被计算的表达式上下文环境的状态,而不使用当前的上下文状态。你可以在需要地方通过调用Kernel#binding来获得一个上下文环境。

def get_a_binding

val = 123

binding

end

val = "cat"

the_binding = get_a_binding

eval("val", the_binding) => 123

eval("val") => "cat"

第一个eval计算binding的上下文环境内的valBinding就好像是被执行的get_a_binding方法。在这种绑定情况下,变量val有个123的值。第二个eval计算顶层bindingval,那里它的值是”cat”

 

性能考虑

像我们在这一节看到的,Ruby给我们几种途径来调用一些对象的任意方法:Object#sendMethod#call,和各种样式的eval

使用哪个技术这要依赖于你的需要,但是要小心,eval比其它几个稍微慢一些(或者说,出于优化的原因,sendcall要比eval快一些)

require 'benchmark'

include Benchmark

test = "Stormy Weather"

m = test.method(:length)

n = 100000

bm(12) {|x|

x.report("call") { n.times { m.call } }

x.report("send") { n.times { test.send(:length) } }

x.report("eval") { n.times { eval "test.length" } }

}

produces:

user system total real

call 0.250000 0.000000 0.250000 ( 0.340967)

send 0.210000 0.000000 0.210000 ( 0.254237)

eval 1.410000 0.000000 1.410000 ( 1.656809)

 

系统钩子(Hooks)

hook是可让你俘获一些Ruby事件,如创建对象等的技术。Ruby中最简单的hook技术是用来在系统类内截取对方法的调用。或许你想日志你的程序执行时所操作的系统命令。只要简单地重命名方法Kernel.system并用你自己的日志命令替换它,再调用原有的Kernel方法。

module Kernel

alias_method :old_system, :system

def system(*args)

result = old_system(*args)

puts "system(#{args.join(', ')}) returned #{result}"

result

end

end

system("date")

system("kangaroo", "-hop 10", "skippy")

produces:

Thu Aug 26 22:37:22 CDT 2004

system(date) returned true

system(kangaroo, hop

10, skippy) returned false

更强大的hook是 在对象被创建后捕获它。当每个对象被生成时如果你想知道的话,你可以栋选所有你感兴趣的事情:你可以包装它们,给它们添加方法,从它们移除方法,将它们添 加到容器中以实现永续,你可以给它们起名字。我们在这儿显示了一个简单的例子:我们给每个被创建的对象一个时间戳。首先,我们在系统中为每个对象添加一个timestamp属性。我们可以侵入到Object内部做这件事。

class Object

attr_accessor :timestamp

end

然后我们需要在对象创建时钩住它,以添加这个时间戳。一个方式是通过对Class#new重命名为来完成的,调用这个方法来为一个新对象分配内存空间。这个技术并不像某些内建对象那样完美,如字面值字符串,它没调用new来构造,但它对我们所写对象可很好地工作。

class Class

alias_method :old_new, :new

def new(*args)

result = old_new(*args)

result.timestamp = Time.now

result

end

end

最后,我们运行一个测试。我们将创建一组带有毫秒的对象并检查它们的时间戳。

class Test

end

obj1 = Test.new

sleep(0.002)

obj2 = Test.new

obj1.timestamp.to_f => 1093577843.1312

obj2.timestamp.to_f => 1093577843.14144

重命名方法这种方式很好,也会真正地工作,但要小心,它可能会有问题。如果一个子类做了同样的事情,它使用同样的重命名名字的方法,你会进入到无穷循环中。你可以使用一个唯一的符号名字来别名化你的方法或使用统一的命名习惯来避免这个问题。

另外,更好的途径是在运行时程序的内部。Ruby提供了几个回调(callback)方法来让你在一定控制之下俘获某些事件。