2281 字
11 分钟
仓颉文档阅读-开发指南XI: 包(I) - 包的概述 与 模块管理
NOTE

阅读文档版本:

语言规约 Cangjie-0.53.18-Spec

具体开发指南 Cangjie-LTS-1.0.4

在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证

有条件当然可以直接 配置 Canjie-SDK

WARNING

博主在此之前, 基本只接触过 C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与 C/C++中的相似概念作类比, 见谅

且, 本系列是文档阅读, 而不是仓颉的零基础教学, 所以如果要跟着阅读的话最好有一门编程语言的开发经验

WARNING

在阅读仓颉编程语言的开发指南之前, 已经大概阅读了一遍 仓颉编程语言的语言规约, 已经对仓颉编程语言有了一个大概的了解

所以在阅读开发指南时, 不会对类似: 类、函数、结构体、接口等解释起来较为复杂名称 做出解释

此样式内容, 表示文档原文内容

#

包的概述#

随着项目规模的不断扩大,仅在一个超大文件中管理源代码会变得十分困难

这时可以将源代码根据功能进行分组,并将不同功能的代码分开管理,每组独立管理的代码会生成一个输出文件

在使用时,通过导入对应的输出文件使用相应的功能,或者通过不同功能的交互与组合实现更加复杂的特性,使得项目管理更加高效

在仓颉编程语言中,包是编译的最小单元,每个包可以单独输出 AST 文件、静态库文件、动态库文件等产物

每个包有自己的名字空间,在同一个包内不允许有同名的顶层定义或声明(函数重载除外)

一个包中可以包含多个源文件

模块是若干包的集合,是第三方开发者发布的最小单元

一个模块的程序入口只能在其根目录下,它的顶层最多只能有一个作为程序入口的main,该main没有参数或参数类型为Array<String>,返回类型为整数类型或 Unit 类型

包和模块, 对于没有接触过相关概念的博主来说, 编译的最小单元和发布的最小单元, 两个含义有一些难区分

仓颉中每个包都可以单独输出库文件, 但不能发布的原因是什么呢? 是仓颉自己做了限制吗? 毕竟库发布也是为了导入使用

模块可以存在程序入口, 不过只能在根目录下, 有且只能有一个main函数作为模块的程序入口

包和模块管理#

在仓颉编程语言中,包由一个或多个源码文件组成,同一个包的源码文件必须在同一个目录,并且同一个目录里的源码文件只能属于同一个包

包可以定义子包从而构成树形结构, 子包的目录是其父包目录的子目录

没有父包的包称为root包,root包及其子包(包括子包的子包)构成的整棵树称为模块

仓颉程序常见的组织结构如下:

demo
├── src
│ ├── main.cj
│ └── pkg0
│ ├── pkg0.cj
│ ├── aoo
│ │ └── aoo.cj
│ └── boo
│ └── boo.cj
└── cjpm.toml

cjpm.toml 是当前模块所在工作空间的配置文件,用于定义基础信息、依赖项、编译选项等内容

该文件由仓颉语言的官方包管理工具 cjpm 解析和执行

注意:

对于同一个模块,如果需要为其配置一个有效的包,则该包所在目录必须直接包含至少一个仓颉代码文件,并且其上游目录都需要是有效包

对文档中举例的模块目录结构来说:

  1. 此模块为demo, 即root包为demo

  2. src目录默认为模块的代码根目录

    这个代码根目录, 是可以在cjpm.toml中配置的, 默认为src-dir = "", 此时 模块以src目录为模块的代码根目录

    所以, src内的所有子目录都是demo的子包

  3. src/main.cj中, 如果存在main函数, 那么就编译可执行程序, 否则可以编译发布库

cjpm.toml是管理 模块的各种信息、依赖、编译选项等的, 编译目标也是由cjpm.tmol管理的

这也意味着, 虽然每个包都可以单独编译, 但是cjpm配置的是整个模块, 而不是模块内的子包, 所以 发布也应该是模块

因为项目依赖、结构等是由cjpm管理的

CAUTION

一个有效的包, 所在目录至少包含一个仓颉代码文件, 且至少要声明包名

