【问题标题】:Local variables in `class` definition scope versus `def` method scope`class` 定义范围与`def` 方法范围中的局部变量
【发布时间】:2016-01-14 14:36:12
【问题描述】:

在这里,我在类范围内创建了一个局部变量:

class MyClass
  x = 1
  puts x
end

即使我没有创建MyClass 的任何实例,它也会打印1

我想以某种方式使用x

class MyClass
  x = 1
  def method
    puts x
  end
end

m = MyClass.new
m.method

我不能。为什么?我得到该类定义创建了一个范围,但为什么在方法中无法访问它?方法的作用域不是在类的作用域内吗?

我可以想象这与创建一个类有关。既然任何类都是Class的对象,那么MyClass的作用域可能就是某个Class方法的作用域,而MyClass的方法与那个实例的耦合方式使得它们的作用域完全不同。

在我看来,我不能只用{}(如在C 中)或类似do..end 的东西创建一个范围。我说的对吗?

【问题讨论】:

  • 它打印1,因为类声明只是在加载类时运行的代码。
  • 我没有“类加载”的概念。你能解释一下这是什么意思吗?
  • 在运行时,在解析之后,Ruby实际上会执行所有包含在类定义中的代码。
  • 所以只创建一个没有任何实例的类会导致在运行时实际执行某些东西(甚至可能是分配)?这和 C++ 很不一样。
  • 正确。 Ruby 不需要初始化任何实例来运行类定义中的代码。它在读取时运行,这可能是在读取代码库中代码的其他部分之前。

标签: ruby class scope local-variables


【解决方案1】:

所以只创建一个没有任何实例的类会导致一些事情 实际在运行时执行(甚至可能是分配)?那是很 不像 C++。 ——

查看此代码:

Dog = Class.new do
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

如果您执行该代码,将不会有任何输出,但仍然发生了一些事情。例如,创建了一个名为 Dog 的全局变量,它有一个值。证据如下:

Dog = Class.new do
  attr_accessor :name

  def initialize(name)
    @name = name
  end
end

dog = Dog.new("Ralph")
puts dog.name

--output:--
Ralph

上面对Dog常量的赋值相当于写:

class Dog
  ...
  ...
end

事实上,ruby 会逐行遍历类定义中的每一行并执行每一行——除非代码行在 def 中。 def 已创建,但 def 中的代码在调用 def 之前不会执行。

您将在类定义中看到的一条非常常见的行是:

attr_accessor :name

...可以改写为:

attr_accessor(:name)

...这很明显它是一个方法调用。当您运行包含类定义的文件时,Ruby 会执行该行并调用该方法。然后attr_accessor 方法动态地创建并插入一个getter 和一个setter 方法到类中。在运行时。是的,这不再是 C++ 领域了——欢迎来到 NeverNever Land。

我知道类定义创建了一个范围,但为什么不是 在方法中可访问?

因为这是 Matz 决定事情的方式:def 创建了一个新范围,阻止了 def 之外的变量的可见性。但是,有一些方法可以打开范围之门,可以这么说:blocks 可以看到在周围范围中定义的变量。查看define_method()

class MyClass
  x = 1

  define_method(:do_stuff) do
    puts x
  end

end

m = MyClass.new
m.do_stuff

--output:--
1

块是do...end 之间的所有内容。在 ruby​​ 中,块是一个 闭包,这意味着当创建块时,它会捕获周围范围内的变量,并随身携带这些变量,直到块被执行。块就像一个匿名函数,它作为参数传递给方法。

请注意,如果您使用 Class.new 技巧,您可以打开两个范围门:

x = 1

MyClass = Class.new do

  define_method(:do_stuff) do
    puts x
  end

end

m = MyClass.new
m.do_stuff

--output:--
1

一般来说,ruby 允许程序员做他们想做的任何事情,规则是该死的。

