【仓颉语言入门 · 第20课】
【仓颉语言入门 · 第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.000000
1.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]
2
3.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
更多推荐




所有评论(0)