包管理

随着项目的规模不断地扩大,仅在一个超大文件中管理源代码会变得十分困难。这时可以将源代码根据功能进行分组,并将不同功能的代码分开管理,每组独立管理的代码会生成一个输出文件。在使用时,通过导入对应的输出文件使用相应的功能,或者通过不同功能的交互与组合实现更加复杂的特性,使得项目管理更加高效。

在仓颉编程语言中,包是编译的最小单元,每个包可以单独输出 AST 文件、静态库文件、动态库文件等产物。每个包有自己的名字空间,在同一个包内不允许有同名的顶层定义或声明(函数重载除外)。一个包中可以包含多个源文件。

模块是若干包的集合,是第三方开发者发布的最小单元。一个模块的程序入口只能在其根目录下,它的顶层最多只能有一个作为程序入口的 main ,该 main 没有参数或参数类型为 Array<String>,返回类型为整数类型或 Unit 类型。

仓颉语言提供 CJPM 作为包管理工具,详见工具用户手册 Cangjie Tools User Guide 的对应章节。仓颉提供了包编译的方式对一个包进行编译,形如:

cjc --package path_to_package_directory --module-name moduleName --output path_to_output_directory

其中,命令各部分解析如下:

命令内容命令含义
--package表示编译方式,--package 为包编译(默认为文件编译)
path_to_package_directory包所在目录
--module-name用来指定输出模块名
moduleName输出模块名
--output用来指定输出路径
path_to_output_directory输出文件路径

对于当前包所依赖的包,编译器会先后在 --import-path 指定的路径、当前路径、CANGJIE_PATH 指定的路径和CANGJIE_HOME 指定的路径下查询依赖的相关文件。其中 CANGJIE_PATHCANGJIE_HOME 是用户设置的环境变量,可以效仿[入门指南]进行设置,它们分别是第三方库所在目录和仓颉编译器及标准库所在目录。

可以通过 --scan-dependency 指令获得指定包源码或者一个包的 cjo 文件对于其他包的直接依赖以及其他信息,标准库相关依赖不会输出,以 json 格式输出到命令行。

// this file is placed under directory pkgA
macro package pkgA
import pkgB.*
from std import io.*
from module2 import pkgB.*
cjc --scan-dependency --package pkgA --module-name module1
{"packageName":"module1/pkgA","isMacroPackage":true,"dependencies":[{"moduleName":"module1","packageName":"pkgB"},{"moduleName":"module2","packageName":"pkgB"}]}
cjc --scan-dependency pkgA.cjo
{"packageName":"module1/pkgA","isMacroPackage":true,"dependencies":[{"moduleName":"module1","packageName":"pkgB"},{"moduleName":"module2","packageName":"pkgB"}]}

仓颉命令行指令的详细解析,可以通过 --help 指令获得:

cjc --help

声明包的名字

在仓颉编程语言中,可以通过形如 package name 的方式声明名为 name 的包,其中 package 为关键字,name 须为仓颉的合法标识符。包声明必须在源文件的非空非注释的首行,且同一个包中的不同源文件的包声明必须保持一致。如

// file 1
// Comments are accepted
package test
// declarations...

// file 2
let a = 1 // Error, package declaration must appear first in a file
package test
// declarations...

仓颉的包名需反映当前源文件相对于项目源码根目录 src 的路径,并将其中的路径分隔符替换为小数点。例如包的源代码位于 src/directory_0/directory_1 下,则其源代码中的包声明应为 package directory_0.directory_1

注意:

  • 包所在的文件夹名也必须是合法标识符,不能包含.等其他符号。
  • 源码根目录默认名为 src
  • 源码根目录下的包可以没有包声明,此时编译器将默认为其指定包名 default

假设源代码目录结构如下:

// The directory structure is as follows:
src
`-- directory_0
    |-- directory_1
    |    |-- a.cj
    |    `-- b.cj
    `-- c.cj
`-- main.cj

a.cjb.cjc.cjmain.cj 中的包声明可以为

// a.cj
// in file a.cj, the declared package name must correspond to relative path directory_0/directory_1.

package directory_0.directory_1
// b.cj
// in file b.cj, the declared package name must correspond to relative path directory_0/directory_1.

package directory_0.directory_1
// c.cj
// in file c.cj, the declared package name must correspond to relative path directory_0.

package directory_0
// main.cj
// file main.cj is in the module root directory and may omit package declaration.

main() {
    return 0
}

另外,包声明不能引起命名冲突:

  • 包名与当前包的顶层声明不能重名。

  • 当前包的顶层声明不能与子目录的包名同名。

