# 仓颉基础编程及应用

# 仓颉编程语言入门

# 一、基本概念

仓颉编程语言制定了一些命名规则,符合规则的名字被称为标识符

# 普通标识符:

  1. 由英文字母开头,后接零至多个英文字母(cangjie)数字(cangjie2025)下划线(cangjie_2025_11)
  2. 由一至多个下划线开头,后接一个英文字母(_c),最后可接零至多个英文字母(_cangjie)、数字(_919)下划线(o_o
  3. 原始标识符是在普通表示符或关键字的外面加上一对反引号,主要用于将关键字作为标识符的场景

# 变量

变量将一个名字和一个特定类型的值关联起来

变量分为可变变量、不可变量常量

可变变量:

可变变量在定义后,还可以被赋予其他值

var name: type = expr
函数 意义
var name 变量名
type 变量类型
expr 初始值

不可变量:

不可变量在定义后,它的值不能再改变

let name: type = expr

常量:

常量在被定义后,它的值也不能被改变

const name: type = expr_const(下划线后面是下标)

常量和不可变量的区别:

常量的初始值在编译时确定,而不可变量的初始值在运行时确定

当初始值具有明确类型时,可以省略变量类型标注,编译器会自动推断出变量类型

案例:

我们使用统计方法估算圆周率的值,就是向一个正方形中随机投点,统计落入正方形内接圆的点数,落入圆的概率是 PI/4

1.png

仓颉标准库提供的 random 包:它用于产生随机数

# 数据类型

规定了一块数据的组织结构即相应的解析和操作方式

let a: Int64 = 2024(64 位有符号整数类型)

let b: 67u8(8 位无符号整数类型,u8 后缀指示了这个字面量的类型,所以可以省略变量类型标注)

整数类型名及对应的字面量后缀

整数类型 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64
字面量后缀 i8 i16 i32 i64 u8 u16 u32 u64

let c: Float64 = 6.21(64 位浮点数类型)

浮点数类型名及对应的字面量后缀

浮点数类型 Float16 Float32 Float64
字面量后缀 f16 f32 f64

let d: Bool = ture || false(布尔类型,表示逻辑运算中的真与假,因此它只有两个取值,对应字面量是 ture 和 false, 等号右侧用逻辑 “或” 运算符连接了这两个字面量)

let e: Rune = ' 仓 '(字符类型,可以表示所有 Unicode 字符)

字符类型的字面量由一个 Unicode 字符,外加一个单引号构成

let f: Rune = '\u {9889}'(字符类型的另一种字面量写法,以 Unicode 值定义字符)

这里的 9889 就是仓颉的 “颉” 字对应的 Unicode 值

let g: String = "Cang" + "jie"(字符串类型,它的字面量是在一对双引号之间写零到多个 Unicode 字符)

但不能换行,所以这也被称为单行字符串字面量,字符串之间可以用 + 操作符执行拼接操作

let h: String = " " "

​ 若到江南赶上春,

​ 千万和春往。

“ ” “

给出了多行字符串字面量的写法

let i: String = "Cangjie${a}"(插值字符串的写法,在以上字符串字面量中,支持写一到多个插值表达式)

要求表达式的类型实现了 ToString 接口

let j: Array<UInt8> = [67u8, 97u8, 110u8, 103u8](数组类型,UInt8 是数组类型的参数,表示数组的元素是 UInt8 类型,数组字面量就是在一对方括号之间写一组由逗号分隔的数组元素字面量)

Array 是引用类型

let k: VArray<Rune, $2> = ['C', 'J'](VArray 类型,是值类型的数据)

声明 VArray 类型时,除了要给出数组元素的类型,还要指定数组元素的个数

let l: (Int64, Float64) = (2024, 6.21)(元组类型,是一种组合类型,可以将多个不同类型的值组合在一起)

let m: Range<Int64> = 2019..2024(区间类型,表示一个有固定步长的取值范围,主要用于 for-in 表达式中)

# 表达式

在程序中,取值并不仅仅来源于字面量,我们可以用 “表达式” 这个概念来描述所有可以求值的程序元素

仓颉语言不仅有传统的算数运算表达式,还有条件表达式、try 表达式、match 表达式、flow 表达式等,它们都可以被求值

let result = (x ** 2 + y ** 2) ** 0.5
let result = if (x > 2024) {block} else {block}
let result = try {block} catch (e: Exception) {block}
let result = match (color){
    case Red(value) => block
    case Green(value) => block
    case _ => block
}
let result = data |> fn1 |> fn2 |>fn3
......

示例中的 block 表示代码块,它代表一个顺序执行流,其中的表达式将按编码顺序依次执行

block := (expr(表达示)| decl_var(变量声明,其中var是下标))*

在以上求值场景中,if/try/match 等表达式的值等于执行代码块中最后一个表达式的值。如果代码块是空的,则规定其类型为 Unit,Unit 类型唯一取值的字面量是 ()

# 控制执行流的基本表达式:if 表达式、while 表达式、for-in 表达式

# if 表达式

它会根据一个布尔类型表达示的取值选择执行不同的分支

if 表达式的语法:

if(expr_Bool(Bool是下标)){
    block  (if分支)
} else if (expr_Bool(Bool是下标)){
    block
} else {  ?
    block
}

else 后可接新的 if 表达式或一个代码块

如果圆括号中的 Bool 表达式取值为 ture,则会执行 if 分支;如果取值为 False,则会执行 else 分支,如果 if 表达式具有 else 代码块,则总有一个代码块会被执行,这种 if 表达式的值就等于所执行代码块中最后一个表达式的值

示例:

使用仓颉标准库的 random 包,产生一个浮点类型随机数 speed,表示一个飞行速度,我们使用 if 表达式判断 speed 所属的宇宙速度区间,并在各分支给出相应的描述字符串,这些字符串都是使属代码块的最后一个表达式,且总有一个代码块会被执行

因此,在对这个 if 表达式求值时,将获得所执行代码块中的字符串

1.png

# while 表达式

如何一段程序的执行流程,只会涉及三种基本结构 [顺序结构(代码块)、分支结构(if 表达式)和循环结构(while 表达式)]

while 表达式会根据一个布尔类型表达式的取值,选择是否执行循环体,如果执行了循环体,又会转回执行这个布尔表达式,由此实现一种循环执行流

由于循环体可能不被执行,所以规定 while 表达式的类型为 Unit

示例:

这里我们用二分法估算 2 的平方根,变量 upper 和 lower 设置了估算区间,在循环体中,我们计算这个区间的中间值,并判断这个中间值的平方和 2 和大小关系,由此更新上下界,缩小估算区间,我们需要反复执行这些操作,直到估算区间足够小,

因此,我们的循环条件就是区间长度大于某个足够小的值

2.png

# for-in 表达式

循环结构的另一种表达式

在很多背景中,我们需要遍历一个集合或区间等,对其中每个值作相同的操作

for (name(循环变量) in expr_terable(遍历对象,其中terable是下标)) {
     block(循环体) 
}

for-in 表达式就提供了这样的能力,它可以遍历如何一个实现了迭代器接口的类型实例

其遍历对象的类型需要实现迭代器接口 Iterable<T>,运行时,将逐次调用迭代器取值并执行循环体,在循环体中可以循环变量引用对应值

注意:循环变量是不可变的

由于循环体可能不被执行,所以规定 for-in 表达式的类型为 Unit

实例:

我们计算和输出 2024 年各月的干支纪发,数组 heaven 和 earth 分别存放十天干和十二地支的字符,通过相关转换算法,我们得到 2024 年首月对应的天干序号 index,而每年正月到腊月的地支的固定为寅到丑,所以我们只需要按序遍历地支数组,并从天干数组的 index 处依次取值和地址关联,天干是循环使用的,所以 index 再递增过程中做了模 10 计算

由于仓颉已经为数组等集合类型实现了迭代器接口,所以这里可以直接使用 for-in 去遍历

3.png

for-in 表达式还有其他几种用法:

  1. 遍历对象可以是 Rabge 表达式

    var sum = 0
    for (i in 1..=99:2){
         sum += i * i
    }
    

    实例中的 Range 表达式产生一个从 1 到 99,步长为 2 的区间实例,在遍历过程中将依次取值,并且包含 1 和 99 两个边界值

  2. 当迭代器取值为元组类型时,可以在定义循环变量时进行解构,循环中就可以直接引用元组中的各个值

    let array = [(1, 2), (3, 4), (5, 6)]
    for ((x, y) in array) {
        println("${x}, ${y}")
    }
    
  3. 如果在循环体中无须引用循环变量,只需要重复执行多次循环体,则可以用一个下划线即通配符去替代循环变量,这样可以避免编译器告警

    var number = 2
    for (_ in 0..5) {
         number *= number
    }
    
  4. 可以在遍历对象后,用 where 引导一个布尔表达式,当它取值为 ture 时才会执行循环体

    这个布尔表达式一般会引用循环变量,作为迭代元素的过滤器

    for (i in 0..10 where i % 2 == 1) {
         println(i)
    }
    

# 程序结构

** 包:** 是仓颉程序的最小编译单元,一个包由一到多个源文件组成,在每个文件可以声明当前文件所属包(如果没有文件声明,则默认属于名为 default 的包),也可以导入其他包,由此实现程序的高效管理和复用

在一个包中,也可以通过导入声明来引用其他包,而同一个包中的各个源文件之间总是共享程序元素的

在包的顶级作用域中,可以定义一系列的变量、函数和自定义类型(枚举、结构体、类、接口),以及包的声明与导入等,其中的变量和函数被称为全局变量全局函数

在非顶层作用域中可以定义变量和函数,称为局部变量局部函数。自定义类型中的局部变量和函数,称为成员变量成员函数

如果要将包编译为可执行文件,需要在顶层作用域中定义一个 main 函数作为程序入口

程序启动时将从 main 开始执行,main 函数可以没有参数,也可以声明一个 String 数组类型的参数,程序启动参数将通过这个数组传递给 mian 函数

main 函数的返回值类型可以是整数类型或 Unit 类型

4.png

# 二、函数修改

# 定义函数

函数是一个参数化的代码块,在调用函数时,这个代码块实现特定功能并可以被求值,结合函数参数实现特定范围的代码复用

** 函数定义的要素:** 函数名、参数列表、返回值和函数体

func name(params): type {
    block_func(func是下标)
}
# 参数列表

参数列表是由零到多个参数声明组成的,参数分为普通参数命名参数两种,它们有不同的声明方式

params_normal(normal是下标):= name : type, name : type*
params_name (name是下标):= name !: type, name !: type*

在参数列表中,命名参数只能在普通参数之后进行声明

params := params_normal?params_name?

可以将命名参数设置为默认值,在调用函数时,对这类函数可以省略传参,对应实参将取其默认值

name !: type = expr_const(const是下标)
# 函数体

函数体可以有零到多个表达式,变量声明或函数声明,函数参数将作为不可变变量在函数体中使用

block_func(func是下标) := (expr | decl_var(var是下标) | decl_func(func是下标))*

在函数体中定义的函数被称为嵌套函数,嵌套函数可以捕获其外层作用域中的局部变量,由此构成闭包

在函数体中可以用 return 表达式返回一个值,执行 return 后会跳出当前函数,返回到调用处继续执行

在函数体中返回值

return expr

函数类型的表达方式

(type, type)* -> type  有参
() -> type             无参

在仓颉语言中,函数是一等公民,它不仅可以被调用,也可以作为一个值去传递在这种场景,我们可能需要写出对应的函数类型名

# 调用函数

我们通过函数名和实参列表来调用一个函数

实参列表由零到多个表达式构成,这些表达式的值就是对应位置的参数值

命名参数对应的实参前还要加上参数名前缀

name_func(func是下标)(args(实参列表))
  args := args_normal(下标)?args_named(下标)?
  args_normal := expr, expr*
  args_named := name_param : expr, name_param : expr*

在实参列表中,可以省略有默认值的命名参数,这时对应实参将取其默认值

函数不仅可以被调用,还可以作为值去使用,如赋值给变量、作为函数的参数和返回值等

实例:

在顶层作用域中我们定于了 void 和 node 两个函数,node 函数不仅有字符类型的参数 value,还有两个函数类型的命名参数 left 和 right,并且以 void 函数作为它们的默认值,在 node 函数体中,我们又定义了一个嵌套函数 show,在 show 函数体内引用了外层函数的参数,由此构成了闭包

这个闭包会被分配专属的空间来存储 left、right 和 value 的值,最后 node 返回了 show 函数,基于闭包的特性,node 函数每次被调用时,实际上会返回不同的 show 闭包实例,每个实例可以持有不同的 left、right 和 value 值

在这个基础上,我们仅通过 node 函数调用,得到了一个具有二叉树结构的闭包

image.png

# lambda 表达式

它相当于函数类型的 “字面量”,让函数作为值去使用的开发场景更加高效灵活

lambda 表达式可以让函数的创建和使用更加灵活

lambda 表达式的值就是一个匿名函数

lambda 表达式的语法:

{params => block_func(下标)}

其中参数列表和函数体的写法和普通函数的保持一致

lambda 表达式中无需标注返回值类型,仓颉编译器会从上下文中自动推导

实例:

我们定义一个全局函数 iter,它具有一个函数类型参数 f,iter 函数的功能是以 x0 为初始值迭代计算一元数学函数 n 次,并打印出这个迭代序列,在调用 iter 时,我们直接在实参列表中用 lambda 表达式给出了对应的函数实参(不必像上一个函数实例中专门定义全局函数来作为函数实参),而且在多次调用 iter 时会更显简洁

1.png

这是产生伪随机数的一种方法

# 三、枚举

# 定义与实例化

通常,我们使用枚举类型来定义一组有关联的符号,在一些场景中用来做分类和标记等等,这些符号的字面含义可以增强程序的可读性

仓颉语言的枚举类型不仅支持这种传统用法,还支持定义带参数的枚举项和成员函数等,枚举项的构造函数还支持递归引用枚举自身,基于这些特性,使用仓颉语言的枚举类型还能实现代数表达和符号计算等高级功能

定义枚举类型的语法:

它主要由枚举类型名和枚举项组成,同时还支持定义成员函数和成员属性来操作枚举项

enum name(枚举类型名) {
    item (| item)*      |枚举项
    (decl_func(func是下标,成员函数) | decl_prop(prop是下标,成员属性))*
}

枚举项可以是一个标识符或一个表示符加类型列表组成的构造器

枚举类型的取值可以是一个无参枚举项,也可以是一个给定实参的有参枚举项

item := name | name(type, type)*

在枚举类型中,可以声明一到多个枚举项,由竖线符号分隔

下划线后面都是下标
name_item
name_item(args)

在引用这些枚举项的名字时,为了避免命名冲突,也可以加上枚举类型名前缀(由 “.” 分隔)

# 成员访问规则

在成员函数和成员属性的声明前可以添加一些修饰符,它们将影响成员的访问方式

**private:** 用于设置成员仅在枚举类型定义块中可见

**pubilc:** 设置成员在枚举定类型定义块内外均可见

如果没有使用这两个修饰符,这类成员默认在当前包可见

static:设置成员为静态成员,只能通过枚举类型名访问;默认实例成员,只能通过枚举实例访问

在函数中都能引用枚举项,在实例成员函数中可以引用其他成员,在静态成员函数中只能引用静态成员

在实例成员函数中也可以使用 this 变量,它代表当前枚举实例,this 是不可变变量

实例:

在实例中,定义了一个枚举类型 Tree,可用于组织一棵二叉树,二叉树节点是 Int64 类型,在枚举类型中定义了一个 pubilc 修饰的实例成员函数 traverse,其中引用了枚举项,this 变量和它自身,使用模式匹配解构 this 对应的枚举实例,按中序遍历打印相应的二叉树,另外定义了一个 static 修饰的静态成员函数 generate,其中引用了枚举项和它自身,用于生成一个指定深度的满二叉树实例

在 main 函数中,我们首先调用 Node 构造器手动组织了一棵二叉树,然后调用实例成员函数 traverse 按中序打印了各个节点的值,随后,我们由通过枚举类型名调用静态成员函数 generate,得到了一个 5 层的满二叉树实例,并再次调用 traverse 函数打印这棵树

1.png

2.png