【仓颉语言入门 · 第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~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 数据处理程序

一、扩展 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 是最宽的级别。两个细节:

  1. 接口实现成员必须 public(第 17 课):接口是对外的契约,实现类不能把方法藏起来。
  2. 扩展成员也受可见性控制:扩展里的方法默认 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'
④ 外部调 protectederror: 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/歧义报错。方法命名建议带业务前缀,降低冲突概率。


七、课后练习

  1. 给 Int64 扩展 clamp(low: Int64, high: Int64): Int64:小于下限返回下限,大于上限返回上限,中间原样返回。测试 5.clamp(10, 20)、50.clamp(10, 20)、15.clamp(10, 20)。
  2. 回顾第 15 课定义的 Person 类(或任意一个你写过的类),在不修改原类的前提下,用扩展给它实现 ToString,使 println(person) 能直接输出可读信息;再故意去掉 <: ToString 观察报错。
  3. 给 ArrayList<Int64> 扩展 average(): Float64(空列表返回 0.0),结合第 8 课的 size 与第 3 课的整数转浮点数(Float64(x))实现;用 [70, 80, 90] 验证结果为 80.000000。
  4. 定义 type Score = Int64 和一个 Student2 类(private let name: String、private var scores: ArrayList<Score>),对外提供 addScore(拒绝负数)、highest(): Score、report(): String;思考哪些成员必须 private、哪些方法要公开。
  5. 挑战:用泛型扩展给任意 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
Logo

一站式 AI 云服务平台

更多推荐