回到Ruby系列文章


Ruby的变量作用域规则

这里只考虑局部变量。

1.顶层范围看起来是全局范围,但它是main的局部作用域而不是全局作用域,所以方法内部、类内部、模块内部无法访问顶层变量

2.局部变量有一个特性非常重要:必须先声明才能引用

1
2
3
a=3
puts a
puts b #=> 错误

3.方法内部不能访问方法外部的局部变量,因为方法内部是一个独立的作用域

这种行为和其它语言可能不同。这种行为导致的结果是:

  • 在方法内部只能访问在方法内部作用域中已经声明好的局部变量
  • 要访问方法外部变量,只能通过参数传递的方式

这是一种”邻国相望,鸡犬之声相闻,民至老死,不相往来“的一种表现,内外部方法的变量作用域互不可见。

例如:

1
2
3
4
5
6
7
8
9
10
11
a=5          # 这是局部变量
b=6 # 这是局部变量
def foo{
a=3 # 这会创建一个局部变量
puts a
puts b # 这会报错
c=10
}

foo()
puts c # 这会报错,因为c是foo方法的局部变量

因内部函数无法访问外部变量,这产生了一系列的连锁反应:

  • Ruby中的函数不是一等公民,无法返回一个函数、无法将函数作为参数传递
  • Ruby中的函数无法直接实现闭包
  • Ruby中的函数和匿名函数(严格地应该称为lambda)是不一样的,Ruby中的匿名函数是一种特殊的Proc对象,而非一种没有名称的函数

Ruby中的Proc或(方法式)语句块取代了函数作为一等公民,它可以用于实现闭包,Proc或语句块是一种更为抽象的代码封装结构。

4.方法式代码块内部能访问代码块外部的变量。

方法式代码块经常会应用于一些迭代方法中,比如each() {}、times() {}等,这意味着这部分方法式代码块会在每次迭代过程中被调用一次,相当于多次调用匿名函数,而每次函数调用对于函数内部也即代码块内部来说,作用域是独立的。所以方法式代码块的迭代控制变量、代码块内部赋值的变量都会在每次迭代调用过程中新建。

例如,下面方法式代码块中的控制变量x以及代码块内部的变量y,在每次迭代过程中都是新创建并初始化的新变量,所以y+=1是多余的。

1
10.times {|x| y=0;puts x;y+=1}

虽然代码块内部创建的变量是新的变量,也是代码块内部的局部变量(显然如此),但方法式代码块可以访问并修改外部变量,这和函数定义的代码块很不同。这是一种”你的就是我的,我的还是我的“的表现。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a=5
for i in 1..5
puts a #=> 5
b=6
end
puts b #=> 6

x=1
10.times do
puts x #=> 没错误
x=2 #=> 修改的外部变量x
y=10 #=> 声明了一个局部变量
end

puts x #=> 2
puts y #=> 错误

更深入的,方法式语句块在迭代的时候,每次迭代都会创建新的语句块变量。这一点在了解了Proc对象和call()之后将理解的更清晰。例如:

1
(1..10).each {|x| puts x}

上面的代码中,其实内部声明了10次x变量,每次迭代过程中的x变量都不是同一个。

再例如,下面的两段代码中都通过循环将匿名函数加入到数组中,但是两种迭代方式会导致匿名函数的执行结果不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def f()
arr = []
(1..10).each do |x|
arr[x] = ->{ x }
end
retrun arr
end

def ff()
arr = []
for i in 1..10
arr[i] = ->{i}
end
end

puts f()[0].call() // 0
puts f()[1].call() // 1
puts ff()[0].call() // 10
puts ff()[1].call() // 10

每次迭代执行代码块(Proc对象),等价于下面代码的执行方式。所以方法式语句块有自己的变量作用域,语句块变量也是独立的。

1
2
3
4
p1 = Proc.new {|x| puts x}
p2 = Proc.new {|x| puts x}
p1.call(1)
p2.call(2)