琉离铟落的小窝琉离铟落的小窝
← 返回文章列表
2026-06-24
#Cangjie#入门教程

仓颉的变量

更方便的管理数据

1.4.1 什么是变量?

简单来说,变量就是程序中用来存放数据的"盒子"。你可以把它想象成一个贴了标签的容器——标签是变量名,容器里装的就是数据。

Cangjie
let age = 18      // 用 age 这个"盒子"存放数字 18
let name = "仓颉"  // 用 name 这个"盒子"存放文字"仓颉"

在上面的代码中:

  • agename 就是变量名(标识符)
  • 18"仓颉" 是存放在变量中的数据
  • = 是赋值,意思是把右边的数据放进左边的"盒子"里
  • let 是修饰符,用于设置变量的各类属性

变量的核心作用是让程序能记住和复用数据,而不是每次都写死具体的值。


1.4.2 如何定义变量

在仓颉中,变量的定义格式如下:

Cangjie
修饰符 变量名: 变量类型 = 初始值

看起来有点复杂?别担心,我们逐个拆解。

修饰符

修饰符决定了变量的"性格"——能不能改、谁能看到它等等。常用的修饰符有:

1. 可变性修饰符(最常用,决定了变量能不能被修改)

修饰符 含义 举例
let 不可变——初始化后不能再改 let age = 18
var 可变——随时可以修改 var score = 0
💡TIP

不确定要不要改?默认用 let,需要修改时再改成 var。这样可以避免意外修改数据。

2. const 修饰符(声明常量,比 let 更严格)

  • 必须在声明时立即初始化
  • 一旦赋值,永远不能改变
  • 详见 2.3 const 修饰符

3. 其他修饰符(后续文章详细介绍)

  • 可见性修饰符:privatepublic 等,控制谁能访问这个变量
  • 静态性修饰符:static,影响变量的存储方式

变量名

变量名就是一个合法的仓颉标识符,命名规则在上一篇文章已经介绍过。

变量类型

变量类型告诉编译器这个"盒子"能装什么样的数据,比如:

  • Int64 —— 整数
  • Float64 —— 小数
  • String —— 文本字符串
💡TIP

当初始值的类型很明确时,可以省略类型标注,编译器会自动推断:

Cangjie
let a: Int64 = 6666
let b = a       // 编译器自动推断 b 也是 Int64 类型 

初始值

初始值就是变量创建时放进"盒子"里的第一个数据。

IMPORTANT

如果标注了变量类型,初始值的类型必须和变量类型一致,否则会报错。

🚨CAUTION

局部变量和实例成员变量:可以省略初始值,但必须标注类型,且在引用前必须完成初始化。

全局变量和静态成员变量:必须指定初始值,不能省略。


示例

示例 1:let 与 var 的区别

Cangjie
main() {
    let a: Int64      // 声明不可变变量 a,暂不初始化
    var b: Int64 = 1145  // 声明可变变量 b,初始值为 1145
    b = 1919          // ✅ var 变量可以修改
    a = b             // ✅ a 尚未初始化,这里是首次赋值(初始化),不算修改
    println(a)
    println(b)
}

运行结果:

Bash
1919
1919

示例 2:尝试修改不可变变量会报错

Cangjie
main() {
    let sec: String = "咕咕嘎嘎"
    sec = "哈基米"  // ❌ 报错:cannot assign to immutable value
}

示例 3:const 必须立即初始化

Cangjie
main() {
    const pi  // ❌ 报错:const variable declaration must be initialized
}

示例 4:全局变量和静态成员变量必须初始化

Cangjie
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
}

特殊情况

ℹ️NOTE

以下内容涉及编译器的保守策略,初学者可以先了解,学完后续内容再深入理解。
内容参考 Cangjie 官方文档

1. 编译器无法判断是否一定会初始化时

Cangjie
func calc(a: Int32) {
    println(a)
    return a * a
}

main() {
    let a: String
    if (calc(32) == 0) {  // 编译器无法确定这个条件是否成立
        a = "1"
    }
    a = "2"  // ❌ 报错:编译器不确定 a 是否已被初始化
}

2. 静态成员变量在函数/lambda 中赋值时

Cangjie
func foo(a: () -> Unit) {}

class A {
    public static var ctx: Int64  // ❌ 报错:静态变量没有初始化

