内容 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11

官方 Ruby 常见问题解答

如果您想报告此常见问题解答中的错误或提出改进建议,请访问我们的 GitHub 仓库 并打开一个 issue 或 pull request。

变量、常量和参数

赋值会生成对象的新副本吗?

所有变量和常量都引用(指向)某个对象。(未初始化的局部变量除外,它们不引用任何内容。如果使用这些变量,则会引发 NameError 异常)。当您赋值给一个变量或初始化一个常量时,您将设置变量或常量引用的对象。

因此,赋值本身永远不会创建对象的新副本。

在某些特殊情况下,有更深入的解释。 FixnumNilClassTrueClassFalseClass 的实例直接包含在变量或常量中——不涉及引用。保存数字 42 的变量或常量 true 实际上保存的是值,而不是对其的引用。因此,赋值会物理生成这些类型对象的副本。我们将在 即时对象和引用对象 中更详细地讨论这一点。

局部变量的作用域是什么?

局部变量的新作用域在 (1) 顶层(主程序),(2) 类(或模块)定义或 (3) 方法定义中引入。

var = 1         # (1)
class Demo
  var = 2       # (2)
  def method
    var = 3     # (3)
    puts "in method: var = #{var}"
  end
  puts "in class: var = #{var}"
end
puts "at top level: var = #{var}"
Demo.new.method

产生

in class: var = 2
at top level: var = 1
in method: var = 3

(请注意,类定义是可执行代码:它包含的跟踪消息会在定义类时写入)。

代码块({ ... }do ... end)几乎引入了一个新的作用域 ;-) 在代码块内创建的局部变量在代码块外部不可访问。但是,如果代码块内的局部变量与调用者作用域中存在的局部变量具有相同的名称,则不会创建新的局部变量,并且您随后可以在代码块外部访问该变量。

a = 0
1.upto(3) do |i|
  a += i
  b = i*i
end
a  # => 6
# b is not defined here

当您使用线程时,这一点变得很重要——每个线程都收到其线程代码块局部变量的副本

threads = []

["one", "two"].each do |name|
  threads << Thread.new do
    local_name = name
    a = 0
    3.times do |i|
      Thread.pass
      a += i
      puts "#{local_name}: #{a}"
    end
  end
end

threads.each {|t| t.join }

可能产生(如果调度程序按照 Thread.pass 的提示切换线程;这取决于操作系统和处理器)

one: 0
two: 0
one: 1
two: 1
one: 3
two: 3

whileuntilfor 是控制结构,而不是代码块,因此它们中的局部变量在封闭环境中可访问。但是,loop 是一个方法,并且关联的代码块引入了一个新的作用域。

局部变量何时变得可访问?

实际上,问题可以更好地问成:“Ruby 在什么时间点确定某个东西是一个变量?” 问题的出现是因为简单表达式 a 可以是一个变量,也可以是对不带参数的方法的调用。为了确定是哪种情况,Ruby 会查找赋值语句。如果在源代码中某个位置在 a 使用之前看到它被赋值,它会决定将 a 解析为变量,否则将其视为方法。作为这种情况的一个有点病态的例子,请考虑这段代码片段,最初由 Clemens Hintze 提交

def a
  puts "method `a' called"

  99
end

[1, 2].each do |i|
  if i == 2
    puts "a = #{a}"
  else
    a = 1
    puts "a = #{a}"
  end
end

产生

