【Cangjie】仓颉编程语言学习03——控制流表达式_A篇
本章介绍了仓颉语言中的控制流表达式,包括分支结构(if表达式)和循环结构(while、do-while、for-in表达式)。if表达式分为含else和不含else两种,其求值规则取决于上下文类型要求。循环表达式主要用于重复执行代码块,支持break和continue控制流程。特别讲解了for-in遍历数组、字符串和区间的用法,以及where条件和通配符_的使用。最后通过多个示例(如判断闰年、成绩
本章节同样分为了A、B篇,B篇内容相对较少。B篇也同样是在以后再出,因此看完本章A篇后且完成了代码练习后,就可以直接跳到 C篇,后面笔者会提醒读者回来看本章B篇(前提是我写好了)。
本章节的内容相当实用,学完本章的内容,就能搭建出一个比较完整的程序框架了,包括实现第一章所提出的问题。而且本章学完之后,应该就可以写几个小项目了(代码量很快就可以上来了)。
关于本篇的内容,其实不建议有其他语言基础的读者直接跳过,因为笔者对本篇内容的解析可能会给你提供了一个全新的、基础的角度理解一个语言的语法。对于0基础读者,本篇一定要通读看完,看不懂的可以评论区交流,因为本篇的内容很基础,最起码在学习的时候要能抽象理解。
还记得仓颉中表达式得概念吗?凡是可求值的语言元素都是表达式。
本章节主要介绍仓颉中重要的几个表达式,分为两类:条件表达式和循环表达式。
任何一段程序的执行流程,只会涉及三种基本结构——顺序结构、分支结构和循环结构。实际上,分支结构和循环结构,是由某些指令控制当前顺序执行流产生跳转而得到的,它们让程序能够表达更复杂的逻辑,在仓颉中,这种用来控制执行流的语言元素就是条件表达式和循环表达式。
这些表达式会依附一个大括号"{}",里面包围一组表达式,仓颉称之为代码块。它将作为程序的一个顺序执行流,其中的表达式将按编码顺序依次执行。如果代码块中有至少一个表达式,我们规定此代码块的值与类型等于其中最后一个表达式的值与类型,如果代码块中没有表达式,规定这种空代码块的类型为 Unit、值为 ()。
注意:代码块本身不是一个表达式,不能被单独使用。
一、分支表达式
条件表达式分为 if 表达式和 if-let 表达式两种,它们的值与类型需要根据使用场景来确定。
if-let 表达式会在 B 篇中介绍,因为它与模式匹配有关。
if 表达式
if 表达式的基本形式为:
if (条件) {
分支 1
} else {
分支 2
}
其中“条件”是布尔类型表达式,“分支 1”和“分支 2”是两个代码块。if 表达式将按如下规则执行:
- 计算“条件”表达式,如果值为
true则转到第 2 步,值为false则转到第 3 步。 - 执行“分支 1”,转到第 4 步。
- 执行“分支 2”,转到第 4 步。
- 继续执行
if表达式后面的代码。
在一些场景中,我们可能只关注条件成立时该做些什么,所以 else 和对应的代码块是允许省略的。
let number : Int64 = 128
println(number)
if (number < 0){
print("number 值小于 0")
}
if (number % 2 == 0) {
println("偶数")
} else {
println("奇数")
}
仓颉编程语言是强类型的,if 表达式的条件只能是布尔类型,不能使用整数或浮点数等类型,和 C 语言等不同,仓颉不以条件取值是否为 0 作为分支选择依据。(这一点有其他语言基础的读者要注意了)
在许多场景中,当一个条件不成立时,我们可能还要判断另一个或多个条件、再执行对应的动作,仓颉允许在 else 之后跟随新的 if 表达式,由此支持多级条件判断和分支执行,例如:
let speed :Float64 = 8.2
println("${speed} km/s")
if (speed > 16.7) {
println("第三宇宙速度,鹊桥相会")
} else if (speed > 11.2) {
println("第二宇宙速度,嫦娥奔月")
} else if (speed > 7.9) {
println("第一宇宙速度,腾云驾雾")
} else {
println("脚踏实地,仰望星空")
}
(一)、if 表达式的求值
if 表达式要分为两种,一种是 含 else 分支的 if 表达式,一种是 不含 else 分支的 if 表达式。
先说后者。因为 if 分支的代码块可能不被执行,所以规定这类 不含 else 分支的 if 表达式的类型为 Unit,值为 ()
关于含 else 分支的 if 表达式,它需要分两种情况,一种是需要对其求值,一种是不关心它的求值。
如果我们不关心它的求值(前面写的两个代码就是不关心它的求值),仓颉规定这种场景下的 if 表达式类型为 Unit、值为 (),且各分支不参与上述类型检查。(一会做解释)
当我们需要对其求值时,有两个需要注意的地方:
- 如果上下文明确要求值类型为
T,则if表达式各分支代码块的类型必须是T的子类型,这时if表达式的类型被确定为T,如果不满足子类型约束,编译会报错。 - 如果上下文没有明确的类型要求,则
if表达式的类型是其各分支代码块类型的最小公共父类型,如果最小公共父类型不存在,编译会报错。
看代码抽象理解:
var condition : Int = 1 // Int 是 Int64 的别名 等价于 Int64
var a : Int64 = 32
let b : Array<Int64> = [1, 2, 3]
var c : Int64 = 64
var d : String = "hello cangjie"
// 没有 else 分支的 if 表达式 类型为 Unit、值为 ()
let ret1 : String = "${if (condition == 1) { a }}"
// 有 else 分支的 if 表达式,但不关心求值结果
if (condition == 1)
{
a=32 // 分支代码块不参与类型检查。 (不过实际 赋值表达式类型为 Unit 可回顾上章节内容)
} else {
a // 分支代码块不参与类型检查。 (不过实际 类型为 Int64)
}
// 有 else 分支 的 if 表达式 且上下文明确要求值类型为 Int64
// 如果有一个分支 改变了类型都会报错,比如:去掉a 或 把它注释
let ret3 : Int64 = if (condition == 1)
{
a // 分支代码块类型为 Int64
} else {
c // 分支代码块类型为 Int64
}
// 有 else 分支的 if 表达式,但上下文不明确要求值类型,
// 表达式类型是其 各分支代码块类型的最小公共父类型
let ret4 = if (condition == 1)
{
a // 不影响此分支代码块 类型
d // 类型为 String 最小公共父类型是 ToString
} else {
b // 类型为 Array<Int64> 最小公共父类型是 ToString
}
// 为什么 String 与 Array<Int64> 的最小公共父类型是 ToString,这个学到后面就知道了,不作解释
println("没有 else 分支的 if 表达式求值结果为:${ret1}")
println("有 else 分支 的 if 表达式 且上下文明确要求值类型为 Int64,结果为:${ret3}")
println("有 else 分支的 if 表达式,但上下文不明确要求值类型,表达式类型是其 各分支代码块类型的最小公共父类型,结果为:${ret4}")
其中知识点有些多,慢慢领悟。还有笔者给出的直到现在的代码都是放在 main 后面的代码块中的。
if-else 表达式的求值机制应该是为了替代 C 语言中的 三元运算符(?:),0基础无需理会。
代码在编译构建时,会有警告提示:else分支永不可达或是某个变量未被使用。这些警告不碍事,不是红色报错就不影响程序运行,但如果是以后写项目时,那就尽量黄色警告都要解决了。代码运行结果如下:

做一个练习,写一个程序,判断一个年份是不是属于闰年(能被4整除而不能被100整除或者能被400整除),并打印输出结果。笔者的代码会放到最后面。
看下面的成绩分制,写一个程序判断成绩在哪一个等级,并打印输出。
百分制 五级制
90~100 A
80~89 B
70~79 C
60~69 D
0~59 E
二、循环表达式
循环表达式有四种:for-in 表达式、while 表达式、do-while 表达式和 while-let 表达式,它们的类型都是 Unit、值为 ()。其中 while-let 表达式与模式匹配相关,将在 B 篇介绍。
循环表达式在设定上就没 条件表达式复杂了,东西少一些。
1、while 表达式
while 表达式的基本形式为:
while (条件) {
循环体
}
其中“条件”是布尔类型表达式,“循环体”是一个代码块。while 表达式将按如下规则执行:
- 计算“条件”表达式,如果值为
true则转第 2 步,值为false转第 3 步。 - 执行“循环体”,转第 1 步。
- 结束循环,继续执行
while表达式后面的代码。
示例,从1加到100的程序(这个代码也要手敲哦,不要直接复制粘贴):
var i : Int = 1
var sum : Int = 0
while (i <= 100){
sum += i
i++
}
println("从1加到100结果为${sum}"}
2、do-while 表达式
do-while 表达式的基本形式为:
do {
循环体
} while (条件)
其中“条件”是布尔类型表达式,“循环体”是一个代码块。do-while 表达式将按如下规则执行:
- 执行“循环体”,转第 2 步。
- 计算“条件”表达式,如果值为
true则转第 1 步,值为false转第 3 步。 - 结束循环,继续执行
do-while表达式后面的代码。
var i : Int = 1
var sum : Int = 0
do {
sum += i
i++
}while (i <= 100)
println("从1加到100结果为${sum}"}
do-while 与 while 表达式的区别是,do-while 至少会执行一次 循环体
3、for-in 表达式
for-in 表达式可以遍历那些扩展了迭代器接口 Iterable<T> 的类型实例(以后我们会学这个扩展的,不用着急)。for-in 表达式的基本形式为:
for (迭代变量 in 序列) {
循环体
}
其中“循环体”是一个代码块。“迭代变量”是单个标识符或由多个标识符构成的元组,用于绑定每轮遍历中由迭代器指向的数据,可以作为“循环体”中的局部变量使用。“序列”是一个表达式,它只会被计算一次,遍历是针对此表达式的值进行的,其类型必须扩展了迭代器接口 Iterable<T>。for-in 表达式将按如下规则执行:
- 计算“序列”表达式,将其值作为遍历对象,并初始化遍历对象的迭代器。
- 更新迭代器,如果迭代器终止,转第 4 步,否则转第 3 步。
- 将当前迭代器指向的数据与“迭代变量”绑定,并执行“循环体”,转第 2 步。
- 结束循环,继续执行
for-in表达式后面的代码。
在 for-in 表达式的循环体中,不能修改迭代变量。
在之前学的10个基础数据类型中,Array、String、区间类型都 扩展了迭代器接口。
迭代器的抽象含义是什么,先不必领会,先学会如何使用就行,用着用着就明白了。
var name : String = "hello cangjie\n"
for (char in name){
print(char)
}
现在由读者写一个循环打印 Array 数组的循环,至于区间后面会有。快练习一下吧。
区间遍历一般这样用多一些:
var sum = 0
// let vet : Range<Int64> = 1..=100 // 不会专门手动定义一个区间类型变量
for (i in 1..=100) {
sum += i
}
println(sum)
还记得说 迭代变量不可修改吗?如果想遍历 Array 并修改其元素的值,可以利用区间遍历:
let array = [1, 2, 3, 4]
for (i in 0..array.size){
array[i] = 0
}
以上的代码是完全合法的,但如果我们试图修改 迭代变量 i 的值,就是非法的,编译器会报错。
a. 使用通配符 _ 代替迭代变量
在一些应用场景中,只需要循环执行某些操作,但并不使用迭代变量,这时可以使用通配符 _ 代替迭代变量,例如:
main() {
var number = 2
for (_ in 0..5) {
number *= number
}
println(number)
}
通配符 _ 的使用场景是,如示例代码,不需要使用迭代变量,单纯需要循环指定次数时,这样用就可以了。
b. where 条件
在部分循环遍历场景中,对于特定取值的迭代变量,可能需要直接跳过、进入下一轮循环。仓颉为此提供了更便捷的表达方式——可以在所遍历的“序列”之后用 where 关键字引导一个布尔表达式,这样在每次将进入循环体执行前,会先计算此表达式,如果值为 true 则执行循环体,反之直接进入下一轮循环。例如:
// 把数组中 所有奇数递增变为偶数
var arr : Array<Int64> = [1, 47, 33, 59, 24, 73]
for (i in 0..arr.size where arr[i] % 2 == 1) {
arr[i]++
}
注意:有其他语言基础的读者,一定要搞清楚,如果 where 引导的布尔表达式 计算结果为false时,for-in 循环不终止,只是跳过本次循环而已,紧接着就会进入下一次循环。真正主导 for-in 循环终止的条件是,迭代变量 遍历完整个 序列 之后,循环才终止。简而言之,遍历了整个序列之后,循环才终止。
4、break 与 continue 表达式
在循环结构的程序中,有时需要根据特定条件提前结束循环或跳过本轮循环,为此仓颉引入了 break 与 continue 表达式,它们可以出现在循环表达式的循环体中。
break 用于终止当前循环表达式的执行、转去执行循环表达式之后的代码,continue 用于提前结束本轮循环、进入下一轮循环。
break 与 continue 表达式的类型都是 Nothing。
例如,以下程序使用 for-in 表达式和 break 表达式,在给定的整数数组中,找到第一个能被 5 整除的数字:
let numbers = [12, 18, 25, 36, 49, 55]
for (number in numbers) {
if (number % 5 == 0) {
println(number)
break// 迭代到 第三个元素 下标为2时 执行break表达式,退出 for-in 循环表达式
}
}
continue表达式 与 break表达式的用法一致,就交给读者测试了。代码写起来吧,尝试重写 where条件的示例代码,让它使用continue表达式来达到一样的效果。
5、三个循环表达式的区别
相同:
- 都能循环执行 循环体中的代码(循环表达式的代码块也称为循环体)
- 都能使用 break表达式 或 continue表达式
不同:
- while 表达式 与 do-while 表达式 都由条件主导循环 继续或终止;
for-in 表达式 由序列的迭代主导循环的终止 - do-while 表达式 至少执行一次 循环体
练习:
1、还记得第一章的引言吗?现在要求你创建一个数组,数组的元素类型是元组类型,元组有4个元素,分别是 名字,语数英成绩。由你自己初始化它们。分别使用 for-in 表达式遍历打印一次数组的内容;使用 while 表达式遍历一次数组,找出总分最高的学生,打印这个学生的名字与总分。然后再使用一个循环表达式分别计算该班级3科成绩的平均值。
要求:要使用插值字符串打印信息
2、修改上一题求最高分的代码,改为用 if 表达式 的求值语法,比如 :
var max : Int64 = 0
max = if (_本次循环迭代变量的总分 > max) { _本次循环迭代变量的总分 } else { max }
if 表达式 与 else 分支 放在同一行也是合法的,可正常运行。
附录.
1、练习代码
判断一个年份是不是属于闰年
代码有黄色警报,不过没问题。正说明仓颉是安全度高的语言。
let year : Int = 2025
var ret : String = "${year} "
if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0){
ret += "是闰年"
}else{
ret += "不是闰年"
}
println(ret)
写一个程序判断成绩在哪一个等级.(方法不唯一,主要是想让0基础读者,能回顾更多知识)
let level_arr : Array<Rune> = [r'A', r'B', r'C', r'D', r'E']
var i : Int64
let grade = 78
if (grade >= 90){
i = 0
}else if (grade >= 80){
i = 1
}else if (grade >= 70){
i = 2
}else if (grade >= 60){
i = 3
}else{
i = 4
}
println("成绩 ${grade} 属于等级 ${level_arr[i]}")
还记得第一章的引言吗?现在要求你创建一个数组,数组的元素类型是元组类型,元组有4个元素,分别是 名字,语数英成绩。由你自己初始化它们。分别使用 for-in 表达式遍历打印一次数组的内容;使用 while 表达式遍历一次数组,找出总分最高的学生,打印这个学生的名字与总分。然后再使用一个循环表达式分别计算该班级3科成绩的平均值。
下面是完整的代码:
package study_code
main(): Int64 {
// 还记得第一章的引言吗?现在要求你创建一个数组,数组的元素类型是元组类型,元组有4个元素,分别是 名字,语数英成绩。由你自己初始化它们。分别使用 for-in 表达式遍历打印一次数组的内容;使用 while 表达式遍历一次数组,找出总分最高的学生,打印这个学生的名字与总分。然后再使用一个循环表达式分别计算该班级3科成绩的平均值。
let grades : Array<(name : String, grade_Chinese : Float64, grade_Math : Float64, grade_English : Float64)>
grades = [
("小明", 72.0, 98.0, 69.0),
("小红", 85.5, 88.0, 91.5),
("小刚", 63.0, 75.5, 60.0),
("小芳", 92.0, 89.5, 94.0),
("小强", 58.5, 61.0, 55.0),
("小丽", 87.0, 83.5, 89.0),
("小勇", 76.0, 70.5, 73.0),
("小倩", 95.5, 93.0, 97.5),
("小磊", 68.0, 65.5, 62.0),
("小雨", 81.5, 84.0, 80.5)
]
for (it in grades){
println("${it[0]} 同学:语文:${it[1]} 数学:${it[2]} 英语:${it[3]}")
}
// 下面这行代码 有些超纲,不理解没关系,先知道怎么用
print("\n${Array<Rune>(60, item : r'-')}\n")// 插值表达式 使用构造函数构造一个 Rune数组
// 构造函数 构造 20个r'-' r'-' 是 字符字面量,
// \n 是转义字符,意为 换行符
var i : Int64 = 0
// 变量命名要规范 不只是符合标识符的命名规则,更要让程序员一看就知道这个变量的意义是什么
var max_grade : Float64 = 0.0 // 总分最低分就是 0 分了
var max_name : String = ""
while (i < grades.size){ // 下标从 0开始 直到 grades.size - 1 所以是小于,而不是小于等于
let it = grades[i] // 这里是引用 grades 数组 的元素,没有写出it的类型了,类型太长了,写太麻烦了
var tmp_total_grade : Float64 = it[1] + it[2] + it[3] // 元组的 下标0 是 名字
if (max_grade < tmp_total_grade){// 这里是小于运算,可以直接用 如果是判断两个浮点数 == 相等,就不能直接比较了
max_grade = tmp_total_grade
max_name = it[0]
}
i++// 注意迭代 条件
}
println("总分最高分的同学是 ${max_name}, 总分为 ${max_grade} \n")// 多换一行
var total_grade_chinese : Float64 = 0.0
var total_grade_math : Float64 = 0.0
var total_grade_english : Float64 = 0.0
for (it in grades){
// 代码格式整齐总是好的
total_grade_chinese += it[1]
total_grade_math += it[2]
total_grade_english += it[3]
}
var average_grade_chinese : Float64 = total_grade_chinese / Float64(grades.size) // 类型转换 这个超纲了
var average_grade_math : Float64 = total_grade_math / Float64(grades.size)
var average_grade_english : Float64 = total_grade_english / Float64(grades.size)
println("""
语文平均分:${average_grade_chinese}
数学平均分:${average_grade_math}
英语平均分:${average_grade_english}
""")
return 0
}
修改上一题求最高分的代码,改为用 if 表达式 的求值语法:
var i : Int64 = 0
// 变量命名要规范 不只是符合标识符的命名规则,更要让程序员一看就知道这个变量的意义是什么
var max_grade : Float64 = 0.0 // 总分最低分就是 0 分了
var max_name : String = ""
while (i < grades.size){ // 下标从 0开始 直到 grades.size - 1 所以是小于,而不是小于等于
let it = grades[i] // 这里是引用 grades 数组 的元素,没有写出it的类型了,类型太长了,写太麻烦了
var tmp_total_grade : Float64 = it[1] + it[2] + it[3] // 元组的 下标0 是 名字
//------------------------------------------------------------
(max_grade, max_name) = if (max_grade < tmp_total_grade){
(tmp_total_grade, it[0])
}else{
(max_grade, max_name)
}
//------------------------------------------------------------------
i++// 注意迭代 条件
}
更多推荐


所有评论(0)