【仓颉语言入门 · 第2课】变量与常量:let / var 与基本数据类型

上节课我们已经在 CIDE 里跑通了第一个 Hello World,看到了第一行输出。但那个程序只会"背台词"——每次运行输出都一模一样。真正的程序要能处理数据:年龄、分数、价格、开关……这些值在运行过程中可能变化,就需要先找个地方把它们存起来。

本文是系列第 2 课:学会仓颉里怎么声明变量和常量、仓颉有哪些基本数据类型、类型之间怎么转换,最后兑现上节课的约定——用 CIDE 的调试器亲眼看着变量值一步步变化。


目录(系列导航)

整套路线共 7 个模块、30 课,按每周 2~3 课的节奏,大约 2~3 个月可以走完一遍:

模块课次内容
一、环境与入门01~05环境搭建与 Hello World、变量与基本类型、运算符与输入输出、分支、循环
二、常用类型与数据组织06~10字符串、数组与区间、ArrayList/HashMap/HashSet、可空类型、错误处理
三、函数与函数式11~14函数、Lambda 与高阶函数、闭包、迭代器与惰性序列
四、面向对象与类型系统15~20struct/class、构造与属性、接口、枚举与 match 模式匹配、泛型、扩展
五、工程化与标准库21~25cjpm 包管理与多文件、文件 IO、JSON 处理、网络编程、单元测试
六、并发编程26~28线程、Channel 通道与同步原语、并发实战
七、项目实战29~30命令行小工具、GeoJSON 数据处理实战
  1. 环境搭建与第一个仓颉程序
  2. 变量与常量:let / var 与基本数据类型(本文)
  3. 运算符与标准输入输出
  4. 分支结构:if 与 when 表达式
  5. 循环结构:while / for / Range
  6. 字符串详解与字符串插值
  7. 数组 Array 与区间 Range
  8. 集合框架:ArrayList、HashMap、HashSet
  9. 可空类型 ? 与 Option
  10. 错误处理:异常机制与 Result
  11. 函数定义、参数与返回值
  12. Lambda 表达式与高阶函数
  13. 闭包、作用域与函数类型
  14. 迭代器 Iterator 与 Sequence
  15. 结构体 struct 与类 class
  16. 构造函数、属性与方法
  17. 接口 interface 与实现
  18. 枚举 enum、代数数据类型与 match 模式匹配
  19. 泛型编程
  20. 扩展、类型别名与可见性控制
  21. cjpm 包管理与多文件项目组织
  22. 文件与目录 IO
  23. JSON 处理(结合 stdx 扩展库)
  24. 网络编程入门
  25. 单元测试
  26. 并发基础:线程的创建与等待
  27. Channel 通道与同步原语
  28. 并发实战:多线程任务处理
  29. 实战一:带文件持久化的命令行小工具
  30. 实战二:GeoJSON 数据处理程序

一、为什么需要变量

先看一个没有变量的程序:

println("张三今年 18 岁")

如果我想让年龄加 1,就得手动改代码、重新编译——这显然不是"计算"。有了变量,我们可以给一个值起名字,之后用名字引用它、修改它:

var age: Int64 = 18
println("张三今年 ${age} 岁")
age = 19
println("一年后,张三 ${age} 岁")

你可以把变量理解成一个贴了标签的盒子:标签是变量名(age),盒子里装的是值(18),类型标注 : Int64 则规定了这个盒子只能装 64 位整数。

💡 代码里的 ${age} 是字符串插值:把变量的值拼进字符串里一起输出。字符串本身第 6 课才系统讲,今天照抄即可。


二、let 与 var:常量和变量

仓颉用两个关键字声明绑定:

关键字含义赋值后能否再改使用场景
let常量绑定(不可变)不能重新赋值整个生命周期值不变,如 π、配置项
var变量绑定(可变)可以反复重新赋值值会随程序运行变化,如计数器、年龄

2.1 基本写法

let pi: Float64 = 3.14159   // 常量:圆周率,不允许再改
var score: Int64 = 95       // 变量:考试分数,后面可以改
score = 98                  // 重新赋值,合法
println("pi=${pi}, score=${score}")

输出:

pi=3.141590, score=98

声明语句的三个部分:

var   score   : Int64   =   95
 │      │        │         │
关键字  变量名   类型标注    初始值

