模块

    下面的示例演示了模块的主要功能。它不是为了运行,只是为了方便说明:

    注意,模块中的代码样式不需要缩进,否则的话,会导致整个文件缩进。

    上面的模块定义了一个 MyType 类型,以及两个函数,其中,函数 foo 和类型 MyType 被导出了,因而可以被导入到其它模块,而函数 bar 是模块 MyModule 的私有函数。

    using Lib 意味着一个名称为 Lib 的模块会在需要的时候用于解释变量名。当一个全局变量在当前模块中没有定义时,系统就会从 Lib 中导出的变量中搜索该变量,如果找到了的话,就导入进来。也就是说,当前模块中,所有使用该全局变量的地方都会解释为 Lib 中对应的变量。

    代码 using BigLib: thing1, thing2 显式地将标识符 thing1thing2 从模块 BigLib 中引入到当前作用域。如果这两个变量是函数的话,则不允许给它们增加新的方法,毕竟代码里写的是 “using”(使用)它们,而不是扩展它们。

    关键字所支持的语法与 using 一致。 它并不会像 using 那样将模块添加到搜索空间中。 与 using 不同,import 引入的函数 可以 为其增加新的方法。

    前面的 MyModule 模块中,我们希望给 函数增加一个方法,需要写成 import Base.show。如果用 using 的话,就不能扩展 show 函数。通过 using 导入才可见的名字是不能被扩展的。

    一旦一个变量通过 usingimport 引入,当前模块就不能创建同名的变量了。而且导入的变量是只读的,给全局变量赋值只能影响到由当前模块拥有的变量,否则会报错。

    要导入一个模块,可以用 usingimport 关键字。为了更好地理解它们的区别,请参考下面的例子:

    1. module MyModule
    2. export x, y
    3. x() = "x"
    4. y() = "y"
    5. p() = "p"
    6. end

    这个模块用关键字 export 导出了 xy 函数,此外还有一个没有被导出的函数 p。想要将该模块及其内部的函数导入当前模块有以下方法:

    模块与文件和文件名无关;模块只与模块表达式有关。一个模块可以有多个文件,一个文件也可以有多个模块。

    1. include("file1.jl")
    2. include("file2.jl")
    3. end

    在不同的模块中引入同一段代码,可以提供一种类似 mixin 的行为。我们可以利用这个特性来观察,在不同的定义下,执行同一段代码会有什么结果。例如,在测试的时候,可以使用某些「安全」的运算符。

    标准模块

    有三个重要的标准模块:

    • Base 包含了绝大多数情况下都会用到的基本功能。
    • 是顶层模块,当 julia 启动时,也是当前模块。

    除了默认包含 using Base 之外,所有模块都还包含 eval 和 函数。这两个函数用于在对应模块的全局环境中,执行表达式或文件。

    1. baremodule Mod
    2. using Base
    3. eval(x) = Core.eval(Mod, x)
    4. include(p) = Base.include(Mod, p)
    5. ...
    6. end

    模块的绝对路径和相对路径

    给定语句 using Foo,系统在顶层模块的内部表中查找名为 Foo 的包。如果模块不存在,系统会尝试 require(:Foo),这通常会从已安装的包中加载代码。

    但是,某些模块包含子模块,这意味着你有时需要访问非顶层模块。有两种方法可以做到这一点。第一种是使用绝对路径,例如 using Base.Sort。第二种是使用相对路径,这样可以更容易地导入当前模块或其任何封闭模块的子模块:

    1. module Parent
    2. module Utils
    3. ...
    4. end
    5. using .Utils
    6. ...
    7. end

    这里的模块 Parent 包含一个子模块 Utils,而 Parent 中的代码希望 Utils 的内容可见,这是可以使用 using 加点 . 这种相对路径来实现。添加更多的点会移动到模块层次结构中的更上级别。例如,using ..Utils 会在 Parent 的上级模块中查找 Utils 而不是在 Parent 中查找。

    请注意,相对导入符号 . 仅在 usingimport 语句中有效。

    如果名称是限定的(例如 Base.sin),那么即使它没有被导出,我们也可以访问它。这通常在调试时很有用。若函数名也使用这种限定的方式,就可以为其添加方法。但是,对于函数名仅包含符号的情况,例如一个运算符,Base.+,由于会出现语法歧义,所以必须使用 Base.:+ 来引用它。如果运算符的字符不止一个,则必须用括号括起来,例如:Base.:(==)

    宏名称在导入和导出语句中用 @ 编写,例如:import Mod.@mac。其它模块中的宏可以用 Mod.@mac@Mod.mac 触发。

    不允许使用 M.x = y 这种写法给另一个模块中的全局变量赋值;必须在模块内部才能进行全局变量的赋值。

    global x 声明变量可以仅“保留”名称而不赋值。有些全局变量需要在代码加载后才初始化,这样做可以防止命名冲突。

    模块初始化和预编译

    因为执行模块中的所有语句通常需要编译大量代码,大型模块可能需要几秒钟才能加载。Julia 会创建模块的预编译缓存以减少这个时间。

    当用 importusing 加载一个模块时,模块增量预编译文件会自动创建并使用。这会让模块在第一次加载时自动编译。 另外,你也可以手工调用 Base.compilecache(modulename),产生的缓存文件会放在 DEPOT_PATH[1]/compiled/ 目录下。 之后,当该模块的任何一个依赖发生变更时,该模块会在 usingimport 时自动重新编译; 模块的依赖指的是:任何它导入的模块、Julia 自身、include 的文件或由 显式声明的依赖。

    对于文件依赖,判断是否有变动的方法是:在 includeinclude_dependency 的时候检查每个文件的变更时间(mtime)是否没变,或等于截断变更时间。截断变更时间是指将变更时间截断到最近的一秒,这是由于在某些操作系统中,用 mtime 无法获取亚秒级的精度。此外,也会考虑到 搜索到的文件路径与之前预编译文件中的是否匹配。对于已经加载到当前进程的依赖,即使它们的文件发成了变更,甚至是丢失,Julia 也不会重新编译这些模块,这是为了避免正在运行的系统与预编译缓存之间的不兼容性。

    如果你认为预编译自己的模块是安全的(基于下面所说的各种原因),那么你应该在模块文件中添加 __precompile__(false),一般会将其写在文件的最上面。这就可以触发 Base.compilecache 报错,并且在直接使用 using / import 加载的时候跳过预编译和缓存。这样做同时也可以防止其它开启预编译的模块加载此模块。

    在开发模块的时候,你可能需要了解一些与增量编译相关的固有行为。例如,外部状态不会被保留。为了解决这个问题,需要显式分离运行时与编译期的部分。Julia 允许你定义一个 __init__() 函数来执行任何需要在运行时发生的初始化。在编译期(--output-*),此函数将不会被调用。你可以假设在代码的生存周期中,此函数只会被运行一次。当然,如果有必要,你也可以手动调用它,但在默认的情况下,请假定此函数是为了处理与本机状态相关的信息,注意这些信息不需要,更不应该存入预编译镜像。此函数会在模块被导入到当前进程之后被调用,这包括在一个增量编译中导入该模块的时候(--output-incremental=yes),但在完整编译时该函数不会被调用。

    特别的,如果你在模块里定义了一个名为 __init__() 的函数,那么 Julia 在加载这个模块之后会在第一次运行时(runtime)立刻调用这个函数(例如,通过 importusing,或者 require 加载时),也就是说 __init__ 只会在模块中所有其它命令都执行完以后被调用一次。因为这个函数将在模块完全载入后被调用,任何子模块或者已经载入的模块都将在当前模块调用 __init__ 之前 调用自己的 __init__ 函数。

    注意,在像 __init__ 这样的函数里定义一个全局变量是完全可以的,这是动态语言的优点之一。但是把全局作用域的值定义成常量,可以让编译器能确定该值的类型,并且能让编译器生成更好的优化过的代码。显然,你的模块(Module)中,任何其他依赖于 foo_data_ptr 的全局量也必须在 __init__ 中被初始化。

    涉及大多数不是由 ccall 生成的 Julia 对象的常量,不需要放在 __init__ 中:它们的定义可以预编译并从缓存的模块映像中加载。这包括复杂的堆分配对象,如数组。 但是,任何返回原始指针值的例程都必须在运行时调用,以便进行预编译(除非将 对象隐藏在 isbits 对象中,否则它们将转换为空指针)。 这包括 Julia 函数 cfunction 和 的返回值。

    字典和集合类型,或者通常任何依赖于 hash(key) 方法的类型,都是比较棘手的情况。 通常当键是数字、字符串、符号、范围、Expr 或这些类型的组合(通过数组、元组、集合、映射对等)时,可以安全地预编译它们。但是,对于一些其它的键类型,例如 FunctionDataType、以及还没有定义散列方法的通用用户定义类型,回退(fallback)的散列(hash)方法依赖于对象的内存地址(通过 objectid),因此可能会在每次运行时发生变化。 如果您有这些关键类型中的一种,或者您不确定,为了安全起见,您可以在您的 __init__ 函数中初始化这个字典。或者,您可以使用 IdDict 字典类型,它是由预编译专门处理的,因此在编译时初始化是安全的。

    当使用预编译时,我们必须要清楚地区分代码的编译阶段和运行阶段。在此模式下,我们会更清楚发现 Julia 的编译器可以执行任何 Julia 代码,而不是一个用于生成编译后代码的独立的解释器。

    其它已知的潜在失败场景包括:

    1. DictSet 这种关联集合需要在 __init__ 中 re-hash。Julia 在未来很可能会提供一个机制来注册初始化函数。

    2. 依赖编译期的副作用会在加载时蔓延。例子包括:更改其它 Julia 模块里的数组或变量,操作文件或设备的句柄,保存指向其它系统资源(包括内存)的指针。

    3. 无意中从其它模块中“拷贝”了全局状态:通过直接引用的方式而不是通过查找的方式。例如,在全局作用域下:

      1. #mystdout = Base.stdout #= will not work correctly, since this will copy Base.stdout into this module =#
      2. # instead use accessor functions:
      3. getstdout() = Base.stdout #= best option =#
      4. # or move the assignment into the runtime:
      5. __init__() = global mystdout = Base.stdout #= also works =#

    此处为预编译中的操作附加了若干限制,以帮助用户避免其他误操作:

    1. 调用 来在另一个模块中引发副作用。当增量预编译被标记时,该操作同时会导致抛出一个警告。
    2. __init__() 已经开始执行后,在局部作用域中声明 global const(见 issue #12010,计划为此情况添加一个错误提示)
    3. 在增量预编译时替换模块是一个运行时错误。

    一些其他需要注意的点:

    1. 在源代码文件本身被修改之后,不会执行代码重载或缓存失效化处理(包括由 Pkg.update 执行的修改,此外在 Pkg.rm 执行后也没有清理操作)
    2. 变形数组的内存共享特性会被预编译忽略(每个数组样貌都会获得一个拷贝)
    3. 文件系统在编译期间和运行期间被假设为不变的,比如使用 @__FILE__/source_path() 在运行期间寻找资源、或使用 BinDeps 宏 @checked_lib。有时这是不可避免的。但是可能的话,在编译期将资源复制到模块里面是个好做法,这样在运行期间,就不需要去寻找它们了。
    4. WeakRef 对象和完成器目前在序列化器中无法被恰当地处理(在接下来的发行版中将修复)。
    5. 通常,最好避免去捕捉内部元数据对象的引用,如 MethodMethodInstanceTypeMapLevelTypeMapEntry 及这些对象的字段,因为这会迷惑序列化器,且可能会引发你不想要的结果。此操作不足以成为一个错误,但你需做好准备:系统会尝试拷贝一部分,然后创建其余部分的单个独立实例。