变量作用域

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

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

作用域结构

结构作用域类型可嵌入的作用域块
module, baremodule全局全局
交互式提示符(REPL)全局全局
(mutable) struct, macro局部全局
for, while, try-catch-finally, let局部全局或局部
函数(语法,匿名或者do语法块)局部全局或局部
推导式,broadcast-fusing局部全局或局部

Notably missing from this table are begin blocks and if blocks which do not introduce new scopes. Both types of scopes follow somewhat different rules which will be explained below.

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

  1. julia> module Bar
  2. x = 1
  3. foo() = x
  4. end;

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

  1. julia> import .Bar
  2. julia> x = -1;
  3. julia> Bar.foo()
  4. 1

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

全局作用域

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

  1. julia> module A
  2. a = 1 # a global in A's scope
  3. end;
  4. julia> module B
  5. module C
  6. c = 2
  7. end
  8. b = C.c # can access the namespace of a nested global scope
  9. # through a qualified access
  10. import ..A # makes module A available
  11. d = A.a
  12. end;
  13. julia> module D
  14. b = a # errors as D's global scope is separate from A's
  15. end;
  16. ERROR: UndefVarError: a not defined
  17. julia> module E
  18. import ..A # make module A available
  19. A.a = 2 # throws below error
  20. end;
  21. ERROR: cannot assign variables in other modules

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

局部作用域

A new local scope is introduced by most code blocks (see above table for a complete list). A local scope inherits all the variables from a parent local scope, both for reading and writing. Unlike global scopes, local scopes are not namespaces, thus variables in an inner scope cannot be retrieved from the parent scope through some sort of qualified access.

The following rules and examples pertain to local scopes. A newly introduced variable in a local scope cannot be referenced by a parent scope. For example, here the $z$ is not introduced into the top-level scope:

  1. julia> for i = 1:10
  2. z = i
  3. end
  4. julia> z
  5. ERROR: UndefVarError: z not defined

Note

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

Inner local scopes can, however, update variables in their parent scopes:

  1. julia> for i = 1:1
  2. z = i
  3. for j = 1:1
  4. z = 0
  5. end
  6. println(z)
  7. end
  8. 0

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

  1. julia> for i = 1:1
  2. x = i + 1
  3. for j = 1:1
  4. local x = 0
  5. end
  6. println(x)
  7. end
  8. 2

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

  1. julia> for i = 1:10
  2. global z
  3. z = i
  4. end
  5. julia> z
  6. 10

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

  1. julia> for i = 1:10
  2. z = i
  3. global z
  4. end
  5. julia> z
  6. 10

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

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

  • 赋值会导致全局变量改变,或者
  • 变量专门使用local关键字标记。

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

  1. julia> x, y = 1, 2;
  2. julia> function foo()
  3. x = 2 # assignment introduces a new local
  4. return x + y # y refers to the global
  5. end;
  6. julia> foo()
  7. 4
  8. julia> x
  9. 1

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

Avoiding globals

Avoiding changing the value of global variables is considered by many to be a programming best-practice. Changing the value of a global variable can cause “action at a distance”, making the behavior of a program harder to reason about. This is why the scope blocks that introduce local scope require the global keyword to declare the intent to modify a global variable.

  1. julia> x = 1;
  2. julia> function foobar()
  3. global x = 2
  4. end;
  5. julia> foobar();
  6. julia> x
  7. 2

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

  1. julia> x, y = 1, 2;
  2. julia> function baz()
  3. x = 2 # introduces a new local
  4. function bar()
  5. x = 10 # modifies the parent's x
  6. return x + y # y is global
  7. end
  8. return bar() + x # 12 + 10 (x is modified in call of bar())
  9. end;
  10. julia> baz()
  11. 22
  12. julia> x, y # verify that global x and y are unchanged
  13. (1, 2)

