仓颉初体验:开启仓颉语言的神秘大门

1 概述

1.1 案例介绍

仓颉编程语言作为一款面向全场景应用开发的现代编程语言,通过现代语言特性的集成、全方位的编译优化和运行时实现、以及开箱即用的 IDE工具链支持,为开发者打造友好开发体验和卓越程序性能。

本案例将介绍仓颉编程语言入门知识,并结合简单的编程代码进行知识体验,仓颉开发环境我们使用了开发者空间提供的云主机环境,环境已经预装了仓颉工具链和CodeArts IDE for Cangjie,即开即用,非常便捷。

案例结合代码体验,让大家更直观的了解仓颉语言标识符、程序结构和表达式知识。

1.2 适用对象

  • 个人开发者
  • 高校学生

1.3 案例时间

本案例总时长预计30分钟。

1.4 案例流程

abf6ec3409dff70ce74ed05ed269792a.png{{{width="55%" height="auto"}}}

说明:

  1. 进入华为开发者空间,登录云主机;
  2. 使用CodeArts IDE for Cangjie编程和运行仓颉代码。

1.5 资源总览

资源名称规格单价(元)时长(分钟)
开发者空间——云主机2vCPUs | 4GB | X86 | Ubuntu

4vCPUs | 8GB | ARM | Ubuntu
免费30

2 仓颉编程语言初体验

2.1 初识仓颉语言

仓颉编程语言是一款面向全场景智能的新一代编程语言,主打原生智能化、天生全场景、高性能、强安全。主要应用于鸿蒙原生应用及服务应用等场景中,为开发者提供良好的编程体验。

仓颉编程语言具有以下优势:

  • 智能化:内嵌AgentDSL的编程框架,自然语言&编程语言有机融合;多Agent协同,简化符号表达,模式自由组合,支持各类智能应用开发。
  • 全场景:轻量化可缩放运行时,模块化分层设计,内存再小也能装得下;全场景领域扩展,元编程和eDSL技术,支持面向领域声明式开发。
  • 高性能:终端场景首款全并发 GC ,应用线程更流畅,响应更快。轻量化线程,并发性能更好,开销更少。
  • 强安全:安全DNA融入语言设计,帮助开发者专注于业务逻辑,免于将太多精力投入到防御性编程中,编码即安全,漏洞无处藏。

    在开发仓颉程序时,必用的工具之一是仓颉编译器,它可以将仓颉源代码编译为可运行的二进制文件,但现代编程语言的配套工具并不止于此,实际上,仓颉为开发者提供了编译器、调试器、包管理器、静态检查工具、格式化工具和覆盖率统计工具等一整套仓颉开发工具链,同时提供了友好的安装和使用方式,基本能做到“开箱即用”。

    仓颉编程语言支持 CJNative 和 CJVM 两种后端。其中 CJNative 后端将代码编译为原生二进制代码,直接在操作系统层面上运行;CJVM 后端将代码编译为字节码,基于 VM(虚拟机)进行运行。本文档适配 CJNative 后端。本地环境安装可以参考仓颉工具链安装步骤

    本案例并不需要手动再安装仓颉工具链,而是直接使用华为开发者空间环境,空间提供了免费的云主机,且已经预装了仓颉工具链和编译器CodeArts IDE for Cangjie。

83e99366de8e98e4def50d5420b6b9f5.png

2.2 运行第一个仓颉程序

万事俱备,我们开始编写和运行第一个仓颉程序吧!

首先,进入华为开发者空间云主机桌面。

a907ead1c6f9edd2ae60305287f126e8.png

点击桌面CodeArts IDE for Cangjie,打开编辑器,点击新建工程,保持默认配置,点击创建

产物类型说明

  • executable,可执行文件;
  • static,静态库,是一组预先编译好的目标文件的集合;
  • dynamic,动态库,是一种在程序运行时才被加载到内存中的库文件,多个程序共享一个动态库副本,而不是像静态库那样每个程序都包含一份完整的副本。

