变量作用域

变量作用域

变量的作用域是代码的一个区域,在这个区域中这个变量是可见的。给变量划分作用域有助于解决变量命名冲突。这个概念是符合直觉的:两个函数可能同时都有叫做x的参量,而这两个x并不指向同一个东西。相似地,也有很多其他的情况下代码的不同块会使用同样名字而并不指向同一个东西。相同的变量名是否指向同一个东西的规则被称为作用域规则;这一届会详细地把这个规则讲清楚。

语言中的某个创建会引入作用域块,这是代码中的一个区域,有资格成为一些变量集合的作用域。一个变量的作用域不可能是源代码行的任意集合;相反,它始终与这些块之一关系密切。在Julia中有两个主要类型的作用域,全局作用域局部作用域,后者可以嵌套。引入作用域块的创建是:

值得注意的是,这个表内没有的是 begin 块 if 块,这两个块不会引进新的作用域块。这两种作用域遵循的规则有点不一样,会在下面解释。

Julia使用词法作用域,也就是说一个函数的作用域不会从其调用者的作用域继承,而从函数定义处的作用域继承。举个例子,在下列的代码中foo中的x指向的是模块Bar的全局作用域中的x

julia> module Bar
           x = 1
           foo() = x
       end;

并且在foo被使用的地方x并不在作用域中:

julia> import .Bar

julia> x = -1;

julia> Bar.foo()
1

所以词法作用域表明变量作用域只能通过源码推断。

全局作用域

每个模块会引进一个新的全局作用域,与其他所有模块的全局作用域分开;无所不包的全局作用域不存在。模块可以把其他模块的变量引入到它的作用域中,通过using 或者 import语句或者通过点符号这种有资格的通路,也就是说每个模块都是所谓的命名空间。值得注意的是变量绑定只能在它们的全局作用域中改变,在外部模块中不行。

julia> module A
           a = 1 # a global in A's scope
       end;

julia> module B
           module C
               c = 2
           end
           b = C.c    # can access the namespace of a nested global scope
                      # through a qualified access
           import ..A # makes module A available
           d = A.a
       end;

julia> module D
           b = a # errors as D's global scope is separate from A's
       end;
ERROR: UndefVarError: a not defined

julia> module E
           import ..A # make module A available
           A.a = 2    # throws below error
       end;
ERROR: cannot assign variables in other modules

注意交互式提示行(即REPL)是在模块Main的全局作用域中。

局部作用域

大多数代码块都会引进一个新的局部作用域(参见上面的以获取完整列表)。局部作用域会从父作用域中继承所有的变量,读和写都一样。另外,局部作用域还会继承赋值给其父全局作用域块的所有全局变量(如果由全局if或者begin作用域包围)。不像全局作用域,局部作用域并不是命名空间,所以在其内部作用域中的变量无法通过一些合格的通路在其父作用域中得到。

接下来的规则和例子都适用于局部作用域。 在局部作用域中新引进的变量不会反向传播到其父作用域。 例如,这里$z$并没有引入到顶层作用域:

julia> for i = 1:10
           z = i
       end

julia> z
ERROR: UndefVarError: z not defined

(注意,在这个和以下所有的例子中都假设了它们的顶层作用域是一个工作空间是空的全局作用域,比如一个新打开的REPL。)

在局部作用域中可以使用local关键字来使一个变量强制为新的局部变量。

julia> x = 0;

julia> for i = 1:10
           local x # this is also the default
           x = i + 1
       end

julia> x
0

在局部作用域内部,可以使用global关键字赋值给一个全局变量:

julia> for i = 1:10
           global z
           z = i
       end

julia> z
10

在作用域块中localglobal关键字的位置都无关痛痒。下面的例子与上面最后的一个例子是等价的(虽然在文体上更差):

julia> for i = 1:10
           z = i
           global z
       end

julia> z
10

localglobal关键字都可以用于解构赋值,也就是说local x, y = 1, 2。在这个例子中关键字影响所有的列出来的变量。

大多数块关键字都会引入局部作用域,而beginif是例外。

在一个局部作用域中,所有的变量都会从其父作用域块中继承,除非:

所以全局变量只能通过读来继承,而不能通过写来继承。

