仓颉-包
包的概述
随着项目规模的不断扩大,仅在一个超大文件中管理源代码会变得十分困难。这时可以将源代码根据功能进行分组,并将不同功能的代码分开管理,每组独立管理的代码会生成一个输出文件。在使用时,通过导入对应的输出文件使用相应的功能,或者通过不同功能的交互与组合实现更加复杂的特性,使得项目管理更加高效。
在仓颉编程语言中,包是编译的最小单元,每个包可以单独输出 AST 文件、静态库文件、动态库文件等产物。每个包有自己的名字空间,在同一个包内不允许有同名的顶层定义或声明(函数重载除外)。一个包中可以包含多个源文件。
模块是若干包的集合,是第三方开发者发布的最小单元。一个模块的程序入口只能在其根目录下,它的顶层最多只能有一个作为程序入口的 main ,该 main 没有参数或参数类型为 Array<String>,返回类型为整数类型或 Unit 类型。
包和模块管理
在仓颉编程语言中,包由一个或多个源码文件组成,同一个包的源码文件必须在同一个目录,并且同一个目录里的源码文件只能属于同一个包。包可以定义子包从而构成树形结构。子包的目录是其父包目录的子目录。没有父包的包称为 root 包,root 包及其子包(包括子包的子包)构成的整棵树称为模块。
仓颉程序常见的组织结构如下:
1 | demo |
cjpm.toml 是当前模块所在工作空间的配置文件,用于定义基础信息、依赖项、编译选项等内容。该文件由仓颉语言的官方包管理工具 cjpm 解析和执行。
注意:
对于同一个模块,如果需要为其配置一个有效的包,则该包所在目录必须直接包含至少一个仓颉代码文件,并且其上游目录都需要是有效包。
包的声明
在仓颉编程语言中,包声明以关键字 package 开头,后接 root 包至当前包由 . 分隔路径上所有包的包名。包名必须是合法的普通标识符(不含原始标识符)。例如:
1 | package pkg1 // root 包 pkg1 |
注意:
当前 Windows 平台版本,包名暂不支持使用 Unicode 字符,包名必须是一个仅含 ASCII 字符的合法的普通标识符。
包声明必须在源文件的非空非注释的首行,且同一个包中的不同源文件的包声明必须保持一致。
1 | // file 1 |
仓颉的包名需反映当前源文件相对于项目源码根目录 src 的路径,并将其中的路径分隔符替换为小数点。例如包的源代码位于 src/directory_0/directory_1 下,root 包名为 pkg 则其源代码中的包声明应为 package pkg.directory_0.directory_1。
需要注意的是:
- 包所在的文件夹名必须与包名一致。
- 源码根目录默认名为
src。 - 源码根目录下的包可以没有包声明,此时编译器将默认为其指定包名
default。
假设源代码目录结构如下:
1 | // The directory structure is as follows: |
则 a.cj、b.cj、c.cj、main.cj 中的包声明可以为:
1 | // a.cj |
另外,包声明不能引起命名冲突:子包不能和当前包的顶层声明同名。
以下是一些错误示例:
1 | // a.cj |
顶层声明的可见性
仓颉编程语言中,可以使用访问修饰符来控制对类型、变量、函数等顶层声明的可见性。仓颉语言有 4 种访问修饰符:private、internal、protected、public,在修饰顶层元素时不同访问修饰符的语义如下:
private表示仅当前文件内可见。不同的文件无法访问这类成员。internal表示仅当前包及子包(包括子包的子包)内可见。同一个包内可以不导入就访问这类成员,当前包的子包(包括子包的子包)内可以通过导入来访问这类成员。protected表示仅当前模块内可见。同一个包的文件可以不导入就访问这类成员,不同包但是在同一个模块内的其他包可以通过导入访问这些成员,不同模块的包无法访问这些成员。public表示模块内外均可见。同一个包的文件可以不导入就访问这类成员,其他包可以通过导入访问这些成员。
| 修饰符 | 文件 | 包及子包 | 模块 | 所有包 |
|---|---|---|---|---|
private |
Y | N | N | N |
internal |
Y | Y | N | N |
protected |
Y | Y | Y | N |
public |
Y | Y | Y | Y |
不同顶层声明支持的访问修饰符和默认修饰符(默认修饰符是指在省略情况下的修饰符语义,这些默认修饰符也允许显式写出)规定如下:
package支持使用internal、protected、public,默认修饰符为public。import支持使用全部访问修饰符,默认修饰符为private。- 其他顶层声明支持使用全部访问修饰符,默认修饰符为
internal。
1 | package a |
仓颉的访问级别排序为 public > protected > internal > private。一个声明的访问修饰符不得高于该声明中用到的类型的访问修饰符的级别,参考如下示例:
-
函数声明中的参数与返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// a.cj
package a
class C {}
public func f1(a1: C) // Error, public declaration f1 cannot use internal type C.
{
return 0
}
public func f2(a1: Int8): C // Error, public declaration f2 cannot use internal type C.
{
return C()
}
public func f3 (a1: Int8) // Error, public declaration f3 cannot use internal type C.
{
return C()
} -
变量声明
1
2
3
4
5// a.cj
package a
class C {}
public let v1: C = C() // Error, public declaration v1 cannot use internal type C.
public let v2 = C() // Error, public declaration v2 cannot use internal type C. -
泛型类型的类型实参
1
2
3
4
5// a.cj
package a
public class C1<T> {}
class C2 {}
public let v1 = C1<C2>() // Error, public declaration v1 cannot use internal type C2. -
where约束中的类型上界1
2
3
4// a.cj
package a
interface I {}
public class B<T> where T <: I {} // Error, public declaration B cannot use internal type I.
值得注意的是:
-
public修饰的声明在其初始化表达式或者函数体里面可以使用本包可见的任意类型,包括被public修饰的类型和不被public修饰的类型。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 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修饰的顶层函数。1
2
3
4
5
6
7
8public var t1: () -> Unit = { => } // Ok.
func f1(): Unit {}
public let t2 = f1 // Ok.
public func f2() // Ok.
{
return f1
} -
内置类型诸如
Rune和Int64等默认的修饰符是public。1
2var num = 5
public var t3 = num // Ok.
注意:
同一个包内,
private修饰的同名自定义类型(如struct、class、enum和interface等),在某些场景下不支持,不支持场景由编译器进行报错。
例如在以下程序中,example1.cj 与 example2.cj文件包名相同,在 example1.cj 文件中定义了 private 修饰的类 A, 在 example2.cj 文件中定义了 private 修饰的结构体 A。
1 | // example1.cj |
运行以上程序,将输出:
1 | error: currently, it is not possible to export two private declarations with the same name |
包的导入
使用 import 语句导入其他包中的声明或定义
在仓颉编程语言中,可以通过 import fullPackageName.itemName 的语法导入其他包中的一个顶层声明或定义,fullPackageName 为完整路径包名,itemName 为声明的名字。导入语句在源文件中的位置必须在包声明之后,其他声明或定义之前。例如:
1 | package a |
如果要导入的多个 itemName 同属于一个 fullPackageName,可以使用 import fullPackageName.{itemName[, itemName]*} 语法,例如:
1 | import package1.{foo, bar, fuzz} |
这等价于:
1 | import package1.foo |
除了通过 import fullPackagename.itemName 语法导入一个特定的顶层声明或定义外,还可以使用 import packageName.* 语法将 packageName 包中所有可见的顶层声明或定义全部导入。例如:
1 | import package1.* |
需要注意:
- 导入的成员的作用域级别低于当前包声明的成员。
- 当已导出的包的模块名或者包名被篡改,使其与导出时指定的模块名或包名不一致,在导入时会报错。
- 只允许导入当前文件可见的顶层声明或定义,导入不可见的声明或定义将会在导入处报错。
- 禁止通过
import导入当前源文件所在包的声明或定义。 - 禁止包间的循环依赖导入,如果包之间存在循环依赖,编译器会报错。
示例如下:
1 | // pkga/a.cj |
在仓颉编程语言中,导入的声明或定义如果和当前包中的顶层声明或定义重名且不构成函数重载,则导入的声明和定义会被遮盖;导入的声明或定义如果和当前包中的顶层声明或定义重名且构成函数重载,函数调用时将会根据函数重载的规则进行函数决议。
1 | // pkga/a.cj |
隐式导入 core 包
诸如 String、Range 等类型能直接使用,并不是因为这些类型是内置类型,而是因为编译器会自动为源码隐式的导入 core 包中所有的 public 修饰的声明。
使用 import as 对导入的名字重命名
不同包的名字空间是分隔的,因此在不同的包之间可能存在同名的顶层声明。在导入不同包的同名顶层声明时,支持使用 import packageName.name as newName 的方式进行重命名来避免冲突。没有名字冲突的情况下仍然可以通过 import as 来重命名导入的内容。import as 具有如下规则:
-
使用
import as对导入的声明进行重命名后,当前包只能使用重命名后的新名字,原名无法使用。 -
如果重命名后的名字与当前包顶层作用域的其他名字存在冲突,且这些名字对应的声明均为函数类型,则参与函数重载,否则报重定义的错误。
-
支持
import pkg as newPkgName的形式对包名进行重命名,以解决不同模块中同名包的命名冲突问题。1
2
3// a.cj
package p1
public func f1() {}1
2
3// d.cj
package p2
public func f3() {}1
2
3// b.cj
package p1
public func f2() {}1
2
3// c.cj
package pkgc
public func f1() {}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// main.cj
import p1 as A
import p1 as B
import p2.f3 as f // OK
import pkgc.f1 as a
import pkgc.f1 as b // OK
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语句处不报错;在使用处,会因为无法导入唯一的名字而报错。这种情况可以通过import as定义别名或者import fullPackageName导入包作为命名空间。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// a.cj
package p1
public class C {}
// b.cj
package p2
public class C {}
// main1.cj
package pkga
import p1.C
import p2.C
main() {
let _ = C() // Error
}
// main2.cj
package pkgb
import p1.C as C1
import p2.C as C2
main() {
let _ = C1() // Ok
let _ = C2() // Ok
}
// main3.cj
package pkgc
import p1
import p2
main() {
let _ = p1.C() // Ok
let _ = p2.C() // Ok
}
重导出一个导入的名字
在功能繁多的大型项目的开发过程中,这样的场景是非常常见的:包 p2 大量地使用从包 p1 中导入的声明,当包 p3 导入包 p2 并使用其中的功能时,p1 中的声明同样需要对包 p3 可见。如果要求包 p3 自行导入 p2 中使用到的 p1 中的声明,这个过程将过于繁琐。因此希望能够在 p2 被导入时一并导入 p2 使用到的 p1 中的声明。
在仓颉编程语言中,import 可以被 private、internal、protected、public 访问修饰符修饰。其中,被 public、protected 或者 internal 修饰的 import 可以把导入的成员重导出(如果这些导入的成员没有因为名称冲突或者被遮盖导致在本包中不可用)。其他包可以根据可见性直接导入并使用本包中用重导出的内容,无需从原包中导入这些内容。
private import表示导入的内容仅当前文件内可访问,private是import的默认修饰符,不写访问修饰符的import等价于private import。internal import表示导入的内容在当前包及其子包(包括子包的子包)均可访问。非当前包访问需要显式import。protected import表示导入的内容在当前 module 内都可访问。非当前包访问需要显式import。public import表示导入的内容外部都可访问。非当前包访问需要显式import。
在下面的例子中,b 是 a 的子包,在 a 中通过 public import 重导出了 b 中定义的函数 f。
1 | package a |
需要注意的是,包不可以被重导出:如果被 import 导入的是包,那么该 import 不允许被 public、protected 或者 internal 修饰。
1 | public import a.b // Error, cannot re-export package |
程序入口
仓颉程序入口为 main,源文件根目录下的包的顶层最多只能有一个 main。
如果模块采用生成可执行文件的编译方式,编译器只在源文件根目录下的顶层查找 main。如果没有找到,编译器将会报错;如果找到 main,编译器会进一步对其参数和返回值类型进行检查。需要注意的是,main 不可被访问修饰符修饰,当一个包被导入时,包中定义的 main 不会被导入。
作为程序入口的 main 可以没有参数或参数类型为 Array<String>,返回值类型为 Unit 或整数类型。
没有参数的 main:
1 | // main.cj |
参数类型为 Array<String> 的 main:
1 | // main.cj |
使用 cjc main.cj 编译完成后,通过命令行执行:./main Hello, World,将会得到如下输出:
1 | Hello, |
以下是一些错误示例:
1 | // main.cj |