news 2026/10/1 10:31:55

【仓颉语言入门 · 第21课】

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【仓颉语言入门 · 第21课】

【仓颉语言入门 · 第21课】cjpm 包管理与多文件项目组织:从单文件练习迈向工程化开发

前 20 课的代码都活在一个单独的src/main.cj里,靠import std.*使用标准库。但真实项目必然要拆文件、分包、复用别人写的库。本课带你掌握cjpm 包管理:一个工程的目录结构、多文件如何协作、如何把自己的代码打成库包、如何通过本地路径引用第三方包,以及cjpm.toml的完整配置项。

本文所有代码与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。


目录(系列导航)

整套路线共7 个模块、30 课:

模块课次内容
一、环境与入门01~05环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环
二、常用类型与数据组织06~10字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理
三、函数与函数式11~14函数、Lambda 与高阶函数、闭包、迭代器与惰性序列
四、面向对象与类型系统15~20struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展
五、工程化与标准库21~25cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试
六、并发编程26~28线程、Channel 通道与同步原语、并发实战
七、项目实战29~30命令行小工具、GeoJSON 数据处理实战
  1. 环境搭建与第一个仓颉程序
  2. 变量与常量:let / var 与基本数据类型
  3. 运算符与标准输入输出
  4. 分支结构:if 与 match 表达式
  5. 循环结构:while / for / Range
  6. 字符串详解与字符串插值
  7. 数组 Array 与区间 Range
  8. 集合框架:ArrayList、HashMap、HashSet
  9. 可空类型?与 Option
  10. 错误处理:异常机制与 Result
  11. 函数定义、参数与返回值
  12. Lambda 与高阶函数
  13. 闭包、作用域与函数类型
  14. 迭代器 Iterator 与 Sequence
  15. 结构体 struct 与类 class
  16. 构造函数、属性与方法
  17. 接口 interface 与实现
  18. 枚举 enum、代数数据类型与 match 模式匹配
  19. 泛型编程
  20. 扩展、类型别名与可见性控制
  21. cjpm 包管理与多文件项目组织(本文)
  22. 文件与目录 IO
  23. JSON 处理(结合 stdx 扩展库)
  24. 网络编程入门
  25. 单元测试
  26. 并发基础:线程的创建与等待
  27. Channel 通道与同步原语
  28. 并发实战:多线程任务处理
  29. 实战一:带文件持久化的命令行小工具
  30. 实战二:GeoJSON 数据处理程序

一、单文件项目的瓶颈

回顾前 20 课,我们的代码都是这个结构:

myproj/ ├── cjpm.toml └── src/ └── main.cj ← 所有代码塞在这一个文件里

cjpm.toml最少需要这几个必填字段(cjpm init会自动生成完整配置):

[package] name = "myproj" version = "1.0.0" cjc-version = "1.2.0" output-type = "executable"

随着功能变多,单文件会迅速失控:

  • 一个文件几千行,找代码靠搜索;
  • 不同功能混在一起,改一处怕动全身;
  • 想复用之前写的工具类?只能复制粘贴;
  • 团队协作时,多人改同一个文件冲突不断。

工程化的第一步就是拆文件、分包。


二、cjpm 项目的标准目录结构

cjpm是仓颉官方的包管理器兼构建工具(类似 Rust 的 cargo、Go 的 go mod)。一个标准 cjpm 项目的目录结构如下:

myproj/ ├── cjpm.toml # 项目配置文件(必须) ├── src/ # 源码目录(必须) │ ├── main.cj # 程序入口(可执行项目) │ ├── utils.cj # 工具函数 │ └── models.cj # 数据模型 ├── target/ # 编译输出目录(cjpm 自动生成,不用管) └── cjpm.lock # 依赖锁定文件(cjpm 自动生成,不用管)

2.1cjpm init:一键创建项目

不用手动建目录,cjpm 提供了脚手架命令:

cjpm init--namemyproj

生成的目录结构:

myproj/ ├── cjpm.toml └── src/ └── main.cj

cjpm.toml自带完整配置(cjc-version、version、output-type等必填字段已填好),main.cj是一个可运行的骨架:

package myproj main(): Int64 { println("hello world") return 0 }

2.2cjpm.toml详解

cjpm.toml是项目的"身份证",控制项目名、版本、依赖、编译选项。先看一个完整的可执行项目配置:

[package] name = "myproj" # 项目名(包名),只能用小写字母/数字/下划线,不能含连字符 version = "1.0.0" # 语义化版本(必填) cjc-version = "1.2.0" # 编译器版本要求(必填) output-type = "executable" # 输出类型:executable(可执行)或 static(静态库,必填) src-dir = "src" # 源码目录,默认 "src" description = "我的第一个仓颉项目" license = "MIT" [dependencies] # 本地路径依赖 # mylib = { path = "../mylib" } # 远程仓库依赖(需要配置 registry) # cangjie-mysql-driver = { version = "0.1.0" }

