变量作用域

变量的作用域是指代码中可以访问该变量的区域。变量作用域有助于避免变量命名冲突。这个概念很直观:两个函数都可以有名为x的参数,但这两个x并不指向同一个东西。类似地,还有很多其他情况,不同的代码块可以使用相同的名称,但并不指向同一个东西。关于同一个变量名称何时指向同一个东西,何时不指向同一个东西的规则被称为作用域规则;本节将详细解释这些规则。

语言中的某些结构会引入作用域块,这些代码区域可以成为某些变量集的作用域。变量的作用域不能是任意的源代码行集;相反,它总是与这些块中的一个块对齐。Julia 中有两种主要的作用域类型,全局作用域局部作用域。后者可以嵌套。Julia 还区分了引入“硬作用域”和仅引入“软作用域”的结构,这会影响是否允许使用相同名称的变量来遮蔽全局变量。

作用域结构

引入作用域块的结构如下:

结构作用域类型允许在
modulebaremodule全局全局
struct局部(软)全局
forwhiletry局部(软)全局,局部
局部(硬)全局
函数,do 块,let 块,推导,生成器局部(硬)全局,局部

值得注意的是,此表中没有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

因此,词法作用域意味着特定代码片段中的变量所指的内容可以仅从其出现的代码中推断出来,并且不依赖于程序的执行方式。嵌套在另一个作用域内部的作用域可以“看到”它所包含的所有外部作用域中的变量——这意味着它们可以在内部作用域中读取和写入——除非有一个名为 x 的局部变量“遮蔽”了名为 x 的外部变量。即使在外部局部变量在内部块之后(在文本上低于)声明的情况下,也是如此。当我们说一个变量在给定作用域中“存在”时,这意味着在当前作用域嵌套的所有作用域(包括当前作用域本身)中都存在一个名为 x 的变量。

全局作用域

每个模块都引入一个新的全局作用域,它与所有其他模块的全局作用域分离——不存在一个包含所有作用域的全局作用域。模块可以通过using 或 import语句或使用点符号进行限定访问将其他模块的变量引入其作用域,即每个模块既是一个所谓的命名空间,也是一个将名称与值关联起来的一级数据结构。

如果顶级表达式包含使用关键字local声明的变量声明,那么该变量在该表达式之外不可访问。表达式内的变量不会影响同名的全局变量。例如,在顶级beginif块中声明local x

julia> x = 1
       begin
           local x = 0
           @show x
       end
       @show x;
x = 0
x = 1

注意,交互式提示(也称为 REPL)位于模块Main的全局作用域中。

局部作用域

大多数代码块都会引入一个新的局部作用域(有关完整列表,请参见上面的表格)。如果这样的块在语法上嵌套在另一个局部作用域内部,那么它创建的作用域将嵌套在它所包含的所有局部作用域内部,这些作用域最终都嵌套在评估代码的模块的全局作用域内部。外部作用域中的变量在任何包含它们的范围中都是可见的——这意味着它们可以在内部作用域中读取和写入——除非有一个同名的局部变量“遮蔽”了同名的外部变量。即使在外部局部变量在内部块之后(在文本上低于)声明的情况下,也是如此。当我们说一个变量在给定作用域中“存在”时,这意味着在当前作用域嵌套的所有作用域(包括当前作用域本身)中都存在一个名为 x 的变量。

一些编程语言要求在使用新变量之前显式声明它们。在 Julia 中也可以使用显式声明:在任何局部作用域中,编写local x都会在该作用域中声明一个新的局部变量,无论在外部作用域中是否已存在名为x的变量。但是,这种方式为每个新变量都这样声明会比较冗长且乏味,因此 Julia 像许多其他语言一样,将对不存在的变量名称的赋值视为隐式声明该变量。如果当前作用域是全局的,那么新变量是全局的;如果当前作用域是局部的,那么新变量是局部作用域中最内层的,并且在该作用域内可见,但在该作用域之外不可见。如果你对现有的局部变量进行赋值,它总是会更新该现有的局部变量:你只能通过在嵌套作用域中使用local关键字显式声明一个新的局部变量来遮蔽一个局部变量。特别是,这适用于在内部函数中赋值的变量,这可能会让习惯了 Python 的用户感到困惑,在 Python 中,如果变量没有显式声明为非局部的,那么在内部函数中赋值会创建一个新的局部变量。

这在很大程度上是相当直观的,但就像许多直观行为的东西一样,细节比人们想象的要微妙。