2.2 类型标注可以省略:类型推导

如果声明时就给了初始值,编译器能根据值自动推断类型,这时 : 类型 可以不写:

let age = 18        // 整数字面量,64 位平台上推导为 Int64
var price = 9.9     // 小数字面量,推导为 Float64
let passed = true   // 推导为 Bool

这叫类型推导——类型并没有消失,只是编译器替你写了。我们可以做个实验验证默认类型:

let n = 42
let x: Int32 = n    // 故意把 Int64 的值放进 Int32 的盒子

编译器直接报错:

error: mismatched types
  ==> main.cj:10:20
   |
10 |     let x: Int32 = n
   |                    ^ expected 'Int32', found 'Int64'

这证明了整数字面量默认被推导成 Int64。

2.3 let 赋值后不能改

let 绑定再次赋值会编译失败,而且报错信息非常直白:

let pi: Float64 = 3.14
pi = 3.15           // 错误:不能给不可变的值赋值
error: cannot assign to immutable value
 ==> main.cj:5:5
  |
5 |     pi = 3.15
  |     ^^^^^^^^^
  |
note: variable 'pi' is immutable
 ==> main.cj:4:9

💡 编码建议:默认优先用 let。只有当这个值确实需要被修改时才用 var。不可变的值更安全——读代码的人不用时刻担心它在别处被改掉,编译器也能帮你拦住误修改。这是现代语言共同的推荐风格。

2.4 先声明,后赋值

var 允许声明时先不给初始值,但必须写上类型,并且在第一次使用之前完成赋值:

var total: Int64     // 先声明:盒子先准备好
total = 100          // 再赋值
println("${total}")  // 使用时已有值,合法

编译器会做"明确赋值"检查——如果用的时候还没赋值,照样编译不过:

var total: Int64
println("${total}")  // 错误:使用前未初始化
error: variable 'total' is used before initialization

2.5 命名规则与约定

仓颉标识符的规则很宽松:

  1. 以**字母、下划线或其他 Unicode 字符(包括中文)**开头,后面可以跟数字;
  2. 大小写敏感:age 和 Age 是两个不同的名字;
  3. 不能使用 let、var、if 等关键字;
  4. 社区约定:变量/函数名用小驼峰(userName、totalScore),类型名用大驼峰(Int64、ArrayList)。
let userName = "张三"
var totalScore = 0
let 分数 = 100        // 中文标识符合法,但工程上不推荐

⚠️ 中文变量名虽然编译器支持,但为了团队协作和避免编码问题,正式项目还是老老实实用英文。


三、仓颉的基本数据类型

仓颉是强类型静态语言:每个值都有确定的类型,类型不同不能随便混用。基本(内置)类型如下:

3.1 整数类型

类型位宽取值范围
Int88 位-128 ~ 127
Int1616 位-32,768 ~ 32,767
Int3232 位约 -21 亿 ~ 21 亿
Int6464 位约 -9.2×10¹⁸ ~ 9.2×10¹⁸
UInt8~UInt64同上无符号(没有负数,从 0 开始)
Int / UInt平台相关主流 64 位平台上等价于 Int64 / UInt64

日常写业务代码直接用 Int64(或省略标注让编译器推导)即可;处理二进制数据、网络协议时才需要精确控制位宽,用 UInt8(字节)等类型。

整数字面量的花样写法(编译器实测全部合法):

let population = 1_000_000   // 下划线分隔,长数字一眼看清,值就是 1000000
let bin = 0b1010             // 0b 开头:二进制,等于十进制 10
let oct = 0o17               // 0o 开头:八进制,等于十进制 15
let hex = 0xFF               // 0x 开头:十六进制,等于十进制 255
println("${population} ${bin} ${oct} ${hex}")

输出:

1000000 10 15 255

3.2 浮点类型(小数)

类型位宽精度
Float3232 位单精度,约 7 位有效数字
Float6464 位双精度,约 15 位有效数字,最常用
let pi: Float64 = 3.14159     // 小数字面量默认推导为 Float64
let f: Float32 = Float32(2.5) // 需要 32 位时显式转换
println("${pi} ${f}")

输出:

3.141590 2.500000

💡 Float32(2.5) 这种"把类型名当函数调用"的写法,就是仓颉的显式类型转换,3.7 节细讲。

