instance_eval的块参数 - 记录?目的?

时间:2012-09-28 22:25:38

标签: ruby metaprogramming

刚才意识到instance_eval产生self作为相关块的参数(除了1.9.2版本中的错误:http://www.ruby-forum.com/topic/189422

1.9.3p194 :003 > class C;end
1.9.3p194 :004 > C.new.instance_eval {|*a| a}
 => [#<C:0x00000001f99dd0>] 
1.9.3p194 :005 > 

这是在某处记录/指定的吗?看ruby-doc:BasicObject,看不到任何块参数。

是否有一个原因 - 来自一些纯粹历史的理由 - 当它自始至终被定义时明确地传递它?


我受此打击的方式是:

l = lambda {  }
myobj.instance_eval(&l)  # barks

这在1.8.x中运行良好(我猜是因为没有强制执行块arity)。

然后升级到1.9.2 - 它仍然有效!这是一个奇怪的巧合,尽管lambda块参数是严格执行的(所以它会因为没有声明自己的参数而抱怨),但是由于上面链接的bug - 实际上并没有在这个版本中传递..

然后升级到1.9.3,那个bug得到了解决,所以它开始抛出参数错误 - 对于一个小版本更改恕我直言,这非常令人惊讶。

因此,一种解决方法是声明参数,或者将lambda改为块:

 l = proc {  }
  myobj.instance_eval(&l) # fine

只是想要描述完整的故事,以帮助他人避免像我一样浪费时间 - 直到这被正确记录。

3 个答案:

答案 0 :(得分:3)

阅读Ruby的源代码,我能解释的是:

instance_eval正在执行:

return specific_eval(argc, argv, klass, self)

反过来运行:

 if (rb_block_given_p()) {
     if (argc > 0) {
         rb_raise(rb_eArgError, "wrong number of arguments (%d for 0)", argc);
     }
     return yield_under(klass, self, Qundef);
 }

您可以看到他们通过Qundef获取VALUES参数。

if (values == Qundef) {
    return vm_yield_with_cref(th, 1, &self, cref);
}

在该特定代码行中,他们手动将argc(参数计数)设置为1,将参数设置为“self”。稍后,准备块的代码将块的参数设置为这些参数,因此第一个参数=“self”,其余为nil。

设置块参数的代码正在执行:

   arg0 = argv[0];

   ... bunch of code ...

     else {
         argv[0] = arg0;
     }

     for (i=argc; i<m; i++) {
         argv[i] = Qnil;
     }

导致:

1.9.3p194 :006 > instance_eval do |x, y, z, a, b, c, d| x.class end
 => Object 
1.9.3p194 :008 > instance_eval do |x, y, z, a, b, c, d| y.class end
 => NilClass 

为什么?我不知道,但代码似乎是故意的。很高兴向实施者提出这个问题并看看他们对此有什么看法。

[编辑]

这可能就是这样,因为你传递给instance_eval的块可能会或可能不会为它设计(代码依赖于自己被设置为你希望块修改的类),而是他们可能会假设你要将它们希望它们作为参数修改的实例传递给它们,这样它们也可以与instance_eval一起使用。

irb(main):001:0> blk = Proc.new do |x| x.class end
#<Proc:0x007fd2018447b8@(irb):1>
irb(main):002:0> blk.call
NilClass
irb(main):003:0> instance_eval &blk
Object

当然这只是一个理论,没有官方文件,我只能猜测。

答案 1 :(得分:1)

我刚刚发现,与主要用于字符串评估的#instance_eval不同,主要用于块评估的#instance_exec没有描述的行为:

o = Object.new
o.instance_exec { |*a| puts "a.size is #{a.size}" }
  => a.size is 0

这可能是意外的不一致,因此您可能发现了一个错误。将其发布在Ruby bugs

答案 2 :(得分:0)

我在这里问了同样的问题:Ruby lambda's proc's and 'instance_eval'

在阅读了答案并完成了一些代码之后,我认为我理解了为什么ruby具有这种奇怪的(IMHO)不一致之处。

它基本上允许Symbol#to_proc正常工作。

例如["foo", "bar"].each(&:puts)代表[...].each { |x| puts x }

[...].each { self.puts }

所以ruby 将self作为第一个参数传递给proc,因此基本上proc可以使用self或其第一个参数。

由于实例eval 根据定义没有明确地传递参数,因此,几乎总是不可见的行为。

例外是当proc是lambda时。这不起作用:

2.4.1 :015 > foo = -> { puts 'hi' }
 => #<Proc:0x007fcb578ece78@(irb):15 (lambda)> 
2.4.1 :016 > [1, 2, 3].each(&foo)
ArgumentError: wrong number of arguments (given 1, expected 0)
    from (irb):15:in `block in irb_binding'
    from (irb):16:in `each'
    from (irb):16

因此,我认为这唯一的问题是使用instance_eval时使用一些未知值,而您不知道proc是否为lambda。在这种情况下,您必须这样做:

proc_var.lambda? ? instance_exec(&proc_var) : instance_eval(&proc_var)

(对我而言)奇怪的是,红宝石根本不为您做这件事。

但是我想你可以做到:

alias original_instance_eval instance_eval 
def instance_eval(*args, &block)
  block&.lambda? ? instance_exec(&block) : original_instance_eval(*args, &block)
end