1.4.1 什么是变量?
简单来说,变量就是程序中用来存放数据的"盒子"。你可以把它想象成一个贴了标签的容器——标签是变量名,容器里装的就是数据。
let age = 18 // 用 age 这个"盒子"存放数字 18
let name = "仓颉" // 用 name 这个"盒子"存放文字"仓颉"
在上面的代码中:
age和name就是变量名(标识符)18和"仓颉"是存放在变量中的数据=是赋值,意思是把右边的数据放进左边的"盒子"里let是修饰符,用于设置变量的各类属性
变量的核心作用是让程序能记住和复用数据,而不是每次都写死具体的值。
1.4.2 如何定义变量
在仓颉中,变量的定义格式如下:
修饰符 变量名: 变量类型 = 初始值
看起来有点复杂?别担心,我们逐个拆解。
修饰符
修饰符决定了变量的"性格"——能不能改、谁能看到它等等。常用的修饰符有:
1. 可变性修饰符(最常用,决定了变量能不能被修改)
| 修饰符 | 含义 | 举例 |
|---|---|---|
let |
不可变——初始化后不能再改 | let age = 18 |
var |
可变——随时可以修改 | var score = 0 |
不确定要不要改?默认用 let,需要修改时再改成 var。这样可以避免意外修改数据。
2. const 修饰符(声明常量,比 let 更严格)
- 必须在声明时立即初始化
- 一旦赋值,永远不能改变
- 详见 2.3 const 修饰符
3. 其他修饰符(后续文章详细介绍)
- 可见性修饰符:
private、public等,控制谁能访问这个变量 - 静态性修饰符:
static,影响变量的存储方式
变量名
变量名就是一个合法的仓颉标识符,命名规则在上一篇文章已经介绍过。
变量类型
变量类型告诉编译器这个"盒子"能装什么样的数据,比如:
Int64—— 整数Float64—— 小数String—— 文本字符串
当初始值的类型很明确时,可以省略类型标注,编译器会自动推断:
let a: Int64 = 6666
let b = a // 编译器自动推断 b 也是 Int64 类型
初始值
初始值就是变量创建时放进"盒子"里的第一个数据。
如果标注了变量类型,初始值的类型必须和变量类型一致,否则会报错。
局部变量和实例成员变量:可以省略初始值,但必须标注类型,且在引用前必须完成初始化。
全局变量和静态成员变量:必须指定初始值,不能省略。
示例
示例 1:let 与 var 的区别
main() {
let a: Int64 // 声明不可变变量 a,暂不初始化
var b: Int64 = 1145 // 声明可变变量 b,初始值为 1145
b = 1919 // ✅ var 变量可以修改
a = b // ✅ a 尚未初始化,这里是首次赋值(初始化),不算修改
println(a)
println(b)
}
运行结果:
1919
1919
示例 2:尝试修改不可变变量会报错
main() {
let sec: String = "咕咕嘎嘎"
sec = "哈基米" // ❌ 报错:cannot assign to immutable value
}
示例 3:const 必须立即初始化
main() {
const pi // ❌ 报错:const variable declaration must be initialized
}
示例 4:全局变量和静态成员变量必须初始化
const pi: Float64 // ❌ 报错:const variable declaration must be initialized
var px: String // ❌ 报错:var variable declaration must be initialized
main() {}
class Player {
static let score: Int32 // ❌ 报错:static variable 'score' needs to be initialized
}
特殊情况
以下内容涉及编译器的保守策略,初学者可以先了解,学完后续内容再深入理解。
内容参考 Cangjie 官方文档
1. 编译器无法判断是否一定会初始化时
func calc(a: Int32) {
println(a)
return a * a
}
main() {
let a: String
if (calc(32) == 0) { // 编译器无法确定这个条件是否成立
a = "1"
}
a = "2" // ❌ 报错:编译器不确定 a 是否已被初始化
}
2. 静态成员变量在函数/lambda 中赋值时
func foo(a: () -> Unit) {}
class A {
public static var ctx: Int64 // ❌ 报错:静态变量没有初始化
static init() {
let lambda = {=> ctx = 10} // 编译器无法确定 lambda 是否会被执行
foo(lambda)
}
}
main() {}
3. try-catch 场景
main() {
let a: String
try {
a = "1"
} catch (_) {
a = "2" // ❌ 报错:编译器假设 try 块可能抛异常,a 可能未被初始化
}
}
1.4.3 const 修饰符
const 用来声明常量——在编译时就确定值,运行时不可改变。
const pi: Float64 = 3.14159
const greeting = "Hello" // 可以省略类型标注
- const 变量不能省略初始化表达式
- const 变量可以是全局变量、局部变量、静态成员变量
- const 变量不能在扩展中定义
- const 变量可以访问对应类型的所有实例成员,也可以调用对应类型的所有非 mut 实例成员函数
const 变量初始化后,其类型的所有成员都是 const 的(深度 const,包含成员的成员),因此其成员也不可变。
1.4.4 值类型和引用类型变量
变量根据数据存储方式的不同,分为值类型和引用类型两类。
简单理解
| 类型 | 类比 | 特点 |
|---|---|---|
| 值类型 | 复印件——每个人手里一份独立的副本 | 修改自己的副本不影响别人 |
| 引用类型 | 共享文档——多个人看同一份文件 | 一个人改了,其他人也能看到变化 |
具体区别
值类型变量(如 Int64、Float64、struct 等):
- 每个变量有自己独立的数据副本
- 赋值时会产生拷贝,原来的数据会被覆盖
let修饰的值类型变量,数据完全不可变
引用类型变量(如 class、Array 等):
- 多个变量可以引用同一份数据
- 赋值只是改变引用关系,不会拷贝数据
let修饰的引用类型变量,只是引用关系不可变,但引用的数据本身是可以被修改的
简单记忆:基本数据类型和 struct 是值类型,class 和 Array 是引用类型。
示例
struct Copy { // 值类型
var data = 2012
}
class Share { // 引用类型
var data = 2012
}
main() {
// ─── 值类型演示 ───
let c1 = Copy()
var c2 = c1 // c2 拿到 c1 的一份独立拷贝
c2.data = 2023 // 修改 c2 不影响 c1
println("${c1.data}, ${c2.data}")
// ─── 引用类型演示 ───
let s1 = Share()
let s2 = s1 // s2 和 s1 指向同一份数据
s2.data = 2023 // 修改 s2 也会影响 s1
println("${s1.data}, ${s2.data}")
}
运行结果:
2012, 2023 // c1 没变,c2 改了 → 值类型:各管各的
2023, 2023 // s1 也变了 → 引用类型:一改全改
解读:
- 值类型
Copy:c2 = c1时复制了一份独立的数据,改c2不影响c1 - 引用类型
Share:s2 = s1时两者指向同一份数据,改s2连s1也跟着变
记住:值类型赋值 = 复印(各管各的),引用类型赋值 = 共享链接(一改全改)。
如果将上面程序中的 var c2 = c1 改成 let c2 = c1,则编译会报错:
struct Copy {
var data = 2012
}
main() {
let c1 = Copy()
let c2 = c1
c2.data = 2023 // ❌ 报错:cannot assign to immutable value
}
因为 let 修饰的值类型变量,其数据完全不可变,连成员字段都不能修改。
1.4.5 作用域
什么是作用域?
作用域就是变量的"有效范围"——一个变量在哪些地方能被使用,哪些地方看不到它。
你可以把作用域想象成房间:
- 在一个房间里放的东西,这个房间里的人都能看到
- 里屋的人能看到外屋的东西,但外屋的人看不到里屋的东西
- 如果里屋和外屋有同名的东西,里屋的人优先用里屋的
仓颉中的作用域规则
在仓颉中,一对大括号 {} 就圈出了一个作用域。大括号可以嵌套,形成"外层作用域"和"内层作用域"。
核心规则只有三条:
- 当前作用域定义的变量,在当前和内层作用域都能用
- 内层作用域定义的变量,外层看不到
- 内层可以和外层定义同名变量,内层的会"遮盖"外层的
仓颉不允许单独写一对大括号 {},大括号必须依附于 if、for、函数体、类体等语法结构。
示例
let element = "仓颉" // ① 顶层作用域
main() {
println(element) // ② 此时 element = "仓颉"(来自外层)
let element = 9 // ③ main 作用域内重新定义 element
if (element > 0) {
let element = 2023 // ④ if 作用域内再次重新定义 element
println(element) // ⑤ 此时 element = 2023(最内层优先)
}
println(element) // ⑥ 此时 element = 9(if 里的已失效)
}
运行结果:
仓颉
2023
9
逐行解读:
| 行号 | 输出 | 原因 |
|---|---|---|
| ② | 仓颉 |
此时 main 里还没定义 element,用的是顶层作用域的 |
| ⑤ | 2023 |
if 里重新定义了 element,遮盖了外层的 9 |
| ⑥ | 9 |
离开 if 块后,if 里的 element 失效,回到 main 作用域的 9 |
简单记忆:变量从内向外查找——先找当前作用域,找不到就往外层找,直到找到为止。