3.3 布尔类型 Bool

布尔类型 Bool 只有两个值:true(真)和 false(假),专门用于表示"是/否"这种二元状态,第 4 课 if 分支会大量用到:

let isStudent: Bool = true
let vip = false
println("${isStudent} ${vip}")

输出:

true false

3.4 字符类型 Rune

仓颉用 Rune 表示单个字符,字面量用 r 加单引号:r'A'、r'好'。它本质是一个 32 位的 Unicode 码点,可以表示世界上几乎所有文字:

let grade: Rune = r'A'
let hanzi = r'好'
println("${grade} ${hanzi}")                 // 直接插值输出字符本身
println("好 的 Unicode 码点:${UInt32(hanzi)}") // 转成整数看码点值

输出:

A 好
好 的 Unicode 码点:22909

注意区分:

  • r'A' 是 Rune 字符(单引号、带 r 前缀);
  • "A" 是 String 字符串(双引号),哪怕只有一个字符也是字符串。字符串第 6 课详讲。

常用转义字符和字符串里一样:r'\t'(制表符)、r'\n'(换行)、r'\\'(反斜杠)、r'\''(单引号)。

3.5 Unit 类型

Unit 类型只有一个值 (),表示"没有有意义的返回值",类似其他语言里的 void。目前先混个脸熟,第 11 课讲无返回值函数时会再见面:

let u = ()
println("${u}")   // 输出 ()

3.6 一张图记住类型选择

需要整数?        → Int64(99% 的场景)
需要小数?        → Float64
表示是非开关?    → Bool
处理单个字符?    → Rune
一段文本?        → String(第 6 课)
与字节/协议打交道?→ UInt8 等定宽类型

3.7 显式类型转换:仓颉不会"偷偷"帮你转

强类型意味着整数和小数之间不会自动转换。比如下面这行就编译不过:

var count: Int64 = 3.5
error: cannot convert a floating-point literal to type 'Int64'

想转换,必须在代码里显式写出来,用"类型名(值)"的形式:

let i: Int64 = 10
let d: Float64 = Float64(i)   // 整数 -> 小数,安全无损:10.000000
let back: Int64 = Int64(3.99) // 小数 -> 整数,小数部分直接截断:3
println("${d} ${back}")

输出:

10.000000 3

⚠️ 注意 Int64(3.99) 的结果是 3,不是 4——浮点转整数是截断(直接砍掉小数部分),不做四舍五入。想要四舍五入,以后用标准库的取整函数。

同样,Int32 和 Int64 这种不同位宽的整数之间赋值,也需要显式转换。看似麻烦,但它杜绝了一大批"精度悄悄丢失"的隐蔽 bug。


四、在 CIDE 调试器里"看见"变量

上节课说过,学完变量要正式玩调试器。现在正是时候——调试器最擅长的就是让你暂停程序,逐个检查变量的当前值。

4.1 准备程序

继续用上节课的 helloCangjie 项目(或者新建一个),把 src/main.cj 替换为下面这份"个人档案"代码,它把今天学的每种类型都用上了:

package helloCangjie

main(): Int64 {
    // ===== 个人档案:每种基本类型亮个相 =====
    let name = "张三"              // String 字符串,第 6 课详讲
    var age: Int64 = 18            // 64 位整数,var 表示后面可以改
    let height: Float64 = 1.75     // 64 位小数,单位:米
    let isStudent: Bool = true     // 布尔:只有 true / false
    let level: Rune = r'A'         // 字符:一个 Unicode 码点

    age = 19 // 过了一年长一岁:var 变量允许重新赋值

    println("===== 个人档案 =====")
    println("姓名:${name}")
    println("年龄:${age}")
    println("身高:${height} 米")
    println("是否学生:${isStudent}")
    println("等级:${level}")
    println("字符 好 的 Unicode 码点:${UInt32(r'好')}")

    // 身高换算成厘米:Float64 -> Int64 必须显式转换,小数部分直接截断
    let heightCm = Int64(height * 100.0)
    println("身高(取整):${heightCm} 厘米")
    return 0
}

