0. 前言及需求
运行时动态编程也算是Ruby的卖点之一,心血来潮想要尝试一下我是否能够在运行时动态对Ruby原有的方法进行调整?可能生产线上用不上这些黑科技,不过对于个人折腾来说这还是蛮好的,可以打发一下单身的时光。
我们设定一个简单的方法,做的事情很简单,只是输出了对应的方法名,并且前后有形如p xxx
的打印语句
def method_with_print p "begin method" puts "This is '#{self.method(:method_with_print).name}' method" p "end method"endif __FILE__ == $0 method_with_printend
运行结果如下
>> ruby xx.rb"begin method"This is 'method_with_print' method"end method"
我们有什么办法把上述方法中的p
语句去掉,我能想到的方式是
获取方法的字符串版本。
使用正则表达式匹配并替换
p
语句,得到一个新的定义方法的字符串。在恰当的上下文运行新的字符串,重新定义方法。
以Ruby的方式包装一个方法,去改造并调用原来的方法。
看起来好像也不难,一步步来试试看。
1. 获取方法定义的字符串版本
如果我们是用的JavaScript,想要获取对应方法的字符串版本只需要调用方法对象的toString
便可
> function a() { ... return "lan"... }undefined> a.toString()'function a() {\nreturn "lan"\n}'
但Ruby似乎并没有这么直接的方式获取定义方法的字符串,估计是社区觉得这种需求其实用处不大吧,从网上搜索了一下有一个叫做method_source的包可以做到这一点,他是以补丁的方式来增强Ruby原有的模块方法,进而为每一个非绑定方法添加一个source
的属性,我们通过这个属性就可以获取到方法的源代码了。不过这里有一个问题,这个方法的实现机制是通过调用目标方法原有的source_location
方法获取到定义该方法的具体位置,然后访问对应的文件,截取出指定方法对应的定义字符串。换句话说如果我们的方法是在REPL 里面定义的话就不能获取到对应方法的字符串了。目前我们只能在获取在脚本里(xx.rb)
定义对应方法的字符串了。
上面所说的这个库,其实已经包含在我们平时用得比较多的pry工具里面了,这是一个比较常用的REPL工具,现在我只需要在原有代码的可执行部分里面添加
if __FILE__ == $0 # method_with_print require 'pry' puts Object.instance_method(:method_with_print).sourceend
运行对应脚本 xx.rb
就能得到目标方法的定义字符串了
>> ruby xx.rbdef method_with_print p "begin method" puts "This is '#{self.method(:method_with_print).name}' method" p "end method"end
可以看到以上的做法相比JavaScript来说有点绕,因为Ruby的方法是不需要添加括号就可以调用的,直接写方法名的话就是调用方法。这里想要操作对应方法名的方法对象,并且是非绑定版本的。可以通过Object.instance_method
来获取。
2. 正则匹配且替换
我们已经获取了对应的字符串版本了,那么接下来要做的就是匹配并且替换掉原来的p xxxx
语句了。
我就写一个比较简单的正则匹配就好了,毕竟如果要匹配ruby所有的打印语句的话,会占用比较多的时间以及篇幅。
根据上面的思路我得到了这样一个程序版本
if __FILE__ == $0 # method_with_print require 'pry' REG_CONSOLE = /\s+p\s+.+/ method_string = Object.instance_method(:method_w\ ith_print).source method_string.gsub!(REG_CONSOLE, '') method_stringend
运行看看
>> ruby xx.rbdef method_with_print puts "This is '#{self.method(:method_with_print).name}' method"end
可见,对应的p xxx
语句已经从字符串中删除了,我们已经得到了改版之后的方法定义字符串了。
3. 重新定义方法
如何重新定义方法? 我们应该都听过JavaScript有名为eval
的方法,可以动态执行字符串。类似的的Ruby也有Kernel#eval
。而且它在类层面还提供了Class#class_eval
,在对象层面提供了Object#instance_eval
方法,让你可以操作不同的上下文。这里讲一下比较直观的Kernel#eval
, 我们要直接执行Ruby代码可以像这样执行
>> eval("p 'I love ruby'")"I love ruby" => "I love ruby"
那之前定义的方法是不是也能以字符串的形式,通过Kernel#eval
方法来重新定义?我把代码写成这样
if __FILE__ == $0 # method_with_print require 'pry' REG_CONSOLE = /\s+p\s+.+/ method_string = Object.instance_method(:method_with_print).source method_string.gsub!(REG_CONSOLE, '') eval(method_string) method_with_printend
运行看看结果是否符合预期
>> ruby xx.rb This is 'method_with_print' method
Awesome, 已经满足了我们这次的需求了,我们可以在运行时删除方法的p xxx
语句,并且重新定义了原有的方法。最后我试试用Ruby的方式来处理一下个问题,肯定不是最优雅的,不过这是我目前能想到的足够折腾的处理方式。
help
4. Ruby的处理方式
Ruby是“真”面向对象的编程语言,因为他真的能够做到一切都是对象,比如
[1] pry(main)> 1.to_s => "1"[2] pry(main)> '2'.to_i => 2
平时我们定义的函数,其实也是方法
[3] pry(main)> def m[3] pry(main)* 'lan'[3] pry(main)* end=> :m[4] pry(main)> self.m => "lan"[5] pry(main)> self=> main
m
其实是挂在main
这个对象上的方法。用面向对象的方式来解决上面的问题,我们是否可以给方法添加一个属性,通过这个属性来调用原有方法的删除了p xxxx
语句之后的版本呢?我们首先来看看方法对象的继承链条
[6] pry(main)> m_method = Object.instance_method(:m) [10] pry(main)> m_method.class.ancestors => [UnboundMethod, MethodSource::MethodExtensions, MethodSource::SourceLocation::UnboundMethodExtensions, Object, PP::ObjectMixin, Kernel, BasicObject]
可见方法对象所属类的祖先链如下
[UnboundMethod, MethodSource::MethodExtensions, MethodSource::SourceLocation::UnboundMethodExtensions, Object, PP::ObjectMixin, Kernel, BasicObject]
祖先链有这一大堆的东西,那要不我们就斗胆一点扩展一下MethodSource::MethodExtensions
这个模块吧。 你怎么知道他是一个模块而不是类?
[13] pry(main)> MethodSource::MethodExtensions.class=> Module
我尝试在模块里面添加MethodSource::MethodExtensions#remove_p_statement
方法
require 'pry'module MethodSource::MethodExtensions REG_CONSOLE = /\s+p\s+.+/ def remove_p_statement(*params) method_string = self.source.gsub(REG_CONSOLE, '') method_owner = self.owner new_method = method_owner.instance_eval(method_string) method_owner.send(new_method, *params) endend
它是方法的方法,只需要在方法的后面调用它。它会在对象的上下文Object#instance_eval
重新定义这个方法,然后在内部自动发派这个方法,并附带上一个可变参数。最后我把执行脚本的主体内容改为
if __FILE__ == $0 puts "==========" puts "new method result:\n" Object.instance_method(:method_with_print).remove_p_statement() puts "==========" puts "==========" puts "old method result:\n" method_with_print() puts "=========="end
PS: 由于
method_with_print
方法定义的时候没有参数,我们这里括号里面的内容都是空。
最后的结果如下
========== new method result: This is 'method_with_print' method ========== ========== old method result:"begin method"This is 'method_with_print' method"end method"==========
可见,我们的方法调用了 MethodSource::MethodExtensions#remove_p_statement
这个方法之后得到了一个新的方法并执行,但是却不会影响到执行脚本上下文中最初定义的原始方法的行为。
5. 再见
以上代码有什么用? .........其实还真没什么卵用,纯属瞎折腾。
play
Happy Coding and Writing !!!
作者:lanzhiheng
链接:https://www.jianshu.com/p/3ea533d96a67