仓颉编程语言青少年基础教程:class(类)(上)
仓颉编程语言青少年基础教程:class(类)(上)
class (类)类型是面向对象编程中的经典概念,仓颉中同样支持使用 class 来实现面向对象编程。class 与 struct 的主要区别在于:class 是引用类型,struct 是值类型,它们在赋值或传参时行为是不同的;class 之间可以继承,但 struct 之间不能继承。注意:class 只能定义在源文件的顶层作用域。
class 是面向对象编程的核心类型,与 以前介绍的struct(结构)类型 的核心区别在于:
引用类型:赋值或传参时传递引用(多个变量指向同一对象);
支持继承:类之间可通过单继承复用代码。
class 类型
class 类型的定义以关键字 class 开头,后跟 class 的名字,接着是定义在一对花括号中的 class 定义体。class 定义体中可以定义一系列的成员变量(member variables)、成员属性(member properties)、静态初始化器(static initializers)、构造函数(constructors)、成员函数(member functions)和操作符函数(operator functions)。
【关于Property(属性)的说明
在仓颉语言(Cangjie)的官方文档中,统一使用 Property 来表示“属性”。
属性(Properties)提供了一个 getter 和一个可选的 setter 来间接获取和设置值。
仓颉语言的class 定义体中
数据成员变量不像有些语言那样称为attribute。
成员属性就是指属性(Properties)。
术语 | 英文 | 定义 |
属性 | Property | 通过getter/setter间接访问的成员,提供计算、验证等逻辑 |
成员变量 | Member Variable | 直接存储数据的普通字段(无getter/setter) |
attribute | ❌ 非官方术语 | 在仓颉中不适用(其他语言如Python的术语) |
】
注意:class 只能定义在源文件的顶层作用域。class是引用类型,定义为引用类型的变量,变量名中存储的是指向数据值的引用,因此在进行赋值或函数传参等操作时,拷贝的是引用。
class 定义与使用
先给出示例,以便有直观的认识,接着总结给出语法。
class 定义与基础示例
class 以 class 关键字开头,定义体包含成员变量、构造函数、成员函数等。
基础示例:定义一个矩形类:
class Rectangle {// 实例成员变量(需在构造函数中初始化)let width: Int64let height: Int64// 普通构造函数:初始化所有实例变量public init(width: Int64, height: Int64) {this.width = width // this 指代当前对象this.height = height}// 实例成员函数:计算面积public func area(): Int64 {width * height // 可省略 this,也可写为:this.width * this.height}
}main(){// 创建对象(调用构造函数)let rect = Rectangle(10, 20)print(rect.area()) // 输出:200
}
对上面示例改进:演示了静态成员、主构造函数、实例方法、引用语义等,源码:
class Rectangle {// 静态计数器static var count: Int64 = 0// 主构造函数:参数前加 let/var 即自动成为成员变量public Rectangle(public var width: Int64, public var height: Int64) {// 不需要再写 this.width = widthRectangle.count += 1}// 计算面积public func area(): Int64 {width * height}// 打印信息public func printInfo(): Unit {println("Rectangle ${width}×${height}, area=${area()}, total=${Rectangle.count}")}
}// 程序入口
main(): Int64 {let r1 = Rectangle(3, 4)r1.printInfo() // Rectangle 3×4, area=12, total=1let r2 = Rectangle(5, 6)r2.printInfo() // Rectangle 5×6, area=30, total=2// 引用语义:r3 与 r2 指向同一对象let r3 = r2r3.width = 10r2.printInfo() // Rectangle 10×6, area=60, total=2return 0
}
类使用 'class' 关键字定义,语法:
// 访问修饰符(可选,默认internal) + 继承修饰符(open/sealed,非抽象类需open才允许继承)
[访问修饰符] [open/sealed] class 类名 [<: 父类名>] {
// 1. 成员变量(实例变量/静态变量)
// ①实例成员变量
[修饰符] let/var 实例变量名: 类型 [= 初始值];
[修饰符] static let/var 静态变量名: 类型 [= 初始值];
// ②静态成员变量
// 2. 静态初始化器(可选,仅初始化静态变量,最多1个)
static init() {
静态变量赋值; // 必须初始化所有未设初始值的静态变量
}
// 3. 构造函数(普通构造/主构造,初始化实例变量)
// ①普通构造:init开头;可重载;必须先初始化所有未赋值实例成员
[访问修饰符] init(参数列表) {
this.实例变量 = 参数; // 必须初始化所有未设初始值的实例变量
}
// ②主构造:与类名相同(简化定义,参数可直接定义成员变量:参数前可加 let/var 一次性声明+初始化)
[访问修饰符] 类名(let/var 参数1: 类型, let/var 参数2: 类型) {
// 无需重复定义成员变量,参数已自动成为实例变量
}
// 4. 成员函数(实例函数/静态函数)
// ①实例函数
[访问修饰符] func 实例函数名(参数列表) [-> 返回类型] {
函数体; // 可通过this访问实例变量
}
// ②静态函数
[访问修饰符] static func 静态函数名(参数列表) [-> 返回类型] {
函数体; // 仅可访问静态变量/函数,不可访问实例成员
}
// 5. 终结器(可选,GC回收时调用,释放资源;无修饰符、不能手动调用)
~init() {
资源释放逻辑; // 如释放指针、关闭文件等
}
}
注意:
①继承修饰符
非抽象类想被继承必须写 open;想“只能本包继承”就写 sealed(同时隐含 open)。
抽象类默认可继承,因此写不写 open 都行,但仍可再加 sealed 限制包内继承。
②构造函数“链式调用”约束
普通构造里只能在第一行写 super(...) 或 this(...),二者只能出现其一;主构造只能用 super(...),不能用 this(...)。
③终结器限制(再强调一次)
带 ~init 的类不允许再加 open 修饰;终结器不能手动调用,也不能放在 extend 里。
④使用 abstract 修饰的类为抽象类,与普通类不同的是,在抽象类中除了可以定义普通的函数,还允许声明抽象函数(没有函数体)。抽象类定义时的 open 修饰符是可选的,也可以使用 sealed 修饰符修饰抽象类,表示该抽象类只能在本包被继承。
创建对象语法:
①基本语法,类名 + 构造参数(参数需匹配类中的构造函数)
let/var 变量名 = 类名(参数1, 参数2, ...)
②如果构造函数有命名参数,可以使用命名参数语法
let/var 变量名 = 类名(参数名1: 值1, 参数名2: 值2, ...)
类的成员包括实例成员(实例变量、实例函数)和静态成员(静态变量、静态函数),访问方式不同:
①实例成员:通过实例对象访问,语法实例.成员名
②静态成员:通过类名访问,语法类名.成员名
上述内容是概括性的对新手而言,不必期望一下子理解,可先大体理解,逐步体会学习,多看示例和动手实践,多回头看几次,就会逐步深入理解。
示例1:
class Counter {var value: Int64 = 0 // 实例变量static var total: Int64 = 0 // 静态变量public func inc() { // 实例方法value += 1Counter.total += 1 // 静态变量通过类名访问}public static func resetAll() {// 静态方法total = 0}
}main(): Int64 {// 1. 创建对象(引用)let c1 = Counter()let c2 = Counter()// 2. 访问实例成员c1.inc()c1.inc()c2.inc()println("c1.value = ${c1.value}") // 2println("c2.value = ${c2.value}") // 1println("Counter.total = ${Counter.total}") // 3// 3. 静态方法调用Counter.resetAll()println("after reset: ${Counter.total}") // 0// 4. 引用别名演示let c3 = c1 // 只是别名,不是新对象c3.value = 99println("c1.value = ${c1.value}") // 99return 0
}
编译运行截图:
示例2:
class Student {// 主构造函数:用let声明的参数直接成为成员变量(无需单独定义)public Student(let name: String, let grade: Int64, var score: Float64) {// 无需写 this.name = name 等赋值语句// 因为let/var参数已自动作为成员变量}// 实例方法:打印学生信息public func showInfo() {println("姓名:${name},年级:${grade},分数:${score}")}// 实例方法:修改分数(score是var变量,可修改)public func updateScore(newScore: Float64) {score = newScore}
}// 程序入口
main() : Int64 {// 创建Student对象(参数直接初始化成员变量)let student = Student("李四", 3, 85.5)// 调用方法展示信息student.showInfo() // 访问主构造函数定义的成员变量// 修改分数并再次展示student.updateScore(92.0)student.showInfo()return 0
}
运行输出
姓名:李四,年级:3,分数:85.500000
姓名:李四,年级:3,分数:92.000000
抽象类(abstract class)
使用 abstract 修饰的类为抽象类,与普通类不同的是,在抽象类中除了可以定义普通的函数,还允许声明抽象函数(没有函数体)。抽象类定义时的 open 修饰符是可选的,也可以使用 sealed 修饰符修饰抽象类,表示该抽象类只能在本包被继承。
用 abstract 修饰的类,可包含抽象函数(无函数体,子类必须实现)。注意:
抽象类中禁止定义 private 的抽象函数;
抽象类不能实例化;
抽象类的非抽象子类必须实现父类中的所有抽象函数。
示例:抽象形状类,源码:
// 抽象类:定义抽象方法 area
abstract class Shape {public func area() : Float64 // 无函数体
}// 子类:矩形(实现抽象方法)
class Rectangle <: Shape {let width: Float64let height: Float64init(width: Float64, height: Float64) {this.width = widththis.height = height}// 必须实现父类的抽象方法public override func area(): Float64 {width * height}
}// 子类:圆形(实现抽象方法)
class Circle <: Shape {let radius: Float64init(radius: Float64) {this.radius = radius}public override func area(): Float64 {3.14 * radius * radius // 简化计算(π≈3.14)}
}main(){let rect = Rectangle(5.0, 4.0)let circle = Circle(2.0)println(rect.area()) // 输出:20.000000println(circle.area()) // 输出:20.000000
}
class的成员说明
成员变量(实例变量 vs 静态变量)
• 实例变量:属于对象,通过对象访问;定义时可无初值(需在构造函数中初始化)。
• 静态变量:属于类,用 static 修饰,通过类名访问;无静态初始化器时必须有初值。
示例
class Student {// 实例变量:每个学生有自己的姓名var name: String// 静态变量:所有学生共享的学校名称(无初始化器时必须有初值)static let school: String = "阳光小学"// 静态变量:通过静态初始化器初始化(必须可变才能计数)static var totalCount: Int64static init() { // 静态初始化器:初始化未赋值的静态变量totalCount = 0}init(name: String) {this.name = nameStudent.totalCount += 1 // 实例函数中访问静态变量}
}main(){// 访问静态变量(通过类名)print(Student.school) // 输出:阳光小学// 创建对象(访问实例变量)let s1 = Student("小明")let s2 = Student("小红")println(s1.name) // 输出:小明println(Student.totalCount) // 输出:2(两个学生被创建)
}
静态初始化器
静态初始化器只在类第一次被用到时执行一次,专门给静态成员变量做“晚初始化”。
• 关键字 static init(),后跟无参参数列表和函数体、最多 1 个。
• 函数体里必须把所有尚未赋初值的静态成员全部赋完,否则编译报错。
示例:
// 定义一个带有静态初始化器的类
class MyClass {public static let constantValue: Int64 // ① 显式加修饰符public static var mutableValue: Stringlet instanceValue: Int64 = 42 // 实例成员// ② 静态初始化器,必须初始化所有未赋初值的静态成员static init() {constantValue = 100mutableValue = "Initialized"println("Static initializer for MyClass called.")}// 实例方法public func printValues() {println("Instance value: ${instanceValue}")println("Static constant: ${constantValue}")println("Static mutable: ${mutableValue}")}
}main(): Int64 {// 第一次访问静态成员 → 触发 static init()println("MyClass constant: ${MyClass.constantValue}")println("MyClass mutable: ${MyClass.mutableValue}")// 创建类的实例let obj1 = MyClass()obj1.printValues()// 再次访问,不再触发println("Accessing static members again:")println("MyClass constant: ${MyClass.constantValue}")return 0
}
编译运行输出:
Static initializer for MyClass called.
MyClass constant: 100
MyClass mutable: Initialized
Instance value: 42
Static constant: 100
Static mutable: Initialized
构造函数(普通构造函数与主构造函数)
• 普通构造函数:用 init 定义,需初始化所有实例变量,支持重载(参数列表不同)。一个类中可以定义多个普通构造函数,但它们必须构成重载,否则报重定义错误。
• 主构造函数:与类名同名,参数前加 let/var 可直接定义成员变量,简化代码。
示例 1:普通构造函数重载
class Rectangle {let width: Int64let height: Int64// 构造函数1:接收宽和高init(width: Int64, height: Int64) {this.width = widththis.height = height}// 构造函数2:正方形(宽高相等)init(side: Int64) { // 重载:参数不同this.width = sidethis.height = side}// 计算面积 public func area(): Int64 {width * height}
}main(){let rect1 = Rectangle(3, 4)let square = Rectangle( 5)println(rect1.area()) // 输出:12println(square.area()) // 输出:25
}
示例 2:主构造函数
主构造函数的名称与类名相同,参数列表中可以直接定义成员变量——简化代码。
class Rectangle {// 主构造函数:直接定义成员变量width和heightpublic Rectangle(let width: Int64, let height: Int64) {}// 构造函数重载:通过this调用主构造函数(正方形)init(side: Int64) {this(side, side) // 调用主构造函数,宽高均为side}// 计算面积public func area(): Int64 {width * height}
}main() {// 调用主构造函数创建矩形let rect1 = Rectangle(3, 4)// 调用重载构造函数创建正方形let square = Rectangle(5)println(rect1.area()) // 输出:12println(square.area()) // 输出:25
}
创建对象时,构造函数执行步骤:
1.初始化主构造函数外有默认值的实例变量;
2.若未显式调用父类 / 本类其他构造函数,自动调用父类无参构造函数 super();
3.执行当前构造函数体。
构造函数执行步骤示例1:
func trace(msg: String) { println(msg) }open class Base {init() {trace("Base构造函数体")}
}class Derived <: Base {// 步骤1: 初始化变量var a = trace("初始化变量a")var b = trace("初始化变量b")// 主构造函数public Derived() {// 步骤2: 自动插入 super()// 步骤3: 执行构造函数体trace("3.Derived构造函数体")}
}main(){// 测试执行let obj = Derived()
}
/* 输出:
初始化变量a
初始化变量b
Base构造函数体
3.Derived构造函数体
*/
构造函数执行步骤示例2:构造函数执行顺序,构造函数链 + super / this 调用顺序验证:
open class Base {let tag: Stringinit() { tag = "Base-default"; println("Base()") }init(t: String) { tag = t; println("Base(${t})") }
}class Child1 <: Base {let v: Int64init(v: Int64) {super("Child1") // 显式调用父类构造this.v = vprintln("Child1(${v})")}
}class Child2 <: Base {init() {this(99) // 调用本类其他构造}init(v: Int64) {super() // 编译器不会自动插入 super()println("Child2(${v})")}
}main() {let _ = Child1(42)let _ = Child2()
}
/* 输出顺序:
Base(Child1)
Child1(42)
Base-default
Child2(99)
*/
成员函数(实例函数 vs 静态函数)
• 实例函数:通过对象调用,可访问实例变量和静态变量;
• 静态函数:用 static 修饰,通过类名调用,不能访问实例变量
示例:实例函数与静态函数
class MathUtil {// 静态函数:计算平方(无实例依赖)static func square(x: Int64): Int64 {x * x}// 实例变量:当前数值var value: Int64init(value: Int64) {this.value = value}// 实例函数:计算当前值与参数的和(依赖实例变量)func add(x: Int64): Int64 {value + x}
}main() {// 调用静态函数(类名访问)println(MathUtil.square(5)) // 输出:25// 调用实例函数(对象访问)let math = MathUtil(10)println(math.add(3)) // 输出:13
}
class终结器(~init)
终结器(finalizer),对象被垃圾回收时调用,用于释放资源(如文件句柄、内存)。
• 无参数、无返回值,不能显式调用;
• 带终结器的类不能用 open 修饰;
• 触发时机不确定。
开发者通常不需要手动定义终结器。有人说,对于普通开发者:
不需要主动定义 ~init:日常业务开发中,依赖自动内存管理和显式资源管理模式即可,避免因终结器的不确定性引入问题。
了解其存在即可:知道 ~init 是对象销毁前的钩子,在阅读底层库代码或处理特殊资源时,能理解其作用即可。
引用类型特性
class 是引用类型,赋值或传参时传递引用(多个变量指向同一对象)。
示例:引用传递
class Counter {var count: Int64 = 0func increment() {count += 1}
}main(){let c1 = Counter()let c2 = c1 // c2 与 c1 指向同一对象c1.increment() println(c1.count) // 输出:1println(c2.count) // 输出:1(c1修改影响c2)
}
class 成员的访问修饰符
控制成员的可见性,共 4 种
修饰符 可见范围
private 仅当前类内部
internal 当前包及子包(默认)
protected 当前模块及当前类的子类
public 模块内外均可见
【在仓颉(Cangjie)语言里,包(package) 与 模块(module) 的对应关系可以一句话总结为:
一个模块 = 一个 cjpm (Cangjie Package Manager:仓颉包管理器)项目 = 一个仓颉独立的构建和分发单元。
包(package)只是模块内部的“命名空间”层级,用来组织源码目录。】
protected 和 public 的区别
protected
• 不仅是“能访问”,还隐含“可继承”——子类可以覆写该成员。
• 同时受“模块”限制:别的模块即使继承了该类,也看不见 protected 成员;
• 只有同一个模块内的子类才能继承/覆写。
public
• 只是“能看见”,不隐含可继承;
• 任何模块都能看见,但覆写仍需要 open + override。
• 没有模块边界限制,跨模块也能用。
【在仓颉里,“跨模块”就是跨 cjpm 项目。
你可以把 A 项目编译成 .cjpm(库),然后在 B 项目里把它声明成依赖,就能“跨模块”使用。】
示例:访问修饰符效果
包结构如下:
demo8
│
src/
├── main.cj // 入口
└── business/ // business包
├── finance.cj // business包的finance.cj
└── utils.cj // business包的utils.cj
1. business/utils.cj 的源码:
package demo8.business// 声明为public 和open 才能被其他包访问
public open class PaymentProcessor {private var transactionId: Int64 = 0internal var merchant = "Default"protected var feeRate: Float64 = 0.03public var currency = "USD"
}// 内部工具类保持 internal
internal class SecurityUtils {public func encrypt(data: String): String {return "ENCRYPTED:${data}"}
}
2. business/finance.cj 的源码:
package demo8.business// 声明为 public
public class CreditProcessor <: PaymentProcessor {public func process(amount: Float64) {let fee = amount * feeRate println("收取 ${fee} ${currency}")merchant = "CreditCo" let utils = SecurityUtils()println(utils.encrypt("敏感数据"))}
}
3.主程序入口 (main.cj)
package demo8import demo8.business.PaymentProcessor // 导入business包的类PaymentProcessor
import demo8.business.CreditProcessor // 导入business包的类 CreditProcessormain() {// 场景1:直接使用基类let base = PaymentProcessor() // 创建基类实例println(base.currency) // ✓ public: 输出 "USD"// base.merchant = "Shop" // ✗ internal: 跨包不可访问//println(base.feeRate) //// 场景2:使用子类let credit = CreditProcessor() // 创建子类实例credit.process(100.0) // ✓ 子类内部可访问protected 。 (要有添加小数点)
}
要注意import的路径。运行输出:
USD
收取 3.000000 USD
ENCRYPTED:敏感数据
This 类型
在类内部,支持 This 类型占位符,代指当前类的类型。它只能被作为实例成员函数的返回类型来使用,当使用子类对象调用在父类中定义的返回 This 类型的函数时,该函数调用的类型会被识别为子类类型,而非定义所在的父类类型。
如果实例成员函数没有声明返回类型,并且只存在返回 This 类型表达式时,当前函数的返回类型会推断为 This。
在实例方法返回类型中使用,代表当前类类型(子类调用时返回子类类型)。
注意:
1.this 指代当前对象的实例,因此只能在实例函数、构造函数、实例属性的 getter/setter 中使用;静态成员(静态函数、静态属性)中不能使用 this(静态成员属于类,不属于某个实例)。
2.不能在类外部使用:this 仅在类的实例成员内部有意义,类外部无法使用编译报错。
this的作用
1.指代“当前对象”
在实例成员函数、属性 getter/setter 内部,
用 this.成员名 明确访问的是“本对象”的成员,避免与局部变量/参数重名冲突。
(不写 this. 也能访问,但加上可读性更高。)
2.当作 This 类型占位符
只能出现在实例成员函数的返回类型位置,
表示“返回当前运行时对象的实际类型”,而不是声明时所在的类名。
常用于链式调用或“返回自身”的接口。
示例 1 :区分参数与成员变量,消除名字冲突
class Person {let name: String; // 成员变量var age: Int64; // 成员变量// 构造函数参数与成员变量同名(name、age)public init(name: String, age: Int64) {// this.name 明确指向成员变量,而非参数namethis.name = name; // this.age 明确指向成员变量,而非参数agethis.age = age; }// 实例函数:打印信息(参数与成员变量同名)public func printInfo(name: String) {// this.name 指成员变量name,name指参数nameprintln("成员name:${this.name},参数name:${name}");println("年龄:${this.age}");}
}main(): Int64 {let p = Person("张三", 20);p.printInfo("李四"); // 传入参数name="李四"return 0;
}
编译运行结果:
成员name:张三,参数name:李四
年龄:20
示例 2 :This 类型做返回值,支持链式方法调用
class StringBuilder {private var content: String = ""public func append(s: String): This { // 返回类型为 This,通常代表当前类型content += sreturn this // 返回当前对象,以便进行链式调用}public func newLine(): This {content += "\n"return this}public func toString(): String {content}
}main(): Int64 {let sb = StringBuilder()let result = sb.append("Hello").append(" World!").newLine().append("仓颉语言").toString()println(result)return 0
}
编译运行结果:
Hello World!
仓颉语言