Ruby 动态编程
在介绍ruby动态编程之前,首先看一下,什么叫“动态”语言:
在大部分的编译语言和解释语言中,编写程序和运行程序是两个截然不同的操作,换句话说,编写的代码是确定的,在运行的时候,可能需要对其进行修 改。这对于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方法:
这个函数可以把一个字符串当作一个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方法。(尽管可能会使对象执行失败)
下面是另一个示例:
- 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类的包装),就可以达到同样的效果:
现在可以通过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同样可以用来接收变量的值(但是需要记住,这会加强对具体实现类的依赖,破坏了封装性):
事实上,class_eval可以执行随意复杂的表达式。例如可以通过一个字符串来添加一个新的方法:
- X.class_eval( 'def hi;puts("hello");end' )
- ob.hi #=> “hello”
- ob = X.new
考虑刚才的在类的外部增加和调用变量的例子(使用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方法返回一个包含所有类变量的数组:
当然,也可通过instance_variable_set方法,来为类的实例添加实例变量:
- ob = X.new
- ob.instance_variable_set("@aname", "Bert")
- 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类增加一个新的方法:
尽管addMethod这个方法是由一个具体的实例调用的(这里是ob这个实例),但它的作用针对的却是这个类,所以该类的其它实例(如ob2),也可以调用由ob实例所添加的新方法
- ob2 = X.new
- ob2.instance_variable_set("@aname", "Mary")
- ob2.xyz #=> My name is Mary
如果不考虑数据的封装性,可以通过实例的instance_variable_get方法来取得实例变量的值:
同样地,也可以设置和获取常数的值:
- 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"
- uts( 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
- uts( 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
- ob.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
- xx #=>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
- 转自http://www.javaeye.com/topic/375531