OpenHarmony鸿蒙应用开发 | 仓颉基础概念前端视角

标识符

在仓颉编程语言中,开发者可以给一些程序元素命名,这些名字也被称为“标识符”(变量名称),标识符分为普通标识符和原始标识。

普通标识符

普通标识符不能和仓颉关键字相同,可以取自以下两类字符序列:

  • 由 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等。
对于零基础的同学,对于值类型和引用类型的区别,我们可以通过以下代码来理解:

值类型变量对它所绑定的数据/存储空间是独占的,而引用类型变量所绑定的数据/存储空间可以和其他引用类型变量共享。

差异:

  1. 值类型变量赋值时,是拷贝操作,原来绑定的数被覆写。引用类型变量赋值时,只是改变了引用关系,原来绑定的数据/存储空间不会被覆写。
  2. 用 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. 当前作用域中定义的程序元素与名字的绑定关系,在当前作用域和其内层作用域中是有效的,可以通过此名字直接访问对应的程序元素。
  2. 内层作用域中定义的程序元素与名字的绑定关系,在外层作用域中无效。
  3. 内层作用域可以使用外层作用域中的名字重新定义绑定关系,根据规则 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值