a = 1
method `a' called
a = 99

在解析过程中,Ruby 在第一个 puts 语句中看到了 a 的使用,并且由于它尚未看到对 a 的任何赋值,因此假设它是一个方法调用。但是,当它到达第二个 puts 语句时,它已经看到了一个赋值,因此将 a 视为一个变量。

请注意,该赋值不必执行——Ruby 只需要看到它即可。此程序不会引发错误

a = 1 if false; a  # => nil

变量的这个问题通常不是问题。如果您确实遇到它,请尝试在第一次访问变量之前放置一个赋值,例如 a = nil。这还具有加快随后在循环中出现的局部变量的访问时间的好处。

常量的作用域是什么?

在类或模块定义中定义的常量可以直接在该类或模块的定义中访问。

您可以从嵌套类和模块中直接访问外部类和模块中的常量。

您还可以直接访问超类和包含模块中的常量。

除了这些情况外,您可以使用 :: 运算符访问类和模块常量,例如 ModuleName::CONST1ClassName::CONST2

参数是如何传递的?

当调用该方法时,实际参数将分配给形式参数。(有关赋值语义的更多信息,请参见 赋值。)

def add_one(number)
  number += 1
end

a = 1
add_one(a)  # => 2
a           # => 1

由于您正在传递对象引用,因此方法可能会修改传递给它的可变对象的内容。

def downer(string)
  string.downcase!
end

a = "HELLO"  # => "HELLO"
downer(a)    # => "hello"
a            # => "hello"

没有其他语言的按引用传递语义的等效项。

对形式参数的赋值会影响实际参数吗?

形式参数是局部变量。在方法内部,赋值给形式参数只会更改参数以引用另一个对象。

当我通过形式参数调用方法时会发生什么?

所有 Ruby 变量(包括方法参数)都充当对对象的引用。您可以在这些对象中调用方法以获取或更改对象的状态,并使对象执行某些操作。您可以使用传递给方法的对象来执行此操作。这样做时需要小心,因为这些类型的副作用可能会使程序难以理解。

前置于参数的 * 是什么意思?

当用作形式参数列表的一部分时,星号允许将任意数量的参数传递给方法,方法是将它们收集到一个数组中,并将该数组分配给带星号的参数。

def foo(prefix, *all)
  all.each do |element|
    puts "#{prefix}#{element}"
  end
end

foo("val = ", 1, 2, 3)

产生

val = 1
val = 2
val = 3

当在方法调用中使用时,* 会展开一个数组,将其各个元素作为参数传递。

a = [1, 2, 3]
foo(*a)

您可以将 * 前置于以下最后一个参数:

  1. 多重赋值的左侧。
  2. 多重赋值的右侧。
  3. 方法形式参数的定义。
  4. 方法调用中的实际参数。
  5. case 结构的 when 子句中。

例如

x, *y = [7, 8, 9]
x                  # => 7
y                  # => [8, 9]
x,    = [7, 8, 9]
x                  # => 7
x     = [7, 8, 9]
x                  # => [7, 8, 9]

前置于参数的 & 是什么意思?

如果方法的最后一个形式参数前面有一个和号 (&),则方法调用之后的代码块将转换为 Proc 对象并分配给形式参数。

如果方法调用中的最后一个实际参数是一个 Proc 对象,则可以在其名称前面加上一个和号以将其转换为代码块。然后,该方法可以使用 yield 来调用它。

def meth1(&b)
  puts b.call(9)
end

meth1 {|i| i + i }

def meth2
  puts yield(8)
end

square = proc {|i| i * i }

meth2 {|i| i + i }
meth2 &square

产生

18
16
64

如何为形式参数指定默认值?

def greet(p1="hello", p2="world")
  puts "#{p1} #{p2}"
end

greet
greet("hi")
greet("morning", "mom")

产生

hello world
hi world
morning mom

默认值(可以是任意表达式)在调用方法时计算。它使用该方法的作用域进行计算。

如何将参数传递给代码块?

代码块的形式参数出现在代码块开头的竖线之间

proc {|a, b| a <=> b }

这些参数实际上是局部变量。如果执行代码块时存在同名的现有局部变量,则该变量将被对代码块的调用修改。这可能是一件好事,也可能不是一件好事。

通常,使用 yield (或调用 yield 的迭代器)或使用 Proc.call 方法将参数传递给代码块。

为什么我的对象意外更改?

A = a = b = "abc"
b.concat("d")  # => "abcd"
a              # => "abcd"
A              # => "abcd"

变量保存对对象的引用。赋值 A = a = b = "abc" 将对字符串 "abc" 的引用放入 Aab

当您调用 b.concat("d") 时,您在该对象上调用 concat 方法,将其从 "abc" 更改为 "abcd"。由于 aA 也引用同一个对象,因此它们的明显值也会更改。

实际上,这比看起来问题要小得多。

此外,所有对象都可以被冻结,从而保护它们免受更改。

常量的值会改变吗?

常量是一个名称以大写字母开头的变量。常量不能从实例方法中重新分配,但在其他情况下可以随意更改。当为常量分配新值时,会发出警告。

为什么我无法从单独的文件加载变量?

假设 file1.rb 包含

var1 = 99

并且一些其他文件将其加载

require_relative "file1"
puts var1

产生

prog.rb:2:in `<main>': undefined local variable or method `var1' for main:Object (NameError)

您会收到错误,因为 loadrequire 会安排将局部变量存储到一个单独的匿名命名空间中,从而有效地丢弃它们。这是为了保护您的代码免受污染而设计的。