The reason to allow modifying local variables of parent scopes in nested functions is to allow constructing closures which have private state, for instance the state variable in the following example:

  1. julia> let state = 0
  2. global counter() = (state += 1)
  3. end;
  4. julia> counter()
  5. 1
  6. julia> counter()
  7. 2

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

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

  1. julia> x, y = 1, 2;
  2. julia> function bar()
  3. x = 10 # local, no longer a closure variable
  4. return x + y
  5. end;
  6. julia> function quz()
  7. x = 2 # local
  8. return bar() + x # 12 + 2 (x is not modified)
  9. end;
  10. julia> quz()
  11. 14
  12. julia> x, y # verify that global x and y are unchanged
  13. (1, 2)

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

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

  1. julia> f = y -> y + a;
  2. julia> f(3)
  3. ERROR: UndefVarError: a not defined
  4. Stacktrace:
  5. [...]
  6. julia> a = 1
  7. 1
  8. julia> f(3)
  9. 4

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

  1. julia> even(n) = (n == 0) ? true : odd(n - 1);
  2. julia> odd(n) = (n == 0) ? false : even(n - 1);
  3. julia> even(3)
  4. false
  5. julia> odd(3)
  6. true

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

let块

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

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

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

  1. julia> Fs = Vector{Any}(undef, 2); i = 1;
  2. julia> while i <= 2
  3. Fs[i] = ()->i
  4. global i += 1
  5. end
  6. julia> Fs[1]()
  7. 3
  8. julia> Fs[2]()
  9. 3

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

  1. julia> Fs = Vector{Any}(undef, 2); i = 1;
  2. julia> while i <= 2
  3. let i = i
  4. Fs[i] = ()->i
  5. end
  6. global i += 1
  7. end
  8. julia> Fs[1]()
  9. 1
  10. julia> Fs[2]()
  11. 2

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

  1. julia> let
  2. local x = 1
  3. let
  4. local x = 2
  5. end
  6. x
  7. end
  8. 1

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

对于循环和推导式

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

  1. julia> Fs = Vector{Any}(undef, 2);
  2. julia> for j = 1:2
  3. Fs[j] = ()->j
  4. end
  5. julia> Fs[1]()
  6. 1
  7. julia> Fs[2]()
  8. 2

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

  1. julia> function f()
  2. i = 0
  3. for i = 1:3
  4. end
  5. return i
  6. end;
  7. julia> f()
  8. 0

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

  1. julia> function f()
  2. i = 0
  3. for outer i = 1:3
  4. end
  5. return i
  6. end;
  7. julia> f()
  8. 3

常量

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

  1. julia> const e = 2.71828182845904523536;
  2. julia> const pi = 3.14159265358979323846;

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

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

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

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

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

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

  • 如果一个新值的类型与常量类型不一样时会扔出一个错误:
  1. julia> const x = 1.0
  2. 1.0
  3. julia> x = 1
  4. ERROR: invalid redefinition of constant x
  • 如果一个新值的类型与常量一样会打印一个警告:
  1. julia> const y = 1.0
  2. 1.0
  3. julia> y = 2.0
  4. WARNING: redefining constant y
  5. 2.0
  • 如果赋值不会导致变量值的变化,不会给出任何信息:
  1. julia> const z = 100
  2. 100
  3. julia> z = 100
  4. 100

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

  1. julia> const s1 = "1"
  2. "1"
  3. julia> s2 = "1"
  4. "1"
  5. julia> pointer.([s1, s2], 1)
  6. 2-element Array{Ptr{UInt8},1}:
  7. Ptr{UInt8} @0x00000000132c9638
  8. Ptr{UInt8} @0x0000000013dd3d18
  9. julia> s1 = s2
  10. "1"
  11. julia> pointer.([s1, s2], 1)
  12. 2-element Array{Ptr{UInt8},1}:
  13. Ptr{UInt8} @0x0000000013dd3d18
  14. Ptr{UInt8} @0x0000000013dd3d18

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

  1. julia> const a = [1]
  2. 1-element Array{Int64,1}:
  3. 1
  4. julia> a = [1]
  5. WARNING: redefining constant a
  6. 1-element Array{Int64,1}:
  7. 1

注意,改变一个声明为常量的变量的值虽然有时是可能的,但是十分不推荐这样做,并且在交互式使用中这样做仅仅是为了更加方便。举个例子,如果一个方法引用了一个常量并且在常量被改变之前已经被编译了,那么这个变量还是会保留使用原来的值:

  1. julia> const x = 1
  2. 1
  3. julia> f() = x
  4. f (generic function with 1 method)
  5. julia> f()
  6. 1
  7. julia> x = 2
  8. WARNING: redefining constant x
  9. 2
  10. julia> f()
  11. 1