4.2 调试三步走

  1. 在第一行 println("===== 个人档案 =====") 左侧行号沟槽点一下,打上红点断点;
  2. 菜单 Run → Debug(Shift + F9)启动调试,程序在断点处暂停,当前行高亮;
  3. 观察调试工具窗口:
  • Variables(变量)面板:当前作用域内的所有局部变量一览无余——名字、值、类型三列。你会看到 name = "张三"、age = 19(赋值语句已执行过)、height = 1.75、isStudent = true、level = 'A';
  • 鼠标悬停:把光标移到编辑器里任意变量名上悬停,会浮出一个小提示框显示它的当前值,不用来回切面板;
  • 按 F8 单步跳过,逐行执行,每按一下看 Variables 面板和 Result 面板的变化——特别是走到 let heightCm = ... 那一行之前和之后,看着 heightCm 从"不存在"变成 175。

📸 配图建议:断点命中后 Variables 面板中各变量名/值/类型三列 + 编辑器里鼠标悬停 age 浮出值的截图。

两个小实验:

  1. 把断点打在 age = 19 那一行,F8 执行前后对比 age 的值(18 → 19);
  2. 试试把鼠标悬停在 heightCm 上——声明语句执行前它还不存在,提示框里是找不到它的,这能帮你直观理解"变量的作用域从声明处开始"。

调试结束点停止按钮,结束调试会话。


五、常见问题 FAQ

Q1:到底什么时候用 let,什么时候用 var?
默认一律用 let;当你确实需要在后面给它重新赋值(比如累加、状态翻转)时,再改成 var。拿不准就先写 let,编译器报错了再改,成本极低。

Q2:Int 和 Int64 有什么区别?写哪个?
Int 的位宽跟随平台,在主流 64 位平台上就是 Int64。学习期和普通业务直接用 Int64 最省心;写需要跨平台/跨语言交换的底层数据时,定宽类型语义更明确。

Q3:为什么 var count: Int64 = 3.5 会报错?3.5 不就是个数吗?
仓颉是强类型语言,整数盒子不能直接装小数,编译器不会替你做隐式转换。写 Float64(3.5) 放进浮点变量,或写 Int64(3.5) 显式取整。

Q4:Int64(3.99) 是四舍五入得到 4 吗?
不是,是截断,结果为 3。需要四舍五入时使用标准库提供的取整函数,后面课程会讲。

Q5:r'A' 和 "A" 有什么区别?
r'A' 是 Rune(单字符,32 位码点);"A" 是 String(字符串)。即使看起来只有一个字符,双引号的也是字符串,二者类型不同。

Q6:变量声明时可以先不赋值吗?
var 可以(必须带类型标注),但编译器保证你在第一次读取它之前完成赋值,否则报 used before initialization;let 声明时必须直接给值。

Q7:调试暂停了,但 Variables 面板是空的?
先确认程序确实停在断点(当前行高亮);再看面板里选中的栈帧是不是 main。另外面板只显示当前作用域、已经声明过的局部变量,作用域外和声明语句执行前的变量自然看不到。


六、课后练习

  1. 声明 5 个变量描述你自己:姓名(字符串)、年龄(Int64)、身高(Float64)、是否学生(Bool)、等级(Rune),全部输出;然后把其中会变化的项改成 var 并重新赋值一次,对比哪些必须是 let、哪些必须是 var。
  2. 故意把一个 let 常量重新赋值,编译并阅读 Build 面板的报错(cannot assign to immutable value),把报错文案和 2.3 节对照一下。
  3. 先心算:Int64(7.8)、Int64(-2.3) 的结果分别是几?写程序验证你的答案,体会"截断"和"四舍五入"的区别。
  4. 在练习 1 的程序里打断点,用 Shift + F9 启动调试,F8 单步走完全程,在 Variables 面板里确认每个变量的类型列显示的正是你预期的类型。

下节预告

现在我们会"存数据"了,但还不会"算数据"。第 3 课进入运算符与标准输入输出:算术、比较、逻辑运算符,以及让程序接收用户键盘输入——到那时程序就能根据不同的输入产生不同的输出,真正"活"起来。我们下节课见!


系列说明:本系列基于 Windows 平台 + CIDE + 仓颉 SDK(1.1.3)编写,所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入,以你本地版本为准,欢迎评论区交流。


📥 工具下载

本系列全程使用的仓颉 IDE —— CIDE(免费开源、社区版):

Logo

一站式 AI 云服务平台

更多推荐