Skip to content

Latest commit

 

History

History
301 lines (225 loc) · 16.1 KB

模块.md

File metadata and controls

301 lines (225 loc) · 16.1 KB

在Julia中模块是分离的变量工作区,例如引入一个新的全局作用域。 是带分隔符的语法(delimited syntactically),在module Nameend内。 模块允许创建顶层定义(即全局变量)而不用担心自己的代码和别人的代码一起用的时候的命名冲突。 在一个模块内部,可以控制来自别的模块的哪些命名可见(通过导入),可以指定哪些本模块的命名可暴露(通过导出)。

下述例程演示包的主要特征。 并非意味着可运行,但为解说目的而展示:

module MyModule
using Lib

using BigLib: thingx, thingy

import Base.show

export MyType, fuck

struct MyType
    x
end

bar(n) = 2n

fuck(mt::MyType) = bar(mt.x) + 9527

show(io::IO, mt::MyType) = print(io, "1314")
end

注意上述代码风格没有缩进模块体,因为会通常导致整个文件都缩进。

该模块定义了一个MyType类型,两个函数barfuck。 函数fuck和类型MyType被导出,因此可用于导入到别的模块中。 函数barMyModule模块的私有函数。

声明using Lib意思是该模块调用Lib将被用于按需解决命名。 当碰到未在当前模块中定义的全局变量,系统将会在Lib导出的变量中搜索,并将搜索到的命名导入其中。 这意味着,所有当前模块中用到的那个全局变量都会解析到Lib中的定义。

声明using BigLib: thingx, thingy仅从BigLib引入标识符thingxthingy到当前作用域。 如果这些命名引用的是函数,则给这些函数添加方法是不允许的(只能用而不能扩展)。

关键字import支持和using相同的语法,但一次只操作在一个命名。 并且import并未像using一样将模块添加到搜索路径。 导入函数上import还和using有区别,前者导入的函数可在当前作用域中扩展新方法。

在上述MyModule中,想要给标准show函数添加方法,因此需要些import Base.show。 仅通过using可见的函数命名不可被扩展。

一旦一个变量被usingimport操作可见,当前模块便不能创建同名对象。 导入的变量是只读的;给全局变量赋值总是影响当前模块自己的变量,或者抛出错误。

模块用法汇总

加载一个模块,要用到两个关键字:usingimport。 要理解这两个关键字的区别,考虑下述例程:

module MyModule

export x, y

x() = "x"
y() = "y"
z() = "z"

end

在该模块,咱导出xy函数(用export关键字),还有个未导出的z函数。 加载模块及其内部函数到当前工作区有若干方式:

导入命令 给当前作用域带来了什么 可用的方法扩展
using MyModule 所有导出的命名(xy),MyModule.xMyModule.yMyModule.z MyModule.xMyModule.yMyModule.z
using MyModule: x, z xz
import MyModule MyModule.xMyModule.yMyModule.z MyModule.xMyModule.yMyModule.z
import MyModule.x, MyModule.z xz xz
import MyModule: x, z xz xz

模块和文件

文件和文件名几乎和模块无关;模块仅和模块表达式相关。 每个模块可以有多个文件,也可以一个文件多个模块。

module Fuck

include("alice.jl")
include("bob.jl")

end

在不同模块中包含相同代码提供类似混合(mixin)的行为。 可以运用该特性以不同基础定义执行相同代码,举个例子,以某些操作的安全版本执行测试代码:

module Normal
include("normal.jl")
end

module Testing
include("safes.jl")
include("normal.jl")
end

标准模块

有三个重要的标准模块:MainCoreBase

Main是顶层模块,且Julia以Main为当前模块开始。在提示符(prompt)进入Main时定义的变量,可通过varinfo()列出。

Core包含所有认为是语言内建的标识符,也就是,是编程语言核心的一部分,而不是库的。每个模块隐式指定using Core,因为没有这些定义,啥也干不了。

Base是包含基本功能(即base/下的内容)的模块。所有模块隐式包含using Base,因为绝大多数情况下需要Base中的定义。

默认顶层定义和裸模块

除了using Base,模块也自动包含evalinclude函数定义,这些函数用来计算该模块全局作用域内的表达式和文件。

如果这些默认的定义不是想要的,可以用baremodule关键字定义(注意Core仍然如上述被导入)。 按照baremodule,标准module看起来如是:

baremodule BM

using Base

eval(x) = Core.eval(BM, x)
include(p) = Base.include(BM, p)

# 继续表演

end

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

给定using Fuck声明,系统查阅内部顶层模块表寻找名为Fuck的模块。 如果该模块不存在,系统尝试require(:Fuck),它典型地导致从已安装地包中加载代码。

然而,某些模块包含子模块,这意味着有些时候需要访问一个非顶层模块。 有两种方式。 第一种用绝对路径,如using Base.Sort。 第二种用相对路径,让导入当前模块地子模块或任何它的闭包模块很容易:

module Parent

module Utils

# 继续表演

end

using .Utils

