Ruby - 词法作用域 vs 继承

34
这是原始的SO问题的延续:使用“::”代替Ruby命名空间中的“module …” 在原始的SO问题中,介绍了以下场景,我仍然难以理解:
FOO = 123

module Foo
  FOO = 555
end

module Foo
  class Bar
    def baz
      puts FOO
    end
  end
end

class Foo::Bar
  def glorf
    puts FOO
  end
end

puts Foo::Bar.new.baz    # -> 555
puts Foo::Bar.new.glorf  # -> 123

有人能解释一下为什么第一个调用返回555,第二个调用返回123吗?


1
威尔森,您认为下面哪个答案值得获得悬赏?谢谢。 - rainkinz
请在您的代码中的两个puts语句后添加"puts Module.nesting"。参见此链接:http://coderrr.wordpress.com/2008/03/11/constant-name-resolution-in-ruby/。 - user1220978
4个回答

36
您可以将每个module Somethingclass Somethingdef something的出现视为进入新作用域的“门户”。当Ruby正在查找已被引用的名称的定义时,它首先查找当前范围(方法、类或模块),如果在那里找不到,它将通过每个包含的“门户”返回并在那里搜索范围。
在您的示例中,方法baz被定义为
module Foo
  class Bar
    def baz
      puts FOO
    end
  end
end

当尝试确定FOO的值时,首先检查Bar类,由于Bar不包含FOO,因此搜索通过“class Bar gateway”进入包含范围的Foo模块。 Foo包含一个常量FOO(555),因此这就是您看到的结果。 glorf方法定义如下:
class Foo::Bar
  def glorf
    puts FOO
  end
end

这里的“门户”是class Foo :: Bar ,所以当FOOBar内找不到时,“门户”会通过Foo模块直接进入顶层,在那里有另一个FOO(123),这就是显示的内容。
请注意,使用class Foo :: Bar 创建了一个单一的“门户”,跳过了Foo的范围,但module Foo; class Bar ...则打开了两个独立的“门户”。

4
顺便提一下,关键术语是“网关术语”。在 Ruby 源代码中似乎有一种可以称之为“作用域栈”的东西。因此,每次输入 classmodule 时,新的作用域就会被推入这个栈中。当 Ruby 查找变量或常量时,它会从底部到顶部查询这个栈,如果在上升路径中没有找到变量,则最终会落到顶级的 main 上。在 class Foo::Bar 的情况下,它确实应该将两个作用域(FooBar)都推入栈中,但它只推入了一个作用域,因此我们遇到了“问题”。 - Casper
这与原始答案有何不同? - rainkinz
1
@Casper 这很有道理。我在某处读到过“网关”这个想法(不幸的是我不记得在哪里了),它是一种思考正在发生的事情的方法,但我还没有看过实现。我想,这种行为的一个解释是它允许您打开嵌套类(以进行猴子补丁),而不需要担心封闭作用域的干扰。 - matt

6

哇,好问题。我能想到的最好答案是,在这种情况下,您正在使用模块来定义命名空间。

看看这个:

FOO = 123

module Foo
  FOO = 555
end

module Foo
  class Bar
    def baz
      puts FOO
    end

    def glorf3
      puts ::FOO
    end
  end
end

class Foo::Bar
  def glorf2
    puts Foo::FOO
  end

  def glorf
    puts FOO
  end
end

puts Foo::Bar.new.baz    # -> 555
puts Foo::Bar.new.glorf  # -> 123
puts Foo::Bar.new.glorf2  # -> 555
puts Foo::Bar.new.glorf3  # -> 123

所以我的想法是,当你定义:

module Foo
  FOO = 555
end

您正在Foo命名空间中创建FOO。因此,当您在此处使用它时:
module Foo
  class Bar
    def baz
      puts FOO
    end
  end
end

您现在位于Foo命名空间。但是,当您在以下地方引用它时:

class Foo::Bar
  def glorf
    puts FOO
  end
end

FOO来自默认命名空间(如::FOO所示)。


1
谢谢提供示例!除了一个细节之外,这很有意义:当您定义类Foo :: Bar时,您不是将其命名空间化为Foo吗?“Foo :: Bar”中的“Foo ::”部分难道不意味着您正在对该类进行命名空间处理吗? - wmock
当Foo::Bar.new.baz返回555时,Foo::Bar.new.glorf返回123对我来说很难理解。 - wmock
1
我也曾这样想,但看起来实际情况是你通过 Foo::Bar 明确地为 Bar(在 Foo 下)设置了命名空间,而该上下文中的其他所有内容仍然来自默认命名空间。 - rainkinz
1
有趣的是,当你显式地为类命名空间时,常量将解析为默认命名空间,除非你另行指定(即在glorf2中的Foo :: FOO),而当你隐式地为类命名空间时,常量将解析为命名空间,除非你另行指定(即在glorf3中的:: FOO)。 - wmock
2
没错,这就是我想的。希望其他人也能发表评论。这真的很有趣... - rainkinz

0

第一次调用:

puts Foo::Bar.new.baz    # -> 555

打印调用类 Foo::Bar 实例的方法 baz 的结果

请注意,Foo::Bar#baz 的定义实际上是在 FOO 上的闭包。遵循 Ruby 的作用域规则:

  1. Foo::Bar(类而非实例)的作用域中搜索 FOO,未找到,
  2. 在封闭作用域Foo中搜索 FOO(因为我们在模块定义内),并在那里找到它(555)

第二次调用:

puts Foo::Bar.new.glorf  # -> 123

打印调用类 Foo::Bar 实例的方法 glorf 的结果

请注意,Foo::Bar#glorf 的定义这次也是在 FOO 上的闭包,但如果我们遵循 Ruby 的作用域规则,您会注意到这次封闭的值是以下方式中的 ::FOO(顶级作用域 FOO):

  1. Foo::Bar(类而非实例)命名空间中搜索 FOO,未找到
  2. 在封闭作用域(“顶级”)中搜索 FOO,并在那里找到它(123)

0

glorf是类Foo的一个方法,在=> [Foo,Module,Object,Kernel,BasicObject]中。

在该范围内(即默认/主模块),FOO被分配为123。

模块Foo被定义为

module Foo
  FOO = 555
  class Bar
    def baz
      puts FOO
    end
  end
end

方法baz属于模块Foo中的类Bar

=> [Bar, Foo, Object, Kernel, BasicObject]

在该作用域中,FOO被赋值为555


上面提到的“main”是irb的一个产物,实际上FOO=123是一个顶层方法,它进入了Object类。 - aug2uag

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