注意:name、version、cjc-version、output-type是必填字段,缺了cjpm build会报错。cjpm init生成的配置已包含所有必填项。

关键字段说明:

字段说明常用值
name项目名,也是默认包名小写字母/数字/下划线,不能含-
version语义化版本1.0.0
output-type输出类型executable(默认)/static
src-dir源码目录src(默认)
[dependencies]依赖表见第四节

三、多文件协作:把代码拆出去

3.1 同一个包里的多文件

src/目录下所有.cj文件默认属于同一个包(包名 =cjpm.toml里的name)。拆文件不需要任何 import,直接引用即可:

src/models.cj:

package myproj class User { let name: String let age: Int64 init(name: String, age: Int64) { this.name = name this.age = age } func isAdult(): Bool { return this.age >= 18 } }

src/utils.cj:

package myproj func formatUser(user: User): String { let status = if (user.isAdult()) { "成年" } else { "未成年" } return "${user.name}(${user.age}岁,${status})" }

src/main.cj:

package myproj main(): Int64 { let u = User("小明", 20) println(formatUser(u)) return 0 }

运行cjpm run,输出:

小明(20岁,成年)

注意:三个文件都写了package myproj,它们共享同一个包作用域,不需要import。

3.2 子目录 = 子包

当文件更多时,需要按功能分包。仓颉的规则是:src/下的子目录自动成为子包,包名 = 项目名 + 子目录路径。

myproj/ ├── cjpm.toml └── src/ ├── main.cj # package myproj ├── models/ │ └── user.cj # package myproj.models └── utils/ └── format.cj # package myproj.utils

src/models/user.cj:

package myproj.models public class User { public let name: String public let age: Int64 public init(name: String, age: Int64) { this.name = name this.age = age } public func isAdult(): Bool { return this.age >= 18 } }

src/utils/format.cj:

package myproj.utils import myproj.models.User public func formatUser(user: User): String { let status = if (user.isAdult()) { "成年" } else { "未成年" } return "${user.name}(${user.age}岁,${status})" }

src/main.cj:

package myproj import myproj.models.User import myproj.utils.formatUser main(): Int64 { let u = User("小明", 20) println(formatUser(u)) return 0 }

运行结果同上。注意三个要点:

  1. 子包里的类/函数要加public——第 20 课讲过,跨包访问默认internal,包外不可见;
  2. import 写完整路径:import myproj.models.User,不是import models.User;
  3. import 的是符号,不是文件——import myproj.utils.formatUser导入的是formatUser这个函数,不是format.cj这个文件。

3.3 子包之间的依赖

子包之间也可以互相 import,但会形成依赖链。建议保持单向依赖:main→utils→models,避免循环引用。


四、依赖管理:引用别人的包

4.1 本地路径依赖

最常见的场景:你写了一个通用库mylib,想在另一个项目myproj里用。

目录结构:

workspace/ ├── mylib/ │ ├── cjpm.toml │ └── src/ │ └── lib.cj └── myproj/ ├── cjpm.toml └── src/ └── main.cj

mylib/cjpm.toml:

[package] name = "mylib" version = "0.1.0" output-type = "static" # 静态库

mylib/src/lib.cj:

package mylib public func greet(name: String): String { return "Hello, ${name}!" }

myproj/cjpm.toml:

[package] name = "myproj" [dependencies] mylib = { path = "../mylib" }

myproj/src/main.cj:

package myproj import mylib.greet main(): Int64 { println(greet("仓颉")) return 0 }

运行cjpm run,输出:

Hello, 仓颉!

cjpm 会自动编译mylib,再编译myproj并链接。

4.2 远程仓库依赖

如果包发布到了 cjpm 官方仓库(或私服),可以用版本号引用:

[dependencies] cangjie-mysql-driver = { version = "0.1.0" }

首次cjpm build会自动下载并缓存到本地。

4.3 依赖锁定:cjpm.lock

cjpm build后会在项目根目录生成cjpm.lock,记录所有依赖(包括间接依赖)的精确版本。团队开发时应该把cjpm.lock提交到 git,保证所有人用同一套依赖版本。


五、静态库 vs 可执行程序

特性可执行程序(executable)静态库(static)
output-typeexecutable(默认)static
入口必须有main(): Int64不需要main
被依赖不能作为其他项目的依赖可以被其他项目依赖
输出生成.exe生成.a(Linux/macOS)或.lib(Windows)

什么时候用 static?

  • 写通用工具库、SDK 封装、算法库;
  • 把一个大项目拆成"核心逻辑库 + 命令行壳"。