这意味着, 如果你要声明一个有效包, 就需要在包目录下创建至少一个.cj文件, 且必须当前声明包名

举个例子:

Second/
├── cjpm.lock
├── cjpm.toml
└── src
├── main.cj
└── test
└── test.cj

如果想要成功声明有效的Second.test子包, 就必须在src/test下创建至少一个.cj文件, 且内容至少为:

package Second.test

注意: 包名就是目录名, .cj文件命名不需要与包名保持一致

包的声明#

在仓颉编程语言中,包声明以关键字 package 开头,后接 root 包至当前包由 . 分隔路径上所有包的包名

包名必须是合法的普通标识符(不含原始标识符)

例如:

package pkg1 // root 包 pkg1
package pkg1.sub1 // root 包 pkg1 的子包 sub1

注意:

当前Windows平台版本,包名暂不支持使用Unicode字符,包名必须是一个仅含ASCII字符的合法的普通标识符

包声明必须在源文件的非空非注释的首行,且同一个包中的不同源文件的包声明必须保持一致

// file 1
// 注释是允许的
package test
// 声明 ...
// file 2
let a = 1 // Error, 包声明必须出现在文件的第一位
package test
// 声明 ...

仓颉的包名需反映当前源文件相对于项目源码根目录src的路径,并将其中的路径分隔符替换为小数点

例如包的源代码位于 src/directory_0/directory_1 下,root包名为 pkg 则其源代码中的包声明应为 package pkg.directory_0.directory_1

需要注意的是:

  • 包所在的文件夹名必须与包名一致

  • 源码根目录默认名为src

  • 源码根目录下的包可以没有包声明,此时编译器将默认为其指定包名default

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

// 目录结构如下:
src
├-- directory_0
│ |-- directory_1
│ | |-- a.cj
│ | └-- b.cj
│ └-- c.cj
└-- main.cj

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

a.cj
// 在文件 a.cj 中, 声明的包名必须与相对路径对应 directory_0/directory_1
package default.directory_0.directory_1
b.cj
// 在文件 b.cj 中, 声明的包名必须与相对路径对应 directory_0/directory_1
package default.directory_0.directory_1
c.cj
// 在文件 c.cj 中, 声明的包名必须与相对路径对应 directory_0
package default.directory_0
main.cj
// 文件 main.cj 位于模块根目录, 可以省略包声明
main() {
return 0
}

另外,包声明不能引起命名冲突:子包不能和当前包的顶层声明同名

以下是一些错误示例:

a.cj
package a
public class B { // Error, 'B' 与子包 'a.B' 冲突
public static func f() {}
}
b.cj
package a.B
public func f {}
main.cj
import a.B // 'a.B'的用法不明确
main() {
a.B.f()
return 0
}

仓颉中, 包声明必须处于源代码文件的非注释首行

包名必须与所在的目录名保持一致

仓颉的包名, 需反映当前源文件相对于项目源码根目录的路径, 项目的源码根目录默认为src

那么, src下的子目录就是项目root包的子包, 那么在子包的源文件中声明包名是, 就需要这样声明:

如果存在Project/src/pkg1/pkg2/pkg3, 且没有使用cjpm项目管理:

  1. 如果在src下声明了root包名为Project

    那么, 子包就需要声明为:

    Project.pkg1Project.pkg1.pkg2Project.pkg1.pkg2.pkg3

  2. 如果在src下没有声明root包名

    那么, 子包就需要声明为:

    default.pkg1default.pkg1.pkg2default.pkg1.pkg2.pkg3

如果项目的源码根目录下的源文件, 没有声明包名, 那么包名默认为default, 如果需要声明包名, 那么包名推荐与项目路径名相同

但, 声明子包时必须以root包名开头

TIP

如果不使用cjpm管理项目, 尝试直接使用cjc编译项目

通过指定父包和子包的源文件一起进行编译是不允许的: 即 cjc src/main.cj src/pkg1/c.cj 是不被允许的

需要先编译子包, 将子包编译为静态或动态库, 然后使用cjc src/main.cj lib进行编译