28acbca9146a8a6aacbfdd4f6ac3791b.png

创建完成后,打开src/main.cj,参考下面代码简单修改后,点击编辑器右上角运行按钮直接运行,终端窗口可以看到打印内容。

package demo
// 第一个仓颉程序
main(): Int64 {
    println("hello world")
    println("你好,仓颉!")
    return 0
}

(* 注意:后续替换main.cj文件代码时,package demo保留,如果创建项目不为demo,同样保留第一行package xxx即可)

(* 仓颉注释语法:// 符号之后写单行注释,也可以在一对 /* 和 */ 符号之间写多行注释)

d1e16b48f5f9620fafe9fc59ae62a367.png

到这里,我们第一个仓颉程序就运行成功啦,后面案例中的示例代码都可以放到main.cj文件中进行执行,接下来我们继续探索仓颉语言。

2.3 标识符

标识符是程序中的命名元素,用于表示变量、函数、类/结构体、模块、常量、类型别名、命名空间。标识符分为普通标识符原始标识符两类。

2.3.1 普通标识符

组成规则:

(1)首字符:Unicode字母或下划线(_)

(2)后续字符:Unicode字母、数字或下划线

(3)支持多语言字符(包括中文、日文等)

(4)普通标识符不能和仓颉关键字相同

  • 合法的普通标识符示例:
abc
_abc
abc_
a1b2c3
a_b_c
a1_b2_c3
仓颉
__こんにちは
  • 不合法的普通标识符示例:
ab&c  // 使用了非法字符 “&”
3abc  // 数字不能出现在第一个字符
while // 不能使用仓颉关键字
2.3.2 原始标识符

在仓颉关键字或普通标识符的首尾加上一对反引号,主要用于将仓颉关键字作为标识符的场景。

  • 合法的原始标识符示例:
`abc`
`_abc`
`a1b2c3`
`if`
`while`
`à֮̅̕b`
  • 不合法的原始标识符示例:
`ab&c`
`3abc`

2.4 程序结构

仓颉程序所在扩展名为.cj的文本文称为源文件,仓颉程序成为源代码,在程序开发的最后阶段,这些源代码将被编译为特定格式的二进制文件。

在仓颉程序的顶层作用域中,可以定义一系列的变量、函数和自定义类型(如 struct、class、enum 和 interface 等),其中的变量和函数分别被称为全局变量全局函数。如果要将仓颉程序编译为可执行文件,需要在顶层作用域中定义一个 main 函数作为程序入口,它可以有 Array\<String> 类型的参数,也可以没有参数,它的返回值类型可以是整数类型或 Unit 类型。

例如在以下程序中,在顶层作用域定义了全局变量 a 和全局函数 b,还有自定义类型 C、D 和 E,以及作为程序入口的 main 函数。

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

main() {
    println(a)
}

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

(* 注意:enum 和 interface 中仅支持定义成员函数,不支持定义成员变量)

2.4.1 变量

在仓颉编程语言中,一个变量由对应的变量名、数据(值)和若干属性构成,开发者通过变量名访问变量对应的数据,但访问操作需要遵从相关属性的约束(如数据类型、可变性和可见性等)。

变量定义的具体形式为:

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

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

(1)可变性修饰符:let 与 var,分别对应不可变和可变属性,可变性决定了变量被初始化后其值还能否改变,仓颉变量也由此分为不可变变量和可变变量两类。

(2)可见性修饰符:private 与 public 等,影响全局变量和成员变量的可引用范围。

(3)静态性修饰符:static,影响成员变量的存储和引用方式。

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

(1)变量名应是一个合法的仓颉标识符。

(2)变量类型指定了变量所持有数据的类型。当初始值具有明确类型时,可以省略变量类型标注,此时编译器可以自动推断出变量类型。

(3)初始值是一个仓颉表达式,用于初始化变量,如果标注了变量类型,需要保证初始值类型和变量类型一致。在定义全局变量或静态成员变量时,必须指定初始值。在定义局部变量或实例成员变量时,可以省略初始值,但需要标注变量类型,同时要在此变量被引用前完成初始化,否则编译会报错。

例如,下列程序定义了两个 Int64 类型的不可变变量 a 和可变变量 b,随后修改了变量 b 的值,并调用 println 函数打印 a 与 b 的值。

main() {
    let a: Int64 = 20
    var b: Int64 = 12
    b = 23
    println("${a}${b}")
}

运行以上程序,将输出:

cea888b12d1e2fbab263d0f6b437dbea.png

2.4.2 值类型和引用类型

从编译器实现层面看:

  • 任何变量总会关联一个值(一般是通过内存地址/寄存器关联),只是在使用时,对有些变量,将直接取用这个值本身,这被称为值类型变量。值类型变量通常在线程栈上分配,每个变量都有自己的数据副本。
  • 而对另一些变量,将这个值作为索引、取用这个索引指示的数据,这被称为引用类型变量。引用类型变量通常在进程堆中分配,多个变量可引用同一数据对象,对一个变量执行的操作可能会影响其他变量。

从存储空间层面看:

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

基于上述原理,在使用值类型变量和引用类型变量时,会存在一些行为差异,注意:

(1)在给值类型变量赋值时,一般会产生拷贝操作,且原来绑定的数据/存储空间被覆写。在给引用类型变量赋值时,只是改变了引用关系,原来绑定的数据/存储空间不会被覆写。

(2)用 let 定义的变量,要求变量被初始化后都不能再赋值。对于引用类型,这只是限定了引用关系不可改变,但是所引用的数据是可以被修改的。

(* 注意:在仓颉编程语言中,class 和 Array 等类型属于引用类型,其他基础数据类型和 struct 等类型属于值类型。)

例如,以下程序演示了 struct 和 class 类型变量的行为差异:

//值类型
struct Copy {
    var data = 2012
}
//引用类型
class Share {
    var data = 2012
}
main() {
//值类型,每个变量都有自己的副本
    let c1 = Copy()
var c2 = c1
//修改变量c2,c1变量不会受影响
c2.data = 2023  
//所以,应输出2012,2023
    println("${c1.data}, ${c2.data}")
//引用类型,多变量引用统一数据对象
    let s1 = Share()
let s2 = s1
//修改变量s2,s1变量也会受影响
s2.data = 2023 
//所以,应输出两个变量都被修改为了2023
    println("${s1.data}, ${s2.data}")
}

运行以上程序,将输出:

53f3778e25606115871a9c01c3376c82.png

2.4.3 作用域

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

(* 注意:仓颉不允许使用单独的大括号“{}”,大括号必须依赖 if、match、函数体、类体、结构体等其他语法结构存在)

例如,在以下仓颉源文件里,在顶层作用域中定义了名字 element,它和字符串“仓颉”绑定,而 main 和 if 引导的代码块中也定义了名字 element,分别对应整数 9 和整数 2023。由上述作用域规则,在第 4 行,element 的值为“仓颉”,在第 8 行,element 的值为 2023,在第 10 行,element 的值为 9。

let element = "仓颉"
main() {
    println(element)
    let element = 9
    if (element > 0) {
        let element = 2023
        println(element)
    }
    println(element)
}

运行以上程序,将输出:

047456a37bf4fa4860f08323b48f5193.png

2.5 表达式

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

  1. 条件表达式分为 if 表达式和 if-let 表达式两种,它们的值与类型需要根据使用场景来确定。
  2. 循环表达式有四种:for-in 表达式、while 表达式、do-while 表达式和 while-let 表达式,它们的类型都是 Unit、值为 ()。

在仓颉程序中,由一对大括号“{}”包围起来的一组表达式,被称为“代码块”,它将作为程序的一个顺序执行流,其中的表达式将按编码顺序依次执行。

(* 注意:代码块本身不是一个表达式,不能被单独使用,需要依附于函数、条件表达式和循环表达式等执行和求值。)

2.5.1 if表达式

if 表达式的基本形式为:

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

其中“条件”是布尔类型表达式,“分支 1”和“分支 2”是两个代码块。

当一个条件不成立时,可能还要判断另一个或多个条件、再执行对应的动作,仓颉允许在 else 之后跟随新的 if 表达式,由此支持多级条件判断和分支执行,例如:

import std.random.*

main() {
    let speed = Random().nextFloat64() * 20.0
    println("${speed} km/s")
    if (speed > 16.7) {
        println("第三宇宙速度,鹊桥相会")
    } else if (speed > 11.2) {
        println("第二宇宙速度,嫦娥奔月")
    } else if (speed > 7.9) {
        println("第一宇宙速度,腾云驾雾")
    } else {
        println("脚踏实地,仰望星空")
    }
}

运行以上程序,将输出:

97e37125605bc3df851324bc5ea49271.png

(* 注意:if-let 表达式和 while-let 表达式都与模式匹配相关,请参见if-let 表达式while-let 表达式介绍)

2.5.2 while 表达式

while 表达式的基本形式为:

while (条件) {
  循环体
}

其中“条件”是布尔类型表达式,“循环体”是一个代码块。只要布尔表达式为 true,循环就会一直执行下去。

2.5.3 do-while 表达式
do {
  循环体
} while (条件)

其中“条件”是布尔类型表达式,“循环体”是一个代码块。对于 while 语句而言,如果不满足条件,则不能进入循环。但有时候我们需要即使不满足条件,也至少执行一次。

do…while 循环和 while 循环相似,不同的是,do…while 循环至少会执行一次。

(* 注意:布尔表达式在循环体的后面,所以语句块在检测布尔表达式之前已经执行了。 如果布尔表达式的值为 true,则语句块一直执行,直到布尔表达式的值为 false)

2.5.4 for-in 表达式

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

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

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

例如,以下程序使用 for-in 表达式,遍历中国地支字符构成的数组 noumenonArray(仓颉语言中字符类型使用 Rune 表示,一个 Rune 字面量由字符 r 开头,后跟一个由一对单引号或双引号包含的字符),输出农历 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
    }
}

