Ruby的eval家族

元编程与eval

所谓元编程就是"生成代码的代码".

对于"解释型"的编程语言,由于程序整个运行时期都依赖于解释器,最简单的方式就是让语言提供一个eval方法,将字符串当作该语言喂给解释器执行, Ruby,Python,JavaScript都提供了eval方法;

对于区分"编译时"和"运行时"的"编译型"编程语言,可以给这种语言添加一种非常特殊的机制,让它可以在"编译时"生成代码,典型的例子就是C/C++的宏和C++的模板.这种宏或者是模板实际上和语言本身差别很大,C++的模板本身甚至是一门图灵完备的编程语言.

eval的缺点

eval很强大,但是也有很多缺点:
1.将代码作为字符串不能使用IDE/编辑器的语法检查功能,提高了出错的几率,当然这个问题容易解决,只要语法检查工具将eval内的字符串特殊处理即可;
2.字符串由程序拼接而来,有被注入的风险,这就和SQL注入一样;
3.eval内的字符串可能会污染上下文环境或者被上下文环境污染,引发意想不到的bug
因此Ruby提供了几个弱化版的eval来消除这些缺点,它们分别是instance_eval, class_eval, instance_exec

block

讲解instance_eval之前先说一下Ruby的代码块(block)和proc,block作为Ruby的一种基本语言要素,本身不能被当做数据使用,不能动态定义,只能事先写好,但是block可以和proc互相转换,而proc是ruby中的一等公民, 因此block仍然相当于一等公民.
block拥有闭包的性质,在形成的时候会包裹当前词法作用域内的绑定,这和其他正确实现闭包的语言没有区别,但是block中有两类变量会被特殊处理: 当前实例(self和instance variable)和当前类
block有两种使用方式(就我目前了解到的):

1.作为普通的闭包

@a = 1
b = 1
puts_self = Proc.new do
  puts self, @a, b
end

puts_self.call # main 1 1

class A
  def call_proc(proc)
      @a = 2
      b = 2
      proc.call
  end
end

A.new.call_proc(puts_self) # main 1 1

这种用法会捕获词法作用域内的绑定(包括self和@a).当使用proc.call或者yield(args)这种方法去调用proc,都是将block当作了普通的闭包.

2.作为可改变上下文的闭包

@a = 1
b = 1
puts_self = Proc.new do
  puts self, @a, b
end

puts_self.call # main 1 1

class B
  def call_proc(proc)
      @a = 2
      b = 2
      instance_eval(&proc)
  end
end
B.new.call_proc(puts_self) #  #<B:0x00000001338410> 2 1

如果block用instance_eval去执行,block里的@a和self都被改变了,它们的上下文变成了instance_eval的调用者----B的一个实例 #<B:0x00000001338410>;而b仍然被当作普通的变量----它来自于定义闭包时的词法作用域.

可见,block的当前实例是可以被改变的
此外,block的当前类也能被改变,如果block里有取决于当前类的语句/表达式(如def),那么改变了执行结果也会随着上下文的改变而改变

instance_eval

instance_eval方法如其名,将它的调用者作为当前实例(instance)去eval一个block,block里和instance有关的上下文变量self和instance variable都被相应改变了.

instance_eval在改变了当前实例的同时,还改变了当前类

def_method = Proc.new do
 def test_method
 end
end

class C
 def call_proc(proc)
   instance_eval(&proc)
 end  
end

c = C.new
c.call_proc(def_method)

m = c.method(:test_method)
m.owner # #<Class:#<C:0x00000001fb93b0>>,这是m的eigen class

def是作用于当前类上的,它会把它定义的方法放在当前类里面,test_method位于m的eigen class,说明instance_eval将当前类修改为当前实例的eigen class了.

class_eval

instance_eval类似,class_eval是将它的调用者作为当前类(class)去eval一个block,block里和class有关的上下文变量都会被改变

p = Proc.new do
  def test_method
  end
  puts self
end

class D
end

D.class_eval(&p) # D
d = D.new
m = d.method(:test_method)
m.owner # D

因为只有Class的实例才具有class_eval方法,所以我们直接用D去调用class_eval,很容易看出class_eval将当前类和当前实例都修改成了class_eval的调用者.

instance_exec

前面的instance_eval已经很强大了,但是总感觉少了些什么,加入哪天我们闲得蛋疼了想实现一个可以自定义二元运算的类宏def_calc_method, 它接受一个方法名和一个怎么去计算的代码块,效果就是给它的调用者添加一个可以做这种计算的实例方法
具体来说就是要这样的效果

class LeftValue
  extend BinaryCalcDefiner

  def_calc_method :add do |y|
     @x + y
  end

  def_calc_method :times do |y|
    @x * y
  end

 def initialize(x)
      @x = x
  end
end

five = LeftValue.new(5)
five.add(1) # 6
five.times(4) # 20

看起来还是有一丝酷炫,虽然并没有什么卵用.

怎样实现呢?
def_calc_method的架子大概是这样的

module BinaryCalcDefiner
  def def_calc_method(method_name, &calc_proc)
     define_method method_name do |y|
        ______(y, &proc)
     end
  end
end

空白处要填入哪个方法呢?
因为要绑定实例的@x,所以必须要用instance_eval这种能改变上下文的方法去eval传入的块,然而这个块还需要接受一个参数,instance_evalclass_eval肯定是不行的,所以就理所当然地引入了instance_exec来解决这种问题,它eval一个块的时候可以将一个参数作为块的参数

module BinaryCalcDefiner
  def def_calc_method(method_name, &calc_proc)
     define_method method_name do |y|
        instance_exec(y, &proc)
     end
  end
end

大功告成.

总结

eval大大加强了Ruby的元编程能力,而eval本身问题较多.instance_eval, class_eval, instance_exec作为eval方法的弱化版,实际上和SQL的预编译技术差不多,都是将代码中的可变部分控制在很小的范围,以免引入注入风险.此外由于三者都是接受的Ruby的代码块而不是字符串,所以加强了可读性,上下文环境也得以清晰明了(相当于普通的闭包).
三个方法的本质都是通过改变代码块的上下文而使得代码块拥有更强的表达能力,都可以改变当前类和当前实例,特别的,instance_eval可以给带参数的block注入参数.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容