论坛首页 编程语言技术论坛

ruby动态编程

浏览 15338 次
该帖已经被评为良好帖
作者 正文
   发表时间:2009-04-27   最后修改:2009-05-07
Ruby 动态编程
在介绍ruby动态编程之前,首先看一下,什么叫“动态”语言:

维基百科 写道
动态语言就是一种在运行时可以改变其结构的语言:例如新的函数可以被引进,已有的函数可以被删除等在结构上的变化。众所周知的ECMAScript(JavaScript)便是一个动态语言,除此之外如PHP、Ruby、Python等也都属于动态语言,而C、C++等语言则不属于动态语言。


在大部分的编译语言和解释语言中,编写程序和运行程序是两个截然不同的操作,换句话说,编写的代码是确定的,在运行的时候,可能需要对其进行修改。这对于ruby来说非常简单,一个ruby程序,可以在运行的过程中进行修改,甚至可以加入新的代码并且运行,而不需要重新运行该程序。
这种能够修改可执行应用程序数据的能力就叫做元编程。
维基百科 写道
元编程是指某类[计算机程序]的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在[运行时]完成部分本应在[编译时]完成的工作。很多情况下比手工编写全部代码相比工作效率更高。编写元程序的语言称之为元语言,被操作的语言称之为目标语言。一门语言同时也是自身的元语言的能力称之为反射。


在ruby代码中,其实我们一直都在进行元编程,虽然可能只是一句非常简单的代码,比如说,在“”中嵌入一个表达式,这就是元编程。毕竟,嵌入的的表达式并非真正的代码,它只是一个字符串,但是ruby却可以将它转换成真正的ruby代码并执行它。
大多数情况下,可以在双引号分隔的字符串中嵌入一些简单的以“#{”和“}”分隔的代码,通常会嵌入一个变量,或是一个表达式:
aStr = 'hello world' 
puts( "#{aStr}" ) 
puts( "#{2*10}" )

但是在实际情况中并不能满足于如果简单的表达式,如意愿意的话,应该可以在“”字符串中嵌入任何东西,甚至不需要使用print或puts来显示最终的结果。只要将字符串放置到程序中,ruby就会执行它:
	"#{def x(s)
       puts(s.upcase)
    	end;
 (1..3).each{x('hello')}}"

在一个字符串中写出一整个程序可能需要相当的努力。然而,在某些场合,这些功能可能会更有效率。例如,在rails中使用了大量的元编程。事实上,任何程序都将因为可以在程序执行过程中修改程序的行为而受益,这也是元编程最重要的意义。

动态(元编程)特性在RUBY中无处不在。例如,attribute accessors,attr_accessor :aValue就导致了 aValue 和 aValue= 两个方法被创建

eval魔法

在介绍ruby中eval之前,先来看一下大家都比较熟悉的javascript中的eval方法:

w3cschool 写道
The eval() function evaluates a string and executes it as if it was script code.                 
这个函数可以把一个字符串当作一个JavaScript表达式一样去执行它。

在ruby中,eval方法同样地,提供了在字符串中执行ruby表达式的功能。乍看之下,eval方法同在字符串中嵌入#{}的作用一样:
puts( eval("1 + 2" ) ) 
puts( "#{1 + 2}" )

但是,有些时候,结果却并非想象的那样,考虑下面的例子:
exp = gets().chomp() 
puts( eval( exp )) 
puts( "#{exp}" )

假如键入2*4并赋值给变量exp。当使用eval执行exp后结果为8。当使用执行#{ exp }后,结果为“2*4”。这是因为,通过gets()方法接收到的是一个字符串,“#{ }”将它当成字符串处理,而不是一个表达式,但是eval( exp )将它作为一个表达式处理。
为了能够在字符串中执行,需要在字符串中使用eval方法。(尽管可能会使对象执行失败)
puts( "#{eval(exp)}" )

下面是另一个示例:
print( "Enter the name of a string method (e.g. reverse or upcase): " ) 
                                         # user enters: upcase 
methodname = gets().chomp() 
exp2 = "'Hello world'."<< methodname 
puts( eval( exp2 ) )                     #=> HELLO WORLD 
puts( "#{exp2}" )                        #=> “Hello world”.upcase 
puts( "#{eval(exp2)}" )                  #=> HELLO WORLD