什么时候用 executable?

  • 写最终给用户运行的程序;
  • 写测试用的 demo。

六、实战:把订单系统拆成多文件工程

我们把第 20 课的订单金额工具箱拆成一个规范的 cjpm 工程。

6.1 目录结构

ordersys/ ├── cjpm.toml └── src/ ├── main.cj ├── models/ │ └── order.cj └── utils/ └── money.cj

6.2 代码实现

ordersys/cjpm.toml:

[package] name = "ordersys" version = "1.0.0" cjc-version = "1.2.0" output-type = "executable"

src/models/order.cj:

package ordersys.models public class Order { public let id: String private var paid: Bool = false private let amountFen: Int64 public init(id: String, amountFen: Int64) { this.id = id this.amountFen = amountFen } public func pay(): Unit { this.paid = true } public func isPaid(): Bool { return this.paid } public func getAmountFen(): Int64 { return this.amountFen } }

src/utils/money.cj:

package ordersys.utils public func fenToYuan(fen: Int64): String { let sign = if (fen < 0) { "-" } else { "" } let abs = if (fen < 0) { -fen } else { fen } let yuan = abs / 100 let cents = abs % 100 let centsText = if (cents < 10) { "0${cents}" } else { "${cents}" } return "${sign}${yuan}.${centsText}" } public func yuanText(fen: Int64): String { return fenToYuan(fen) }

src/main.cj:

package ordersys import ordersys.models.Order import ordersys.utils.yuanText main(): Int64 { let orders = [ Order("NO.1001", 1250), Order("NO.1002", 800), Order("NO.1003", 99) ] var total = 0 for (o in orders) { o.pay() let status = if (o.isPaid()) { "已支付" } else { "待支付" } println("订单(${o.id}) ${yuanText(o.getAmountFen())} 元 [${status}]") total += o.getAmountFen() } println("实收合计:${yuanText(total)} 元") return 0 }

运行cjpm run,输出:

订单(NO.1001) 12.50 元 [已支付] 订单(NO.1002) 8.00 元 [已支付] 订单(NO.1003) 0.99 元 [已支付] 实收合计:21.49 元

6.3 设计要点

  • 金额用整数分存储,yuanText()负责展示层格式化——和第 20 课的方案一致;
  • paid是private,外部只能通过pay()改状态,不能绕过;
  • models 和 utils 单向依赖:main→models+utils,utils不依赖models;
  • 每个子包只暴露必要的public成员,内部实现细节保持默认可见性。

七、常用 cjpm 命令速查

命令作用
cjpm init --name xxx创建新项目
cjpm build编译项目
cjpm run编译并运行(可执行项目)
cjpm clean清理编译输出
cjpm check类型检查,不生成二进制
cjpm test运行单元测试(第 25 课讲)

八、常见问题 FAQ

Q1:子目录里的文件一定要写package 项目名.子目录吗?

是的。仓颉按目录推导包名,src/models/下的文件必须写package myproj.models,写package myproj会报包声明冲突(第 464 号错误)。

Q2:我可以把多个类塞在一个文件里吗?

可以,一个.cj文件里可以写任意多个类、函数、扩展。但建议一个文件只放一类功能,方便维护。

Q3:import 写myproj.models.User还是myproj.models?

import 的是符号(类名、函数名),不是包名。import myproj.models.User导入User这个类;如果写import myproj.models,编译器会报"找不到符号"。

Q4:本地路径依赖写相对路径还是绝对路径?

推荐相对路径(如../mylib),因为绝对路径在别人电脑上会失效。相对路径以当前项目的cjpm.toml所在目录为基准。

Q5:静态库能直接运行吗?

不能。output-type = "static"的项目没有main函数,编译产物是.lib/.a,只能被可执行项目链接。

Q6:一个项目能同时是库和可执行程序吗?

可以,但仓颉的做法是拆成两个项目:一个static库项目 + 一个executable壳项目。壳项目依赖库项目,只做参数解析和打印。


九、课后练习

  1. 用cjpm init --name calc创建一个新项目,把第 11 课的加、减、乘、除四个函数拆到src/ops.cj,main.cj里调用并打印3 + 5、10 - 2、4 * 6、20 / 4的结果。
  2. 在calc项目里新建src/advanced/子目录,写一个power(base: Int64, exp: Int64): Int64函数(幂运算),在main.cj里 import 并计算2^10。
  3. 创建两个项目:stringutils(static 库,提供reverse(s: String): String)和myapp(可执行,本地路径依赖stringutils)。在myapp里调用reverse("hello")并打印结果。
  4. 把第 20 课的订单金额工具箱按本课 6.1 节的结构拆成ordersys工程,确保能cjpm run出正确结果。
  5. 挑战:给stringutils加isPalindrome(s: String): Bool(判断回文),在myapp里测试"level"和"hello";思考为什么stringutils里的函数要加public。

