变量的范围是在其中可见变量的代码区域。变量作用域有助于避免变量命名冲突。这个概念很直观:两个函数都可以具有被调用x
的参数,而两个函数都没有x
引用相同的东西。同样,在许多其他情况下,不同的代码块可以使用相同的名称而无需引用相同的内容。相同变量名称何时引用或不引用相同事物的规则称为作用域规则。本节详细说明了它们。
该语言中的某些构造引入了作用域块,它们是有资格成为某些变量集范围的代码区域。变量的范围不能是源代码行的任意集合;相反,它将始终与这些块之一对齐。Julia中有两种主要的作用域类型:全局作用域和局部作用域,后者可以嵌套。引入作用域块的构造为:
module
,,
baremodule
在交互式提示(REPL)下
该表中明显缺少开始块和if块,它们没有引入新的作用域块。所有这三种类型的作用域都遵循略有不同的规则,下面将对这些规则以及某些块的一些额外规则进行说明。
Julia使用词法作用域,即函数的作用域不是从调用者的作用域继承,而是从定义函数的作用域继承。例如,在以下代码中,x
内部在其模块的全局范围内foo
引用:xBar
julia> module Bar
x = 1
foo() = x
end;
而不是x
在使用范围内foo
:
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
请注意,交互式提示(aka REPL)在模块的全局范围内Main
。
大多数代码块都引入了新的本地范围,有关完整列表,请参见上表。本地范围通常从其父范围继承所有变量,以进行读取和写入。局部作用域有两种子类型,硬性和软性,关于继承什么变量的规则略有不同。与全局作用域不同,局部作用域不是名称空间,因此内部作用域中的变量无法通过某种合格的访问从父作用域中检索。
以下规则和示例同时适用于硬本地作用域和软本地作用域。在本地范围内新引入的变量不会反向传播到其父范围。例如,此处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
x = i + 1
end
julia> x
0
在局部范围内,可以使用关键字定义新的全局变量global
:
julia> for i = 1:10
global z
z = i
end
julia> z
10
两者的位置local
和global
关键字的范围块内是无关紧要的。以下等价于最后一个示例(尽管从风格上讲更糟):
julia> for i = 1:10
z = i
global z
end
julia> z
10
在软本地范围内,所有变量均从其父范围继承,除非使用关键字专门标记了变量
local
。
软局部作用域由for循环,while循环,理解,try-catch-finally块和let块引入。对于Let块和For循环和理解有一些额外的规则。
在以下示例中,x
和y
始终引用相同的变量,因为软本地作用域继承了读取和写入变量:
julia> x, y = 0, 1;
julia> for i = 1:10
x = i + y + 1
end
julia> x
12
在软作用域内,尽管允许使用global关键字,但从不需要。唯一会改变语义的情况是(当前)语法错误:
julia> let
local j = 2
let
global j = 3
end
end
ERROR: syntax: `global j`: j is local variable in the enclosing scope
硬局部作用域由函数定义(以其所有形式),结构类型定义块和宏定义引入。
在硬本地作用域中,所有变量均从其父作用域继承,除非:
local
。因此,全局变量仅继承用于读取,而不能继承:
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
需要一个显式分配给全局变量:
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
(1, 2)
继承全局变量和局部变量进行分配之间的区别可能导致在局部作用域和全局作用域中定义的函数之间存在一些细微差异。通过转到bar
全局范围,考虑对最后一个示例的修改:
julia> x, y = 1, 2;
julia> function bar()
x = 10 # local
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
(1, 2)
请注意,上述细微之处与类型和宏定义无关,因为它们只能出现在全局范围内。关于默认值和关键字函数自变量的评估,有一些特殊的作用域规则,这些规则在“ 功能”部分中进行了介绍。
引入一个在函数,类型或宏定义内部使用的变量的赋值不必先于其内部使用:
julia> f = y -> y + a
(::#1) (generic function with 1 method)
julia> f(3)
ERROR: UndefVarError: a not defined
Stacktrace:
[1] (::##1#2)(::Int64) at ./none:1
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提供了内置的高效函数来测试所调用的奇数和偶数iseven()
,isodd()
因此上述定义仅应作为示例。
引入软局部作用域的块(例如循环)通常用于在其父作用域中操作变量。因此,它们的默认值是完全访问其父作用域中的所有变量。
相反,引入硬本地作用域(功能,类型和宏定义)的块内代码可以在程序中的任何位置执行。远程更改其他模块中全局变量的状态时应格外小心,因此这是一个需要global
关键字的选择功能。
允许在嵌套函数中修改父作用域的局部变量的原因是允许构造具有私有状态的闭包,例如state
以下示例中的变量:
julia> let
state = 0
global counter
counter() = state += 1
end;
julia> counter()
1
julia> counter()
2
另请参见下两节示例中的闭包。
与分配给局部变量不同,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 = Array{Any}(2); i = 1;
julia> while i <= 2
Fs[i] = ()->i
i += 1
end
julia> Fs[1]()
3
julia> Fs[2]()
3
在这里,我们创建并存储两个返回变量的闭包i
。但是,它始终是相同的变量i
,因此两个闭包的行为相同。我们可以用来let
为创建新的绑定i
:
julia> Fs = Array{Any}(2); i = 1;
julia> while i <= 2
let i = i
Fs[i] = ()->i
end
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
循环将变量用于所有迭代的循环形成对比。因此,这些构造类似于内部while
带有let
块的循环:
julia> Fs = Array{Any}(2);
julia> for j = 1:2
Fs[j] = ()->j
end
julia> Fs[1]()
1
julia> Fs[2]()
2
for
循环将现有变量重用于其迭代变量:
julia> i = 0;
julia> for i = 1:3
end
julia> i
3
但是,理解并不能做到这一点,而是总是重新分配其迭代变量:
julia> x = 0;
julia> [ x for x = 1:3 ];
julia> x
0
变量的常见用法是为特定的不变值命名。此类变量仅分配一次。可以使用const
关键字将该意图传达给编译器:
julia> const e = 2.71828182845904523536;
julia> const pi = 3.14159265358979323846;
该const
声明是允许全球和局部变量,但对全局尤其有用。编译器很难优化涉及全局变量的代码,因为它们的值(甚至它们的类型)几乎可以随时更改。如果全局变量不变,则添加const
声明可以解决此性能问题。
局部常数有很大的不同。编译器能够自动确定局部变量何时为常量,因此出于性能目的,不需要局部常量声明。
默认情况下,特殊的顶级分配(例如,function
和struct
关键字执行的顶级分配)是恒定的。
注意,这const
仅影响变量绑定;变量可以绑定到可变对象(例如数组),并且该对象仍可以被修改。