eval方法也可以执行多行字符串,可以用来执行嵌在字符串中的整个程序:
eval( 'def aMethod( x )
   return( x * 2 )
end
 
num = 100
puts( "This is the result of the calculation:" )
puts( aMethod( num ))' )


根据eval的特性,看一下下面的程序:
input = ""
until input == "q"
   input = gets().chomp()
   if input != "q" then eval( input ) end
end


虽然代码并不是太多,但这个小程序可以让你在命令行中创建和执行实际可运行的ruby代码。试着输入下面两个方法(不能输入’q’):
def x(aStr); puts(aStr.upcase);end 
def y(aStr); puts(aStr.reverse);end

注意你必须在命令行输入全部的方法,程序会分析刚才输入的方法,eval方法转换刚才输入的方法为真实的可执行的ruby代码。可以输入下面的代码去校验一下:
x("hello world") 
y("hello world")

输入结果为:

>>x('hello world')
>>HELLO WORLD
>>y('hello world')
>>dlrow olleh

eval的特殊类型

eval还有几个方法变体:instance_eval, module_eval, class_eval。
instance_eval方法可以通对对象调用,它提供了访问对象实例变量的能力。instance_eval可以通过代码块或是字符串调用:
class MyClass 
 def initialize 
   @aVar = "Hello world" 
 end 
end 
 
ob = MyClass.new 
p( ob.instance_eval { @aVar } )            #=> "Hello world" 
p( ob.instance_eval( "@aVar" ) )            #=> "Hello world"

另外,eval方法不能访问对象中私有的方法(尽管instance_eval方法是公有的)。不过,你可以通过调用public :eval方法明确地改变eval方法的访问属性。但是胡乱地改变基类方法的访问属性并不被推荐。
(严格地说,eval是核心模块的功能并混入到Object类中。)
你可以改变eval方法的访问属性,通过在Object类中添加下面的定义:
class Object 
   public :eval 
end 

当然,当编写独立的代码时,都处于Object类的生命周期内,只需要简单地输入下面的代码(没有Object类的包装),就可以达到同样的效果:
  
 public :eval 

现在可以通过ob实例来调用eval方法:
p( ob.eval( "@aVar" ) )                #=> "Hello world"

module_eval,class_eval方法用于模块和类上操作。例如,下面的代码在模块X中添加xyz方法(xyz方法在一个代码块中通过define_method方法定义,并作为X模块的实例方法),在类Y中添加abc方法:
module X 
end 
 
class Y 
  @@x = 10 
  include X 
end 
 
X::module_eval{ define_method(:xyz){ puts("hello" ) } } 
Y::class_eval{ define_method(:abc){ puts("hello, hello" ) } } 

所以,Y的实例就拥有了abc方法,和由X模块混入的xyz方法:
ob = Y.new 
ob.xyz                  #=> “hello” 
ob.abc                  #=> “hello, hello”

不管它们的名字,module_eval和class_eval在功能上完全相同,并且可以都可以用在模块或者类中:
X::class_eval{ define_method(:xyz2){ puts("hello again" ) } } 
Y::module_eval{ define_method(:abc2){ puts("hello, hello again" ) } } 

当然也可以以同样的方式给ruby标准类添加方法:
String::class_eval{ define_method(:bye){ puts("goodbye" ) } } 
"Hello".bye            #=> “goodbye” 


添加变量和方法

module_eval和class_eval同样可以用来接收变量的值(但是需要记住,这会加强对具体实现类的依赖,破坏了封装性):
Y.class_eval( "@@x" )  #=>10


事实上,class_eval可以执行随意复杂的表达式。例如可以通过一个字符串来添加一个新的方法:
ob = X.new 
X.class_eval( 'def hi;puts("hello");end' ) 
ob.hi               #=> “hello”


考虑刚才的在类的外部增加和调用变量的例子(使用class_eval);事实上,在类的内部也提供了同样的方法。这个方法就是 class_variable_get( 该方法接接受一个参数:变量名)和class_variable_set(该方法接受两个参数,第一个参数为变量名,第二个参数为变量的值),下面是使用 这两个方法的示例:

class X
  @@aParam = 1000

  def self.addvar( aSymbol, aValue )
    class_variable_set( aSymbol, aValue )
  end

  def self.getvar( aSymbol )
    return class_variable_get( aSymbol )
  end
end

X.addvar( :@@newvar, 2000 )
puts( X.getvar( :@@newvar ) )   #=> 2000

puts( X.getvar( :@@aParam ) )


可以通过class_variables方法返回一个包含所有类变量的数组:
p( X.class_variables )      #=> ["@@abc", "@@newvar"]


当然,也可通过instance_variable_set方法,来为类的实例添加实例变量:
ob = X.new 
ob.instance_variable_set("@aname", "Bert")
p ob.instance_variable_get("@aname")     #=> "Bert"


通过组合这些能力,程序员可以在外部完全更改类的结构。例如,可以为类X定义一个addMehtod方法,通过参数m(方法名), 参数&block(方法体)来动态地添加方法:

def addMethod( m, &block ) 
   self.class.send( :define_method, m , &block ) 
end

(send方法会根据第一个参数辨认出相应的方法并调用,并且将其它参数传递给该方法。)

现在,X对象可以调用addMehtod方法给X类增加一个新的方法:
ob.addMethod( :xyz ) { puts("My name is #{@aname}") }


尽管addMethod这个方法是由一个具体的实例调用的(这里是ob这个实例),但它的作用针对的却是这个类,所以该类的其它实例(如ob2),也可以调用由ob实例所添加的新方法:
ob2 = X.new 
ob2.instance_variable_set("@aname", "Mary") 
ob2.xyz   #=> My name is Mary


如果不考虑数据的封装性,可以通过实例的instance_variable_get方法来取得实例变量的值:
ob2.instance_variable_get( :@aname )


同样地,也可以设置和获取常数的值:
X::const_set( :NUM, 500 ) 
puts( X::const_get( :NUM ) )


既然const_get可以返回一个常量的值,那么就可以通过这个方法来得到一个类的名字,然后使用这个类名,并通过new方法来创建这个类实例。这样就可以在运行时通过提示用户输入相应的类名或方法名来动态地创建类实例以及调用相应的方法:
class X 
   def y 
      puts( "ymethod" ) 
   end 
end 
 
print( "Enter a class name: ")                           #<= Enter: X 
cname = gets().chomp 
ob = Object.const_get(cname).new 
p( ob ) 
print( "Enter a method to be called: " )              #<= Enter: y 
mname = gets().chomp 
ob.method(mname).call


运行时刻创建类

在上面,我们已经可以修改类的结构,或者创建一个类的实例,但是,我们是否可以在运行时创建一个全新的类?正像const_get可以访问存在的类一 样,const_set方法就可以用来创建一个新的类。下面这个示例将提示用户输入类名,然后创建类,添加一个方法(方法名为myname),创建类的实 例,然后调用刚才添加的方法:
puts("What shall we call this class? ") 
className = gets.strip().capitalize() 
Object.const_set(className,Class.new)           
puts("I'll give it a method called 'myname'" ) 
className = Object.const_get(className) 
className::module_eval{ define_method(:myname){  
   puts("The name of my class is '#{self.class}'" ) }  
   } 
x = className.new 
x.myname


绑定

eval方法有一个可选的参数--binding,如果为指定的话,那么表达式的值就会是一个具体的范围或上下文环境绑定。不过不必为这个有所意外,在 Ruby中,binding方法会返回一个Binding对象的实例,可以使用binding方法返回绑定的值。下是是ruby文档中提供的一个示例:
def getBinding(str) 
   return binding() 
end 
str = "hello" 
puts( eval( "str + ' Fred'" )   )                                     #=> "hello Fred" 
puts( eval( "str + ' Fred'", getBinding("bye") ) )              #=> "bye Fred"


binding方法是内核的一个私有方法。getBinding方法通过调用binding方法返回当前上下文环境中str的值。在第一次调用eval方 法的时候,当前上下文环境是main对象,并且str的值就是定义的局部变量str的值。在第二次调用eval方法是,当前的上下文环境则是 getBinding方法内部,局部变量str的值现在则为getBinding方法中参数str的值。Binding方法经常作为eval的第二个参数,这样eval就不会因为找不到变量而出错了。

上下文环境也可以在类中定义。在下面的例子中,可以看到,实例变量@mystr和类变量@@x根据类而不同:

class MyClass
   @@x = " x"
   def initialize(s)
      @mystr = s
   end
   def getBinding
      return binding()
   end
end

class MyOtherClass
   @@x = " y"
   def initialize(s)
      @mystr = s
   end
   def getBinding
      return binding() 
   end
end

@mystr = self.inspect
@@x = " some other value"

ob1 = MyClass.new("ob1 string")
ob2 = MyClass.new("ob2 string")
ob3 = MyOtherClass.new("ob3 string")
puts(eval("@mystr << @@x", ob1.getBinding))  #=> ob1 string x
puts(eval("@mystr << @@x", ob2.getBinding))  #=> ob2 string x
puts(eval("@mystr << @@x", ob3.getBinding))  #=> ob3 string y
puts(eval("@mystr << @@x", binding))             #=> main some other value



SEND

可以使用send方法来调用参数指定的方法:
name = "Fred"
puts( name.send( :reverse ) )      #=> derF
puts( name.send( :upcase ) )       #=> FRED


尽管文档规定send方法必须需要一个方法符号作为参数,但是也可以直接使用一个字符串作为参数,或者,为了保持一致,也可以使用to_sym进行方法名称进行相应的转换后调用:
name = MyString.new( gets() )     # 输入upcase
methodname = gets().chomp.to_sym               #<= to_sym 并非必需,输入upcase
puts name.send(methodname)                     #=>UPCASE


下面的这个例子显示在运行状态中通过send方法动态地执行指定的方法:
class MyString < String
  def initialize( aStr )
    super aStr
  end

  def show
    puts self
  end
  def rev
    puts self.reverse
  end
end

print("Enter your name: ")                     #<= Enter: Fred
name = MyString.new( gets() )
print("Enter a method name: " )                #<= Enter: rev
methodname = gets().chomp.to_sym
puts( name.send(methodname) )                  #=> derF


回忆一下上面使用define_method来创建方法的例子,传递了方法的名称m,还为要创建的新方法传递了一个代码块@block

def addMethod( m, &block ) 
   self.class.send( :define_method, m , &block ) 
end


移除方法

除了创建新的方法,有的时候你可能需要移除现有的方法。可以在方法内部使用remove_method方法完成,这将为移除指定的方法:
puts( "hello".reverse ) 
class String 
   remove_method( :reverse ) 
end 
puts( "hello".reverse )     #=> „undefined method‟ error!


但是如果子类重写父类的方法,在子类中通过remove_method移除该方法,但父类的方法不会被移除:

class Y 
   def somemethod 
      puts("Y's somemethod") 
   end 
end 
 
class Z < Y 
   def somemethod 
      puts("Z's somemethod") 
   end    
end 
 
zob = Z.new 
zob.somemethod                                   #=> “Z‟s somemethod” 
 
class Z 
   remove_method( :somemethod )    
end 
 
zob.somemethod                                   #=> “Y‟s somemethod”


相比之下,undef_method方法,就可以避免由于父子类之间存在相同名称的方法而造成最终调用了父类的方法:
zob = Z.new 
zob.somemethod                                   #=> “Z‟s somemethod” 
 
class Z 
   undef_method( :somemethod )           
end 
 
zob.somemethod                                   #=> „undefined method‟ error



处理丢失的方法

当ruby试着去调用一个不存在的方法时( 或者,一个对象发送了一个不能被处理的消息 ),就可能会引起错误并造成程序的终止。你可能更喜欢你编写的程序能够从这样的错误中恢复过来。可以使用method_missing方法,该方法接受一 个方法名,如果该方法不存在,method_missing方法就会被调用:
def method_missing( methodname )
   puts( "#{methodname} does not exist" )
end

xxx #=>xxx does not exist


method_missing也可以处理还有参数的根本就不存在的方法:

def method_missing( methodname, *args ) 
      puts( "Class #{self.class} does not understand: 
                            #{methodname}( #{args.inspect} )" ) 
end 


method_missing方法甚至可以动态地创建没有定义的方法:

def method_missing( methodname, *args ) 
       self.class.send( :define_method, methodname,  
                  lambda{ |*args| puts( args.inspect) } ) 
end 
 

冻结对象

上面讲了许多修改对象的方法,这样对象就可以会在无意中被修改而造成一些不希望结果。事实上,可以使用freeze方法来冻结对象的状态。一旦对象被冻结了,那么任何对此对象的修改,都会引发一个TypeError
异常。注意,一旦对象被冻结,将不能够解冻。

s = "Hello" 
s << " world" 
s.freeze 
s << " !!!"     # Error: "can't modify frozen string (TypeError)" 



可以使用frozen?方法来检查对象是否被冻结:
a = [1,2,3] 
a.freeze 
if !(a.frozen?) then 
   a << [4,5,6] 
end 


frozen方法也可以用来直接冻结一个类(如上面使用的X):
X.freeze 

   发表时间:2009-04-28   最后修改:2009-04-28
"def x(s)
  puts(s.upcase)
end;
(1..3).each { x('hello')  }"

引用
只要将字符串放置到程序中,ruby就会执行它


会吗?我试了要eval才行.
0 请登录后投票
   发表时间:2009-04-28  
看完了.但绑定那块没太明白.
0 请登录后投票
   发表时间:2009-04-28  
aone 写道
"def x(s)
  puts(s.upcase)
end;
(1..3).each { x('hello')  }"

引用
只要将字符串放置到程序中,ruby就会执行它


会吗?我试了要eval才行.

这个例子不需要eval吧?我试过结果是:

C:\Documents and Settings\xp2008>irb
irb(main):001:0> "#{def x(s)
irb(main):002:0"        puts(s.upcase)
irb(main):003:0"     end;
irb(main):004:0"  (1..3).each{x('hello')}}"
HELLO
HELLO
HELLO
=> "1..3"

0 请登录后投票
   发表时间:2009-04-28   最后修改:2009-04-28
C:\Documents and Settings\w>irb
irb(main):001:0> "def x(s);puts(s.upcase);end;(1..3).each{x('hello')}"
=> "def x(s);puts(s.upcase);end;(1..3).each{x('hello')}"
irb(main):002:0> "def x(s)
irb(main):003:0" puts(s.upcase)
irb(main):004:0" end;
irb(main):005:0" (1..3).each{x('hello')}"
=> "def x(s)\nputs(s.upcase)\nend;\n(1..3).each{x('hello')}"
irb(main):006:0>
irb(main):007:0*
irb(main):008:0* eval("def x(s);puts(s.upcase);end;(1..3).each{x('hello')}")
HELLO
HELLO
HELLO
=> 1..3
irb(main):009:0>


为什么不一样呢?ruby版本问题?我1.8.6
0 请登录后投票
   发表时间:2009-04-28  
aone 写道

为什么不一样呢?ruby版本问题?我1.8.6


你少了 #{ 和 }
1 请登录后投票
   发表时间:2009-04-28  
好文~

rainlife 写道
在ruby代码中,其实我们一直都在进行元编程,虽然可能只是一句非常简单的代码,比如说,在“”中嵌入一个表达式,这就是元编程。毕竟,嵌入的的表达式并非真正的代码,它只是一个字符串,但是ruby却可以将它转换成真正的ruby代码并执行它。
大多数情况下,可以在双引号分隔的字符串中嵌入一些简单的以“#{”和“}”分隔的代码,通常会嵌入一个变量,或是一个表达式

Ruby的string interpolation,字符串插值应该还算不上元编程吧。至少Ruby的字符串插值并不带来任何“程序操纵或改变程序”的能力。
在调用Ruby的eval时,作为参数传给eval的字符串就是普通的字符串,在Ruby解析源码的时候不会做任何特殊的处理。而使用#{}形式(及其简写形式)的字符串插值时,Ruby解析器会为插值代码生成对应的AST,保存在一个NODE_EVSTR里。
可以简单的验证它们行为的差别:
puts 'start'
puts "#{1 +* 2}" # 这段插值代码有语法错误
puts 'end'

puts 'start'
puts eval('1 +* 2') # 这段传给eval的参数中代码有语法错误
puts 'end'

可以观察到前者在还没有输出start就报语法错了,而后者是输出start之后才报语法错。这是解析时机的差异造成的,也显示了Ruby的字符串插值中的代码并不“只是一个字符串”。
0 请登录后投票
   发表时间:2009-05-04  
aone 写道
看完了.但绑定那块没太明白.

绑定 还是讲的比较清楚的,受益良多
0 请登录后投票
   发表时间:2009-05-06  
我可是全都看完了,发现了个小错
引用
ob = X.new 
ob.instance_variable_set("@aname", "Bert")
p ob.instance_variable_set("@aname")     #=> "Bert"


下面的那个应该是instance_variable_get 
0 请登录后投票
   发表时间:2009-05-07  
wolfplanet 写道
我可是全都看完了,发现了个小错
引用
ob = X.new 
ob.instance_variable_set("@aname", "Bert")
p ob.instance_variable_set("@aname")     #=> "Bert"


下面的那个应该是instance_variable_get 

确实应该是instance_variable_get,谢谢指出,已经改正。
1 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics