1、包的概述
随着项目规模的不断扩大,仅在一个超大文件中管理源代码会变得十分困难。这时可以将源代码根据功能进行分组,并将不同功能的代码分开管理,每组独立管理的代码会生成一个输出文件
。在使用时,通过导入对应的输出文件使用相应的功能,或者通过不同功能的交互与组合实现更加复杂的特性
,使得项目管理更加高效。
在仓颉编程语言中,包
是编译的最小单元,每个包可以单独输出 AST 文件
、静态库文件
、动态库文件
等产物。每个包有自己的名字空间,在同一个包内不允许有同名的顶层定义或声明
(函数重载除外)。一个包中可以包含多个源文件
。
模块
是若干包的集合,是第三方开发者发布的最小单元。一个模块的程序入口只能在其根目录下
,它的顶层最多只能有一个作为程序入口的 main
,该 main 没有参数
或参数类型为 Array<String>
,返回类型为整数类型
或 Unit 类型
。
2、包的声明
在仓颉编程语言中,包声明以关键字 package
开头,后接 root 包至当前包由 .
分隔路径上所有包的包名。包名必须是合法标识符。例如:
package pkg1 // root 包 pkg1
package pkg1.sub1 // root 包 pkg1 的子包 sub1
包声明必须在源文件的非空非注释的首行
,且同一个包中的不同源文件的包声明必须保持一致
。
// 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
下,root 包名
为 pkg
则其源代码中的包声明应为 package pkg.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.cj、b.cj、c.cj、main.cj 中的包声明可以为:
// a.cj
// in file a.cj, the declared package name must correspond to relative path directory_0/directory_1.
package default.directory_0.directory_1
// b.cj
// in file b.cj, the declared package name must correspond to relative path directory_0/directory_1.
package default.directory_0.directory_1
// c.cj
// in file c.cj, the declared package name must correspond to relative path directory_0.
package default.directory_0
// main.cj
// file main.cj is in the module root directory and may omit package declaration.
main() {
return 0
}
另外,包声明不能引起命名冲突:子包不能和当前包的顶层声明同名
。
以下是一些错误示例:
// a.cj
package a
public class B { // Error, 'B' is conflicted with sub-package 'a.B'
public static func f() {}
}
// b.cj
package a.B
public func f {}
// main.cj
import a.B // ambiguous use of 'a.B'
main() {
a.B.f()
return 0
}
3、顶层声明的可见性
仓颉中,可以使用访问修饰符
来控制对类型
、变量
、函数
等顶层声明的可见性。仓颉有 4 种访问修饰符:private
、internal
、protected
、public
,在修饰顶层元素时不同访问修饰符的语义如下。
private
表示仅当前文件内可见。不同的文件无法访问这类成员。internal
表示仅当前包及子包(包括子包的子包)内可见。同一个包内可以不导入就访问这类成员,当前包的子包(包括子包的子包)内可以通过导入来访问这类成员。protected
表示仅当前模块内可见。同一个包的文件可以不导入就访问这类成员,不同包但是在同一个模块内的其它包可以通过导入访问这些成员,不同模块的包无法访问这些成员。public
表示模块内外均可见。同一个包的文件可以不导入就访问这类成员,其它包可以通过导入访问这些成员。
不同顶层声明支持的访问修饰符和默认修饰符
(默认修饰符是指在省略情况下的修饰符语义,这些默认修饰符也允许显式写出)规定如下:
- pacakge 支持使用 internal、protected、public,默认修饰符为 public。
- import 支持使用全部访问修饰符,默认修饰符为 private。
- 其他顶层声明支持使用全部访问修饰符,默认修饰符为 internal。
仓颉的访问级别排序为 public > protected > internal > private
。一个声明的访问修饰符不得高于该声明中用到的类型的访问修饰符的级别,参考如下示例:
- 函数声明中的参数与返回值
// 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()
}
- 变量声明
// 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.
- 类声明中继承的类
// a.cj
package a
open class C1 {}
public class C2 <: C1 {} // Error, public declaration C2 cannot use internal type C1.
- 类型实现的接口
// a.cj
package a
interface I {}
public enum E <: I { A } // Error, public declaration uses internal types.
- 泛型类型的类型实参
// 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
约束中的类型上界
// 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
修饰的类型。
// 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
}
- 内置类型诸如 Rune、Int64 等也都默认是 public 的。
var num = 5
public var t3 = num // Ok.
4、包的导入
4.1 使用 import 语句导入其它包中的声明或定义
在仓颉编程语言中,可以通过 import fullPackageName.itemName
的语法导入其他包中的一个
顶层声明或定义,fullPackageName
为完整路径包名,itemName
为声明的名字。导入语句在源文件中的位置必须在包声明之后,其他声明或定义之前。例如:
package a
import std.math.*
import package1.foo
import {package1.foo, package2.bar}
如果要导入的多个 itemName
同属于一个 fullPackageName
,可以使用 import fullPackageName.{itemName[, itemName]*}
语法,例如:
import package1.{foo, bar, fuzz}
这等价于:
import package1.foo
import package1.bar
import package1.fuzz
除了通过 import fullPackagename.itemName
语法导入一个特定的顶层声明或定义外,还可以使用 import packageName.*
语法将 packageName
包中所有可见
的顶层声明或定义全部导入。例如:
import package1.*
import {package1.*, package2.*}
需要注意:
import
可以被private
、internal
、protected
、public
访问修饰符修饰。不写访问修饰符的import
等价于private import
。- 导入的成员的作用域级别低于当前包声明的成员。
- 当已导出的包的模块名或者包名被篡改,使其与导出时指定的模块名或包名不一致,在导入时会报错。
- 只允许导入当前文件可见的顶层声明或定义,导入不可见的声明或定义将会在导入处报错。
- 禁止通过
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
}
4.2 隐式导入 core 包
诸如 String
、Range
等类型能直接使用
,并不是因为这些类型是内置类型,而是因为编译器会自动为源码隐式的导入 core 包中所有的 public 修饰的声明
。
4.3 使用 import as 对导入的名字重命名
不同包的名字空间是分隔的,因此在不同的包之间可能存在同名的顶层声明。在导入不同包的同名顶层声明时,我们支持使用 import packageName.name as newName
的方式进行重命名来避免冲突
。没有名字冲突的情况下
仍然可以通过import as
来重命名导入的内容。import as
具有如下规则:
-
使用
import as
对导入的声明进行重命名后,当前包只能使用重命名后的新名字,原名无法使用。 -
如果重命名后的名字与当前包顶层作用域的其它名字存在冲突,且这些名字对应的声明均为函数类型,则参与函数重载,否则报重定义的错误。
-
支持
import pkg as newPkgName
的形式对包名进行重命名,以解决不同模块中同名包的命名冲突问题。
// a.cj
package p1
public func f1() {}
// d.cj
package p2
public func f3() {}
// b.cj
package p1
public func f2() {}
// c.cj
package pkgc
public func f1() {}
// 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
导入包作为命名空间。
// 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
}
4.4 重导出一个导入的名字
在功能繁多的大型项目的开发过程中,这样的场景是非常常见的:包 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。
package a
public let x = 0
public import a.b.f
internal package a.b
public func f() { 0 }
import a.f // ok
let _ = f() // ok
需要注意的是,包不可以被重导出
:如果被 import 导入的是包,那么该 import 不允许被 public、protected 或者 internal 修饰。
public import a.b // Error, cannot re-export package
5、程序入口
仓颉程序入口为 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
main(args: Array<Int8>): Int64 { // Error, 'main' cannot be defined with parameter whose type is not Array<String>.
return 0
}