【仓颉语言入门 · 第16课】
【仓颉语言入门 · 第16课】构造函数、属性与方法
第 15 课我们把 struct/class 的骨架搭好了,知道了值类型和引用类型的根本差异。但这还不够——写真实项目时,每个类型都要回答这些问题:构造函数有几种?字段的默认值怎么给?属性有没有 getter/setter?方法要不要被外部调用?静态成员怎么共享?本课把第 15 课埋下的所有伏笔一次性讲透。
本文所有代码与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。
⚠️ 勘误:第 15 课说"struct 的方法不能修改
this,仓颉没有mutating关键字"——这是不准确的。仓颉一直用mut关键字实现这个能力(public mut func),从早期版本就支持,本课第四节会讲清楚。第 15 课的错误结论会在本课同步更正。
目录(系列导航)
整套路线共 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 数据处理程序
一、构造函数:init 与重载
第 15 课写过一个最基本的构造函数:
struct Point {
var x: Int64
var y: Int64
init(x: Int64, y: Int64) {
this.x = x
this.y = y
}
}
现实项目中,一个类型往往有多种初始化方式。仓颉的解决方案是构造函数重载——一个类型里可以定义多个 init,只要参数列表不同就行。
1.1 重载构造函数
class Point {
var x: Int64
var y: Int64
// 无参构造:默认原点
init() {
this.x = 0
this.y = 0
}
// 单参数构造:对称点
init(v: Int64) {
this.x = v
this.y = v
}
// 双参数构造:任意坐标
init(x: Int64, y: Int64) {
this.x = x
this.y = y
}
}
main(): Int64 {
let p1 = Point() // (0, 0)
let p2 = Point(5) // (5, 5)
let p3 = Point(3, 4) // (3, 4)
println("p1(${p1.x}, ${p1.y}), p2(${p2.x}, ${p2.y}), p3(${p3.x}, ${p3.y})")
return 0
}
p1(0, 0), p2(5, 5), p3(3, 4)
编译器根据你传几个参数、什么类型,自动匹配到对应的 init。和函数重载(第 11 课)的规则一样。
1.2 构造函数委托:this()
多个构造函数之间可能有重复逻辑。仓颉支持在一个构造函数里调用另一个构造函数,用 this(参数):
class Point {
var x: Int64
var y: Int64
init() {
this(0, 0) // 委托给双参数构造
}
init(v: Int64) {
this(v, v) // 委托给双参数构造
}
init(x: Int64, y: Int64) {
this.x = x
this.y = y
}
}
📌
this()必须出现在构造函数体的第一行,且只能调一次。不能搞循环委托(A 调 B、B 调 A),编译器会检查。
1.3 默认参数值
另一种减少重载的方式是给参数默认值。仓颉的默认参数必须用命名参数形式——参数名前加 !:
class Point {
var x: Int64
var y: Int64
init(x!: Int64 = 0, y!: Int64 = 0) {
this.x = x
this.y = y
}
}
main(): Int64 {
let p1 = Point() // (0, 0)
let p2 = Point(x: 5) // (5, 0) ——只传 x,y 取默认值
let p3 = Point(x: 3, y: 4) // (3, 4)
return 0
}
0,0
5,0
3,4
参数有默认值后,调用时可以省略。注意:默认参数必须放在参数列表末尾(和函数默认参数规则一致)。
1.4 字段默认值 + 无参构造
如果所有字段都有默认值,可以不写 init,编译器自动生成无参构造函数:
class Config {
var debug: Bool = false
var port: Int64 = 8080
var host: String = "localhost"
}
main(): Int64 {
let cfg = Config() // 所有字段取默认值
println("${cfg.host}:${cfg.port}, debug=${cfg.debug}")
return 0
}
localhost:8080, debug=false
⚠️ 如果任何一个字段没有默认值,就必须显式写
init,否则编译报错:'x' is not initialized in the constructor。
二、主构造函数:最简写法
仓颉还有一种更简洁的构造函数形式——主构造函数(primary constructor),和普通 init 的区别在于:
- 函数名和类型名相同(不是
init) - 参数列表里可以直接用
let/var声明成员变量形参,它既是构造参数,也是字段声明
2.1 class 的主构造函数
class Person {
// 主构造函数:Person 就是函数名
public Person(let name: String, var age: Int64) {}
}
main(): Int64 {
let p = Person("Alice", 30)
println("${p.name}, ${p.age}")
// p.age = 31 // ✅ age 是 var,可改
// p.name = "Bob" // ❌ name 是 let,不能改
return 0
}
Alice, 30
一行 public Person(let name: String, var age: Int64) {} 同时完成了三件事:
- 声明了一个
let name: String字段(不可变) - 声明了一个
var age: Int64字段(可变) - 定义了一个
Person(name, age)构造函数,自动把参数赋给字段
2.2 struct 的主构造函数
struct 同样支持:
import std.math.*
struct Vec2 {
public Vec2(let x: Float64, let y: Float64) {}
public func length(): Float64 {
return sqrt(this.x * this.x + this.y * this.y)
}
}
main(): Int64 {
let v = Vec2(3.0, 4.0)
println("length = ${v.length()}")
return 0
}
length = 5.000000
2.3 主构造函数 + 额外逻辑
如果主构造函数里需要额外逻辑,直接在大括号里写:
class Rectangle {
public Rectangle(let width: Float64, let height: Float64) {
if (width <= 0.0 || height <= 0.0) {
throw Exception("边长必须为正数")
}
}
}
📌 一个类型里最多只能有一个主构造函数(否则报重定义),但可以和多个普通
init共存。例如:主构造函数负责常规初始化,init()负责无参默认值。
三、属性 prop:封装字段的艺术
前面我们一直在直接暴露字段:p.name、p.age。实际项目中,字段往往不能随便改——比如年龄不能是负数、余额不能为负。仓颉用 属性(prop) 解决这个问题。
3.1 只读属性(getter only)
class Circle {
private let radius: Float64
init(radius: Float64) {
this.radius = radius
}
public prop area: Float64 {
get() {
return 3.14159 * this.radius * this.radius
}
}
}
main(): Int64 {
let c = Circle(5.0)
println("area = ${c.area}") // 像字段一样访问,但只读
return 0
}
area = 78.539750
private let radius:真正的字段被隐藏public prop area:对外暴露一个"伪字段",访问时调用get()计算- 外部只能读
c.area,不能写
3.2 读写属性(getter + setter)
需要外部可写的属性,用 mut prop:
class Account {
private var balanceValue: Float64
init() {
this.balanceValue = 0.0
}
public mut prop balance: Float64 {
get() { return this.balanceValue }
set(v) {
if (v >= 0.0) {
this.balanceValue = v
} else {
throw Exception("余额不能为负数")
}
}
}
}
main(): Int64 {
let a = Account()
a.balance = 100.0 // 调用 setter
println("余额: ${a.balance}") // 调用 getter
// a.balance = -50.0 // ❌ 抛出异常:余额不能为负数
return 0
}
余额: 100.000000
关键点:
public mut prop表示"可变的属性"(外部可读写)get()和set(v)里的v是传入的值- setter 里可以加任意校验逻辑——这就是封装的意义
3.3 prop 不需要 backing field
属性的底层字段(如 balanceValue)不是必须的。如果属性本身就是计算出来的,可以不存:
class Temperature {
private var celsius: Float64
init(celsius: Float64) {
this.celsius = celsius
}
public prop celsiusValue: Float64 {
get() { return this.celsius }
}
public mut prop fahrenheit: Float64 {
get() { return this.celsius * 1.8 + 32.0 }
set(f) { this.celsius = (f - 32.0) / 1.8 }
}
}
main(): Int64 {
let t = Temperature(25.0)
println("华氏度: ${t.fahrenheit}") // 77.0
t.fahrenheit = 98.6
println("摄氏度: ${t.celsiusValue}") // 37.0
return 0
}
华氏度: 77.000000
摄氏度: 37.000000
fahrenheit 没有自己的存储空间,每次访问都实时计算。写入时反向换算存到 celsius 里。
四、mut 关键字:struct 方法改自己的正确姿势
第 15 课我们说过:struct 的普通方法不能修改 this 的字段,想改变就得返回新值。这个结论本身没错——但它只说了"普通方法",漏掉了仓颉真正的解决方案。
4.1 mut 方法:原地修改 struct
仓颉用 mut 关键字修饰实例方法,允许它原地修改 struct 的字段:
struct Point {
var x: Int64
var y: Int64
init(x: Int64, y: Int64) {
this.x = x
this.y = y
}
public mut func moveBy(dx: Int64, dy: Int64) {
this.x += dx
this.y += dy
}
public func toString(): String {
return "(${this.x}, ${this.y})"
}
}
main(): Int64 {
var p = Point(1, 2)
p.moveBy(10, 20) // ✅ 原地修改
println(p.toString())
return 0
}
(11, 22)
public mut func moveBy(...):加了 mut 后,方法里可以改 this 的字段。
4.2 调用 mut 方法的规则
let p = Point(1, 2)
// p.moveBy(10, 20) // ❌ 编译错误:let 绑定的实例不能调用 mut 方法
var q = Point(1, 2)
q.moveBy(10, 20) // ✅ var 绑定才能调用
📌 只有
var绑定的实例才能调用mut方法。let绑定意味着"值不可变",mut方法会破坏这个承诺,编译器直接拒绝。这和 class 不同:class 的let绑定只是引用不可变,对象内部该改还能改(第 15 课)。
4.3 class 方法不需要 mut
class 的方法默认就能改字段(引用类型本来就能改),不允许加 mut:
class Person {
var age: Int64 = 0
public func growOlder() {
this.age += 1 // ✅ 直接改,不需要也不能加 mut
}
}
mut 是值类型专属的修饰符(struct、interface、struct 扩展),解决 struct "普通方法不能改 this" 的问题。写 class 里会报编译错误。
五、可见性控制:谁能访问什么
仓颉有四种访问修饰符,控制字段、方法、构造函数的可见范围:
| 修饰符 | 可见范围 |
|---|---|
public |
任何地方都能访问(默认如果不写,包内可见) |
protected |
当前类及其子类(第 17 课讲继承) |
private |
仅当前类/struct 内部 |
internal |
当前包及子包内可见 |
5.1 private 字段 + public 方法
最常见的封装模式:
class BankAccount {
private let owner: String
private var balanceValue: Float64
init(owner: String) {
this.owner = owner
this.balanceValue = 0.0
}
public func deposit(amount: Float64) {
if (amount > 0.0) {
this.balanceValue += amount
}
}
public func withdraw(amount: Float64): Bool {
if (amount > 0.0 && amount <= this.balanceValue) {
this.balanceValue -= amount
return true
}
return false
}
public prop balance: Float64 {
get() { return this.balanceValue }
}
}
main(): Int64 {
let acc = BankAccount("Alice")
acc.deposit(100.0)
println("余额: ${acc.balance}")
println("取款 30: ${acc.withdraw(30.0)}")
println("余额: ${acc.balance}")
// acc.balanceValue = 999.0 // ❌ private,外部不可访问
return 0
}
余额: 100.000000
取款 30: true
余额: 70.000000
外部只能通过 deposit/withdraw 操作余额,不能直接改 balanceValue。这就是面向对象的封装。
5.2 可见性修饰构造函数
构造函数也可以限制可见性:
class Singleton {
private init() {} // 私有构造,外部无法 new
public static func getInstance(): Singleton {
return Singleton() // 类内部可以调用 private init
}
}
私有构造函数 + 静态工厂方法 = 单例模式的雏形(完整实现需要静态字段持有实例,第 17 课细讲)。
六、静态成员:属于类型,不属于实例
有些数据和函数不属于某个具体对象,而是属于整个类型。比如:
- 所有圆共享一个
pi常量 - 统计创建了多少个对象
仓颉用 static 修饰符实现。
6.1 静态字段
class Circle {
static let pi: Float64 = 3.14159
private let radius: Float64
init(radius: Float64) {
this.radius = radius
}
public func area(): Float64 {
return Circle.pi * this.radius * this.radius // 通过类型名访问
}
}
main(): Int64 {
println("pi = ${Circle.pi}") // 不创建实例也能访问
let c = Circle(5.0)
println("area = ${c.area()}")
return 0
}
pi = 3.141590
area = 78.539750
📌
static字段必须有初值(或在静态初始化器里初始化),不能依赖实例。
6.2 静态初始化器
如果静态字段的初始化需要复杂逻辑,用 static init:
class Config {
static let appName: String
static let version: String
static init() {
appName = "CIDE"
version = "1.2.0"
}
}
main(): Int64 {
println("${Config.appName} v${Config.version}")
return 0
}
CIDE v1.2.0
static init()没有参数、没有返回值- 一个类型里最多只能有一个 static init
- 它会在类型首次使用时自动执行,且只执行一次
- 必须初始化所有未赋初值的
static let字段,否则编译报错
6.3 静态方法
class Math {
public static func max(a: Int64, b: Int64): Int64 {
return if (a > b) { a } else { b }
}
public static func square(x: Int64): Int64 {
return x * x
}
}
main(): Int64 {
println("max(3, 7) = ${Math.max(3, 7)}")
println("square(5) = ${Math.square(5)}")
return 0
}
max(3, 7) = 7
square(5) = 25
静态方法里不能访问实例字段(没有 this),只能访问其他静态成员。
七、CIDE 实操:BankAccount 综合练习
7.1 编写程序
综合运用本课知识:主构造函数、属性、可见性、静态成员、mut 方法。
package BankDemo
class BankAccount {
// 静态计数器
static var count: Int64 = 0
private let id: Int64
private var balanceValue: Float64
// 主构造函数:let owner 同时声明了 public 只读字段
public BankAccount(let owner: String) {
this.balanceValue = 0.0
this.id = BankAccount.count
BankAccount.count += 1
}
public func deposit(amount: Float64): Bool {
if (amount <= 0.0) { return false }
this.balanceValue += amount
return true
}
public func withdraw(amount: Float64): Bool {
if (amount <= 0.0 || amount > this.balanceValue) { return false }
this.balanceValue -= amount
return true
}
public prop balance: Float64 {
get() { return this.balanceValue }
}
public prop info: String {
get() { return "[${this.id}] ${this.owner}: ¥${this.balance}" }
}
public static func getTotalAccounts(): Int64 {
return BankAccount.count
}
}
main(): Int64 {
let a1 = BankAccount("Alice")
let a2 = BankAccount("Bob")
a1.deposit(1000.0)
a1.withdraw(200.0)
a2.deposit(500.0)
println(a1.info)
println(a2.info)
println("总开户数: ${BankAccount.getTotalAccounts()}")
return 0
}
7.2 运行与验证
[0] Alice: ¥800.000000
[1] Bob: ¥500.000000
总开户数: 2
关键点解读:
BankAccount(let owner: String)主构造函数自动创建let owner字段static var count追踪开户数量,所有实例共享private字段只能通过deposit/withdraw修改,外部无法绕过balance和info是只读属性,封装了内部状态getTotalAccounts()是静态方法,不依赖任何实例
7.3 调试观察
- 在
BankAccount.count += 1行打断点,观察每次创建账户时count的变化; - 在
a1.deposit(1000.0)行打断点,确认a1.balanceValue从 0 变为 1000; - 在
println(a1.info)行打断点,展开a1的字段面板,确认id/owner/balanceValue的值。
八、常见问题 FAQ
Q1:主构造函数和普通 init 可以混用吗?
可以。一个类型里最多一个主构造函数,但可以有多个普通 init。例如主构造函数处理常规参数,init() 提供无参默认值。
Q2:prop 和直接暴露字段有什么区别?
字段是存储空间,prop 是访问通道。prop 可以在 getter/setter 里加校验、日志、缓存等逻辑;字段只能存值。工程上建议:对外暴露的全部用 prop,字段全部 private。
Q3:mut prop 和 public var 字段有什么区别?
public var 字段外部可以直接读写,没有任何校验;mut prop 外部读写都走 getter/setter,可以在 setter 里加逻辑。功能上 mut prop 更强大,但 public var 写法更简单。
Q4:struct 的 mut 方法和 class 的方法有什么区别?
struct 的 mut 方法只能在 var 绑定的实例上调用(因为会改值);class 的方法不需要 mut,因为 class 是引用类型,let 绑定只约束引用不变,对象内部该改还能改。
Q5:static init 什么时候执行?
在类型首次被使用时(创建实例或访问静态成员)自动执行,且只执行一次。不要在里面写依赖运行时条件的复杂逻辑。
Q6:private init() 有什么用?
限制外部直接创建实例。配合静态工厂方法(如 getInstance())可以实现单例模式、对象池等设计模式。
Q7:prop 可以没有 backing field 吗?
可以。如果 prop 的值完全是计算出来的(如 fahrenheit 换算),不需要额外的存储字段。但如果 prop 的 setter 需要存值,就得有 backing field。
Q8:第 15 课说 struct 方法不能改 this,这节课又说 mut 方法可以,到底哪个对?
这节课对。mut 从仓颉早期版本就存在(不是 1.2.0 新增),第 15 课的结论有误——当时误以为仓颉没有 mutating 关键字就没有原地修改能力,实际上正确的关键字是 mut。第 15 课的 FAQ Q3、Q4 已同步更正。
九、课后练习
- 给
Point加一个public mut prop distanceFromOrigin: Float64,它的 getter 计算到原点的距离,setter 接受一个新距离,把点移到同角度的新位置(提示:保持角度不变,只改半径)。 - 写一个
class Counter,有private var value: Int64,提供increment()、decrement()和只读属性count。再加一个static var totalIncrements: Int64统计所有 Counter 实例的 increment 总次数。 - 用主构造函数简化第 15 课练习 1 的
Rectangle,写成public Rectangle(let width: Float64, let height: Float64) {},提供area()方法和prop perimeter: Float64(只读)。 - 把第 15 课的
Bookstruct 加一个mut func applyDiscount(rate: Float64),原地修改price,不再需要返回新对象。注意调用时实例必须用var绑定。 - 综合:实现一个
class TemperatureSensor,有private var celsius: Float64,提供mut prop fahrenheit(getter/setter 双向换算)、mut func record(t: Float64)记录温度,以及static var recordCount: Int64统计记录次数。在 main 中创建传感器,记录几次温度,打印华氏度。
下节预告
struct 和 class 都有了,但类之间怎么复用代码?怎么定义"能飞的""能游的"这类抽象能力?第 17 课 接口 interface 与实现 讲清仓颉的接口定义、实现、继承和多态,这是面向对象设计的核心。
系列说明:本系列基于 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

浙公网安备 33010602011771号