仓颉编程语言青少年基础教程:Struct(结构)类型
仓颉编程语言青少年基础教程:Struct(结构)类型
Struct(结构) 是一种用户自定义(组织)相关数据的值类型和操作数据的行为的复合数据类型。还是一种 mutable(可变的) 类型。
struct类型的定义与使用
struct 类型的定义以关键字 struct 开头,后跟 struct 的名字,接着是定义在一对花括号中的 struct 定义体。struct 定义体中可以定义一系列的成员变量(member variables)、成员属性(member properties)、静态初始化器(static initializers)、构造函数(constructors)和成员函数(member functions)。
定义 Struct基本结构的语法:
struct 类型的定义以关键字 struct 开头,后跟 struct 的名字,接着是定义在一对花括号中的 struct 定义体。struct 定义体中可以定义一系列的成员变量(member variables)、成员属性(member properties)、静态初始化器(static initializers)、构造函数(constructors)和成员函数(member functions)。
定义 Struct基本结构的语法:
// 核心语法(必须定义在源文件顶层,不能嵌套在函数/其他结构内)
struct 结构体名 {
// 1. 成员变量(实例变量/静态变量,需标注类型或赋初值)
[static] [访问修饰符] [let/var] 变量名: 类型 [= 初始值];
// 2. 构造相关(静态初始化器/普通构造函数/主构造函数)
static init() { /* 初始化静态变量,最多1个 */ }
[访问修饰符] init(参数列表) { /* 普通构造函数,初始化实例变量,可重载 */ }
[访问修饰符] 结构体名(参数列表) { /* 主构造函数,最多一个,可直接定义实例变量 */ }
// 3. 成员函数(实例函数/静态函数/mut函数)
[访问修饰符] [static/mut] func 函数名(参数列表) [-> 返回类型] { /* 函数体 */ }
}
创建 struct 实例的语法:
// 核心语法(var 表示实例可变,let 表示实例不可变)
[var | let] 实例名 = 结构体名(构造函数参数);
// 特殊场景补充
[var | let] 实例名 = 结构体名(); // 无参构造(自动生成或自定义时,无需传参)
[var | let] 实例名 = 结构体名(参数名: 参数值); // 主构造函数/命名参数(如 Rectangle(width: 10, height: 20))
注意,struct 类型的定义以关键字 struct 开头,后跟 struct 的名字,接着是定义在一对花括号中的 struct 定义体。struct 定义体中可以定义一系列的成员变量、成员属性、静态初始化器、构造函数和成员函数。
定义位置:struct 必须直接定义在源文件的最外层(顶层作用域),不能嵌套在函数、循环、条件块等 “内部代码块” 中。
值类型:struct 变量存储的是数据本身(不是引用地址)。这意味着:
当你把一个 struct 变量赋值给另一个变量,或作为参数传给函数时,会完整拷贝一份数据(而不是共享同一份数据)。
可变类型:struct 内部的成员变量可以被修改(只要变量本身用 var 声明),即它的实例状态是可以变化的。
不能继承,但能实现接口:
struct 是 “密封” 的,不能像类(可能的 class 类型)那样被其他类型继承。
例:struct A {}; struct B: A {} 会直接报错(B 不能继承 A)。
可以实现接口,struct 可以遵循接口(Interface),从而拥有接口规定的方法,具备一定的扩展性。
默认不支持 ==,需手动实现:
两个 struct 实例默认不能用 == 或 != 比较(即使成员变量完全相同)。如果需要判等,必须手动 “重载”== 操作符。
struct递归(自己嵌套自己)、互递归非法。
自己嵌套自己(非法),例如:
struct Node {
value: Int64
next: Node // 错误(error)
}
两个 struct 互相引用(也非法),例如:
struct A {
b: B //错误(error)
}
struct B {
a: A
}
上面所说,初学者一时看不懂不必着急,下面将逐步示例介绍。
仓颉中的 struct 可以简单理解为一个“自定义数据模板”,用来打包一组相关的 “数据”和 “操作这些数据的方法”。就像现实中 “图纸” 和 “实物” 的关系:struct 是图纸,用它创建的 “实例” 就是按图纸造出来的具体实物。
先看一个简单示例:
// 定义 Circle 结构体
struct Circle {// 成员变量let radius: Float32// 构造函数public init(radius: Float32) {this.radius = radius} // 成员属性:计算面积public prop area: Float32 {get() { 3.14 * radius * radius }}// 成员函数:计算面积public func calculateArea(): Float32 {return 3.14 * radius * radius}
}// 使用示例
main() {let circle = Circle(5.0)// 使用成员属性println("圆的面积(属性)是: ${circle.area}")// 使用成员函数println("圆的面积(函数)是: ${circle.calculateArea()}")
}
编译运行截图:
struct的成员介绍
struct 类型的 “内部定义区域”(即花括号 {} 中的内容),里面可以定义 5 类核心元素:成员变量、成员属性、主构造函数、普通构造函数、成员函数(含操作符函数)。这些哪些是必须的?各有什么作用?
☆成员变量(必须):核心作用是存储数据,而成员变量是数据的载体。
struct Point {
// 显式定义成员变量
let x: Int32
let y: Int32
public init(x: Int32, y: Int32) {
this.x = x
this.y = y
}
}
其中
public init(x: Int32, y: Int32) {
this.x = x
this.y = y
}
是构造函数(后面介绍)。
成员变量是数据的 “容器”,分为:实例成员变量和静态成员变量
实例成员变量(无 static)
在结构体或类中定义的变量,不使用 static 修饰符。
实例成员变量存储在每个实例中,每个实例都有自己的独立副本。每次创建一个新的实例时,都会为这些变量分配独立的内存空间。
访问方式,通过实例访问,即 对象.变量名。
静态成员变量(有 static)
在结构体或类中定义的变量,使用 static 修饰符。
静态成员变量存储在类或结构体的类型级别上,而不是在每个实例中。静态成员变量在程序启动时初始化一次,所有实例共享同一个变量。
访问方式,通过类型名访问,即 类型名.变量名。
示例:
// 定义 Rectangle 结构体
struct Rectangle {public var width: Int64public var height: Int64public static var count: Int64 = 0public init(width: Int64, height: Int64) {this.width = widththis.height = heightRectangle.count += 1 // 增加实例计数}
}// 使用示例
main() {let r1 = Rectangle(10, 20)let r2 = Rectangle(5, 5)println("r1 的宽度是: ${r1.width}") // 访问实例成员变量println("r2 的高度是: ${r2.height}")println("Rectangle 的实例数量是: ${Rectangle.count}") // 访问静态成员变量
}
编译运行,输出如下:
r1 的宽度是: 10
r2 的高度是: 5
Rectangle 的实例数量是: 2
☆构造函数
仓颉中的struct类型有两种构造函数:
主构造函数(primary constructor)名字 必须 和 struct 同名
普通构造函数(init)名字固定叫 init
构造函数的作用是初始化所有未赋值的成员变量。如果成员变量都有初始值,构造函数可选(编译器可能生成默认无参构造函数);如果存在未赋值的成员变量,则必须通过主构造函数或普通构造函数初始化,否则编译报错。
例:成员变量有初始值,构造函数可选:
struct Square {
let side: Int32 = 10 // 有初始值,无需构造函数
}
let s = Square() // 编译器生成默认无参构造函数
例:成员变量无初始值,必须有构造函数:
struct Square {
let side: Int32 // 无初始值,必须构造函数初始化
// 必须定义构造函数
init(side: Int32) {
this.side = side
}
}
主构造函数和普通构造函数
主构造函数的名称必须与结构体名称相同。一个结构体最多只能有一个主构造函数。如:
struct Rectangle {
public var width: Int64
public var height: Int64
public Rectangle(width: Int64, height: Int64) {
this.width = width
this.height = height
}
}
普通构造函数使用 init 关键字定义。可以有多个,但必须构成重载(参数列表不同)。例如
struct Rectangle {
public var width: Int64
public var height: Int64
public init(side: Int64) {
this.width = side
this.height = side
}
}
关于构造函数的几点说明:
①如果构造函数的参数名和成员变量名无法区分,可以在成员变量前使用 this 加以区分,this 表示 struct 的当前实例,否则编译报错。
struct Person {
var name: String // 成员变量name
var age: Int // 成员变量age
// 构造函数参数名也叫name和age,与成员变量同名
func Person(name: String, age: Int) {
this.name = name // this.name指成员变量,右侧name指参数
this.age = age // 同理,区分成员变量和参数
}
}
如果不加this,编译器无法区分name是参数还是成员变量,this在这里起到明确标识的作用。否者,编译器会报错。
②一个 struct 中可以定义多个普通构造函数,但它们必须构成重载(参见函数重载),否则报重定义错误。
合法的重载(函数名称相同,参数个数或类型序列不同就合法):
struct Point {
var x: Int
var y: Int
// 构造函数1:两个参数
func Point(x: Int, y: Int) {
this.x = x
this.y = y
}
// 构造函数2:一个参数(参数个数不同,构成重载)
func Point(xy: Int) {
this.x = xy
this.y = xy
}
// 构造函数3:参数类型不同(构成重载)
func Point(x: Float, y: Float) {
this.x = Int(x)
this.y = Int(y)
}
}
如果定义两个参数列表完全相同的构造函数,就会报错(不合法):
struct Point {
var x: Int
var y: Int
func Point(x: Int, y: Int) { ... }
func Point(x: Int, y: Int) { ... } // 错误:参数列表相同,未构成重载
}
下面这样就不行,仅靠参数名不同,不构成重载:
public init(a: Int64) { ... }
public init(b: Int64) { ... } // 报错:redefinition of init(Int64)
③主构造函数的名字和 struct 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 let 或 var),成员变量形参同时扮演定义成员变量和构造函数参数的功能。
换句话说,主构造函数是struct中名字与 struct 类型名相同的构造函数,其参数有两种形式:
普通形参:仅作为构造函数的参数,需手动关联到成员变量(需提前定义成员变量)。
成员变量形参:在参数名前加let或var,此时参数既是构造函数的输入,也会自动成为 struct 的成员变量(无需提前定义)。
示例:
// ① 主构造函数:参数带 let → 成员变量形参
// 编译器自动干三件事:
// 1. 给 Point 增加不可变成员 x: Int64
// 2. 给 Point 增加不可变成员 y: Int64
// 3. 把实参值赋进去
public struct Point {public Point(let x: Int64, let y: Int64) {println("通过主构造函数创建的点: (${x}, ${y})")}
}// ② 传统写法:先手动声明成员,再用普通形参赋值
public struct PointVerbose {var x: Int64var y: Int64public init(x: Int64, y: Int64) { // 普通形参,无 let/varthis.x = xthis.y = yprintln("传统普通构造创建的点: (${x}, ${y})")}
}main(): Int64 {// 主构造:一句搞定声明+赋值let p1 = Point(10, 20)println("p1.x = ${p1.x}, p1.y = ${p1.y}") // 证明成员变量已存在// 普通构造:先声明再赋值let p2 = PointVerbose(30, 40)println("p2.x = ${p2.x}, p2.y = ${p2.y}")return 0
}
输出:
通过主构造函数创建的点: (10, 20)
p1.x = 10, p1.y = 20
传统普通构造创建的点: (30, 40)
p2.x = 30, p2.y = 40
☆成员属性、成员函数(可选)
这两类元素是 “增强功能”,不是 struct 的必备要素。
成员属性:
成员属性通过 prop 关键字定义,包含 get 和可选的 set 方法。
通过 get(读取)和 set(赋值)逻辑,对成员变量的访问进行 “包装”,实现更灵活的数据处理(如计算、验证、联动修改)。成员属性(Properties) 不能接受外部参数,主要作用是提供对成员变量的封装和间接访问,而不是实现复杂的逻辑或处理外部参数。
访问方式,直接访问属性名(对象.属性名)
示例:
// 定义 Circle 结构体
struct Circle {let radius: Float32 // 成员变量// 构造函数public init(radius: Float32) {this.radius = radius}// 成员属性(计算面积,依赖 radius)public prop area: Float32 {get() { 3.14 * radius * radius } // 读取时计算}
}// 使用示例
main() {let circle = Circle(5.0) // 创建一个半径为 5.0 的圆println("圆的面积是: ${circle.area}") // 访问 area 属性,输出:圆的面积是: 78.500000
}
成员函数
成员函数通过 func 关键字定义
成员函数可以接受外部参数,从而实现更灵活的操作。
访问方式,通过方法调用(对象.函数名())
示例:
// 定义 Rectangle 结构体
struct Rectangle {public var width: Int64public var height: Int64// 构造函数public init(width: Int64, height: Int64) {this.width = widththis.height = height}// 成员函数:计算面积public func calculateArea(): Int64 {return width * height}// 成员函数:计算周长public func calculatePerimeter(): Int64 {return 2 * (width + height)}// 成员函数:缩放矩形public mut func scale(factor: Int64) {width *= factorheight *= factor}
}// 使用示例
main() {let rec1 = Rectangle(10, 20)// 调用成员函数println("rec1 的面积是: ${rec1.calculateArea()}") // 输出:200println("rec1 的周长是: ${rec1.calculatePerimeter()}") // 输出:60// 缩放矩形var rec2 = rec1rec2.scale(2)println("缩放后的 rec2 的面积是: ${rec2.calculateArea()}") // 输出:800println("缩放后的 rec2 的周长是: ${rec2.calculatePerimeter()}") // 输出:120
}
运行上述代码后,输出如下:
运行上述代码后,输出如下:
rec1 的面积是: 200
rec1 的周长是: 60
缩放后的 rec2 的面积是: 800
缩放后的 rec2 的周长是: 120
struct成员的访问修饰符:控制成员的可见范围
struct 的所有成员(变量、方法、构造函数)都可以用访问修饰符控制谁能访问,默认是 internal。
private 仅在当前 struct 内部可见(外部无法访问)
internal 默认值,仅当前包及子包可见(跨包无法访问)
protected 仅当前模块可见(模块内任意包可访问,模块外无法访问)
public 模块内外都可见(外部包导入后可访问)
示例
假设有一个项目目录如下:
(在包shape 定义 struct, 在入口文件main.cj导入并使用它)
demo24
│
src/
├── shape/ // shape包
│ └── rectangle.cj // shape包的rectangle.cj文件
└── main.cj // 入口文件
shape包的rectangle.cj 文件源码如下:
package demo24.shape// 公开的 struct(需加 public,否则跨包无法访问)
public struct Rectangle {public var width: Int64 // public:跨包可访问var height: Int64 // 默认 internal:仅 shape 包及子包可访问private var area: Int64 // private:仅 Rectangle 内部可访问// 公开的构造函数(跨包需调用)public init(width: Int64, height: Int64) {this.width = widththis.height = heightthis.area = width * height // 内部计算面积}// 公开方法:间接访问 private 成员public func getArea(): Int64 {this.area}
}
入口main.cj文件源码如下:
package demo24import demo24.shape.* // 导入 shape 包的所有内容main() {// 1. 调用 shape 包的 struct 和构造函数var rect = Rectangle(10,20)// 2. 访问成员的差异rect.width = 15 // Ok:public(跨包可访问)// rect.height = 25 // Error:internal(跨包不可访问)// rect.area = 300 // Error:private(完全不可访问)// 3. 通过 public 方法访问 private 成员print("跨包访问面积:${rect.getArea()}") // 输出:300(15*20)
}
编译运行截图:
mut函数
为什么需要 Mut 函数?
struct 是值类型。默认情况下,它的实例成员函数不能修改实例的成员变量。如果你希望一个函数能修改实例本身,必须用 mut 关键字来标记它。
核心规则:
- mut 函数使用 mut 关键字修饰
- 只能修饰实例成员函数,不能修饰静态成员函数
- mut 函数中的 this 不能被捕获,也不能作为表达式
- 只有 mut func 可以修改实例的成员变量。
- 通过 let 声明的结构体常量,不能调用 mut func。
struct 类型是值类型,其实例成员函数无法修改实例本身。例如,下例中,成员函数 g 中不能修改成员变量 i 的值。
struct Foo {
var i = 0
public func g() {
i += 1 // Error
}
}
mut 函数是一种可以修改 struct 实例本身的特殊的实例成员函数。在 mut 函数内部,this 的语义是特殊的,这种 this 拥有原地修改字段的能力。
注意:只允许在 interface、struct 和 struct 的扩展内定义 mut 函数(class 是引用类型,实例成员函数不需要加 mut 也可以修改实例成员变量,所以禁止在 class 中定义 mut 函数)。
mut 函数定义
mut 函数与普通的实例成员函数相比,多一个 mut 关键字来修饰。
例如,下例中在函数 g 之前增加 mut 修饰符之后,即可在函数体内修改成员变量 i 的值。
struct Foo {
var i = 0
public mut func g() {
i += 1 // Ok
}
}
mut 只能修饰实例成员函数,不能修饰静态成员函数。
Mut函数使用完整示例:
struct Counter {var count: Int64 = 0// 这是一个 mut 函数,它可以修改 countpublic mut func increment() {count += 1 // 允许修改}// 这是一个普通函数,它不能修改 countpublic func getCount(): Int64 {return count// count += 1 // 这里如果取消注释,会编译错误!}
}main() {// 必须用 var 声明变量,才能调用 mut 函数var myCounter = Counter()myCounter.increment() // OKprintln(myCounter.getCount()) // 输出: 1// 用 let 声明的常量,即使实例本身是 var,也不能调用 mut 函数let constantCounter = Counter()// constantCounter.increment() // 错误!常量不能调用 mut 函数
}
接口中的 mut 函数:struct 实现接口时,必须保持 mut 修饰一致;且接口类型变量调用 mut 函数时,会复制实例(不影响原 struct 实例)。示例:
interface IIncrement {mut func add(step: Int64) {}func getValue(): Int64 { 0 }
}struct MyCounter <: IIncrement {private var value: Int64 = 0// 函数参数定义时保留 `step: Int64`(声明参数名)public mut func add(step: Int64) {value += step}public func getValue(): Int64 {value}
}main() {var counter = MyCounter()//直接传值(位置参数)counter.add(3)println("原实例值:${counter.getValue()}") // 输出:3var inc: IIncrement = counter//传值(位置参数)inc.add(2)println("原实例值:${counter.getValue()}") // 输出:3println("接口实例值:${inc.getValue()}") // 输出:5
}