【仓颉语言入门 · 第15课】
【仓颉语言入门 · 第15课】结构体 struct 与类 class
前 14 课,我们用"变量 + 函数"搭出了全部逻辑——变量存数据,函数操作数据。但数据一多就乱了:一个学生有姓名、年龄、学号、成绩;一本书有标题、作者、价格、库存……难道全都摊成一个个独立的变量?这节课进入面向对象的世界:用
struct(结构体)和class(类)把"数据 + 操作数据的方法"打成一个包。本课重点讲清一件事——值类型与引用类型的根本差异,这是整个面向对象模块的地基。本文所有代码与报错文案均在仓颉 SDK 1.1.3 下逐行实测编译运行。途中会澄清三个高频误区:①
let绑定的struct,其var字段也不能改(值类型整体不可变);②struct的方法不能修改this的字段,只能返回新值(仓颉没有mutating关键字);③class的方法可以直接修改var字段,无需任何修饰符。
目录(系列导航)
整套路线共 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 数据处理程序
一、从"散落的数据"到"打包的数据"
假设要在程序里描述一个二维坐标点。目前我们只会这样写:
main(): Int64 {
let px = 3
let py = 4
println("x=${px}, y=${py}")
return 0
}
一个点两个变量还能接受。如果是一个学生呢?姓名、年龄、学号、三门成绩……五个变量起步。如果是一个班 50 个学生,那就是 250 个变量,光命名都要疯掉。
更麻烦的是"传递":写一个函数计算两点距离,得传 4 个参数(x1, y1, x2, y2);写一个函数判断学生是否及格,得传成绩……参数列表越来越长,而且参数顺序一写反就出 bug。
面向对象的第一步:把属于同一个事物的数据打包成一个整体。仓颉提供两种打包方式:
struct(结构体):值类型,适合描述"简单的值"class(类):引用类型,适合描述"有身份、有行为的对象"
两者语法几乎一样,但运行时行为天差地别——这正是本课的核心。
二、struct:值类型的打包
2.1 定义一个 struct
struct Point {
var x: Int64
var y: Int64
init(x: Int64, y: Int64) {
this.x = x
this.y = y
}
}
拆解:
struct Point { ... }:定义一个名为Point的结构体;var x: Int64/var y: Int64:两个字段(field),存储数据;init(x: Int64, y: Int64) { ... }:构造函数,创建实例时调用,用this.x = x把参数赋给字段;this:当前实例的引用,类似 Java/Kotlin 的this、Python 的self。
2.2 创建实例与访问字段
main(): Int64 {
let p = Point(3, 4)
println("x = ${p.x}, y = ${p.y}")
return 0
}
x = 3, y = 4
用 类型名(参数) 创建实例,用 .字段名 访问字段。
2.3 struct 是值类型:赋值即复制
这是 struct 最核心的特性。看代码:
main(): Int64 {
var p1 = Point(1, 2)
var p2 = p1 // 把 p1 赋值给 p2
p2.x = 100 // 修改 p2
println("p1.x = ${p1.x}, p2.x = ${p2.x}")
return 0
}
p1.x = 1, p2.x = 100
p2 = p1 时,仓颉把 p1 的值完整复制了一份给 p2。之后 p2 和 p1 是两个完全独立的点,改 p2.x 不影响 p1.x。
📌 这就是"值类型"的含义:变量里存的是数据本身,赋值时复制数据。
Int64、Float64、Bool、Char这些基本类型也都是值类型,struct 跟它们是同一类。
三、class:引用类型的打包
3.1 定义一个 class
语法和 struct 几乎一模一样,只是关键字换成 class:
class Person {
let name: String
var age: Int64
init(name: String, age: Int64) {
this.name = name
this.age = age
}
}
3.2 创建实例
main(): Int64 {
let p = Person("Alice", 30)
println("${p.name}, ${p.age}")
return 0
}
Alice, 30
创建方式、字段访问和 struct 完全一样。差别藏在赋值行为里。
3.3 class 是引用类型:赋值即共享
main(): Int64 {
let a = Person("Alice", 30)
let b = a // 把 a 赋值给 b
b.age = 99 // 修改 b
println("a.age = ${a.age}, b.age = ${b.age}")
return 0
}
a.age = 99, b.age = 99
b = a 时,仓颉没有复制对象,而是让 b 和 a 指向同一个 Person 实例。所以改 b.age,a.age 也跟着变——它们是同一个人的两个名字。
📌 这就是"引用类型"的含义:变量里存的是指向对象的引用(地址),赋值时复制的是引用,对象本身只有一份。第 13 课用
class当闭包的可变容器,靠的就是这个特性。
四、值类型 vs 引用类型:一张表说清
| 维度 | struct(值类型) | class(引用类型) |
|---|---|---|
| 变量里存的 | 数据本身 | 指向对象的引用 |
| 赋值 | 复制一份新数据 | 共享同一个对象 |
| 传参 | 复制一份传入函数 | 传入引用,函数内可改原对象 |
| 内存位置 | 栈(小对象) | 堆 |
| 能否继承 | 不能 | 能(第 17 课讲) |
| 适合描述 | 坐标、颜色、日期、金额等"值" | 学生、订单、账户等"有身份的对象" |
4.1 传参时的差异
struct 传参是复制,函数内改不了外面的:
func bumpX(p: Point): Point {
var q = p
q.x = q.x + 1
return q
}
main(): Int64 {
let p = Point(1, 2)
let p2 = bumpX(p)
println("p.x = ${p.x}, p2.x = ${p2.x}")
return 0
}
p.x = 1, p2.x = 2
bumpX 拿到的是 p 的副本,改副本不影响原 p,要改得靠返回值。
class 传参是共享,函数内直接改原对象:
func birthday(p: Person) {
p.age += 1
}
main(): Int64 {
let a = Person("Alice", 30)
birthday(a)
println("${a.name} 现在 ${a.age} 岁")
return 0
}
Alice 现在 31 岁
birthday 里改 p.age,外面的 a.age 也变了——因为是同一个对象。
⚠️ 这是新手最容易踩坑的地方。写函数时要清楚:参数是 struct 还是 class?想不想让函数修改调用方的数据?struct 默认不会,class 默认会。
五、字段可变性:let 与 var
第 2 课学过 let(不可变)和 var(可变)。它们用在字段上时,行为和值/引用类型交织在一起,有四种组合,逐一理清。
5.1 struct + let 字段
struct ImmutablePoint {
let x: Int64
let y: Int64
init(x: Int64, y: Int64) { this.x = x; this.y = y }
}
let 字段只能在 init 里赋值一次,之后永远不能改。整个 struct 是"只读的值"。
5.2 struct + var 字段(实例必须是 var)
struct MutablePoint {
var x: Int64
var y: Int64
init(x: Int64, y: Int64) { this.x = x; this.y = y }
}
main(): Int64 {
let p = MutablePoint(1, 2)
// p.x = 100 // ❌ 编译错误:p 是 let
var q = MutablePoint(1, 2)
q.x = 100 // ✅ q 是 var,允许
return 0
}
🚫
let绑定的 struct,其var字段也不能改。 因为 struct 是值类型,let意味着"整个值不可变",连带所有字段一起冻住。想改 struct 字段,实例变量必须用var。这和很多语言(C#、Go)不同——那些语言readonly的 struct 仍可改var字段,仓颉更严格。
5.3 class + let 字段
class Person {
let name: String // 名字一生不变
var age: Int64 // 年龄会增长
init(name: String, age: Int64) { this.name = name; this.age = age }
}
let 字段同样只能在 init 里赋值一次。
5.4 class + var 字段(实例可以是 let)
main(): Int64 {
let a = Person("Alice", 30)
// a.name = "Bob" // ❌ name 是 let 字段,不能改
a.age = 31 // ✅ age 是 var 字段,可以改
return 0
}
📌
let绑定的 class 实例,其var字段仍可改。 因为let a只约束"a 这个引用不能改指向"(不能让a去指另一个对象),但对象内部的var字段该改还能改。这是值类型和引用类型在可变性上的根本差异。
四组组合一句话记忆:
- struct + let 实例:整体只读,啥都不能改
- struct + var 实例:
var字段可改,let字段不能改 - class + let 实例:引用固定,
var字段可改 - class + var 实例:引用可换,
var字段可改
六、方法与 this
光有数据不够,还需要"操作数据的行为"。把函数放进 struct/class 里,就叫方法(method)。
6.1 class 的方法
class Person {
let name: String
var age: Int64
init(name: String, age: Int64) { this.name = name; this.age = age }
public func sayHi(): String {
return "Hi, I'm ${this.name}, age ${this.age}"
}
public func growOlder() {
this.age += 1
}
}
main(): Int64 {
let a = Person("Alice", 30)
println(a.sayHi())
a.growOlder()
println(a.sayHi())
return 0
}
Hi, I'm Alice, age 30
Hi, I'm Alice, age 31
- 方法用
public func定义(public让外部能调用,第 20 课细讲可见性); - 方法里用
this访问字段; - class 的方法可以直接修改
var字段,growOlder()里this.age += 1完全合法。
6.2 struct 的方法:不能修改 this
struct Point {
var x: Int64
var y: Int64
init(x: Int64, y: Int64) { this.x = x; this.y = y }
public func toString(): String {
return "(${this.x}, ${this.y})"
}
public func moveBy(dx: Int64, dy: Int64): Point {
return Point(this.x + dx, this.y + dy) // ✅ 返回新值
}
// public func moveByMut(dx: Int64, dy: Int64) {
// this.x += dx // ❌ 报错:cannot be modified in immutable function
// }
}
🚫 struct 的方法不能修改
this的字段。 仓颉没有 Swift 那种mutating关键字。struct 方法只能读取this,想"改变"就返回一个新的 struct。这是值类型的设计哲学:值不应该被"原地改动",而是"产生一个新值"。
调用方式:
main(): Int64 {
let p = Point(1, 2)
let p2 = p.moveBy(10, 20)
println("${p.toString()} -> ${p2.toString()}")
return 0
}
(1, 2) -> (11, 22)
p 本身没变,moveBy 返回了一个新的 Point。
七、struct 还是 class:选型决策
这是每写一个新类型都要问自己的问题。一个简单的判断框架:
该用 struct 的情况
- 它本质是"一个值":坐标点、颜色(RGB)、日期、金额、尺寸、分数……这些东西的"身份"就是它的值,两个值相等就算"同一个"。
- 希望赋值/传参时自动复制:不担心副作用,改副本不影响原件。
- 不需要继承:struct 不能继承,行为靠组合(第 17 课)。
- 数据小、生命周期短:放在栈上,分配快。
该用 class 的情况
- 它有"身份":学生(有学号)、订单(有订单号)、账户(有账号)……即使两个学生所有字段都一样,他们也是两个人。
- 需要共享和修改:一个对象被多处引用,改一处处处生效(比如购物车里的商品对象)。
- 需要继承:class 支持继承(第 17 课)。
- 有丰富的行为:方法需要修改自身状态(struct 做不到原地修改)。
一个经验法则
问自己:“把这个东西赋值给另一个变量后,我改新变量,旧变量应不应该跟着变?”
- 不应该 → struct(值语义)
- 应该 → class(引用语义)
坐标点:改副本不该影响原点 → struct。
学生:改了"另一个名字指向的学生",原学生也该变 → class。
八、CIDE 实操:图书与书架
8.1 编写程序
综合运用:struct 表示图书(值),class 表示书架(有身份、可变),演示值/引用差异、let/var 字段、class 方法修改状态、struct 方法返回新值。
package BookShelf
// struct:图书是"值",复制一本不影响原书
struct Book {
let title: String
let author: String
var price: Float64
init(title: String, author: String, price: Float64) {
this.title = title
this.author = author
this.price = price
}
public func info(): String {
return "${this.title}(${this.author})¥${this.price}"
}
// struct 方法不能改 this,返回新对象表示"打折后的书"
public func discounted(rate: Float64): Book {
return Book(this.title, this.author, this.price * rate)
}
}
// class:书架是"对象",所有引用共享同一份库存
class Shelf {
var books: ArrayList<Book>
var capacity: Int64
init(capacity: Int64) {
this.books = ArrayList<Book>()
this.capacity = capacity
}
public func add(book: Book): Bool {
if (this.books.size >= this.capacity) {
return false
}
this.books.add(book)
return true
}
public func count(): Int64 {
return this.books.size
}
public func list(): String {
var lines = ArrayList<String>()
for (b in this.books) {
lines.add(b.info())
}
// ArrayList 没有 joinToString,用 fold 手动拼接
return lines.iterator().fold("") { acc, s =>
if (acc.isEmpty()) { s } else { "${acc}\n${s}" }
}
}
}
main(): Int64 {
// —— 值类型:图书赋值即复制 ——
let b1 = Book("仓颉语言入门", "张三", 59.0)
var b2 = b1
b2.price = 0.0 // 把"副本"价格改 0
println("原书:${b1.info()}")
println("副本:${b2.info()}")
// struct 方法返回新值
let b3 = b1.discounted(0.8)
println("八折:${b3.info()}")
// —— 引用类型:书架共享 ——
let shelf = Shelf(3)
let ref = shelf // ref 和 shelf 指向同一个书架
shelf.add(b1)
shelf.add(b3)
println("shelf 上有 ${shelf.count()} 本书")
println("ref 上有 ${ref.count()} 本书") // 同一个书架,也是 2 本
let ok = shelf.add(b2)
println("加第三本:${ok}(容量 3)")
println("再塞一本:${shelf.add(b1)}") // 满了,返回 false
println("--- 书架清单 ---")
println(shelf.list())
return 0
}
8.2 运行与验证
原书:仓颉语言入门(张三)¥59.0
副本:仓颉语言入门(张三)¥0.0
八折:仓颉语言入门(张三)¥47.2
shelf 上有 2 本书
ref 上有 2 本书
加第三本:true(容量 3)
再塞一本:false
--- 书架清单 ---
仓颉语言入门(张三)¥59.0
仓颉语言入门(张三)¥47.2
仓颉语言入门(张三)¥0.0
关键点解读:
b2 = b1复制了一本书,改b2.price不影响b1.price(值类型)。discounted不改原书,返回一本新的打折书——struct 方法的标准写法。let ref = shelf,ref和shelf指向同一个 Shelf 对象,所以shelf.add后ref.count()也是 2(引用类型)。add在 class 里直接修改this.books,合法;如果 Shelf 是 struct,这行就会报 “cannot be modified in immutable function”。- 容量满时
add返回false,调用方据此判断。
可以自己动手改:
- 把
var b2 = b1改成let b2 = b1,观察b2.price = 0.0报什么错; - 把
class Shelf改成struct Shelf,看add方法里this.books.add(book)报什么错(struct 方法不能改 this); - 给
Book加一个var edition: Int64字段,在main里验证let绑定的 Book 不能改 edition、var绑定的可以。
8.3 用调试器观察值与引用
- 在
b2.price = 0.0行打断点,Shift + F9 启动调试; - 断住后展开 Variables 面板的
b1和b2,确认它们是两个独立的对象,price分别是 59.0 和 0.0; - 在
shelf.add(b1)行打断点,观察shelf和ref指向同一个Shelf实例(地址相同); - 单步执行
shelf.add(b3)后,再看ref.count()——确认ref没主动操作,数据却变了(共享引用)。
调试结束点停止按钮。
九、常见问题 FAQ
Q1:struct 和 class 语法几乎一样,我怎么记得住区别?
别记语法,记语义:struct 是值(复制),class 是引用(共享)。写代码时先想"赋值后改副本会不会影响原件",再决定用哪个。语法只是关键字不同。
Q2:为什么 let p = Point(...) 后 p.x = 100 报错,let a = Person(...) 后 a.age = 30 却可以?
因为 struct 是值类型,let 冻住的是整个值(所有字段);class 是引用类型,let 只冻住引用(不能换对象),对象内部的 var 字段不受影响。一句话:struct 的 let 是"值不可变",class 的 let 是"引用不可变"。
Q3:struct 的方法真的不能修改字段吗?那我想给 struct 加个 “自增” 方法怎么办?
仓颉 1.1.3 确实不能。标准做法是返回新值:func incremented(): Counter { return Counter(this.value + 1) },调用时 let c2 = c.incremented()。这是值类型的"函数式"风格,虽然啰嗦但杜绝了副作用。如果确实需要频繁原地修改,考虑用 class。
Q4:仓颉有 mutating 关键字吗(像 Swift 那样)?
没有。写 public mutating func ... 会报 expected declaration, found 'mutating'。struct 方法一律不能改 this。
Q5:struct 和 class 哪个性能更好?
小的 struct 分配在栈上,创建和销毁极快,且没有 GC 压力。class 在堆上分配,需要 GC 回收。简单值用 struct 更高效。但大的 struct 复制成本高,频繁传递时反而不如 class。一般来说,字段少、值语义用 struct;字段多、有行为用 class。
Q6:为什么第 14 课的迭代器用 class 而不是 struct?
迭代器需要"记住游标位置"并在每次 next() 时推进——这要求方法能修改自身状态。struct 做不到(方法不能改 this),所以必须用 class。这也是判断用 struct 还是 class 的一个实用标准:方法需不需要改自己的状态?需要就用 class。
Q7:init 构造函数必须写吗?
如果字段都有默认值(var x: Int64 = 0),可以不写 init,编译器提供无参构造。如果有字段没有默认值,就必须写 init 并在里面初始化所有 let 和无默认值的 var 字段。第 16 课会系统讲构造函数的各种规则。
Q8:可以省略 this 吗?
字段名和参数名不同时可以省略(init(name: String) { this.name = name } 里,this.name 指字段,name 指参数,this 不能省,否则分不清)。如果参数名换一个(比如 init(n: String) { name = n }),就可以省略 this。但工程上建议字段赋值永远写 this,清晰无歧义。
Q9:一个文件里可以定义多个 struct/class 吗?
可以。同一个包下想定义多少个都行。本课的实操例子就同时定义了 Book 和 Shelf。
Q10:struct/class 可以嵌套定义吗?
仓颉 1.1.3 不支持在 struct/class 内部再定义 struct/class。所有类型都定义在包级别(文件顶层)。
十、课后练习
- 定义一个
struct Rectangle,有width和height两个var Float64字段,init构造函数,以及一个area(): Float64方法返回面积。创建Rectangle(3.0, 4.0)并打印面积。 - 给第 1 题的
Rectangle加一个scaled(factor: Float64): Rectangle方法,返回宽高都乘以 factor 的新矩形(注意:struct 方法不能改 this)。验证r.scaled(2.0)后原矩形r不变。 - 定义一个
class BankAccount,有let owner: String和var balance: Float64字段,以及deposit(amount)和withdraw(amount): Bool方法(余额不足返回 false)。创建账户后存款、取款,验证余额变化。 - 写一个函数
transfer(from: BankAccount, to: BankAccount, amount: Float64): Bool,从 from 取款并存入 to(取款失败则不存款)。验证转账后两个账户余额正确,理解 class 传参是共享引用。 - 综合:定义
struct Color(r、g、b三个var Int64字段,0-255)和class Canvas(持有一个ArrayList<Color>调色板)。给Color加darker(): Color返回一个颜色减半的新颜色;给Canvas加add(c: Color)和count()。在 main 中创建调色板,加几种颜色,用darker()生成暗色版本加入,最后打印调色板大小。
下节预告
这节课把 struct/class 的"骨架"搭好了:定义、字段、构造、方法、值与引用的差异。但构造函数和方法还有很多细节没展开——多构造函数、默认参数、属性 getter/setter、方法的可见性与静态成员。第 16 课 构造函数、属性与方法 把这些一次讲透,让你的类型真正"好用"。我们下节课见!
系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入(例如新版本为 struct 方法增加了修改自身状态的能力、放宽了 let struct 的字段修改限制等),以你本地版本为准,欢迎评论区交流。
💬 遇到问题?扫码联系作者
跟着课程练习时,如果在 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)