仓颉语言入门 —— 基本概念

Posted by     "xtcel" on Thursday, July 4, 2024

仓颉语言入门——基本概念

本文将详细介绍仓颉语言的基本概念,包括标识符、程序结构、表达式和函数。这些概念是掌握仓颉语言的基础,理解它们将帮助你更好地编写和维护代码。

标识符

标识符(Identifier)是用于标识变量、函数、类等编程元素的名称。在仓颉语言中,标识符由字母、数字、下划线组成,但不能以数字开头。标识符的命名应具有描述性,以提高代码的可读性。

规则

  1. 标识符必须以字母或下划线 _ 开头,后面可以跟字母、数字或下划线。如果以下划线开头则后面第一个必须跟英文字母。
  2. 标识符区分大小写,例如 variableVariable 是两个不同的标识符。
  3. 避免使用仓颉语言的关键字作为标识符,不过可以在关键字外面加上一对反引号的方式使用关键字作为标识符,这种标识符在仓颉中称为原始标识符。

示例

let userName = "John";   // 有效标识符
let _age = 30;           // 有效标识符
let $price = 99.99;      // 无效标识符,不能包含 $ 符号
let 2ndItem = "Apple";   // 无效标识符,不能以数字开头
let _123 = "123"				 // 以下划线开头,其后第一个必需跟字母

程序结构

在仓颉程序的顶层作用域中,可以定义一系列的变量、函数和自定义类型,其中的变量和函数分别被称为全局变量和全局函数。如果要将仓颉程序编译为可执行文件,您需要在顶层作用域中定义一个 main 函数作为程序入口。 例如在以下程序中,我们在顶层作用域定义了全局变量 a 和全局函数 b,还有自定义类型 C、D 和 E,以及作为程序入口的 main 函数。

// example.cj
let a = 2023
func b() {}
struct C {}
class D {}
enum E { F | G }

main() {
    println(a)
}

在非顶层作用域中不能定义上述自定义类型,但可以定义变量和函数,称之为局部变量局部函数。特别地,对于定义在自定义类型中的变量和函数,称之为成员变量成员函数

变量

在仓颉编程语言中,一个变量由对应的变量名、数据(值)和若干属性构成,开发者通过变量名访问变量对应的数据。 变量定义的具体形式为:

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

其中修饰符用于设置变量的各类属性,可以有一个或多个,常用的修饰符包括:

  • 可变性修饰符:let 与 var,分别对应不可变和可变属性,可变性决定了变量被初始化后其值还能否改变,仓颉变量也由此分为不可变变量和可变变量两类。
  • 可见性修饰符:private 与 public 等,影响全局变量和成员变量的可引用范围,详见后续章节的相关介绍。
  • 静态性修饰符:static,影响成员变量的存储和引用方式,详见后续章节的相关介绍。

在定义仓颉变量时,可变性修饰符是必要的,在此基础上,还可以根据需要添加其他修饰符。

  • 变量名应是一个合法的仓颉标识符。
  • 变量类型指定了变量所持有数据的类型。当初始值具有明确类型时,可以省略变量类型标注,此时编译器可以自动推断出变量类型。
  • 初始值是一个仓颉表达式,用于初始化变量,如果标注了变量类型,需要保证初始值类型和变量类型一致。在定义全局变量或静态成员变量时,必须指定初始值。在定义局部变量或实例成员变量时,可以省略初始值,但需要标注变量类型,同时要在此变量被引用前完成初始化,否则编译会报错。

值类型和引用类型变量

程序在运行阶段,只有指令流转和数据变换,仓颉程序中的各种标识符已不复存在。由此可见,编译器使用了一些机制,将这些名字和编程所取用的数据实体/存储空间绑定起来。 从编译器实现层面看,任何变量总会关联一个值(一般是通过内存地址/寄存器关联),只是在使用时,对有些变量,我们将直接取用这个值本身,这被称为值类型变量,而对另一些变量,我们把这个值作为索引、取用这个索引指示的数据,这被称为引用类型变量。值类型变量通常在线程栈上分配,每个变量都有自己的数据副本;引用类型变量通常在进程堆中分配,多个变量可引用同一数据对象,对一个变量执行的操作可能会影响其他变量。

作用域

作用域将名字和程序元素的绑定关系限制在一定范围里。不同作用域之间可以是并列或无关的,也可以是嵌套或包含关系。一个作用域将明确我们能用哪些名字访问哪些程序元素,具体规则是:

  1. 当前作用域中定义的程序元素与名字的绑定关系,在当前作用域和其内层作用域中是有效的,可以通过此名字直接访问对应的程序元素。
  2. 内层作用域中定义的程序元素与名字的绑定关系,在外层作用域中无效。
  3. 内层作用域可以使用外层作用域中的名字重新定义绑定关系,根据规则 1,此时内层作用域中的命名相当于遮盖了外层作用域中的同名定义,对此我们称内层作用域的级别比外层作用域的级别高。