下节预告

工程能拆分了,但程序还活在内存里——重启就丢数据。第 22 课文件与目录 IO将讲解:如何用std.fs读写文本文件、遍历目录、处理路径,以及把本课的订单数据持久化到 JSON 文件,让程序真正"记住"东西。


系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.2.0)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入,以你本地版本为准,欢迎评论区交流。


💬 遇到问题?扫码联系作者

跟着课程练习时,如果在 SDK 安装、环境变量配置、编译报错或调试上卡住,欢迎扫码加作者企业微信直接咨询(请备注"仓颉课程"):

离线环境下图片可能加载不出来,也可以在 CIDE 菜单Help ▸ 联系作者 / Contact中查看同一张二维码(应用内置兜底图,无需联网)。


📥 工具下载

本系列全程使用的仓颉 IDE ——CIDE(免费开源、社区版):

  • GitCode 仓库 / 安装包下载:https://gitcode.com/wp_upala/cide
  • 打开页面后进入发行版(Releases),两种包任选其一:
    • 安装版:下载CIDE-<版本>-x64-Setup.exe,双击安装,适合日常长期使用;
    • 免安装版(Portable):下载CIDE-<版本>-x64-Portable.zip,解压到任意目录即用,不写注册表、不留安装痕迹,拷到 U 盘也能在别的电脑直接运行(包内附《使用说明.txt》)。适合先试用、或在受限电脑上学习本系列课程。
  • 仓颉 SDK 请前往仓颉编程语言官网下载:https://cangjie-lang.cn
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 10:29:46

PandaAI Qube一站式交易策略平台

今天给大家介绍一款PandaAI出品的非常好用的交易策略平台Qube&#xff0c;通过提示词对话就可以直接生成你想要的交易策略&#xff0c;平台担心大家不知道怎么开始&#xff0c;特意在平台的首页【想从哪儿开始&#xff1f;】功能提供很多提示词模版&#xff0c;并且支持主流的A…

作者头像 李华
网站建设 2026/10/1 10:29:25

Jev是什么?如何把Jev模型接入TraeCode实现AI编程

最近后台私信里被问到最多的一个词就是“Jev”。不管是技术群、AI 编程社区还是推特时间线&#xff0c;都在说“Jev 爆了”“斯坦福有人拿 Jev 搭数据系统”“Jev 在 Codex 里跑得很顺”。但你要真去搜“Jev 是什么”&#xff0c;能搜到的正经解释又少得可怜&#xff0c;大部分…

作者头像 李华
网站建设 2026/10/1 10:27:57

BLE GATT / ATT 基础

BLE GATT / ATT 抓包实战 BLE GATT / ATT 基础 —— 属性、Handle、Characteristic 与 ATT 协议一、GATT 是什么1.1 GATT 站在哪一层1.2 GATT 和 ATT 的关系1.3 属性&#xff08;Attribute&#xff09;—— GATT 的最小单位1.4 Handle&#xff08;句柄&#xff09;—— GATT 的…

作者头像 李华
网站建设 2026/10/1 10:27:00

多Agent调度容错:节点失败后如何让工作流继续运行

先问个问题&#xff1a;你跑过多Agent编排吗&#xff1f;就是那种把大活拆成几个角色&#xff0c;各自调用模型、工具、API&#xff0c;最后拼出一个结果的工作流。如果你跑过&#xff0c;大概率经历过这个场面——5个Agent并行跑&#xff0c;其他4个都快出结果了&#xff0c;突…

作者头像 李华
网站建设 2026/10/1 10:26:37

WeKnora实战:多智能体检索增强引擎如何解决RAG知识库难题

最近项目组要搭一套私有知识库&#xff0c;我把 Dify、RAGFlow、FastGPT 基本试了个遍&#xff0c;结果都卡在同一个地方&#xff1a;文档进来之后&#xff0c;看似能聊&#xff0c;实际问答一追细节就露馅——要么答非所问&#xff0c;要么引用来源张冠李戴。后来看到微信团队…

作者头像 李华
网站建设 2026/10/1 10:26:28

写在前面:这套测试到底在测什么

缘起 电力监控系统里&#xff0c;104 规约&#xff08;IEC 60870-5-104&#xff09;过去是明文跑的&#xff0c;谁都能在链路上看&#xff0c;也能往里塞东西。IEC 62351-3 就是来解决这个问题的&#xff0c;它把 TLS 套在 TCP 之上&#xff0c;让 104 的报文有了加密和双向认…

作者头像 李华