# 更多节目

end

这里的Parent模块包含Utils子模块,而且在Parent中的代码想要看到Utils的内容。 这可以using以句号开始的路径来实现。 添加更多前导句号上移更多模块层级。 如using ..Utils将在Parent闭包模块中查找Utils,而不是在Parent自身内。

注意,相对导入修饰语仅在usingimport声明中有效。

模块文件路径

全局变量LOAD_PATH包含Julia调用require时查找模块的目录集合。 可以通过push!扩展。

push!(LOAD_PATH, "/Path/To/My/Module/")

将该声明放入~/.julia/config/startup.jl将会在每次启动Julia时扩展LOAD_PATH,模块加载路径可通过定义环境变量JULIA_LOAD_PATH来扩展。

命名空间杂七八糟

如果命名是合格的(如Base.sin),则可被访问,即使未被导出。 调试时,这通常有用。 也可以将合格的用作函数名并添加方法。 然而,因为引起语法歧义,如果希望给不同模块中仅包含符号的名称函数添加方法,举个例子,如操作符“Base.+”,必须用Base.:+引用之。 如果操作符不止一个字符,必须用圆括号包裹起来,如Base.:(==)

在导入导出声明中,宏名用@编写,例如import M.@m。 别的模块中的宏,可用M.@m@M.m调用。

语法M.x = y在别的模块中给一个全局变量赋值无效;全局赋值总是模块本地的。

变量名无须通过声明为global x被保留。 这避免加载之后全局初始化的命名冲突。

模块初始化和预编译

大的模块需要若干秒来加载,因为执行模块中全部声明经常包含编译大量的代码。 Julia创建模块的预编译缓存来减少这个耗时。

一个增量预编译模块文件被创建且当用importusing加载模块是被自动使用。 这将导致第一次导入模块时该模块被编译。 可选地,可以手动调用Base.compilecache(modulename)。 作为结果的缓存文件将保存在DEPOT_PATH[1]/compiled/。 随后,模块基于usingimport自动预编译,无论任何依赖变化时;依赖是模块所导入的,Julia构建的,所包含的文件,或在模块文件中include_dependency(path)声明的隐式依赖。

对于文件依赖的更新,通过检测每个用include加载或用include_dependency添加的修改时间(mtime)为变化否,或等于截断到最近修改时间(秒)(适应不能以压秒精度拷贝mtime的系统)决定。 文件路径匹配require搜索逻辑选择的路径创建预编译文件与否,也纳入考虑范围。

已经加载到当前进程的以来集合可算入,但不会重新编译这些模块,即使相关文件被修改或消失,为了避免创建运行中系统和预编译缓存的不兼容。 如果想要变更的源代码反映到运行中的系统,应当在变化的模块上调用reload("Module"),任何依赖该模块的模块,想要让变更生效,也需要这样做。

如果已知某个模块预编译客户模块是不安全的(如后述原因之一),需要在模块文件中放置__precompile__(false)(通常放在顶部)。 这导致Base.compilecache抛出错误,也会导致using/import直接加载到当前进程,跳过预编译和缓存。 这也因此放置模块被任何别的预编译模块导入。

可能需要注意继承的增量共享库创建的特定行为,编写客户模块时须要小心。 举个例子,外部状态未被维护。 适应这个情况,显式分离任何在运行时必然繁盛的初始化步骤和编译时发生的步骤。 为了这个目的,Julia允许用户在自己的模块定义__init__()函数,执行任何必须在运行时发生的初始化步骤。 该函数在编译过程中不会被调用(--output-*)。 高效地,假定在代码生命周期恰好仅返回一次。 当然,可以手动调用,如果需要的话,但是默认假定该函数处理计算本地机器的状态,不需要或不应该在编译镜像中捕捉。 在模块加载到进程中之后会被调用,包括加载到增量编译(--output-incremental=yes)的情况,不包括加载到全量编译过程的情况。

特别是,如果在模块中定义function __init__(),则Julia将会在运行时模块第一次被加载(import/using/require)后直接调用__init__()也就是说__init__()仅被调用一次且在模块中全部声明被执行后)。 因为它在模块被全部导入后调用,任何子模块或被导入的模块都有其__init__函数,在闭包模块的__init__函数被调用之前被调用。

__init__两种典型用发时调用外部C库运行时初始化函数和初始化包含外部库返回指针的全局常量。 例如,假定调用C库libfuck,须要在运行时调用fuck_init()初始化函数。 假定咱也想要定义一个全局常量fuck_data_ptr把持libfuck定义的void *fuck_data()函数返回值,该常量必须在运行时初始化,因为指针地址在每次运行时会变化。 通过在自己模块中定义__init__函数完成这个目标:

const fuck_data_ptr = Ref{Ptr{Cvoid}}(0)
function __init__()
    ccall((:fuck_init, :libfuck), Cvoid, ())
    fuck_data_prt[] = ccall((:fuck_data, :libfuck), Ptr{Cvoid}, ())
    nothing
end