运行以上程序,将输出:

f1898ef0bc09223313d9ee38a09758b9.png

(* 注意:在 for-in 表达式的循环体中,不能修改迭代变量)

在一些应用场景中,只需要循环执行某些操作,但并不使用迭代变量,这时可以使用通配符 _ 代替迭代变量,例如:

main() {
    var number = 2
    for (_ in 0..5) {
        number *= number
    }
    println(number)
}

运行以上程序,将输出:

df6295e8c95e502b983b0c949c474582.png

在部分循环遍历场景中,对于特定取值的迭代变量,可能需要直接跳过、进入下一轮循环。仓颉为此提供了更便捷的表达方式——可以在所遍历的“序列”之后用 where 关键字引导一个布尔表达式,这样在每次将进入循环体执行前,会先计算此表达式,如果值为 true 则执行循环体,反之直接进入下一轮循环。例如:

main() {
    for (i in 0..8 where i % 2 == 1) { // i 为奇数才会执行循环体
        println(i)
    }
}

运行以上程序,将输出:

18ba538a5f66de373b474499c4d4d749.png

2.5.5 break 与 continue 表达式

仓颉引入了 break 与 continue 表达式,它们可以出现在循环表达式的循环体中,break 用于终止当前循环表达式的执行、转去执行循环表达式之后的代码,continue 用于提前结束本轮循环、进入下一轮循环。

break示例:找到第一个能被 5 整除的数字后打印并推出循环。

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

运行以上程序,将输出:

e74a1ec1ab2b477b47d8ef5a1ecf70b1.png

continue示例:将给定整数数组中的奇数打印出来,number 是偶数时,continue 将被执行,这会提前结束本轮循环、进入下一轮循环,println 不会被执行。

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

运行以上程序,将输出:

2deed08d4935ddbe313e43bd20faf22b.png

至此,仓颉入门案例内容全部完成。

如果想了解更多仓颉编程语言知识可以访问: https://cangjie-lang.cn/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值