以下是一些错误示例:

// a.cj

package c    // Error, package name conflicts with top-level class name.

class c {}
// b.cj
package a
public class B {
    /* Error, top-level declaration "B" conflicts with the sub-directory of the same name */
    public func f() {}
}

// c.cj
/* Error, package "a.B" conflicts with the top-level declaration "B" in package "a".*/
package a.B
public func f {}

// main.cj
import a.*
import a.B

main() {
    /* Error, cannot distinguish a.B is the package a.B or the class B in package b. */
    a.B.f()
    return 0
}

顶层声明的可见性

顶层声明可见性为包内可见

如果希望包中某个顶层声明仅在包内被使用,顶层声明前不需要加任何修饰。所有顶层声明的可见性默认是包内可见的(internal)。

// a.cj
package a
// if top-level declaration C has no modifier,
// it's not accessible to other packages except package a.
class C {}

顶层声明可见性为包外可见

如果希望包中的某个顶层声明被导出,从而能被其他包使用,应当在顶层声明前加 public 修饰。被 public 修饰的顶层声明的可见性被认为是包外可见的(external)。

// a.cj
package a
public interface I {}

public 修饰的顶层声明不能使用包外不可见的类型

  • 函数声明中的参数与返回值

    // a.cj
    package a
    class C {}
    public func f1(a1: C) // Error, external declaration f1 cannot use internal type C.
    {
        return 0
    }
    public func f2(a1: Int8): C // Error, external declaration f2 cannot use internal type C.
    {
        return C()
    }
    public func f3 (a1: Int8) // Error, external declaration f3 cannot use internal type C.
    {
        return C()
    }
    
  • 变量声明

    // a.cj
    package a
    class C {}
    public let v1: C = C() // Error, external declaration v1 cannot use internal type C.
    public let v2 = C() // Error, external declaration v2 cannot use internal type C.
    
  • 类声明中继承的类

    // a.cj
    package a
    open class C1 {}
    public class C2 <: C1 {} // Error, external declaration C2 cannot use internal type C1.
    
  • 类型实现的接口

    // a.cj
    package a
    interface I {}
    public enum E <: I {} // Error, external declaration uses internal types.
    
  • 泛型类型的类型实参

    // a.cj
    package a
    public class C1<T> {}
    class C2 {}
    public let v1 = C1<C2>() // Error, external declaration v1 cannot use internal type C2.
    
  • where 约束中的类型上界

    // a.cj
    package a
    interface I {}
    public class B<T> where T <: I {}  // Error, external declaration B cannot use internal type I.
    

值得注意的是:

  • public 修饰的声明在其初始化表达式或者函数体里面可以使用本包可见的任意类型,包括 public 修饰的类型和没有 public 修饰的类型。

    // a.cj
    package a
    class C1 {}
    func f1(a1: C1)
    {
      return 0
    }
    public func f2(a1: Int8) // ok.
    {
      var v1 = C1()
      return 0
    }
    public let v1 = f1(C1()) // ok.
    public class C2 // ok.
    {
      var v2 = C1()
    }
    
  • public 修饰的顶层声明能使用匿名函数,或者任意顶层函数,包括 public 修饰的类型和没有 public 修饰的顶层函数。

    public var t1: () -> Unit = { => } // Ok.
    func f1(): Unit {}
    public let t2 = f1 // Ok.
    
    public func f2() // Ok.
    {
      return f1
    }
    
  • 内置类型诸如 CharInt64 等也都默认是 public 的。

    var num = 5
    public var t3 = num // Ok.
    

包的导入

使用 import 语句导入其它包中的声明或定义

在仓颉编程语言中,可以通过 from moduleName import packageName.itemName 的语法导入其他包中的一个顶层声明或定义,其中 moduleName 为模块名,packageName 为包名,itemName 为声明的名字。导入当前模块中的内容时,可以省略 from moduleName;跨模块导入时,必须使用 from moduleName 指定模块。导入语句在源文件中的位置必须在包声明之后,其他声明或定义之前。例如:

package a
from std import math.*
from module_name import package1.foo
from module_name import package1.foo, package2.bar

如果要导入的多个 itemName 同属于一个 packageName,可以使用 from moduleName import packageName.{itemName[, itemName]*} 语法,例如:

from module_name import package1.{foo, bar, fuzz}

这等价于:

from module_name import package1.foo, package1.bar, package1.fuzz