x = <value>出现在局部作用域中时,Julia 会根据赋值表达式出现的位置和x在该位置之前所指的内容,应用以下规则来决定该表达式的含义

  1. 现有局部变量:如果x已经是局部变量,那么会对现有的局部变量x进行赋值;
  2. 硬作用域:如果x不是局部变量,并且赋值出现在任何硬作用域结构内(即在let块、函数或宏体、推导或生成器内),那么会在赋值的作用域中创建一个名为x的新局部变量;
  3. 软作用域:如果x不是局部变量,并且包含赋值的所有作用域结构都是软作用域(循环、try/catch块或struct块),那么行为取决于全局变量x是否已定义
    • 如果全局x未定义,则会在赋值的作用域中创建一个名为x的新局部变量;
    • 如果全局x已定义,则该赋值会被视为有歧义
      • 非交互式环境(文件、eval)中,会打印一个歧义警告,并创建一个新的局部变量;
      • 交互式环境(REPL、笔记本)中,会对全局变量x进行赋值。

你可能会注意到,在非交互式环境中,硬作用域和软作用域行为是相同的,只是当隐式局部变量(即没有用local x声明的变量)遮蔽了全局变量时,会打印一个警告。在交互式环境中,这些规则遵循更复杂的启发式方法,以方便使用。这将在接下来的示例中进行深入介绍。

既然你已经了解了这些规则,让我们看一些示例。假设每个示例都在一个全新的 REPL 会话中进行评估,因此每个代码片段中唯一的全局变量是该代码块中赋值的变量。

我们将从一个清晰明了的场景开始——在硬作用域(在本例中是函数体)内进行赋值,此时该名称的局部变量不存在。

julia> function greet()
           x = "hello" # new local
           println(x)
       end
greet (generic function with 1 method)

julia> greet()
hello

julia> x # global
ERROR: UndefVarError: `x` not defined

greet 函数内部,赋值 x = "hello" 会使 x 成为函数作用域内的一个新的局部变量。有两个相关的事实:赋值发生在局部作用域中,并且不存在现有的局部 x 变量。由于 x 是局部的,因此是否存在名为 x 的全局变量并不重要。例如,在定义和调用 greet 之前,我们定义 x = 123

julia> x = 123 # global
123

julia> function greet()
           x = "hello" # new local
           println(x)
       end
greet (generic function with 1 method)

julia> greet()
hello

julia> x # global
123

由于 greet 中的 x 是局部的,因此全局 x 的值(或缺乏值)不受调用 greet 的影响。硬作用域规则并不关心是否存在名为 x 的全局变量:在硬作用域中对 x 进行赋值是局部的(除非 x 被声明为全局的)。

我们将考虑的下一个清晰明了的场景是,当已经存在一个名为 x 的局部变量时,在这种情况下,x = <value> 始终会赋值给这个现有的局部 x。无论赋值发生在同一个局部作用域,同一个函数体内的内部局部作用域,还是在嵌套在另一个函数体内的函数体内(也称为 闭包),都是如此。

我们将使用 sum_to 函数作为示例,该函数计算从 1 到 n 的整数之和。

function sum_to(n)
    s = 0 # new local
    for i = 1:n
        s = s + i # assign existing local
    end
    return s # same local
end

与前面的示例一样,对 sum_to 顶部 s 的首次赋值会使 s 成为函数体内的新的局部变量。for 循环在函数作用域内有自己的内部局部作用域。在 s = s + i 出现的点,s 已经是一个局部变量,因此赋值更新了现有的 s,而不是创建一个新的局部变量。我们可以通过在 REPL 中调用 sum_to 来测试这一点。

julia> function sum_to(n)
           s = 0 # new local
           for i = 1:n
               s = s + i # assign existing local
           end
           return s # same local
       end
sum_to (generic function with 1 method)

julia> sum_to(10)
55

julia> s # global
ERROR: UndefVarError: `s` not defined

由于 ssum_to 函数的局部变量,因此调用该函数不会影响全局变量 s。我们还可以看到,for 循环中的更新 s = s + i 必须更新了由初始化 s = 0 创建的相同 s,因为我们获得了从 1 到 10 的整数的正确和 55。

让我们深入研究一下 for 循环体有自己的作用域这一事实,方法是编写一个稍微详细一点的变体,我们将将其称为 sum_to_def,在这个变体中,我们在更新 s 之前将和 s + i 保存到一个变量 t 中。