【讨论】:

  • 从技术上讲,Dog = Class.new do ... end 中对 Dog 的赋值并不完全等同于 class Dog ... end。更接近于类声明的是Dog = Class.new; Dog.class_eval do ... end。这是因为Class.new 临时创建了一个没有名称的匿名类,并且直到对块进行评估并分配Dog = 之后才会分配名称。试试这三种结构,你会发现区别:Dog = Class.new { p self.name } vs class Dog; p self.name; end vs Dog = Class.new; Dog.class_eval { p self.name }
  • 天哪!这给了我一个可怕的想法,在 Ruby 中,我什至可以向某个类的对象添加方法,即使在我创建了该对象之后也是如此。该类的其他对象将无法获得该方法,我可以哦,我的上帝。让我猜猜,对象实际上是某种哈希图;方法的名称是键,方法的“地址”是值?
  • @Amomum 你是对的。这是一个插图:obj = Object.new; def obj.foo() :foo end; obj.foo #=> :foo; Object.new.foo #=> NoMethodError
  • @Amomum: 让我猜猜,对象实际上是某种哈希图;方法的名称是键,方法的“地址”是值? C++ 还使用方法查找表——查找表的实现并不重要。真正的问题是:查找表的访问顺序是什么。在 C++ 中,继承会创建多个查找表。当子对象调用方法时,首先在子类中搜索该方法,然后是父类。继承创建了这两个查找表之间的链接。在 ruby​​ 中,每个对象都有自己独特的类父类,它从这些类继承方法--
  • ...称为单例类。在 ruby​​ 中,当对象调用方法时,搜索该方法的第一个类是对象的单例类。搜索的下一个类是创建对象的类,即对象的类。换句话说,对象的单例类位于继承链中的对象和对象的类之间。然后 ruby​​ 的include 语句或定义从另一个类继承的类将类添加到对象的继承链的顶部。查找路径对初学者来说可能有点混乱。这是一个线程......
【解决方案2】:

在这里,我在类范围内创建了一个局部变量:

class MyClass
  x = 1
  puts x
end

即使我没有创建MyClass 的任何实例,它也会打印1

正确。类定义主体在读取时执行。它只是和其他代码一样的代码,类定义体没有什么特别之处。

问问自己:attr_reader/attr_writer/attr_accessoralias_methodpublic/protected/private 这样的方法在其他情况下如何工作?哎呀,如果 def 在定义类时没有被执行,它将如何工作? (毕竟def 和其他表达式一样只是一个表达式!)

这就是为什么你可以做这样的事情:

class FileReader
  if operating_system == :windows
    def blah; end
  else
    def blubb; end
  end
end

我想以某种方式使用x

class MyClass
  x = 1
  def method
    puts x
  end
end

m = MyClass.new
m.method

我不能。为什么?我得到该类定义创建了一个范围,但为什么在方法中无法访问它?方法的作用域不是在类的作用域内吗?

不,不是。 Ruby 中有 4 个作用域:脚本作用域、模块/类定义作用域、方法定义作用域和块/lambda 作用域。只有块/lambdas 嵌套,所有其他的都创建新的范围。

我可以想象这与创建一个类有关。既然任何类都是Class的对象,那么MyClass的作用域可能就是某个Class方法的作用域,而MyClass的方法与那个实例的耦合方式使得它们的作用域完全不同。

老实说,我没有完全理解你在说什么,但是不,类定义范围不是方法定义范围,类定义范围是类定义范围,方法定义范围是方法定义范围。

在我看来,我不能只用{}(如在C 中)或类似do..end 的东西创建一个范围。我说的对吗?

就像我上面说的:Ruby 中有 4 个作用域。 C 中没有像块作用域这样的东西。(“块”的 Ruby 概念与“块”的 C 概念完全不同。)你能得到的最接近的东西是受 JavaScript 启发的立即调用的 lambda-literal,一些东西像这样:

foo = 1

-> {
  bar = 2
  foo + bar
}.()
# => 3

bar
# NameError

一般来说,这在 Ruby 中是不必要的。在结构良好的代码中,方法会非常小,以至于跟踪局部变量及其作用域和生命周期真的没什么大不了的。