除了通过 import packagename.itemName 语法导入一个特定的顶层声明或定义外,还可以使用 import packageName.* 语法将 packageName 包中所有 public 修饰的顶层声明或定义全部导入。例如:

from module_name import package1.*
from module_name import package1.*, package2.*
from module_name import package1 // Error.

需要注意:

  • 当前源文件中导入的内容在源文件所属包的所有其他源文件中也可见。
  • 当已导出的包的模块名或者包名被篡改,使其与导出时指定的模块名或包名不一致,在导入时,即使通过修改相应的 from ... import 语句找到了对应的包,也会报错。
  • 只允许导入使用 public 修饰的顶层声明或定义,导入无 public 修饰的声明或定义将会在导入处报错。
  • 可以直接使用导入的名字访问导入的声明或定义,也可以通过形如moduleName.packageName.itemName的带路径限定的名字访问导入的声明或定义。
  • 支持使用形如 curPackageName.itemName 的带路径限定的名字访问当前包的内容,但禁止通过 import 导入当前源文件所在包的声明或定义。
  • 禁止包间的循环依赖导入,如果包之间存在循环依赖,编译器会报错。

示例如下:

// pkga/a.cj
package pkga    // Error, packages pkga pkgb are in circular dependencies.
import pkgb.*

class C {}
public struct R {}

// pkgb/b.cj
package pkgb

import pkga.*

// pkgc/c1.cj
package pkgc

import pkga.C // Error, 'C' is not accessible in package 'pkga'.
import pkga.R // OK, R is an external top-level declaration of package pkga.
import pkgc.f1 // Error, package 'pkgc' should not import itself.

public func f1() {}

// pkgc/c2.cj
package pkgc

func f2() {
    /* OK, the imported declaration is visible to all source files of the same package
     * and accessing import declaration by its name is supported.
     */
    R()

    // OK, accessing imported declaration by fully qualified name is supported.
    pkga.R()

    // OK, the declaration of current package can be accessed directly.
    f1()

    // OK, accessing declaration of current package by fully qualified name is supported.
    pkgc.f1()
}

在仓颉编程语言中,导入的声明或定义如果和当前包中的顶层声明或定义重名且不构成函数重载,则导入的声明和定义会被遮盖;导入的声明或定义如果和当前包中的顶层声明或定义重名且构成函数重载,函数调用时将会根据函数重载的规则进行函数决议。

// pkga/a.cj
package pkga

public struct R {}            // R1
public func f(a: Int32) {}    // f1
public func f(a: Bool) {} // f2

// pkgb/b.cj
package pkgb
import pkga.*

func f(a: Int32) {}         // f3
struct R {}                 // R2

func bar() {
    R()     // OK, R2 shadows R1.
    f(1)    // OK, invoke f3 in current package.
    f(true) // OK, invoke f2 in the imported package
}

隐式导入 core 包

诸如 StringRange 等类型能直接使用,并不是因为这些类型是内置类型,而是因为编译器会自动为源码隐式的导入 core 包中所有的 public 修饰的声明。

使用 import as 对导入的名字重命名

不同包的名字空间是分隔的,因此在不同的包之间可能存在同名的顶层声明。在导入不同包的同名顶层声明时,我们支持使用 import packageName.name as newName 的方式进行重命名来避免冲突。没有名字冲突的情况下仍然可以通过 import as 来重命名导入的内容。import as具有如下规则:

  • 使用import as 对导入的声明进行重命名后,当前包只能使用重命名后的新名字,原名无法使用。

  • 同一个声明只能重命名一次。

  • 使用 import as 重命名的新名字的作用域级别和当前包顶层作用域级别一致。(注意,不使用 as 时引入的名字的作用域级别比当前包顶层作用域级别低。)

  • 如果重命名后的名字与当前包顶层作用域的其它名字存在冲突,且这些名字对应的声明均为函数类型,则参与函数重载,否则报重定义的错误。

  • 支持 import pkg.* as newPkgName.* 的形式对包名进行重命名,以解决不同模块中同名包的命名冲突问题。

    // a.cj in module1/p1
    package p1
    public func f1() {}
    
    // d.cj in module2/p2
    package p2
    public func f3() {}
    
    // b.cj in module2/p1
    package p1
    public func f2() {}
    
    // c.cj in module3/pkgc
    package pkgc
    public func f1() {}
    
    // main.cj in module3
    from module1 import p1.* as A.*
    from module2 import p1.* as B.*
    from module1 import p2.f3 as f  // OK
    import pkgc.f1 as a
    import pkgc.f1 as b // Error, 'pkgc.f1' cannot be redefined here.
    
    func f(a: Int32) {}
    
    main() {
        A.f1()  // OK, package name conflict is resolved by renaming package name.
        B.f2()  // OK, package name conflict is resolved by renaming package name.
        p1.f1() // Error, the original package name cannot be used.
        a()     // OK.
        b()     // OK.
        pkgc.f1()    // Error, the original name cannot be used.
    }
    
  • 如果没有对导入的存在冲突的名字进行重命名,在 import 语句处不报错;在使用处,只能使用带路径限定的名字来访问,仅使用冲突的名字来访问将会报错。

    // a.cj in module1
    package p1
    public class C {}
    
    // b.cj in module2
    package p1
    public class C {}
    
    // c.cj in module2
    package pkgc
    public class C {}
    
    // main.cj in module2
    package pkgd
    from module1 import p1.C
    from module2 import p1.C
    import pkgc.C
    
    main() {
        C()             // Error.
        pkgc.C()        // OK.
        p1.C()          // Error.
        module1.p1.C()  // OK.
        module2.p1.C()  // OK.
    }
    