注意,可以在如__init__函数内定义全局变量棒极了;这是运用动态编程语言的一种高级货色。 但是通过使其成为全局作用域的常量,可以保证编译器知道该类型,并且允许产生更优化的代码。 显然(想起了中学数理化证明题),任何客户模块中的别的基于fuck_data_ptr的全局变量也必须在__init__中初始化。

常量,包括大多数Julia对象,不是由ccall产出的就没必要放在__init__中:这些乡党的定义可以被预编译并从缓存模块景象中导入。 这包括复杂的分配在堆上的对象,如数组。 然而,任何返回原始指针值的协程必须在运行时被调用,已让预编译工作(Ptr对象将变为null指针除非隐藏在比特(isbits)对象中)。 这包括Julia函数cfunctionpointer的返回值。

字典和集合类型,或一般的基于hash(key)方法输出的任何事物,是奸诈把戏(trickier case)。 在键是数字、字符串、符号、区间、Expr或这些类型合成物(通过数组、元组、集合、对等)的普通情况下,可被安全地预编译。 然而,对于少数键类型,如FunctionDataType和一般用户自定义却没有定义hash方法的类型,后手hash方法基于对象的内存地址(通过对象的objectid),并且因此每次运行都不同的兄弟姊妹。 如果有这种键类型,或者如果不确定有无,保险起见,可以在骚年的__init__函数中初始化字典。 作为备选,可用IdDict字典类型,它被预编译特殊处理,因此在编译时可安全初始化。

当使用预编译,保持对编译阶段和执行阶段却别清醒的认识很重要。 在该模式中,表面上经常更清楚Julia是一个编译器,允许执行任意的Julia代码,而不是孤立的也产生被编译的代码的解释器。

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

  • 全局计数器(例如尝试唯一识别对象),考虑如下代码片段:

    mutable struct UniquedById
        myid::Int
        let counter = 0
            UniquedById() = new(counter += 1)
        end
    end
    

    虽然代码的意图是给每个实例一个唯一的标识,计数值在编译结束时被记录。 该增量编译模块的后续使用将从相同的计数值开始。 注意objectid(哈希内存指针)有类似的问题(见后边Dict表奏)。 另一种可选的方案是用宏来捕捉@__MODULE__并和当前counter值一起保存,然而,最好还是重新设计代码,不要依赖全局状态。

  • 组合的收纳(如DictSet)须要在__init__中重新哈希。未来,将提供注册初始化函数的机制

  • 贯穿加载时,受编译时副作用的影响。这种例子包括:修改数组或别的Julia模块中的别的变量;维护打开的文件或设备句柄;保存别的系统资源的指针(包括内存)。

  • 创建来自别的模块的全局状态的副本,通过直接引用它而不是查询搜索路径。举个例子(在全局作用域):

    # mystdout = Base.stdout #= 不能直接工作,因为将会拷贝`Base.stdout`到该模块中。 =#
    # 换用访问函数:
    getstdout() = Base.stdout #= 最佳选项 =#
    # 或将赋值移入运行时:
    __init__() = global mystdout = Base.stdout #= 也有效的 =#
    

在预编译代码过程中,可做的操作有若干额外的限制,有助于用户避免别的错误行为的情况:

  • 调用eval给别的模块引入副作用。当设置了增量预编译标志,也将导致警告被忽略。
  • 来自__init__()之后的本地作用域global const声明已经开始(查看问题计划为此添加错误)。
  • 在做增量预编译过程中替换模块是运行时错误。

些许要知道的点:

  • 修改源代码文件自身后并没有代码重载或缓存失效,包括通过Pkg.update,在Pkg.rm之后不会清理。
  • 重塑数组形状的内存共享被预编译无视(每个视图获取自个儿的副本)。
  • 在编译时和运行时期望文件系统不被修改,例如在运行时用@__FILE__/source_path()查找资源,或BinDepschecked_lib宏。有些时候这不可避免。可是,当可能时,在编译时拷贝资源到模块中被认为是优秀实践,因而不需要在运行时再查找。
  • WeakRef对象和终结者(finalizer)目前尚未被序列化(串行器:serializer)正确处理(将在即将发布的版本中修复)。
  • 经常最好避免捕捉引用内部原属对象的实例,如MethodMethodInstanceMehtodTableTypeMapLevelTypeMapEntry和这种对象的字段,因为这会使系列化混乱且可能不会导致期望的结果。此处不必要错误,但可以简单的为需准备,系统尝试拷贝某些这种东西并创建其余的单个唯一的实例。

有些时候,关闭增量预编译在模块开发种是有助的。 命令行标志--compiled-modules={ye|no}使得码农切换模块预编译,打开或关闭。 当Julia以--compiled-modules=no启动,预编译缓存中的序列化模块将在加载模块和模块依赖时被忽略。 仍然可手动调用Base.compilecache。 该命令行标志传递给Pkg.build禁止当安装、更新和显式构建包时触发的自动预编译。


译后感

  • 太像Python啦。