julia> function sum_to_def(n)
           s = 0 # new local
           for i = 1:n
               t = s + i # new local `t`
               s = t # assign existing local `s`
           end
           return s, @isdefined(t)
       end
sum_to_def (generic function with 1 method)

julia> sum_to_def(10)
(55, false)

此版本与以前一样返回 s,但它还使用 @isdefined 宏返回一个布尔值,指示函数的最外部局部作用域中是否定义了一个名为 t 的局部变量。正如你所看到的,在 for 循环体之外没有定义 t。这是因为硬作用域规则:由于对 t 的赋值发生在函数内部,该函数引入了一个硬作用域,因此赋值会使 t 成为其出现局部作用域内的新局部变量,即在循环体内。即使存在一个名为 t 的全局变量,它也不会产生任何影响——硬作用域规则不受全局作用域中的任何内容影响。

请注意,for 循环体的局部作用域与内部函数的局部作用域没有区别。这意味着我们可以重写此示例,以便循环体作为对内部辅助函数的调用的实现,并且它的行为方式相同。

julia> function sum_to_def_closure(n)
           function loop_body(i)
               t = s + i # new local `t`
               s = t # assign same local `s` as below
           end
           s = 0 # new local
           for i = 1:n
               loop_body(i)
           end
           return s, @isdefined(t)
       end
sum_to_def_closure (generic function with 1 method)

julia> sum_to_def_closure(10)
(55, false)

此示例说明了几个关键点。

  1. 内部函数作用域与任何其他嵌套局部作用域一样。特别是,如果一个变量已经是一个外部局部变量,并且你在内部函数中给它赋值,那么外部局部变量就会被更新。

  2. 无论外部局部变量的定义是在其更新位置的下方还是上方,规则都保持不变。整个封闭局部作用域都会被解析,并且在解析内部局部含义之前确定其局部变量。

这种设计意味着你通常可以将代码移入或移出内部函数,而不会改变其含义,这有助于使用闭包在语言中实现许多常见习语(参见 do 块)。

让我们继续讨论由软作用域规则涵盖的一些更模棱两可的情况。我们将通过将 greetsum_to_def 函数的函数体提取到软作用域上下文中来探索这一点。首先,让我们将 greet 的函数体放到一个 for 循环中(它是软的,而不是硬的),并在 REPL 中评估它。

julia> for i = 1:3
           x = "hello" # new local
           println(x)
       end
hello
hello
hello

julia> x
ERROR: UndefVarError: `x` not defined

由于在评估 for 循环时全局 x 未定义,因此软作用域规则的第一条条款适用,并且 x 被创建为 for 循环的局部变量,因此循环执行后全局 x 仍然未定义。接下来,让我们考虑将 sum_to_def 的函数体提取到全局作用域中,并将它的参数固定为 n = 10

s = 0
for i = 1:10
    t = s + i
    s = t
end
s
@isdefined(t)

这段代码做了什么?提示:这是一个陷阱问题。答案是“这取决于”。如果这段代码是在交互式环境中输入的,它的行为方式与在函数体中的行为方式相同。但是,如果这段代码出现在文件中,它会打印一个歧义警告并抛出一个未定义变量错误。让我们先在 REPL 中看看它的工作原理。

julia> s = 0 # global
0

julia> for i = 1:10
           t = s + i # new local `t`
           s = t # assign global `s`
       end

julia> s # global
55

julia> @isdefined(t) # global
false

REPL 通过决定循环内的赋值是赋值给全局变量还是创建新的局部变量来近似地模拟处于函数体内部,这取决于是否存在一个名为 x 的全局变量。如果存在一个名为 x 的全局变量,那么赋值会更新它。如果不存在全局变量,那么赋值会创建一个新的局部变量。在这个示例中,我们看到了这两个情况。

  • 没有名为 t 的全局变量,因此 t = s + i 创建一个新的 t,它是 for 循环的局部变量;
  • 存在一个名为 s 的全局变量,因此 s = t 赋值给它。

第二个事实是为什么循环的执行会改变 s 的全局值,而第一个事实是为什么 t 在循环执行后仍然未定义。现在,让我们尝试将这段代码评估为好像它在文件中一样。

julia> code = """
       s = 0 # global
       for i = 1:10
           t = s + i # new local `t`
           s = t # new local `s` with warning
       end
       s, # global
       @isdefined(t) # global
       """;

