Ruby元编程(三)
DeathKing posted @ 2011年8月28日 10:54 in
Rubywith tags
ruby
send
反射
metaprogramming
元编程
反射机制
内省
自省
reflection
remove_method
undef_method
method_missing , 35878 阅读
1.3 实用元编程方法
本章节将介绍一系列的元编程实用方法,使读者对元编程有一个更为具体的认识。其中一些技术,诸如反射机制,已经有很多文章介绍过了,读者可以根据自身的情况进行选择。
1.3.1 内省、反射
在
Ruby中,你完全有能力在运行时查看类或对象的信息。我们可以使用
class、
instance_methods、
intance_variables等方法来达到目的。我们讲这种技术成为内省(
Introspection)或者反射(
Reflection)。
一说编写元程序的语言称之为 元语言。被操纵的程序的语言称之为 目标语言。一门编程语言同时也是自身的元语言的能力称之为 反射或者 自反。——摘自维基百科 元编程条目
1
2
3
4
5
6
7
8
9
10
11
12
|
class
Rubyist
def
what_does_he_do
@person
=
'A Rubyist'
'Ruby programming'
end
end
an_object = Rubyist.
new
puts an_object.
class
# => Rubyist
puts an_object.
class
.instance_methods(
false
)
# => what_does_he_do
an_object.what_does_he_do
puts an_object.instance_variables
# => @person
|
respond_to?方法是反射机制中另一个有用的方法。使用
respond_to?方法,可以提前知道对象是否能够处理你想要交与他执行的信息。所有的对象都有此方法,使用
respond_to?方法,你可以确定对象是否能使用指定的方法。
1
2
3
4
5
6
|
obj =
Object
.
new
if
obj.respond_to?(
:program
)
obj.program
else
puts
"Sorry, the object doesn't understand the 'program' message."
end
|
关于反射机制,文章
元编程的魅力——反射机制可能会给你一些启迪。
1.3.2 send
send是
Object类的实例方法。
send方法的第一个参数是你期望对象执行的方法的名称。可以是一个字符串(
String)或者符号(
Symbol),但是我们更喜欢使用符号。剩余的参数就直接传递给所指定的方法。
1
2
3
4
5
6
7
8
|
class
Rubyist
def
welcome(*args)
"Welcome "
+ args.join(
' '
)
end
end
obj = Rubyist.
new
puts(obj.send(
:welcome
,
"famous"
,
"Rubyists"
))
# => Welcome famous Rubyists
|
使用
send方法,你所想要调用的方法就顺理成章的变成了一个普通的参数。你可以在运行时,直至最后一刻自由决定到底调用哪个方法。
1
2
3
4
5
6
7
8
9
|
class
Rubyist
end
rubyist = Rubyist.
new
if
rubyist.respond_to?(
:also_railist
)
puts rubyist.send(
:also_railist
)
else
puts
"No such information available"
end
|
上述代码展示了如果
rubyist对象知道如何处理
also_railist方法,那么他将会进行处理。
你可以通过
send方法调用任何方法,即使是私有方法。
1
2
3
4
5
6
7
8
9
|
class
Rubyist
private
def
say_hello(name)
"#{name} rocks!!"
end
end
obj = Rubyist.
new
puts obj.send(
:say_hello
,
'Matz'
)
|
1.3.3 define_method
Module#define_method是
Module类实例的私有方法。因此
define_method方法仅能由类或者模块使用。你可以通过
define_method动态的在
receiver中定义实例方法。而你仅需要传递需要定义的方法的名字,以及一个代码块(
block),就如下面演示的那样:
1
2
3
4
5
6
7
8
|
class
Rubyist
define_method
:hello
do
|my_arg|
my_arg
end
end
obj = Rubyist.
new
puts(obj.hello(
'Matz'
))
# => Matz
|
1.3.4 method_missing
当
Ruby使用
look-up机制找寻方法时,如果方法不存在,那么
Ruby将会在原
receiver中自行调用一个叫做
method_missing的方法。
method_missing方法会以符号的形式传递被调用的那个不存在的方法的名字,以数组的形式传递调用时的参数,以及原调用中传递的块。
method_missing是由
Kernel模块提供的方法,因此任意对象都有此方法。
Kernel#method_missing方法能响应
NoMethodError错误。重载
method_missing方法允许你对不存在的方法进行处理。
1
2
3
4
5
6
7
8
|
class
Rubyist
def
method_missing(m, *args, &block)
puts
"Called #{m} with #{args.inspect} and #{block}"
end
end
Rubyist.
new
.anything
# => Called anything with [] and
Rubyist.
new
.anything(
3
,
4
) { something }
# => Called anything with [3, 4] and #<Proc:0x02efd664@tmp2.rb:7>
|
关于
method_missing,
ihower给出了一个完美的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
car = Car.
new
car.go_to_taipei
# go to taipei
car.go_to_shanghai
# go to shanghai
car.go_to_japan
# go to japan
class
Car
def
go(place)
puts
"go to #{place}"
end
def
method_missing(name, *args)
if
name.to_s =~ /^go_to_(.*)/
go(
$1
)
else
super
end
end
end
|
注意method_missing方法的效率不甚理想,对效率敏感的项目尽量要避免使用此方法。尽管 method_missing的确很强力。
1.3.5 remove_method和undef_method
想要移除已存在的方法,你可以使用在一个打开的类的范围(
Scope)内使用
remove_method方法。即使是父类以及父类的父类等先祖中有同名的方法,那些方法也不会被移除。而相比之下,
undef_method会阻止任何对指定方法的访问,无论该方法是在对象所属的类中被定义,还是其父类及其先祖类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class
Rubyist
def
method_missing(m, *args, &block)
puts
"Method Missing: Called #{m} with #{args.inspect} and #{block}"
end
def
hello
puts
"Hello from class Rubyist"
end
end
class
IndianRubyist < Rubyist
def
hello
puts
"Hello from class IndianRubyist"
end
end
obj = IndianRubyist.
new
obj.hello
# => Hello from class IndianRubyist
class
IndianRubyist
remove_method
:hello
# removed from IndianRubyist, but still in Rubyist
end
obj.hello
# => Hello from class Rubyist
class
IndianRubyist
undef_method
:hello
# prevent any calls to 'hello'
end
obj.hello
# => Method Missing: Called hello with [] and
|
1.3.6 eval
Kernel模块提供了一个叫做
eval的方法,该方法用于执行一个用字符串表示的代码。
eval方法可以计算多行代码,使得将整个程序代码嵌入到字符串中并执行成为了可能。
eval方法很慢,在执行字符串前最好对其预先求值。不过,糟糕的是,
eval方法会变得十分危险。如果外部数据通过
eval传递的话,你就可能会遭遇一个安全漏洞,因为这些数据可能含有任意的代码,并且你的程序将会执行他。现在,
eval方法是在万般无奈的情况下才被选择的。
1
2
|
str =
"Hello"
puts eval(
"str + ' Rubyist'"
)
# => "Hello Rubyist"
|
关于
eval方法,苏小脉给出了下面的建议:
一般来说,能避免 eval 就尽量避免,因为 eval 有额外的“分析时”开销(将字符串作为源代码进行词法、文法分析),而这个“剖析时”却又是在程序“运行时”进行的。把不需要惰性求值的表达式预先进行及早求值,能避免一些分析时开销。如果可能的话,用 instance_exec,或 instance_eval 带块的形式,也比直接在字符串上求值好。——苏小脉在 如果用这种方式来构造一些复杂的对象呢?上的发言
Walter Webcoder有一个非常棒的想法:设计一个 Web算数页面。该页面是含有一个文本域以及按钮的简单 Web表单,并被各种各样的非常酷的数学链接和横幅广告包围,使得看起来丰富多彩。用户输入一个算术表达式到文本域中,并按下按钮,然后结果就会被显示出来。一夜之间,世界上所有计算器都变得无用了; Walter大大获利,然后他退休并把他的余生用于收集车牌号。Walter认为实现这样一个计算器很容易。他可以用 Ruby的 CGI库访问表单域中的内容,再用 eval方法把字符串当做表达式来求值。
1234567891011121314require
'cgi'
cgi =
CGI
:
:new
(
"html4"
)
# Fetch the value of the form field "expression"
expr = cgi[
"expression"
].to_s
begin
result = eval(expr)
rescue
Exception
=> detail
# handle bad expressions
end
# display result back to user...
Walter把这个应用程序放到网上才几秒钟,来自 Waxahachie的一个 12岁小孩在表单中输入了 system("rm"),随他的计算机上的文件一起, Walter的美梦一下子破灭了。Walter得到了一个重要的教训: 所有的外部数据都是有危险的。不要让它们靠近那些可能改动你的系统的接口。在这个案例中,表单中的内容是外部数据,而对eval的调用正是一个安全漏洞。
1.3.7 instance_eval, module_eval, class_eval
instance_eval,
module_eval和
class_eval是
eval方法的特殊形式。
1.3.7.1 instance_eval
Object类提供了一个名为
instance_eval的公开方法,该方法可被一个实例调用。他提供了操作对象的实例变量的途径。可以使用字符串向此方法传递参数或者传递一个代码块。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class
Rubyist
def
initialize
@geek
=
"Matz"
end
end
obj = Rubyist.
new
# instance_eval可以操纵obj的私有方法以及实例变量
obj.instance_eval
do
puts
self
# => #<Rubyist:0x2ef83d0>
puts
@geek
# => Matz
end
|
通过
instance_eval传递的代码块使得你可以在对象内部操作。你可以在对象内部肆意操纵,不再会有任何数据是私有的!
instance_eval亦可用于添加类方法。
1
2
3
4
5
6
7
8
9
10
|
class
Rubyist
end
Rubyist.instance_eval
do
def
who
"Geek"
end
end
puts Rubyist.who
# => Geek
|
还记得我们在之前匿名类的讲述(代码清单第四条)么?这个例子在这里被再一次的使用。
1.3.7.2 module_eval, class_eval
module_eval和
class_eval方法用于模块和类,而不是对象。
class_eval是
module_eval的一个别名。
module_eval和
class_eval可用于从外部检索类变量。
1
2
3
4
|
class
Rubyist
@@geek
=
"Ruby's Matz"
end
puts Rubyist.class_eval(
"@@geek"
)
# => Ruby's Matz
|
module_eval和
class_eval方法亦可用于添加类或模块的实例方法。尽管名字上两个方法时不同的,但他们的功能是相同的,并且都可以在模块或者类上使用。
1
2
3
4
5
6
7
8
9
|
class
Rubyist
end
Rubyist.class_eval
do
def
who
"Geek"
end
end
obj = Rubyist.
new
puts obj.who
# => Geek
|
备注当作用于类时, class_eval将会定义实例方法,而 instance_eval定义类方法。
1.3.8 class_variable_get, class_variable_set
添加或查询一个类变量,
class_variable_get方法和
class_variable_set方法都可以被使用。
class_variable_get方法需要一个代表变量名称的符号作为参数,并返回变量的值。
class_variable_set方法也需要一个代表变量名称的符号作为参数,同时也要求传递一个值,作为欲设定的值。
1
2
3
4
5
6
|
class
Rubyist
@@geek
=
"Ruby's Matz"
end
Rubyist.class_variable_set(:
@@geek
,
'Matz rocks!'
)
puts Rubyist.class_variable_get(:
@@geek
)
# => Matz rocks!
|
1.3.9 class_variables
如果你想知道一个类中有哪些类变量,我们可以使用
class_varibles方法。他返回一个数组(
Array),以符号(
Symbol)的形式返回类变量的名称。
1
2
3
4
5
6
7
8
9
10
11
|
class
Rubyist
@@geek
=
"Ruby's Matz"
@@country
=
"USA"
end
class
Child < Rubyist
@@city
=
"Nashville"
end
print Rubyist.class_variables
# => [:@@geek, :@@country]
puts
p Child.class_variables
# => [:@@city]
|
你可以从程序的输出中观察到
Child.class_variables输出的是在
Child类中定义的类变量(
@@city)。
Child类没有从父类中继承类变量(
@@geek,
@@country)。
1.3.10 instance_variable_get, instance_variable_set
我们可以使用
instance_variable_get方法查询实例变量的值。
1
2
3
4
5
6
7
8
|
class
Rubyist
def
initialize(p1, p2)
@geek
,
@country
= p1, p2
end
end
obj = Rubyist.
new
(
'Matz'
,
'USA'
)
puts obj.instance_variable_get(:
@geek
)
# => Matz
puts obj.instance_variable_get(:
@country
)
# => USA
|
类比于
class_variable_set,你可以使用
instance_variable_set来设置一个对象的实例变量的值。
1
2
3
4
5
6
7
8
9
10
11
|
class
Rubyist
def
initialize(p1, p2)
@geek
,
@country
= p1, p2
end
end
obj = Rubyist.
new
(
'Matz'
,
'USA'
)
puts obj.instance_variable_get(:
@geek
)
# => Matz
puts obj.instance_variable_get(:
@country
)
# => USA
obj.instance_variable_set(:
@country
,
'Japan'
)
puts obj.inspect
# => #<Rubyist:0x2ef8038 @country="Japan", @geek="Matz">
|
这样做的好处就是,你不需要使用
attr_accessor等方法为访问实例变量建立接口。
1.3.11 const_get, const_set
类似的,
const_get和
const_set用于操作常量。
const_get返回指定常量的值:
1
|
puts Float.const_get(:
MIN
)
# => 2.2250738585072e-308
|
const_set为指定的常量设置指定的值,并返回该对象。如果常量不存在,那么他会创建该常量,就是下面示范的那样:
1
2
3
|
class
Rubyist
end
puts Rubyist.const_set(
"PI"
,
22
.
0
/
7
.
0
)
# => 3.14285714285714
|
因为
const_get返回常量的值,因此,你可以使用此方法获得一个类的名字并为这个类添加一个新的实例化对象的方法。这样使得我们有能力在运行时创建类并实例化其实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Let us call our new class 'Rubyist'
# (we could have prompted the user for a class name)
class_name =
"rubyist"
.capitalize
Object
.const_set(class_name,
Class
.
new
)
# Let us create a method 'who'
# (we could have prompted the user for a method name)
class_name =
Object
.const_get(class_name)
puts class_name
# => Rubyist
class_name.class_eval
do
define_method
:who
do
|my_arg|
my_arg
end
end
obj = class_name.
new
puts obj.who(
'Matz'
)
# => Matz
|