为什么Ruby使用类和函数污染全局命名空间但不变量?

时间:2015-02-05 14:23:16

标签: ruby-on-rails ruby

在test1.rb中有这段代码

my_var = 42

def my_func()
  42
end

class MyCLS
  attr_accessor :prop
  def initialize()
    @prop = 42
  end
end

然后在解释器中我需要它在irb

> require './test1.rb'
> MyCLS.new().prop
  => 42
> my_func()
  => 42
> my_var
NameError: undefined local variable or method `my_var' for main:Object

我很困惑,ruby似乎很高兴用类和函数污染全局命名空间,但拒绝对my_var做同样的事情?我想这是为了避免名称冲突和错误。但问题只是部分解决,因为它仍然存在于类和函数中。也许只是不太容易发生?

现在想象一下第二个文件test2.rb

def my_func()
  43
end

class MyCLS
  attr_accessor :prop
  def initialize()
    @prop = 43
  end
end

然后执行它

> require './test1.rb'
> require './test2.rb'
> MyCLS.new().prop
  => 43
> my_func()
  => 43

以前的全局变量MyCLS和my_func被静默覆盖是否正常?这是不是很有可能破坏软件,因为宝石决定在某处添加/重命名一个类或一个函数?所有这一切似乎都非常脆弱和危险。

我知道模块,我尝试了它们但收效甚微(尴尬,再一次是全局变量)

有没有办法防止这种情况或减轻看起来像语言设计的缺陷?

编辑:另一个例子

# test1.rb
def my_func()
  42
end

# test2.rb
puts my_func()

# test3.rb
class Example
  require './test1.rb'
end

class AnotherExample
  require './test2.rb'
end

# command line
$ ruby test3.rb
42

2 个答案:

答案 0 :(得分:4)

Ruby中的常量(以大写字母开头的东西)总是在Object上创建为常量,除非它们明确是另一个常量的成员(即module Foo::BarFoo下创建一个模块{1}}常量,它本身在Object常数之下。

此外,还有一个名为" main"的特殊顶级对象实例。在顶级定义的任何方法都在Object上定义为私有方法,因此可以从main访问。

当你require文件时,机制是:

  • 创建新的匿名模块
  • 将请求的文件加载到该模块中
  • 使用该模块扩展main

始终遵守这些规则;您无法在文件中定义顶级方法,然后通过巧妙地放置require语句将该文件包含到命名空间中。解析该文件后,Ruby会找到一个顶级方法,并使用它扩展main,而不考虑调用require的位置。

如果你想要一个混合到另一个类中的方法,那么你通常会将它放入一个模块中,然后将该模块混合到你的类中。

# test1.rb
module Bar
  def my_func
    42
  end
end

# test2.rb
require 'test1'
class Foo
  include Bar
end

my_func => # NameError: undefined local variable or method `my_func' for main:Object
Foo.new.my_func # => 42

在Ruby中,预计每个文件将完全命名它要公开的常量和方法。你几乎不会在大多数真正的Ruby项目中编写顶级方法;因为有人需要明确地进入你的命名空间来覆盖一些东西,所以不必担心会无意中覆盖这些东西。

如果要在不扩展主对象的情况下执行文件,则可以将Kernel#loadwrap参数一起使用,该参数将负载包装在匿名模块中(但使其内部不可访问,除非你在那个文件中做了一些事情来暴露那个文件中的方法和常量):

load "test1", true
MyCLS # => NameError: uninitialized constant MyCLS

您可以通过自定义加载程序获得此范围内的加载:

# test1.rb
def foo
  42
end

# test2.rb
def relative_load(file)
  Module.new.tap {|m| m.module_eval open(file).read }
end

class Foo
  include relative_load("test1.rb")
end

Foo.new.foo  # => 42
foo          # => NameError: undefined local variable or method `foo' for main:Object

顺便说一下,在你的第一个例子中,MyCLS类没有被覆盖;它与现有的MyCLS类合并。因为两者都声明initialize,后者声明优先。例如:

# test1.rb
class MyCLS
  attr_accessor :prop

  # This definition will get thrown away when we overwrite it from test2.
  # It is test2's responsibility to make sure that behavior is preserved;
  # this can be done with reimplementation, or by saving a copy of this
  # method with `alias` or similar and invoking it.
  def initialize(prop)
    @prop = prop
  end
end

# test2.rb
class MyCLS
  attr_accessor :another_prop

  def initialize(prop, another_prop)
    @prop = prop
    @another_prop = prop
  end
end

# test3.rb
require 'test1'

c = MyCLS.new(1, 2) # => ArgumentError: wrong number of arguments (2 for 1)
c = MyCLS.new(1)
c.prop => 1
c.another_prop =>   # => NoMethodError: undefined method `another_prop'

require 'test2'

c = MyCLS.new(1)    # => ArgumentError: wrong number of arguments (1 for 2)
c = MyCLS.new(1, 2)
c.prop => 1
c.another_prop => 2

答案 1 :(得分:-3)

由于您正在使用全局命名空间,我将冒险尝试使用JavaScript背景深入Ruby,如果我错了,请纠正我。

Ruby不会污染全局命名空间,因为当您需要test_1.rb或test_2.rb等文件时,它们的范围将限制在您需要它们的位置。例如,如果您在名为Example的类中需要“test_1”,那么如果您要在名为AnotherExample的类中要求“test_2”,则该文件无法覆盖:

Class Example
  require 'test_1'
end

Class AnotherExample
  require 'test_2'
end

您的方法会被覆盖,因为您需要在同一范围内的两个文件,而这些文件在较大的应用程序的上下文中不会执行。命名空间可以阻止您覆盖类似命名的变量,方法等。

my_var是一个局部变量,其上下文绑定到test_1.rb。因此,其范围仅限于test_1.rb。