julia> include_string(Main, code)
┌ Warning: Assignment to `s` in soft scope is ambiguous because a global variable by the same name exists: `s` will be treated as a new local. Disambiguate by using `local s` to suppress this warning or `global s` to assign to the existing global variable.
└ @ string:4
ERROR: LoadError: UndefVarError: `s` not defined

这里我们使用 include_string,将 code 评估为好像它是文件的内容。我们也可以将 code 保存到文件中,然后对该文件调用 include——结果将是一样的。正如你所看到的,这与在 REPL 中评估相同的代码的行为方式有很大不同。让我们分解一下这里发生了什么。

  • 全局 s 在评估循环之前被定义为 0
  • 赋值 s = t 发生在软作用域中——在任何函数体或其他硬作用域结构之外的 for 循环中。
  • 因此,软作用域规则的第二条条款适用,并且赋值是模棱两可的,因此会发出警告。
  • 执行继续,使 s 成为 for 循环体的局部变量。
  • 由于 sfor 循环的局部变量,因此在评估 t = s + i 时它未定义,从而导致错误。
  • 评估到此停止,但如果它到达 s@isdefined(t),它将返回 0false

这演示了作用域的一些重要方面:在一个作用域中,每个变量只能有一个含义,并且该含义是在不考虑表达式的顺序的情况下确定的。表达式 s = t 在循环中的存在会导致 s 成为循环的局部变量,这意味着它在 t = s + i 的右侧出现时也是局部的,即使这个表达式出现在前面并被首先评估。有人可能会想象,循环的第一行的 s 可以是全局的,而循环的第二行的 s 可以是局部的,但这不可能,因为这两行在同一个作用域块中,并且每个变量在一个给定作用域中只能表示一个含义。

关于软作用域

我们现在已经涵盖了所有的局部作用域规则,但在结束本节之前,也许应该说几句话来解释为什么模棱两可的软作用域情况在交互式和非交互式上下文中以不同的方式处理。我们可以问两个显而易见的问题。

  1. 为什么它不能在所有地方都像 REPL 一样工作呢?
  2. 为什么它不能在所有地方都像在文件中一样工作呢?也许可以跳过警告?

在 Julia ≤ 0.6 中,所有全局作用域都像当前的 REPL 一样工作:当 x = <value> 出现在一个循环中(或 try/catch,或 struct 体)但在函数体之外(或 let 块或推导)时,它根据是否存在一个名为 x 的全局变量来决定 x 是否应该是循环的局部变量。这种行为的优点是直观且方便,因为它尽可能地近似于函数体内部的行为。特别是,当试图调试函数的行为时,这使得将代码在函数体和 REPL 之间来回移动变得容易。然而,它也有一些缺点。首先,它是一种相当复杂的行为:多年来,许多人对这种行为感到困惑,并抱怨它复杂且难以解释和理解。公正地说。其次,更糟糕的是,它不利于“大规模”编程。当你在一个地方看到一小段这样的代码时,很清楚发生了什么。

s = 0
for i = 1:10
    s += i
end

显然,意图是修改现有的全局变量 s。它还能有什么含义呢?但是,并不是所有真实世界的代码都那么短或那么清晰。我们发现,像下面这样的代码经常出现在现实中。

x = 123

# much later
# maybe in a different file

for i = 1:10
    x = "hello"
    println(x)
end

# much later
# maybe in yet another file
# or maybe back in the first one where `x = 123`

y = x + 234

这里应该发生什么就不那么清楚了。由于 x + "hello" 是一个方法错误,因此似乎很可能是 x 应该是 for 循环的局部变量。但是,运行时值和哪些方法恰好存在不能用来确定变量的作用域。对于 Julia ≤ 0.6 的行为,尤其令人担忧的是,有人可能首先编写了 for 循环,并且它运行良好,但后来当其他人添加一个新的全局变量时——可能是在另一个文件中——这段代码的含义突然改变,要么是发出噪音地崩溃,要么更糟糕的是,静默地做错了事。这种 "超距作用" 是好的编程语言设计应该防止的事情。

所以在 Julia 1.0 中,我们简化了作用域的规则:在任何局部作用域中,对一个之前不是局部变量的名称进行赋值会创建一个新的局部变量。这完全消除了软作用域的概念,并消除了超距作用的可能性。由于消除了软作用域,我们发现并修复了大量错误,证明了这个选择的正确性。而且大家都欢欣鼓舞!好吧,其实不是这样。因为有些人很生气,因为他们现在不得不写以下代码。

s = 0
for i = 1:10
    global s += i
end

