本章节同样分为了A、B篇,B篇内容相对较少。B篇也同样是在以后再出,因此看完本章A篇后且完成了代码练习后,就可以直接跳到 C篇,后面笔者会提醒读者回来看本章B篇(前提是我写好了)。

本章节的内容相当实用,学完本章的内容,就能搭建出一个比较完整的程序框架了,包括实现第一章所提出的问题。而且本章学完之后,应该就可以写几个小项目了(代码量很快就可以上来了)。

关于本篇的内容,其实不建议有其他语言基础的读者直接跳过,因为笔者对本篇内容的解析可能会给你提供了一个全新的、基础的角度理解一个语言的语法。对于0基础读者,本篇一定要通读看完,看不懂的可以评论区交流,因为本篇的内容很基础,最起码在学习的时候要能抽象理解。


还记得仓颉中表达式得概念吗?凡是可求值的语言元素都是表达式。

本章节主要介绍仓颉中重要的几个表达式,分为两类:条件表达式和循环表达式。

任何一段程序的执行流程,只会涉及三种基本结构——顺序结构分支结构循环结构。实际上,分支结构和循环结构,是由某些指令控制当前顺序执行流产生跳转而得到的,它们让程序能够表达更复杂的逻辑,在仓颉中,这种用来控制执行流的语言元素就是条件表达式和循环表达式。

 这些表达式会依附一个大括号"{}",里面包围一组表达式,仓颉称之为代码块。它将作为程序的一个顺序执行流,其中的表达式将按编码顺序依次执行。如果代码块中有至少一个表达式,我们规定此代码块的值与类型等于其中最后一个表达式的值与类型,如果代码块中没有表达式,规定这种空代码块的类型为 Unit、值为 ()

注意:代码块本身不是一个表达式,不能被单独使用。

一、分支表达式

条件表达式分为 if 表达式和 if-let 表达式两种,它们的值与类型需要根据使用场景来确定。

 if-let 表达式会在 B 篇中介绍,因为它与模式匹配有关。

if 表达式

if 表达式的基本形式为:

if (条件) {
  分支 1
} else {
  分支 2
}

其中“条件”是布尔类型表达式,“分支 1”和“分支 2”是两个代码块。if 表达式将按如下规则执行:

  1. 计算“条件”表达式,如果值为 true 则转到第 2 步,值为 false 则转到第 3 步。
  2. 执行“分支 1”,转到第 4 步。
  3. 执行“分支 2”,转到第 4 步。
  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 表达式将按如下规则执行:

  1. 计算“条件”表达式,如果值为 true 则转第 2 步,值为 false 转第 3 步。
  2. 执行“循环体”,转第 1 步。
  3. 结束循环,继续执行 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 表达式将按如下规则执行:

  1. 执行“循环体”,转第 2 步。
  2. 计算“条件”表达式,如果值为 true 则转第 1 步,值为 false 转第 3 步。
  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 表达式将按如下规则执行:

  1. 计算“序列”表达式,将其值作为遍历对象,并初始化遍历对象的迭代器。
  2. 更新迭代器,如果迭代器终止,转第 4 步,否则转第 3 步。
  3. 将当前迭代器指向的数据与“迭代变量”绑定,并执行“循环体”,转第 2 步。
  4. 结束循环,继续执行 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++// 注意迭代 条件
    }

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