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

官方 Ruby FAQ

如果您希望报告错误或对本 FAQ 提出改进建议,请访问我们的 GitHub 仓库,并提出 issue 或 pull request。

变量、常量和参数

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

所有变量和常量都引用(指向)某个对象。(未初始化的局部变量除外,它们引用任何东西。如果使用它们,将引发 `NameError` 异常)。当你给变量赋值或初始化常量时,你设置的是变量或常量引用的对象。

因此,赋值本身从不创建对象的副本。

在某些特殊情况下,解释会更深入一些。`Fixnum`、`NilClass`、`TrueClass` 和 `FalseClass` 的实例直接包含在变量或常量中——不涉及引用。持有数字 `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

`while`、`until` 和 `for` 是控制结构,而不是块,因此其中的局部变量将在封闭环境中可访问。但是,`loop` 是一个方法,关联的块会引入一个新的作用域。

局部变量何时可访问?

实际上,这个问题最好问:“Ruby 在何时弄清楚某物是变量?” 问题在于简单表达式 `a` 可以是变量,也可以是调用没有参数的方法。为了决定是哪种情况,Ruby 会查找赋值语句。如果在 `a` 的使用之前的源代码的某个点上,它被赋值了,Ruby 就会将其解析为变量,否则就将其视为方法。作为对此的一个有点病态的例子,请看这段代码片段,最初由 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` 的任何赋值,它假定 `a` 是一个方法调用。然而,当它到达第二个 `puts` 语句时,它已经看到了一个赋值,因此将 `a` 视为一个变量。

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

a = 1 if false; a  # => nil

变量的这个问题通常不是问题。如果你确实遇到了,可以尝试在第一次访问变量之前放置一个赋值,例如 `a = nil`。这还有一个额外的好处,就是可以加快在循环中随后出现的局部变量的访问速度。

常量的作用域是什么?

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

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

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

除了这些情况,你还可以使用 `::` 运算符、`ModuleName::CONST1` 或 `ClassName::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]

参数前加 `&` 是什么意思?

如果方法的最后一个形式参数前面带有 `&`(ampersand),则方法调用后面的块将被转换为 `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"` 的引用放入 `A`、`a` 和 `b` 中。

当你调用 `b.concat("d")` 时,你在该对象上调用 `concat` 方法,将其从 `"abc"` 更改为 `"abcd"`。由于 `a` 和 `A` 也引用同一对象,因此它们的值也会改变。

实际上,这比看起来要麻烦得多。

此外,所有对象都可以被冻结,以防止其被更改。

常量的值会改变吗?

常量是一个以大写字母开头的变量。常量不能在实例方法内部重新赋值,但除此之外可以随意更改。当常量被赋新值时,会发出警告。

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

假设 `file1.rb` 包含

var1 = 99

而另一个文件加载了它

require_relative "file1"
puts var1

产生

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

你得到一个错误,因为 `load` 和 `require` 会安排将局部变量存储在一个单独的匿名命名空间中,实际上会丢弃它们。这是为了保护你的代码不被污染。