    static init() {
        let lambda = {=> ctx = 10}  // 编译器无法确定 lambda 是否会被执行
        foo(lambda)
    }
}

main() {}

3. try-catch 场景

Cangjie
main() {
    let a: String
    try {
        a = "1"
    } catch (_) {
        a = "2"  // ❌ 报错:编译器假设 try 块可能抛异常,a 可能未被初始化
    }
}

1.4.3 const 修饰符

const 用来声明常量——在编译时就确定值,运行时不可改变。

Cangjie
const pi: Float64 = 3.14159
const greeting = "Hello"  // 可以省略类型标注
IMPORTANT
  • const 变量不能省略初始化表达式
  • const 变量可以是全局变量、局部变量、静态成员变量
  • const 变量不能在扩展中定义
  • const 变量可以访问对应类型的所有实例成员,也可以调用对应类型的所有非 mut 实例成员函数
⚠️WARNING

const 变量初始化后,其类型的所有成员都是 const 的(深度 const,包含成员的成员),因此其成员也不可变。


1.4.4 值类型和引用类型变量

变量根据数据存储方式的不同,分为值类型引用类型两类。

简单理解

类型 类比 特点
值类型 复印件——每个人手里一份独立的副本 修改自己的副本不影响别人
引用类型 共享文档——多个人看同一份文件 一个人改了,其他人也能看到变化

具体区别

值类型变量(如 Int64Float64struct 等):

  • 每个变量有自己独立的数据副本
  • 赋值时会产生拷贝,原来的数据会被覆盖
  • let 修饰的值类型变量,数据完全不可变

引用类型变量(如 classArray 等):

  • 多个变量可以引用同一份数据
  • 赋值只是改变引用关系,不会拷贝数据
  • let 修饰的引用类型变量,只是引用关系不可变,但引用的数据本身是可以被修改的
💡TIP

简单记忆:基本数据类型struct 是值类型,classArray 是引用类型。

示例

Cangjie
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}")
}

运行结果:

Bash
2012, 2023    // c1 没变,c2 改了 → 值类型:各管各的
2023, 2023    // s1 也变了 → 引用类型:一改全改

解读

  • 值类型 Copyc2 = c1 时复制了一份独立的数据,改 c2 不影响 c1
  • 引用类型 Shares2 = s1 时两者指向同一份数据,改 s2s1 也跟着变
💡TIP

记住:值类型赋值 = 复印(各管各的),引用类型赋值 = 共享链接(一改全改)。


如果将上面程序中的 var c2 = c1 改成 let c2 = c1,则编译会报错:

Cangjie
struct Copy {
    var data = 2012
}

main() {
    let c1 = Copy()
    let c2 = c1
    c2.data = 2023  // ❌ 报错:cannot assign to immutable value
}

因为 let 修饰的值类型变量,其数据完全不可变,连成员字段都不能修改。


1.4.5 作用域

什么是作用域?

作用域就是变量的"有效范围"——一个变量在哪些地方能被使用,哪些地方看不到它。

你可以把作用域想象成房间

  • 在一个房间里放的东西,这个房间里的人都能看到
  • 里屋的人能看到外屋的东西,但外屋的人看不到里屋的东西
  • 如果里屋和外屋有同名的东西,里屋的人优先用里屋的

仓颉中的作用域规则

在仓颉中,一对大括号 {} 就圈出了一个作用域。大括号可以嵌套,形成"外层作用域"和"内层作用域"。

核心规则只有三条:

  1. 当前作用域定义的变量,在当前和内层作用域都能用
  2. 内层作用域定义的变量,外层看不到
  3. 内层可以和外层定义同名变量,内层的会"遮盖"外层的
ℹ️NOTE

仓颉不允许单独写一对大括号 {},大括号必须依附于 iffor、函数体、类体等语法结构。

示例

Cangjie
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 里的已失效)
}

运行结果:

Bash
仓颉
2023
9

逐行解读

行号 输出 原因
仓颉 此时 main 里还没定义 element,用的是顶层作用域的
2023 if 里重新定义了 element,遮盖了外层的 9
9 离开 if 块后,if 里的 element 失效,回到 main 作用域的 9
💡TIP

简单记忆:变量从内向外查找——先找当前作用域,找不到就往外层找,直到找到为止。