你看到了那里的 global 注释了吗?太难看了。显然这种情况无法忍受。但说真的,在对这种顶层代码强制使用 global 的情况下,主要有两个问题。

  1. 将代码从函数体内部复制粘贴到 REPL 中进行调试不再方便——你必须添加 global 注释,然后在返回时将其删除;

  2. 初学者会编写这种不带 global 的代码,并且不知道为什么他们的代码无法工作——他们得到的错误是 s 未定义,这似乎不会启发任何犯了这种错误的人。

从 Julia 1.5 开始,这段代码在像 REPL 或 Jupyter 笔记本这样的交互式环境中可以工作(就像 Julia 0.6 一样),在文件和其他非交互式环境中,它会打印这个非常直接的警告。

在软作用域中对s的赋值存在歧义,因为存在同名全局变量:s将被视为新的局部变量。使用local s消除此警告或使用global s赋值给现有的全局变量,以消除歧义。

这同时解决了这两个问题,并保留了 1.0 行为的“大规模编程”优势:全局变量不会对可能很远的代码的含义产生任何奇怪的影响;在 REPL 中,复制粘贴调试工作,初学者不会遇到任何问题;任何时候,只要有人忘记global注释,或在软作用域中不小心用局部变量覆盖了现有的全局变量,这本身就会让人困惑,他们都会收到一个清晰明确的警告。

此设计的关键特性是,在没有警告的情况下执行文件中的任何代码,其行为方式与新的 REPL 中相同。另一方面,如果将 REPL 会话保存到文件中,如果其行为与 REPL 中的行为不同,则会收到警告。

Let 块

let 语句创建一个新的硬作用域块(见上文),并在每次运行时引入新的变量绑定。该变量不需要立即赋值。

julia> var1 = let x
           for i in 1:5
               (i == 4) && (x = i; break)
           end
           x
       end
4

赋值可能会将新值重新赋值给现有的值位置,而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,因此两个闭包的行为相同。我们可以使用leti 创建一个新的绑定。

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 是不同的变量。这个特定示例等同于

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

循环和推导

在循环和推导中,在它们主体作用域内引入的新变量会为每次循环迭代重新分配,就好像循环主体被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
               # empty
           end
           return i
       end;

julia> f()
0

但是,有时将现有的局部变量重新用作迭代变量很有用。这可以通过添加关键字outer 来方便地实现。

julia> function f()
           i = 0
           for outer i = 1:3
               # empty
           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 声明可以解决此性能问题。

局部常量完全不同。编译器能够自动确定局部变量何时为常量,因此不需要局部常量声明,事实上目前也不支持局部常量声明。

特殊的高级赋值,例如由functionstruct 关键字执行的赋值,默认情况下是常量。

请注意,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: redefinition of constant y. This may fail, cause incorrect answers, or produce other errors.
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 Vector{Int64}:
 1

julia> a = [1]
WARNING: redefinition of constant a. This may fail, cause incorrect answers, or produce other errors.
1-element Vector{Int64}:
 1

请注意,虽然有时可以修改const 变量的值,但强烈建议不要这样做,它只适用于交互式使用的方便性。修改常量可能会导致各种问题或意外行为。例如,如果一个方法引用了一个常量,并且在修改常量之前已经编译,那么它可能会继续使用旧的值。

julia> const x = 1
1

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

julia> f()
1

julia> x = 2
WARNING: redefinition of constant x. This may fail, cause incorrect answers, or produce other errors.
2

julia> f()
1

类型化的全局变量

Julia 1.8

Julia 1.8 中添加了对类型化全局变量的支持。

类似于声明为常量,全局绑定也可以声明为始终具有常量类型。这可以使用语法global x::T 完成,而无需赋值实际值,或者在赋值时使用x::T = 123 完成。

julia> x::Float64 = 2.718
2.718

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

julia> Base.return_types(f)
1-element Vector{Any}:
 Float64

对于对全局变量的任何赋值,Julia 首先会尝试使用convert 将其转换为适当的类型。

julia> global y::Int

julia> y = 1.0
1.0

julia> y
1

julia> y = 3.14
ERROR: InexactError: Int64(3.14)
Stacktrace:
[...]

该类型不需要是具体的,但使用抽象类型的注释通常没有多少性能优势。

一旦全局变量被赋值或其类型被设置,绑定类型就不能再更改。

julia> x = 1
1

julia> global x::Int
ERROR: cannot set type for global x. It already has a value or is already set to a different type.
Stacktrace:
[...]