在仓颉编程语言中,用一对大括号“{}”包围一段仓颉代码,即构造了一个新的作用域,其中可以继续使用大括号“{}”包围仓颉代码,由此产生了嵌套作用域,这些作用域均服从上述规则。特别的,在一个仓颉源文件中,不被任何大括号“{}”包围的代码,它们所属的作用域被称为“顶层作用域”,即当前文件中“最外层”的作用域,按上述规则,其作用域级别最低。

表达式

在仓颉编程语言中,凡是可求值的语言元素都是表达式。因此,仓颉不仅有传统的算术运算表达式,还有条件表达式、循环表达式和 try 表达式等,它们都可以被求值,并作为值去使用,如作为变量定义的初值和函数实参等。此外,因为仓颉是强类型的编程语言,所以仓颉表达式不仅可求值,还有确定的类型。 任何一段程序的执行流程,只会涉及三种基本结构——顺序结构、分支结构和循环结构。实际上,分支结构和循环结构,是由某些指令控制当前顺序执行流产生跳转而得到的,它们让程序能够表达更复杂的逻辑,在仓颉中,这种用来控制执行流的语言元素就是条件表达式和循环表达式。

算术运算表达式

算术运算表达式用于进行数学计算,包括加减乘除和取余操作。

let sum = 10 + 20;        // 加法
let difference = 30 - 10; // 减法
let product = 5 * 6;      // 乘法
let quotient = 20 / 4;    // 除法
let remainder = 7 % 3;    // 取余
let isAdult = age > 18;               // 大于运算
let isTeenager = age >= 13 && age <= 19; // 逻辑与运算
let isNotAdult = !isAdult;           // 逻辑非运算

条件表达式

条件语句用于根据条件执行不同的代码块。在仓颉编程语言中,条件表达式分为 if 表达式和 if-let 表达式两种,它们的值与类型需要根据使用场景来确定。仓颉没有 switch-case 条件表达式,但完全可以使用更强大的 math 模式匹配表达式来替代。

if 表达式

let age = 20;

if (age > 18) {
    print("Adult");
} else {
    print("Minor");
}

循环表达式

循环表达式用于重复执行某段代码,主要有 for-in 循环、while 循环和 do-while 循环。

while 表达式

while 表达式的基本形式为:

while (条件) {
  循环体
}

其中“条件”是布尔类型表达式,“循环体”是一个代码块。while 表达式将按如下规则执行:

  1. 计算“条件”表达式,如果值为 true 则转第 2 步,值为 false 转第 3 步。
  2. 执行“循环体”,转第 1 步。
  3. 结束循环,继续执行 while 表达式后面的代码。

例如,以下程序使用 while 表达式,基于二分法,近似计算数字 2 的平方根:

main() {
    var root = 0.0
    var min = 1.0
    var max = 2.0
    var error = 1.0
    let tolerance = 0.1 ** 10


    while (error ** 2 > tolerance) {
        root = (min + max) / 2.0
        error = root ** 2 - 2.0
        if (error > 0.0) {
            max = root
        } else {
            min = root
        }
    }
    println("2 的平方根约等于:${root}")
}

运行以上程序,将输出:

2 的平方根约等于:1.414215

do-while 表达式

do-while 表达式的基本形式为:

do {
    循环体
} while (条件)

其中“条件”是布尔类型表达式,“循环体”是一个代码块。do-while 表达式将按如下规则执行:

  1. 执行“循环体”,转第 2 步。
  2. 计算“条件”表达式,如果值为 true 则转第 1 步,值为 false 转第 3 步。
  3. 结束循环,继续执行 do-while 表达式后面的代码。

例如,以下程序使用 do-while 表达式,基于蒙特卡洛算法,近似计算圆周率的值:

import std.random.*


main() {
    let random = Random()
    var totalPoints = 0
    var hitPoints = 0


    do {
        // 在 ((0, 0), (1, 1)) 这个正方形中随机取点
        let x = random.nextFloat64()
        let y = random.nextFloat64()
        // 判断是否落在正方形内接圆里
        if ((x - 0.5) ** 2 + (y - 0.5) ** 2 < 0.25) {
            hitPoints++
        }
        totalPoints++
    } while (totalPoints < 1000000)


    let pi = 4.0 * Float64(hitPoints) / Float64(totalPoints)
    println("圆周率近似值为:${pi}")
}

运行以上程序,将输出:

圆周率近似值为:3.141872

for-in 表达式

for-in 表达式可以遍历那些扩展了迭代器接口 Iterable 的类型实例。for-in 表达式的基本形式为:

for (迭代变量 in 序列) {
    循环体
}

