julia:变量作用域(完结)

变量作用域


变量的范围是代码中可见变量的区域。变量作用域有助于避免变量命名冲突。这个概念很直观:两个函数都可以有名为x的参数,而不需要两个x引用相同的东西。类似地,还有很多其他情况,不同的代码块可以在不引用相同的东西的情况下使用相同的名称。当相同的变量名引用或不引用相同的事物时,规则称为范围规则;本节详细说明了这些规则。

语言中的某些构造引入了作用域块,它们是代码的区域,有资格成为某种变量集的作用域。变量的作用域不能是任意一组源行;相反,它将始终与这些块之一保持一致。Julia中有两种主要类型的作用域,全局作用域和局部作用域,后者可以嵌套。引入作用域块的构造如下:

  • 只能嵌套在其他全局作用域块中的作用域块:
    全局作用域:
    ###############模块,裸模块
    ###############在交互提示(Repl)
    本地范围(不允许嵌套):
    ###############(可变的)结构,宏
  • 可以在任何地方嵌套的作用域块(在全局或本地范围内):
    区域范围:
    #############for, while, try-catch-finally, let
    #############函数(语法、匿名)
    #############理解,广播-融合

值得注意的是,这个表中缺少的是开始块和如果块没有引入新的作用域块。这两种类型的作用域遵循一些不同的规则,将在下面解释。

Julia使用词法作用域,这意味着函数的作用域不是从调用方的作用域继承的,而是从函数定义的作用域继承的。例如,在下面的代码中,foo中的x引用其模块栏的全局作用域中的x:

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

而在使用foo的范围内没有x:

julia> import .Bar

julia> x = -1;

julia> Bar.foo()
1

因此,词法作用域意味着可以仅从源代码中推断变量的作用域。

全局作用域


每个模块都引入了新的全局范围,独立于所有其他模块的全局范围;没有包罗万象的全局范围。模块可以通过使用或导入语句或通过使用点符号的合格访问将其他模块的变量引入其范围,即每个模块都是所谓的命名空间。注意,变量绑定只能在它们的全局范围内而不是从外部模块中更改。

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
 注意,交互提示(AK-RePL)位于模块主体的全局范围内。

区域范围


大多数代码块都会引入新的本地作用域(完整列表见上表)。本地作用域继承父本地作用域的所有变量,用于读写。此外,本地作用域继承其父全局作用域块中分配给的所有全局变量(如果它被全局if或开始作用域包围)。与全局作用域不同,本地作用域不是名称空间,因此,不能通过某种形式的限定访问从父作用域中检索内部作用域中的变量。

以下规则和示例与本地作用域有关。在本地范围中新引入的变量不会反向传播到其父作用域。例如,这里没有将zz引入顶级作用域:

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

julia> z
ERROR: UndefVarError: z not defined

(请注意,在下面的所有示例中,假设它们的顶层是一个具有干净工作区的全局范围,例如一个新启动的repl。)

在本地范围内,可以使用本地关键字强制变量为新局部变量:

julia> x = 0;

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

julia> x
0

在本地范围内,可以使用关键字全局分配全局变量:

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

julia> z
10

局部变量块中的本地关键字和全局关键字的位置都是无关的。以下内容相当于最后一个示例(尽管在风格上更糟):

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

julia> z
10

局部和全局关键字也可以应用于销毁赋值,例如局部x,y=1,2。在这种情况下,关键字会影响所有列出的变量。
局部作用域由大多数块关键字引入,但BEGIN和IF除外。
在本地作用域中,所有变量都从其父全局作用域块继承,除非:

  • 赋值将导致修改的全局变量,或
  • 变量被明确标记为关键字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

要将全局变量赋值给全局变量,需要一个显式全局变量:
回避全局:
避免更改全局变量的值被许多人认为是编程的最佳实践。其中一个原因是远程更改其他模块中的全局变量的状态应该小心,因为它使程序的本地行为难以推理。这就是为什么引入本地作用域的作用域块需要globalglobal关键字来声明修改全局变量的意图。

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)

允许在嵌套函数中修改父作用域的局部变量的原因是为了允许构造具有私有状态的闭包,例如,下面的示例中的statEstate变量:

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

julia> counter()
1

julia> counter()
2

另请参阅后面两个部分中示例中的闭包。一个变量(如第一个示例中的x)和第二个由内部函数从封闭作用域继承的状态有时被称为捕获变量。捕获的变量可以显示性能技巧中讨论的性能挑战。

继承全局范围和嵌套局部范围之间的区别会导致在变量分配的本地范围和全局范围中定义的函数之间存在一些细微的差异。考虑最后一个示例的修改,将移动条移到全局范围:

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提供了内置的、有效的函数来测试奇异性和均匀度,称为is偶数和isodity,因此上述定义只应被视为范围的示例,而不是有效的设计。

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块可以重新定义变量绑定,如果与全局变量名相同,则不会影响全局变量
赋值是按顺序计算的,在引入左边的新变量之前,在范围内计算每个右侧的赋值。因此,编写类似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构造没有引入新的作用域,所以使用零参数让它只引入一个新的作用域块而不创建任何新的绑定是有用的:

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

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

用于循环和理解

对于循环,而循环和理解具有以下行为:在它们的主体范围中引入的任何新变量都为每个循环迭代新分配,就好像循环主体被一个let块包围一样:

ulia> 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

常量(Constants)

变量的一种常用用法是为特定的、不变的值指定名称。这些变量只分配一次。这个意图可以使用Const关键字传达给编译器:

julia> const e  = 2.71828182845904523536;

julia> const pi = 3.14159265358979323846;

可以在单个Const语句中声明多个变量:

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

Const声明只应在全局范围内使用。编译器很难优化涉及全局变量的代码,因为它们的值(甚至类型)几乎在任何时候都可能发生变化。如果全局变量不会改变,添加一个Const声明可以解决这个性能问题。

本地常量是非常不同的。编译器能够自动确定局部变量是常量,因此本地常量声明是不必要的,而且实际上目前不支持本地常量声明。

特殊的顶级赋值,例如由函数和struct关键字执行的任务,在默认情况下是常量的。

请注意,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

请注意,尽管可能,更改声明为常数的变量的值是非常不可取的。例如,如果一个方法引用了一个常量,并且在更改该常量之前已经编译,那么它可能会继续使用旧值:

请注意,尽管可能,更改声明为常数的变量的值是非常不可取的。例如,如果一个方法引用了一个常量,并且在更改该常量之前已经编译,那么它可能会继续使用旧值:

完结于2018-08-29 21:03

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值