为什么实例变量在块内部看起来会消失?

28

大家原谅我,我在Ruby方面充其量也只是个新手。我只是好奇地想要了解一下似乎相当奇怪的行为的解释。

我正在使用Savon库与我的Ruby应用程序中的SOAP服务进行交互。我注意到以下代码(在我编写的一个类中处理此交互)似乎会传递空值,而我希望将成员字段的值传递进去:

create_session_response = client.request "createSession" do
  soap.body = {
    :user => @user, # This ends up being empty in the SOAP request,
    :pass => @pass  # as does this.
  }
end
尽管@user@pass都被初始化为非空字符串,但仍然存在问题。
当我改用locals后,它按照我预期的方式工作:
user = @user
pass = @pass

create_session_response = client.request "createSession" do
  soap.body = {
    :user => user, # Now this has the value I expect in the SOAP request,
    :pass => pass  # and this does too.
  }
end

我猜这种(对我来说)奇怪的行为一定与我在块内有关,但实际上,我一点都不知道。有人能给我解释一下吗?

4个回答

37
首先,@user 不是 Ruby 中的“私有变量”;它是一个实例变量。实例变量在当前对象的范围内可用(即self所指的对象)。我已经编辑了你的问题标题,以更准确地反映你的问题。
一个块就像一个函数,一组代码将在以后执行。通常,该块将在定义块的作用域中执行,但也可以在另一个上下文中评估该块:
class Foo
  def initialize( bar )
    # Save the value as an instance variable
    @bar = bar
  end
  def unchanged1
    yield if block_given? # call the block with its original scope
  end
  def unchanged2( &block )
    block.call            # another way to do it
  end
  def changeself( &block )
    # run the block in the scope of self
    self.instance_eval &block
  end
end

@bar = 17
f = Foo.new( 42 )
f.unchanged1{ p @bar } #=> 17
f.unchanged2{ p @bar } #=> 17
f.changeself{ p @bar } #=> 42

所以,要么您在@user设置的范围之外定义了该块,要么client.request的实现导致该块在稍后的另一个范围中被评估。您可以通过编写以下内容来查找:

client.request("createSession"){ p [self.class,self] }

为了了解当前代码块中的self是什么样的对象,可以获得一些见解。

它们在您的情况下“消失”的原因是Ruby允许您宽容地请求任何实例变量的值,即使该值从未针对当前对象设置过。如果变量从未被设置,您将只会收到nil(如果启用了警告,则还会收到警告):

$ ruby -e "p @foo"
nil

$ ruby -we "p @foo"
-e:1: warning: instance variable @foo not initialized
nil

正如您所发现的那样,块也是闭包。这意味着当它们运行时,它们可以访问在块定义的相同作用域中定义的局部变量。这就是为什么您的第二组代码按预期工作的原因。闭包是一种很好的方式,可以在以后使用一个值,例如在回调中。
继续上面的代码示例,您可以看到无论在评估块的范围内还是在哪个作用域中,该局部变量都是可用的,并且优先于该作用域中同名的方法(除非您提供一个显式接收器):
class Foo
  def x
    123
  end
end
x = 99 
f.changeself{ p x } #=> 99
f.unchanged1{ p x } #=> 99
f.changeself{ p self.x } #=> 123
f.unchanged1{ p self.x } #=> Error: undefined method `x' for main:Object

谢谢!我一直在努力复制这个代码块,但是看到你使用instance_eval的例子后,帮助我理解了它。 - Dan Tao
@Dan 不客气。我在示例的结尾添加了一点内容,展示了闭包在简单情况下的运作方式。 - Phrogz
1
有没有一个优美的解决方案?否则我的 N.times {} 块无法访问实例变量,而使用 o=self 的解决方法现在变得语法混乱 :( - Konrads
@Konrads 一个漂亮的解决方案是针对什么的?请用您的代码、需求和期望提出一个新问题。在别人的问题答案下随意评论一段代码是得不到好的帮助的。 - Phrogz

5

来自文档:

Savon::Client.new接受一个块,在其中可以访问本地变量,甚至可以从您自己的类中访问公共方法,但实例变量将不起作用。如果想知道为什么,请阅读有关使用委托的instance_eval。

在这个问题被提出时可能没有很好的记录。


2
另一种修复该问题的方法是将对象的引用传递到代码块中,而不是多次枚举每个需要的属性:
o = self
create_session_response = client.request "createSession" do
  soap.body = {
    :user => o.user,
    :pass => o.pass
  }
end

但现在你需要属性访问器。


2
在第一种情况下,self被评估为client.request('createSession'),它没有这些实例变量。
在第二种情况下,变量作为闭包的一部分引入到块中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接