其中“循环体”是一个代码块。“迭代变量”是单个标识符或由多个标识符构成的元组,用于绑定每轮遍历中由迭代器指向的数据,可以作为“循环体”中的局部变量使用。“序列”是一个表达式,它只会被计算一次,遍历是针对此表达式的值进行的,其类型必须扩展了迭代器接口 Iterable。for-in 表达式将按如下规则执行:

  1. 计算“序列”表达式,将其值作为遍历对象,并初始化遍历对象的迭代器。
  2. 更新迭代器,如果迭代器终止,转第 4 步,否则转第 3 步。
  3. 将当前迭代器指向的数据与“迭代变量”绑定,并执行“循环体”,转第 2 步。
  4. 结束循环,继续执行 for-in 表达式后面的代码。

例如,以下程序使用 for-in 表达式,遍历中国地支字符构成的数组 noumenonArray,输出农历 2024 年各月的干支纪法:

main() {
    let metaArray = [r'甲', r'乙', r'丙', r'丁', r'戊',
                     r'己', r'庚', r'辛', r'壬', r'癸']
    let noumenonArray = [r'寅', r'卯', r'辰', r'巳', r'午', r'未',
                         r'申', r'酉', r'戌', r'亥', r'子', r'丑']
    let year = 2024
    // 年份对应的天干索引
    let metaOfYear = ((year % 10) + 10 - 4) % 10
    // 此年首月对应的天干索引
    var index = (2 * metaOfYear + 3) % 10 - 1
    println("农历 2024 年各月干支:")
    for (noumenon in noumenonArray) {
        print("${metaArray[index]}${noumenon} ")
        index = (index + 1) % 10
    }
}

运行以上程序,将输出:

农历 2024 年各月干支:
丙寅 丁卯 戊辰 己巳 庚午 辛未 壬申 癸酉 甲戌 乙亥 丙子 丁丑

break 与 continue 表达式

在循环结构的程序中,有时我们需要根据特定条件提前结束循环或跳过本轮循环,为此仓颉引入了 break 与 continue 表达式,它们可以出现在循环表达式的循环体中,break 用于终止当前循环表达式的执行、转去执行循环表达式之后的代码,continue 用于提前结束本轮循环、进入下一轮循环。break 与 continue 表达式的类型都是 Nothing。 例如,以下程序使用 for-in 表达式和 break 表达式,在给定的整数数组中,找到第一个能被 5 整除的数字:

main() {
    let numbers = [12, 18, 25, 36, 49, 55]
    for (number in numbers) {
        if (number % 5 == 0) {
            println(number)
            break
        }
    }
}

当 for-in 迭代至 numbers 数组的第三个数 25 时,由于 25 可以被 5 整除,所以将执行 if 分支中的 println 和 break,break 将终止 for-in 循环,numbers中的后续数字不会被遍历到,因此运行以上程序,将输出:

25

以下程序使用 for-in 表达式和 continue 表达式,将给定整数数组中的奇数打印出来:

main() {
    let numbers = [12, 18, 25, 36, 49, 55]
    for (number in numbers) {
        if (number % 2 == 0) {
            continue
        }
        println(number)
    }
}

在循环迭代中,当 number 是偶数时,continue 将被执行,这会提前结束本轮循环、进入下一轮循环,println 不会被执行,因此运行以上程序,将输出:

25
49
55

函数

函数是具有特定功能的代码块,可以通过函数名进行调用。函数的定义包括函数名、参数列表和函数体。

函数定义

函数定义使用 func 关键字,后跟函数名、参数列表、可选的函数返回值类型和函数体。

func greet(name: String) {
    print("Hello, " + name)
}

在上述代码中,greet 是一个函数,接受一个 String 类型参数 name,并打印问候语。

函数调用

函数调用通过函数名和实参列表进行。

greet("John")

函数返回值

函数可以返回一个值,使用 return 语句。例如:

func add(a: Int64, b: Int64): Int64 {
    return a + b
}

let result = add(10, 20)
println(result) // 输出 30

函数作用域

函数内部定义的变量在函数外部是不可见的,这称为函数作用域。例如:

func testScope() {
    let localVar = "I'm local"
    println(localVar)
}

testScope() // 输出 "I'm local"
// println(localVar) // 错误,localVar 在此不可见

匿名函数和箭头函数

仓颉语言支持匿名函数和箭头函数,用于简化函数定义。

// 匿名函数
let multiply = { a: Int64, b: Int64 => 
    return a * b;
}

// 箭头函数
let divide = (a, b) => a / b;

结论

本文详细介绍了仓颉语言的基本概念,包括标识符、程序结构、表达式和函数。掌握这些基础知识,将有助于你更好地理解和使用仓颉语言进行编程。在实际开发中,建议结合具体项目进行实践,以巩固所学内容。希望本文对你的仓颉语言学习之旅有所帮助!