使用 public import 重导出一个导入的名字

在功能繁多的大型项目的开发过程中,这样的场景是非常常见的:包 p2 大量地使用从包 p1 中导入的声明,当包 p3 导入包 p2 并使用其中的功能时,p1 中的声明同样需要对包 p3 可见。如果要求包 p3 自行导入 p2 中使用到的 p1 中的声明,这个过程将过于繁琐。因此希望能够在 p2 被导入时一并导入 p2 使用到的 p1 中的声明。

在仓颉编程语言中,可以在导入包的时候使用 public import 指定被重导出的声明。其它包可以直接导入并使用本包中用 public import 重导出的内容,无需从原包中导入这些内容。

需要注意的是,当导入的名字出现在当前包被 public 修饰的函数的参数类型或返回类型中时,它们必须被重导出。在如下例子的 pkg2 中,f 是一个 public 修饰的函数,它的返回值类型是导入的类型 C1,另一个包 pkg3 导入了 pkg2 的函数 f。若 pkg3 未手动导入 pkg1 中的类 C1,或者在 pkg2 中没有使用 public import 指明类 C1 被重导出,则 pkg3 在使用函数 f 时将会因为找不到类 C1 而报错。强行限制 pkg2 重导出类 C1,可以使 pkg3 在使用 pkg2 中函数 f 时无需手动导入 pkg1 中的类 C1

使用 public import 导入的名字相当于是在本包内定义了一个新的名字,因此,public import 导入的名字和 import as 重命名的名字具有相同的规则。

// a.cj
package pkg1
public class C1 {}
public class C2 {}

// b.cj
package pkg2
public import pkg1.C1       // class C1 of pkg1 must be re-exported.
public func f() {
    let c = C1()
    return c
}
public class C2 {}

// main.cj
package pkg3
import pkg2.*
public import pkg1.C2       // Error, C2 is redefined.
public import pkg2.C2       // Error, C2 is redefined.

public class C2{}           // Error, C2 is redefined.

main(): Int64 {
    let obj1 = f()          // OK.
    let obj2: C1 = f()      // OK, class C1 is visible to current package.
    return 0
}

程序入口

仓颉程序入口为 main,源文件根目录下的包的顶层最多只能有一个 main

如果模块采用生成可执行文件的编译方式,编译器只在源文件根目录下的顶层查找main。如果没有找到,编译器将会报错;如果找到main,编译器会进一步对其参数和返回值类型进行检查。需要注意的是,main不可被访问修饰符修饰,当一个包被导入时,包中定义的main不会被导入。

作为程序入口的 main 可以没有参数或参数类型为 Array<String>,返回值类型为 Unit 或整数类型。

没有参数的 main

// main.cj
main(): Int64 { // OK.
    return 0
}

参数类型为 Array<String>main

// main.cj
main(args: Array<String>): Unit { // OK.
    for (arg in args) {
        println(arg)
    }
}

使用 cjc main.cj 编译完成后,通过命令行执行:./main Hello, World,将会得到如下输出:

Hello,
World

以下是一些错误示例:

// main.cj
main(): String { // Error, return type of 'main' is not 'Integer' or 'Unit'.
    return ""
}
// main.cj
main(args: Array<Int8>): Int64 { // Error, 'main' cannot be defined with parameter whose type is not Array<String>.
    return 0
}
// main.cj
// Error, multiple 'main's are found in source files.
main(args: Array<String>): Int32 {
    return 0
}

main(): Int8 {
    return 0
}