【讨论】:

  • 块局部变量怎么样,例如块参数中分号后面的变量?
  • 我更愿意将它们视为“未捕获的变量”。 IOW,它们只是普通的局部变量,而且它们不嵌套,即它们隐藏最终存在的同名外部变量。 (我想到了一个简洁的语法扩展,多年来我一直想在 bugtracker 中提出这个建议,但还没有解决:{|a, b; c, d, *| stuff },即允许在参数列表中使用通配符,基本上意味着“这个块不是闭包”。这在语义上很清楚,您实际上并不打算捕获任何变量。它还允许编译器优化。)
  • 这太奇怪了。一个方法的条件def让我大开眼界。我总是忘记 ruby​​ 是一种脚本语言,这里不存在“编译时间”。谢谢,现在更有意义了!
  • "老实说,我没有完全理解你在说什么,但是不,类定义范围不是方法定义范围,类定义范围是类定义范围,方法定义范围是方法定义范围“我的意思是我不知道阶级究竟是如何产生的;如果 class 是 Class 类的对象,也许我们可以认为 class MyClass ... end - 以某种方式粘在“空” Class 对象中......或其他东西。我在某处读到,类级别的实例变量实际上是在 Class 方法的范围内,并朝那个方向思考。
【解决方案3】:

方法的作用域不是在类内。每种方法都有自己全新的作用域。

每当您使用 classmoduledef 关键字时,都会创建新范围。使用括号,就像在 C 中一样,不会创建新的作用域,实际上您不能使用括号任意组合代码行。 Ruby 块周围的方括号(或do...end)创建一个块级作用域,其中先前在周围作用域中创建的变量可用,但在块作用域内创建的变量不会逃逸到周围作用域中之后。

实例方法与其他实例方法共享其实例变量的范围。在类定义范围内定义的实例变量在类级别的单例方法中可用,但在类的实例方法中不可用。

插图:

class Foo
  x = 1  # available only here
  @y = 2 # class-wide value

  def self.class_x
    @x # never set; nil value
  end

  def self.class_y
    @y # class-wide value
  end

  def initialize(z)
    x = 3  # available only here
    @z = z # value for this instance only
  end

  def instance_x
    @x # never set; nil
  end

  def instance_y
    @y # never set; nil
  end

  def instance_z
    @z # value for this instance only
  end
end

Foo.class_x # => nil
Foo.class_y # => 2

Foo.new(0).instance_x # => nil
Foo.new(0).instance_y # => nil

foo3 = Foo.new(3)
foo4 = Foo.new(4)

foo3.instance_z # => 3
foo4.instance_z # => 4

您可以使用类级别的 getter 从实例中访问类级别的实例变量。继续上面的例子:

class Foo
  def get_class_y
    self.class.class_y
  end
end

foo = Foo.new(0)
foo.get_class_y # => 2

Ruby 中存在“类变量”的概念,它使用@@ 印记。在实践中,这种语言结构几乎没有一个合理的用例。通常,使用类级实例变量可以更好地实现目标,如下所示。

【讨论】:

  • 感谢您的解释!请你告诉我,为什么类/方法范围和方法/让我们说-while-loop在该方法中的范围行为如此不同?在循环中,我可以访问方法局部变量。我的意思是,这有什么特别的原因吗?
  • 您能够在方法内部定义的循环内访问方法局部变量的原因是您可以使用闭包的强大功能。至于为什么方法定义了自己的范围,与类级别的范围完全无关……这只是 Ruby 的创建者做出的语言实现选择。
  • 问题是关于 local 变量。我不明白您关于 instance 变量的长示例/解释与这个问题有何关系。
  • @sawa,对我还是很有帮助的!
  • @sawa 问题是关于如何在其他地方定义变量时在方法中使用变量。实例变量的讨论非常相关。
猜你喜欢
  • 2013-10-14
  • 1970-01-01
  • 1970-01-01
  • 2016-11-13
  • 1970-01-01
  • 2014-03-02
  • 2021-08-18
  • 2013-05-10
  • 2014-07-14
相关资源
最近更新 更多