【仓颉语言入门 · 第20课】扩展、类型别名与可见性控制:不改源码,给类型加装能力
学到这里你可能遇到两个尴尬:① 觉得标准库的
String、Int64要是再多一个顺手的方法就好了,可它们的源码你改不了,写一堆工具函数用着又别扭;② 自己写的类,内部字段、辅助函数全暴露在外部,谁都能乱改。本课用三件套一次解决:扩展extend(不改源码、不写子类,给已有类型加方法/属性/接口/运算符)、类型别名type(给复杂类型起一个有业务含义的名字)、可见性控制(public/private/internal/protected四级权限,含两个工程对打的跨包实测)。至此模块四"面向对象与类型系统"收官。本文所有代码与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。
目录(系列导航)
整套路线共7 个模块、30 课:
| 模块 | 课次 | 内容 |
|---|---|---|
| 一、环境与入门 | 01~05 | 环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环 |
| 二、常用类型与数据组织 | 06~10 | 字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理 |
| 三、函数与函数式 | 11~14 | 函数、Lambda 与高阶函数、闭包、迭代器与惰性序列 |
| 四、面向对象与类型系统 | 15~20 | struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展 |
| 五、工程化与标准库 | 21~25 | cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试 |
| 六、并发编程 | 26~28 | 线程、Channel 通道与同步原语、并发实战 |
| 七、项目实战 | 29~30 | 命令行小工具、GeoJSON 数据处理实战 |
- 环境搭建与第一个仓颉程序
- 变量与常量:let / var 与基本数据类型
- 运算符与标准输入输出
- 分支结构:if 与 match 表达式
- 循环结构:while / for / Range
- 字符串详解与字符串插值
- 数组 Array 与区间 Range
- 集合框架:ArrayList、HashMap、HashSet
- 可空类型
?与 Option - 错误处理:异常机制与 Result
- 函数定义、参数与返回值
- Lambda 与高阶函数
- 闭包、作用域与函数类型
- 迭代器 Iterator 与 Sequence
- 结构体 struct 与类 class
- 构造函数、属性与方法
- 接口 interface 与实现
- 枚举 enum、代数数据类型与 match 模式匹配
- 泛型编程
- 扩展、类型别名与可见性控制(本文)
- cjpm 包管理与多文件项目组织
- 文件与目录 IO
- JSON 处理(结合 stdx 扩展库)
- 网络编程入门
- 单元测试
- 并发基础:线程的创建与等待
- Channel 通道与同步原语
- 并发实战:多线程任务处理
- 实战一:带文件持久化的命令行小工具
- 实战二:GeoJSON 数据处理程序
一、扩展 extend:不改源码,给类型"加装"方法
1.1 第一个扩展:给任意类型加方法
扩展的语法非常直白:extend 类型名 { ... },大括号里写要追加的成员。先看一个自定义类和标准库Int64的例子:
class Counter { var count: Int64 = 0 init(count: Int64) { this.count = count } } // 给已有的 Counter 类追加两个方法 extend Counter { func inc(): Unit { this.count++ } func show(): Unit { println("count = ${this.count}") } } // 给标准库的 Int64 追加方法——它的源码你改不了,但扩展可以 extend Int64 { func twice(): Int64 { return this * 2 } func isEven(): Bool { return this % 2 == 0 } } main(): Int64 { let c = Counter(10) c.inc() c.inc() c.show() println(21.twice()) println(7.isEven()) println(8.isEven()) return 0 }运行结果:
count = 12 42 false true扩展方法的调用方式和普通方法一模一样:对象.方法()。Counter定义在前面、方法"补"在后面,对使用方完全透明——没人看得出来inc是亲生的还是扩展来的。
1.2 扩展计算属性与静态方法
扩展体里除了普通方法,还能写第 16 课的计算属性prop和静态方法static func:
class Temperature { let celsius: Float64 init(celsius: Float64) { this.celsius = celsius } } extend Temperature { // 扩展只读计算属性:华氏度 = 摄氏度 × 1.8 + 32 prop fahrenheit: Float64 { get() { return this.celsius * 1.8 + 32.0 } } // 扩展静态方法:通过 类型名.方法名() 调用 static func boiling(): Temperature { return Temperature(100.0) } } main(): Int64 { let t = Temperature(25.0) println("${t.fahrenheit}") let b = Temperature.boiling() println("${b.fahrenheit}") return 0 }运行结果:
77.000000 212.0000001.3 扩展的四条红线
扩展很自由,但有四条不能碰的红线,全部在编译期拦下。
红线一:不能在扩展里加存储字段。扩展只是"加装行为",不会改变对象的内存布局:
extend Temperature { var note: String = "" }error: unexpected variable declaration in extend body红线二:不能在扩展里加构造函数init。对象必须先被完整构造,扩展无权插手诞生过程:
extend Temperature { init(fahrenheit: Float64) { this.celsius = (fahrenheit - 32.0) / 1.8 } }error: unexpected constructor in extend body红线三:扩展成员不能和原类型的成员同名。扩展是"追加",不是"覆盖"。下面的代码在调用t.label()时会报歧义,编译器同时给出两条错误:
class Temperature { let celsius: Float64 init(celsius: Float64) { this.celsius = celsius } func label(): String { return "原方法" } } extend Temperature { func label(): String { return "扩展方法" } } main(): Int64 { let t = Temperature(25.0) println(t.label()) return 0 }error: extend member 'label' is not allowed to shadow members of 'Class-Temperature' note: shadowed definition of 'label' is here想改原有方法的行为?请回到第 17 课的继承 +open/override,那才是为重写设计的机制。
红线四:扩展在类的外部,碰不到private成员。扩展不是穿墙术:
class Secret { private let password: String = "123456" init() {} } extend Secret { func leak(): String { return this.password } }error: can not access field 'password'📌 扩展必须写在源文件顶层(和 class、func 平级),不能塞进函数体或另一个类里。编译后对整个包生效;跨包使用的规则见第四节。
二、扩展接口与运算符
扩展最有价值的用法,是让一个早已定义好的类型事后获得新能力。
2.1 事后实现接口
第 17 课的接口都是在定义类时一次性挂上去的。有了扩展,可以让一个"出生时没挂接口"的类,事后补上实现:
interface Describable { func describe(): String } class Point { let x: Int64 let y: Int64 init(x: Int64, y: Int64) { this.x = x this.y = y } } // Point 已经定义完了,这里通过扩展让它实现 Describable extend Point <: Describable { public func describe(): String { return "点(${this.x}, ${this.y})" } } func printDesc(d: Describable): Unit { println(d.describe()) } main(): Int64 { let p = Point(3, 4) printDesc(p) // Point 现在是一个合法的 Describable return 0 }运行结果:
点(3, 4)语法是extend 类型 <: 接口 { 实现成员 }。别忘了第 17 课的铁律:接口的实现成员必须写public,否则报the visibility of the base ... is 'public'。
这个能力在实战中极其重要:类型可能来自别人的包、来自标准库,你没法在它定义时加接口,但扩展能让它适配你正在用的体系。
2.2 注意:写了 toString() 不等于实现了 ToString
一个高频踩坑点:在扩展里定义了toString()方法,println却依然不收:
class Vec2 { let x: Int64 let y: Int64 init(x: Int64, y: Int64) { this.x = x this.y = y } } extend Vec2 { public func toString(): String { // 只是一个普通同名方法 return "Vec2(${this.x}, ${this.y})" } } main(): Int64 { let c = Vec2(1, 2) println(c) return 0 }error: mismatched types expected 'Struct-String', found 'Class-Vec2'第 17、18 课强调过:能不能println,看的是类型有没有实现ToString接口,而不是有没有一个叫toString的方法。正确写法是在扩展上显式声明:
extend Vec2 <: ToString { public func toString(): String { return "Vec2(${this.x}, ${this.y})" } }2.3 用扩展加运算符
第 18 课给枚举实现过operator func ==。运算符同样可以在扩展里追加——老类型原地获得运算能力:
extend Vec2 <: ToString { public operator func +(right: Vec2): Vec2 { return Vec2(this.x + right.x, this.y + right.y) } public func toString(): String { return "Vec2(${this.x}, ${this.y})" } } main(): Int64 { let a = Vec2(1, 2) let b = Vec2(10, 20) let c = a + b println(c) return 0 }运行结果:
Vec2(11, 22)一个extend块可以同时挂接口、加运算符、加普通方法。
2.4 扩展 struct(mut)与 enum
struct 的方法想修改自身,第 16 课讲过必须标mut,扩展方法也一样。枚举也能被扩展,第 18 课的 match 正好用来实现:
struct Point3 { var x: Int64 var y: Int64 init(x: Int64, y: Int64) { this.x = x this.y = y } } extend Point3 { mut func translate(dx: Int64, dy: Int64): Unit { this.x += dx this.y += dy } } enum Grade { | A | B | C } extend Grade { func passText(): String { match (this) { case A => "优秀" case B => "良好" case C => "合格" } } } main(): Int64 { var p = Point3(0, 0) p.translate(5, 7) println("(${p.x}, ${p.y})") let g = Grade.B println(g.passText()) return 0 }运行结果:
(5, 7) 良好2.5 泛型扩展
第 19 课的泛型类型照样能扩展,有两种写法。
写法一:把类型参数填成具体类型,只给"某一种元素的容器"加专用方法:
import std.collection.* extend ArrayList<Int64> { func sum(): Int64 { var total = 0 for (x in this) { total += x } return total } } main(): Int64 { let list = ArrayList<Int64>() list.add(1) list.add(2) list.add(3) println("sum = ${list.sum()}") return 0 }运行结果:
sum = 6写法二:保留类型参数,在extend后重新声明<T>,还能带第 19 课的where约束:
struct Box2<T> { let value: T init(value: T) { this.value = value } } extend <T> Box2<T> where T <: ToString { func mapLabel(): String { return "盒装:${this.value}" } } main(): Int64 { let b = Box2<Int64>(42) println(b.mapLabel()) let b2 = Box2<String>("你好") println(b2.mapLabel()) return 0 }运行结果:
盒装:42 盒装:你好方法体里用到了字符串插值,所以约束T <: ToString必不可少——扩展的泛型约束规则与第 19 课完全相同。
三、类型别名 type:给类型起个"业务名"
3.1 基本写法
看这个函数签名:
func transfer(from: Int64, to: Int64, amount: Int64): Bool三个Int64分别是"付款账号、收款账号、金额",但类型层面完全分不出来,把账号当金额传了编译器都不吭声。仓颉用type关键字给类型起别名:
type AccountNo = Int64 type Money = Int64⚠️ 注意仓颉的关键字是
type,不是其他语言常见的typealias。写成typealias Money = Int64会报:error: expected declaration, found 'typealias'
3.2 别名只是"外号",不是新类型
别名不会创造一个新类型,它和原类型在编译器眼里完全等价,可以互相赋值、直接做运算:
type UserId = Int64 func bump(id: UserId): UserId { return id + 1 // UserId 能直接做 Int64 运算 } main(): Int64 { let a: UserId = 100 let b: Int64 = a // 别名赋给原类型 let c: UserId = b // 原类型赋给别名 println("${bump(a)}, ${b}, ${c}") return 0 }运行结果:
101, 100, 100所以别名的价值在可读性和语义约束(提醒自己和同事"这个 Int64 是账号,别拿去算金额"),而不是类型安全。想要"真正不同的类型",请用第 15 课的 struct 包一层。
3.3 函数类型别名与泛型别名
第 12、13 课的函数类型签名可以很长,别名能让它短下来;泛型类型也能起别名:
import std.collection.* // 函数类型别名:一个接收 Int64、返回 Bool 的判断函数 type Filter = (Int64) -> Bool // 泛型别名:键固定为 String 的 HashMap type StringMap<V> = HashMap<String, V> func filterList(items: Array<Int64>, f: Filter): ArrayList<Int64> { var result = ArrayList<Int64>() for (x in items) { if (f(x)) { result.add(x) } } return result } main(): Int64 { let nums = [1, 2, 3, 4, 5, 6] let evens = filterList(nums, { x => x % 2 == 0 }) println("${evens}") let scores = StringMap<Int64>() scores.add("数学", 95) scores.add("语文", 88) println("${scores.size}") return 0 }运行结果:
[2, 4, 6] 23.4 别名只能定义在顶层
类型别名必须和 class、func 一样写在源文件顶层,写在函数体内会报错:
error: unexpected type alias declaration in function body需要跨包使用时,在前面加public:public type Money = Int64(下节实测)。
四、可见性控制:谁能看见什么
第 16 课已经用过private字段 +public方法做封装,第 17 课讲了protected和open。本节把四级可见性系统化,并通过两个工程对打给出跨包实测证据。
4.1 四级可见性总览
| 修饰符 | 类成员的可见范围 | 顶层声明(类/函数/别名) |
|---|---|---|
private | 仅声明它的类型内部 | 仅当前源文件 |
不写(默认internal) | 整个包内可见 | 整个包内可见 |
protected | 包内可见;跨包时仅子类可见 | — |
public | 跨包也能访问 | 跨包也能访问 |
前 19 课所有代码都在一个包里,所以"不写修饰符"一路畅通;一旦代码分成多个包(第 21 课的主题),边界立刻生效。
4.2 private:只允许"自己人"碰
类成员标private,只有这个类自己的方法能访问,同文件的其他类也不行。这是封装的第一道闸门:
class Wallet { private var balance: Int64 = 100 init() {} func deposit(v: Int64): Unit { if (v > 0) { // 存款必须为正:校验逻辑只此一处 this.balance += v } } func show(): Unit { println("余额:${this.balance}") } } class Inspector { init() {} func check(w: Wallet): Int64 { return w.balance // ❌ 别的类想直接读? } }error: can not access field 'balance'把check删掉后正常运行,外部只能走deposit这扇带校验的门,负数存款被自然挡在外面:
余额:150顶层声明加private时,作用域是当前源文件。在文件 A 定义、文件 B 调用,编译器直接当作没这个声明:
error: undeclared identifier 'topSecret'4.3 默认级别 internal:包内公开,包外隐形
什么修饰符都不写时,默认就是internal(显式写internal也可以)。同一个包里谁都能用,所以你在前 19 课从没感觉到它的存在;但包外代码完全看不到它。具体的报错证据在 4.5 节。
经验法则:先默认不写,等确认真的要给别的包用时再改成public——最小暴露原则。
4.4 public 与扩展的可见性
public是最宽的级别。两个细节:
- 接口实现成员必须
public(第 17 课):接口是对外的契约,实现类不能把方法藏起来。 - 扩展成员也受可见性控制:扩展里的方法默认
internal,想让别的包用到就给方法加public。注意修饰符加在成员上,extend关键字前面不能加任何修饰符:
public extend Account { // ❌ 错误写法 public func extPublic(): String { return "公开扩展方法" } }error: expected no modifier before extend declaration, found 'public'正确写法:
extend Account { public func extPublic(): String { // ✅ 成员标 public return "公开扩展方法" } }4.5 跨包实测:两个工程眼见为实
这一小节需要两个 package,这正是第 21 课的主题,这里只需最小配置,先有个感性认识。
第一步:建一个静态库包cjlib20(cjpm init --path cjlib20 --type static),在它的源文件里放这些声明:
package cjlib20 // public 类:包外能看到 public class Account { public init(name: String) { this.name = name } let name: String // 不写修饰符 = internal 字段 internal func internalHint(): String { // internal 方法 return "internal 方法" } public func publicHello(): String { // public 方法 return "你好,${this.name}" } } // 不写 public = internal 类:包外看不到 class InternalThing { init() {} } // 带 protected 方法的 open 类(第 17 课) public open class Device { public init() {} protected func familyName(): String { return "设备家族" } } // 同包的另一个类:internal 成员对它完全开放 public class Neighbor { public init() {} public func useAccount(): String { let a = Account("李四") return "${a.internalHint()}:${a.name}" } } // public 扩展成员:包外也能调用 extend Account { public func extPublic(): String { return "公开扩展方法" } }第二步:在可执行工程cjprobe20的cjpm.toml里加一行本地依赖(第 21 课细讲):
[dependencies] cjlib20 = { path = "../cjlib20" }第三步:合法的跨包使用长这样:
package cjprobe20 import cjlib20.* // 跨包继承:子类内部可以使用 protected 成员 class Phone <: Device { init() { super() } func intro(): String { return "手机属于${this.familyName()}" } } main(): Int64 { println(Account("张三").publicHello()) // public 方法 println(Account("张三").extPublic()) // public 扩展成员 println(Neighbor().useAccount()) // 同包协作的结果对外公开 println(Phone().intro()) // 跨包子类用 protected return 0 }运行结果:
你好,张三 公开扩展方法 internal 方法:李四 手机属于设备家族注意第三行:internalHint()和name虽然对cjprobe20不可见,但它们在库包内部可以被Neighbor自由使用——"包内公开、包外隐形"就是这个意思。
第四步:把越界的访问逐个试一遍,拿到真实报错:
let a = Account("张三") println(a.internalHint()) // ① 包外调用 internal 方法 println(a.name) // ② 包外访问 internal 字段 let t = InternalThing() // ③ 包外使用 internal 类 println(Phone().familyName()) // ④ 类外部调用 protected 方法| 越界操作 | 编译报错 |
|---|---|
| ① internal 方法 | error: 'internalHint' is not a member of class 'Account' |
| ② internal 字段 | error: can not access field 'name' |
| ③ internal 类 | error: undeclared identifier 'InternalThing' |
| ④ 外部调 protected | error: no matching function declaration for function call 'familyName' |
再补两个实测结论:
protected在包内是开放的:库里的Neighbor与Device没有继承关系,照样能调用familyName();只有跨包时才收紧为"仅子类"。所以它比internal宽的地方就在于:跨包的子类仍可使用。- 想让一个类跨包被继承,记得类要标
open、方法要标open(第 17 课),protected通常与open配合使用。
五、实战:订单金额工具箱
把本课三件套串成一个完整程序:用type给金额、订单号加业务语义;用扩展给Int64加"分转元"格式化、给订单实现ToString、给订单列表加汇总;用private把订单状态封装起来,外部只能走受控方法。
package cjprobe20 import std.collection.* // ===== 1. 类型别名:给裸类型加上业务语义 ===== type Money = Int64 // 金额一律以"分"存储,避免浮点误差 type OrderId = String // 订单号 // ===== 2. 给 Int64 扩展"分转元"展示能力(扩展标准库类型) ===== extend Int64 { func yuanText(): String { let fen = this 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} 元" } } // ===== 3. 订单:private 封装内部状态,对外只给受控方法 ===== class Order { private let id: OrderId private var amount: Money private var paid: Bool = false init(id: OrderId, amount: Money) { this.id = id this.amount = amount } // 付款:重复支付会被拒绝,外部无法直接改 paid func pay(): Bool { if (this.paid) { return false } this.paid = true return true } func statusText(): String { if (this.paid) { return "已支付" } return "待支付" } func getId(): OrderId { return this.id } func getAmount(): Money { return this.amount } } // ===== 4. 通过扩展让 Order 实现 ToString,获得打印能力 ===== extend Order <: ToString { public func toString(): String { return "订单(${this.getId()}) ${this.getAmount().yuanText()} [${this.statusText()}]" } } // ===== 5. 给 ArrayList<Order> 扩展"汇总实收金额" ===== extend ArrayList<Order> { func totalPaid(): Money { var total: Money = 0 for (o in this) { // 扩展在类外部,只能走公开方法,碰不到 private 字段 if (o.statusText() == "已支付") { total += o.getAmount() } } return total } } main(): Int64 { println(1250.yuanText()) println(5.yuanText()) println((-308).yuanText()) let orders = ArrayList<Order>() orders.add(Order("NO.1001", 1250)) orders.add(Order("NO.1002", 800)) orders.add(Order("NO.1003", 99)) orders[1].pay() orders[0].pay() for (o in orders) { println(o) } println("实收合计:${orders.totalPaid().yuanText()}") return 0 }运行结果:
12.50 元 0.05 元 -3.08 元 订单(NO.1001) 12.50 元 [已支付] 订单(NO.1002) 8.00 元 [已支付] 订单(NO.1003) 0.99 元 [待支付] 实收合计:20.50 元值得体会的设计:
- 金额用整数分存储,展示层才通过
yuanText()转成两位小数的元——全程用整数运算,避开了浮点表示金额常见的对不齐、除不尽隐患(第 3 课讲过整数除法与Float64()转换)。 paid状态是private,外部无法绕过pay()直接改单;而totalPaid()作为扩展方法,同样只能调用公开的statusText()/getAmount(),封装边界对扩展一视同仁。- 打印能力没有写死在
Order类里,而是通过extend Order <: ToString后加——如果哪天想要另一种展示格式,再扩展一组方法即可,类的核心代码不用动。
六、常见问题 FAQ
Q1:扩展和第 17 课的继承,什么时候用哪个?
两者目的不同:继承表达"是一种"(Dog 是一种 Animal),伴随状态扩展(可加字段)和多态重写(open/override),但要求父类可继承、且继承层次在设计时确定;扩展表达"给现成类型顺手加行为",不能加字段、不能改构造、不能覆盖原方法,但类型来源不受你控制(标准库、第三方包)时只有它能用。经验法则:改不了源码、只想加几个工具方法 → 扩展;需要共享状态与多态 → 继承。
Q2:为什么扩展不允许加字段和构造函数?
扩展是"行为的横向追加",不改变类型的内存布局与诞生过程。如果允许加字段,对象的大小就要随扩展数量变化,已构造的对象如何安放新字段会成为无解的问题。需要新状态,就用组合(类里持有另一个对象)或继承。
Q3:type别名既然和原类型等价,那它有什么用?
作用在"人"而不是"机器":给Int64起名为Money、AccountNo后,函数签名会自我解释,代码评审时一眼能发现"把账号传给了金额参数"这类语义错误;长类型名(如第 13 课的复杂函数类型、嵌套泛型)也能靠别名缩短。它不提供额外的编译期类型隔离,要强隔离请定义 struct。
Q4:扩展里明明写了toString(),为什么还不能 println?
因为println认的是ToString接口,不是方法名。在扩展上显式声明extend T <: ToString并把方法标为public即可(见 2.2 节)。
Q5:internal和protected到底差在哪?
包内使用时两者几乎无差别;差别只在跨包:internal成员出了包谁都看不见;protected成员对跨包的子类仍然可见。所以写库时,希望允许使用者通过继承来定制的钩子方法用protected,纯内部实现细节用默认/internal 或private。
Q6:我在库里写的扩展方法,别的包为什么点不出来?
扩展成员默认是internal,包外不可见,报错形如'xxx' is not a member of class 'Yyy'。给扩展里的方法加public;注意extend关键字本身前面不能加修饰符(会报expected no modifier before extend declaration)。
Q7:一个类型可以被 extend 多次吗?多个扩展定义了同名方法会怎样?
可以多次扩展,同一个类型在不同extend块里追加方法是常见做法。但任何两个同名同签名方法都不允许——无论其中一个来自原类型还是另一个扩展,都会触发 shadow/歧义报错。方法命名建议带业务前缀,降低冲突概率。
七、课后练习
- 给
Int64扩展clamp(low: Int64, high: Int64): Int64:小于下限返回下限,大于上限返回上限,中间原样返回。测试5.clamp(10, 20)、50.clamp(10, 20)、15.clamp(10, 20)。 - 回顾第 15 课定义的
Person类(或任意一个你写过的类),在不修改原类的前提下,用扩展给它实现ToString,使println(person)能直接输出可读信息;再故意去掉<: ToString观察报错。 - 给
ArrayList<Int64>扩展average(): Float64(空列表返回0.0),结合第 8 课的size与第 3 课的整数转浮点数(Float64(x))实现;用[70, 80, 90]验证结果为80.000000。 - 定义
type Score = Int64和一个Student2类(private let name: String、private var scores: ArrayList<Score>),对外提供addScore(拒绝负数)、highest(): Score、report(): String;思考哪些成员必须private、哪些方法要公开。 - 挑战:用泛型扩展给任意
ArrayList<T>加second(): Option<T>(元素不足两个时返回None),分别用ArrayList<Int64>和ArrayList<String>验证;再尝试给它加max()并想想需要什么where约束(提示:参考第 19 课的Comparable<T>)。
下节预告
前 20 课的代码都活在一个单独的src/main.cj里,靠import std.*使用标准库。但真实项目必然要拆文件、分包、复用别人写的库——就像本课 4.5 节提前露面的[dependencies]。第 21 课cjpm 包管理与多文件项目组织将正式讲解:一个工程的目录结构、多文件如何协作、如何把自己的代码打成库包、如何通过本地路径和远程仓库引用第三方包,从"单文件练习"迈进"工程化开发"。
系列说明:本系列基于 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