julia> x, y = 1, 2;

julia> function foo()
           x = 2        # assignment introduces a new local
           return x + y # y refers to the global
       end;

julia> foo()
4

julia> x
1

为一个全局变量赋值需要显式的global

为了使得编出来的程序是最好的,很多人都考虑了避免改变全局变量的值。一个原因是远程改变其他模块中的全局变量的状态会导致程序的局部行为变得难以琢磨,应该小心行事。这也是为什么引入局部作用域的作用域块需要$global$关键字来声明其改变一个全局变量的意图。

julia> x = 1;

julia> function foobar()
           global x = 2
       end;

julia> foobar();

julia> x
2

注意嵌套函数会改变其父作用域的局部变量:

julia> x, y = 1, 2;

julia> function baz()
           x = 2 # introduces a new local
           function bar()
               x = 10       # modifies the parent's x
               return x + y # y is global
           end
           return bar() + x # 12 + 10 (x is modified in call of bar())
       end;

julia> baz()
22

julia> x, y # verify that global x and y are unchanged
(1, 2)

允许嵌套函数修改其父作用域的局部变量的原因是允许构建闭包, 闭包中有一个私有的态,例如下面例子中的$state$变量:

julia> let state = 0
           global counter() = (state += 1)
       end;

julia> counter()
1

julia> counter()
2

也可以参见接下来两节例子中的闭包。例如在第一个例子中的x与在第二个例子中的state,内部函数从包含它的作用域中继承的变量有时被称为捕获变量。捕获变量会带来性能挑战,这会在性能建议中讨论。

继承全局作用域与嵌套局部作用域的区别会导致在局部或者全局作用域中定义的函数在变量赋值上的稍许区别。考虑一下上面最后一个例子的一个变化,把bar移动到全局作用域中:

julia> x, y = 1, 2;

julia> function bar()
           x = 10 # local, no longer a closure variable
           return x + y
       end;

julia> function quz()
           x = 2 # local
           return bar() + x # 12 + 2 (x is not modified)
       end;

julia> quz()
14

julia> x, y # verify that global x and y are unchanged
(1, 2)

注意到在上面的嵌套规则并不适用于类型和宏定义因为他们只能出现在全局作用域中。涉及到函数中提到的默认和关键字函数参数的评估的话会有特别的作用域规则。

在函数,类型或者宏定义内部使用的变量,将其引入到作用域中的赋值行为不必在其内部使用之前进行:

julia> f = y -> y + a;

julia> f(3)
ERROR: UndefVarError: a not defined
Stacktrace:
[...]

julia> a = 1
1

julia> f(3)
4

这个行为看起来对于普通变量来说有点奇怪,但是这个允许命名过的函数 – 它只是连接了函数对象的普通变量 – 在定义之前就能被使用。这就允许函数能以符合直觉和方便的顺序定义,而非强制以颠倒顺序或者需要前置声明,只要在实际调用之前被定义就行。举个例子,这里有个不高效的,相互递归的方法去检验正整数是奇数还是偶数的方法:

julia> even(n) = (n == 0) ? true : odd(n - 1);

julia> odd(n) = (n == 0) ? false : even(n - 1);

julia> even(3)
false

julia> odd(3)
true

Julia提供了叫做isevenisodd的内置的高效的奇偶性检验的函数,所以之上的定义只能被认为是作用域的一个例子,而非高效的设计。

let块

不像局部变量的赋值行为,let语句每次运行都新建一个新的变量绑定。赋值改变的是已存在值的位置,let会新建新的位置。这个区别通常都不重要,只会在通过闭包跳出作用域的变量的情况下能探测到。let语法接受由逗号隔开的一系列的赋值和变量名:

julia> x, y, z = -1, -1, -1;

julia> let x = 1, z
           println("x: $x, y: $y") # x is local variable, y the global
           println("z: $z") # errors as z has not been assigned yet but is local
       end
x: 1, y: -1
ERROR: UndefVarError: z not defined

这个赋值会按顺序评估,在左边的新变量被引入之前右边的每隔两都会在作用域中被评估。所以编写像let x = x这样的东西是有意义的,因为两个x变量是不一样的,拥有不同的存储位置。这里有个例子,在例子中let的行为是必须的:

julia> Fs = Vector{Any}(undef, 2); i = 1;

julia> while i <= 2
           Fs[i] = ()->i
           global i += 1
       end

julia> Fs[1]()
3

julia> Fs[2]()
3

这里我创建并存储了两个返回变量i的闭包。但是这两个始终是同一个变量i。所以这两个闭包行为是相同的。我们可以使用let来为i创建新的绑定:

julia> Fs = Vector{Any}(undef, 2); i = 1;

julia> while i <= 2
           let i = i
               Fs[i] = ()->i
           end
           global i += 1
       end

julia> Fs[1]()
1

julia> Fs[2]()
2

以为begin结构不会引入新的作用域,使用没有参数的let来只引进一个新的作用域块而不创建新的绑定是有用的:

julia> let
           local x = 1
           let
               local x = 2
           end
           x
       end
1

因为let引进了一个新的作用域块,内部的局部x与外部的局部x是不同的变量。

对于循环和推导式

for循环,while循环,和Comprehensions拥有下述的行为:任何在它们的内部的作用域中引入的新变量在每次循环迭代中都会被新分配一块内存,就像循环体是被let块包围一样。

julia> Fs = Vector{Any}(undef, 2);

julia> for j = 1:2
           Fs[j] = ()->j
       end

julia> Fs[1]()
1

julia> Fs[2]()
2

for循环或者推导式的迭代变量始终是个新的变量:

julia> function f()
 i = 0
 for i = 1:3
 end
 return i
 end;

julia> f()
0

但是,有时重复使用一个存在的变量作为迭代变量是有用的。 这能够通过添加关键字outer来方便地做到:

julia> function f()
 i = 0
 for outer i = 1:3
 end
 return i
 end;

julia> f()
3

常量

变量的经常的一个使用方式是给一个特定的不变的值一个名字。这样的变量只会被赋值一次。这个想法可以通过使用const关键字传递给编译器:

julia> const e  = 2.71828182845904523536;

julia> const pi = 3.14159265358979323846;

多个变量可以使用单个const语句进行声明:

julia> const a, b = 1, 2
(1, 2)

const声明只应该在全局作用域中对全局变量使用。编译器很难为包含全局变量的代码优化,因为它们的值(甚至它们的类型)可以任何时候改变。如果一个全局变量不会改变,添加const声明会解决这个问题。

局部常量却大有不同。编译器能够自动确定一个局部变量什么时候是不变的,所以局部常量声明是不必要的,其实现在也并不支持。

特别的顶层赋值,比如使用functionstructure关键字进行的,默认是不变的。

注意const只会影响变量绑定;变量可能会绑定到一个可变的对象上(比如一个数组)使得其任然能被改变。另外当尝试给一个声明为常量的变量赋值时下列情景是可能的:

julia> const x = 1.0
1.0

julia> x = 1
ERROR: invalid redefinition of constant x
julia> const y = 1.0
1.0

julia> y = 2.0
WARNING: redefining constant y
2.0
julia> const z = 100
100

julia> z = 100
100

最后一条规则适用于不可变对象,即使变量绑定会改变,例如:

julia> const s1 = "1"
"1"

julia> s2 = "1"
"1"

julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
 Ptr{UInt8} @0x00000000132c9638
 Ptr{UInt8} @0x0000000013dd3d18

julia> s1 = s2
"1"

julia> pointer.([s1, s2], 1)
2-element Array{Ptr{UInt8},1}:
 Ptr{UInt8} @0x0000000013dd3d18
 Ptr{UInt8} @0x0000000013dd3d18

但是对于可变对象,警告会如预期出现:

julia> const a = [1]
1-element Array{Int64,1}:
 1

julia> a = [1]
WARNING: redefining constant a
1-element Array{Int64,1}:
 1

注意,即使可能,改变一个声明为常量的变量的值是十分不推荐的。举个例子,如果一个方法引用了一个常量并且在常量被改变之前已经被编译了,那么这个变量还是会保留使用原来的值:

julia> const x = 1
1

julia> f() = x
f (generic function with 1 method)

julia> f()
1

julia> x = 2
WARNING: redefining constant x
2

julia> f()
1