标识符
在仓颉编程语言中,开发者可以给一些程序元素命名,这些名字也被称为“标识符”(变量名称),标识符分为普通标识符和原始标识。
普通标识符
普通标识符不能和仓颉关键字相同,可以取自以下两类字符序列:
- 由 XID_Start 字符开头,后接任意长度的 XID_Continue 字符
- 由一个_开头,后接至少一个 XID_Continue 字符
合法的普通标识符:
abc
_abc
abc_
a1b2c3
a_b_c
a1_b2_c3
仓颉
__こんにちは
不合法的普通标识符:
ab&c // 使用了非法字符 “&”
3abc // 数字不能出现在头部
while // 不能使用仓颉关键字
原始标识符
原始标识符是在普通标识符或仓颉关键字的外面加上一对反引号,主要用于将仓颉关键字作为标识符的场景。
合法的原始标识符:
`abc`
`_abc`
`a1b2c3`
`if`
`while`
`à֮̅̕b`
不合法的原始标识符
`ab&c`
`3abc`
程序结构
仓颉语言接口DevEco Studio工具中,在新建文件的时候文件夹右击New>Cangjie File,即可新建一个后缀是.cj的文本文件并编写仓颉程序。
仓颉在编写程序时跟JS很像,作为前端小伙伴理解会很快。所以在编写程序时,需要注意以下几点:
- 顶层作用域中,可以的变量、函数和自定义类型(如 struct、class、enum 和 interface 等),变量和函数分别被称为
全局变量
和全局函数
; - 将仓颉程序编译为可执行文件,需在顶层作用域中定义一个 main 函数作为程序入口,可以有 Array 类型的参数,也可以没有参数,它的返回值类型可以是整数类型或 Unit 类型;
- 非顶层作用域不能定义上述自定义类型,但可以定义变量和函数,分别被称为局部变量和局部函数
- 对于定义在自定义类型中的变量和函数,称之为成员变量和成员函数
// example.cj
let a = 2023
func b() {}
struct C {}
class D {}
enum E { F | G }
main() {
println(a)
}
注意
enum 和 interface 中仅支持定义成员函数,不支持定义成员变量。
案例:官方案例在顶层作用域定义了全局函数 a 和自定义类型 A,在函数 a 中定义了局部变量 b 和局部函数 c,在自定义类型 A 中定义了成员变量 b 和成员函数 c。
// example.cj
func a() {
let b = 2023
func c() {
println(b)
}
c()
}
class A {
let b = 2024
public func c() {
println(b)
}
}
main() {
a()
A().c()
}
运行以上程序,将输出:
2023
2024
是不是跟学习JS很像,不过仓颉是强类型语言。但是没有关系,只要你会JS/TS,你就可以很快上手仓颉。
仓颉变量
变量定义的具体形式为:
修饰符 变量名: 变量类型 = 初始值
修饰符可以一个或者多个,目前支持的修饰符有:
- 可变性修饰符:let 与 var 前端同学非常熟悉,分别对应不可变和可变属性,这里前端同学要注意一下,仓颉定义var是可变,let不可变变量。跟JS/TS不一样。
- 可见性修饰符:private 与 public 等,影响全局变量和成员变量的可引用范围
- 静态性修饰符:static,影响成员变量的存储和引用方式
main() {
let a: Int64 = 20
var b: Int64 = 12
b = 23
println("${a}${b}")
}
// 输出结构 2023
如果尝试修改不可变变量,编译时会报错,例如:
main() {
let pi: Float64 = 3.14159
pi = 2.71828 // Error, cannot assign to immutable value
}
在定义全局变量和静态成员变量时必须初始化,否则编译会报错,例如:
// example.cj
let global: Int64 // Error, variable in top-level scope must be initialized
class Player {
static let score: Int32 // Error, static variable 'score' needs to be initialized when declaring
}
值类型和引用类型变量
这里对于前端和编程基础的同学很容易理解,前端同学可以把值类型理解为基本类型,比如Int、Float、String等,引用类型理解为对象类型,比如Array、Map、Set等。
对于零基础的同学,对于值类型和引用类型的区别,我们可以通过以下代码来理解:
值类型变量对它所绑定的数据/存储空间是独占的,而引用类型变量所绑定的数据/存储空间可以和其他引用类型变量共享。
差异:
- 值类型变量赋值时,是拷贝操作,原来绑定的数被覆写。引用类型变量赋值时,只是改变了引用关系,原来绑定的数据/存储空间不会被覆写。
- 用 let 定义的变量,要求变量被初始化后都不能再赋值。对于引用类型,这只是限定了引用关系不可改变,但是所引用的数据是可以被修改的。
跟JS的变量性质非常像是,可以无缝理解。
仓颉编程语言,class 和 Array 等类型属于引用类型,其他基础数据类型和 struct 等类型属于值类型。
例如,以下程序演示了 struct 和 class 类型变量的行为差异:
struct Copy {
var data = 2012
}
class Share {
var data = 2012
}
main() {
let c1 = Copy()
var c2 = c1
c2.data = 2023
println("${c1.data}, ${c2.data}")
let s1 = Share()
let s2 = s1
s2.data = 2023
println("${s1.data}, ${s2.data}")
}
// 运行后,输出结果
// 2012, 2023
// 2023, 2023
如果将以上程序中的 var c2 = c1 改成 let c2 = c1,则编译会报错,例如:
struct Copy {
var data = 2012
}
main() {
let c1 = Copy()
let c2 = c1
c2.data = 2023 // Error, cannot assign to immutable value
}
作用域
仓颉的作用域也跟JS非常像,前端同学可以把作用域理解为作用域链。
- 当前作用域中定义的程序元素与名字的绑定关系,在当前作用域和其内层作用域中是有效的,可以通过此名字直接访问对应的程序元素。
- 内层作用域中定义的程序元素与名字的绑定关系,在外层作用域中无效。
- 内层作用域可以使用外层作用域中的名字重新定义绑定关系,根据规则 1,此时内层作用域中的命名相当于遮盖了外层作用域中的同名定义,对此我们称内层作用域的级别比外层作用域的级别高。
仓颉语言中 “{}” 包围的代码块,称为一个作用域。在一个仓颉源文件中,不被任何大括号“{}”包围的代码,它们所属的作用域被称为“顶层作用域”,即当前文件中“最外层”的作用域,按上述规则,其作用域级别最低。
例如在以下名为 test.cj 的仓颉源文件里,在顶层作用域中定义了名字 element,它和字符串“仓颉”绑定,而 main 和 if 引导的代码块中也定义了名字 element,分别对应整数 9 和整数 2023。由上述作用域规则,在第 4 行,element 的值为“仓颉”,在第 8 行,element 的值为 2023,在第 10 行,element 的值为 9。
// test.cj
let element = "仓颉"
main() {
println(element)
let element = 9
if (element > 0) {
let element = 2023
println(element)
}
println(element)
}
/*
运行以上程序,将输出:
仓颉
2023
9
*/
总结:
从变量到作用域跟Javascript非常相识,但是也要注意let和var的区别。修饰符可以是一个或者多个,目前支持的修饰符有:let、var 等,分别对应不可变和可变属性,这里前端同学要注意一下,仓颉定义var是可变,let不可变变量。跟JS/TS不一样。public、private等,影响全局变量和成员变量的可引用范围,static,影响成员变量的存储和引用方式。
在仓颉里,class是引用类型,所以在赋值的时候,是引用赋值。struct是值类型,所以在赋值的时候,是拷贝赋值。
表达式
仓颉不仅有传统的算术运算表达式,还有条件表达式、循环表达式和 try 表达式等
仓颉是强类型的编程语言,所以仓颉表达式不仅可求值,还有确定的类型。
条件表达式
条件表达式分为 if 表达式和 if-let 表达式两种,它们的值与类型需要根据使用场景来确定。
if 表达式
基本形式:
if (条件) {
分支 1
} else {
分支 2
}
其中“条件”是布尔类型表达式,“分支 1”和“分支 2”是两个代码块。
注意:
仓颉编程语言是强类型的,if 表达式的条件只能是布尔类型,不能使用整数或浮点数等类型,和 C 语言和Javascript等不同,仓颉不以条件取值是否为 0 作为分支选择依据
例如以下程序将编译报错:
main() {
let number = 1
if (number) { // Error, mismatched types
println("非零数")
}
}
if 表达式的值和类型,需要根据使用形式与场景来确定
- 当含 else 分支的 if 表达式被求值时,需要根据求值上下文确定 if 表达式的类型:
- 如果上下文明确要求值类型为 T,则 if 表达式各分支代码块的类型必须是 T 的子类型,这时 if 表达式的类型被确定为 T,如果不满足子类型约束,编译会报错。
- 如果上下文没有明确的类型要求,则 if 表达式的类型是其各分支代码块类型的最小公共父类型,如果最小公共父类型不存在,编译会报错。
如果编译通过,则 if 表达式的值就是所执行分支代码块的值。
- 如果含 else 分支的 if 表达式没有被求值,在这种场景里,开发者一般只想在不同分支里做不同操作,不会关注各分支最后一个表达式的值与类型,为了不让上述类型检查规则影响这一思维习惯,仓颉规定这种场景下的 if 表达式类型为 Unit、值为 (),且各分支不参与上述类型检查。
- 最后就是没有 else is的 类型是Unit、值是()
最小共公父类型:多个变量的类型的最小共公父类型,例如:Int8、Int16、Int32、Int64 的最小共公父类型是 Int32。
一下就是if模拟的换算过程:
main() {
let zero: Int8 = 0
let one: Int8 = 1
let voltage = 5.0
let bit = if (voltage < 2.5) {
zero
} else {
one
}
}
循环表达式
for-in表达式
for (迭代变量 in 序列) {
循环体
}
- 迭代变量:“迭代变量”是单个标识符或由多个标识符构成的元组,用于绑定每轮遍历中由迭代器指向的数据
- 序列:一个表达式,遍历表达式的值,类型必须扩展了迭代接口Iterable<T>,例如:数组、字符串、元组等
- 循环体:代码块
for-in 表达式可以遍历区间类型实例: 例如:
main(){
var sum = 0;
for (i in 1..=100) {
sum +=i
}
println("总数是${sum}")
}
// 输入
// 总数是5050
遍历元组类型:例如:
main(){
let array = [(1, 2), (3, 4), (5, 6)]
for ((a, b) in array) {
println("${a}, ${b}")
}
}
结果:
1, 2
3, 4
5, 6
迭代变量不可修改
main(){
for (i in 1..10){
i = i * 10 // 这里的i是只读的,不能修改。会报错
println(i)
}
}
使用通配符 _
在一些应用场景中,只需要循环,并不使用迭代变量,这时可以用通配符 _ 代替迭代变量,例如:
main() {
var number = 2
for (_ in 0..5) {
number *= number
}
println(number)
}
输出:
4294967296
where条件
为循环遍历,可能需要直接跳过、进入下一轮循环。在所遍历的“序列”之后用 where 关键字引导一个布尔表达式,只有当布尔表达式的值为 true 时,才会进入下一轮循环。例如:
for (i in 0..8 where i % 2 == 1) { // i 为奇数才会执行循环体
println(i)
}
输出:
1
3
5
7
while表达式
基本形式为:
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 (条件)
计算1~100 之间所有整数的和
main(){ // 1~100 之间所有整数的和
var sum = 0;
var j = 1;
do{
sum += j;
j++;
} while( j <= 100)
println("${sum}")
}
输出:
5050
break、continue
循环中的 break 和 continue 用法与 C 语言和 Js 语言一样。break 用于终止当前循环表达式的执行、转去执行循环表达式之后的代码,continue 用于提前结束本轮循环、进入下一轮循环。break 与 continue 表达式的类型都是 Nothing。
函数
Javscript的函数关键词是function,但是仓颉使用关键字 func 来表示函数定义的开始,func 之后依次是函数名、参数列表、可选的函数返回值类型、函数体。
案例:
func add(a: Int64, b: Int64):Int64 {
return a + b;
}