
我是一名仓颉编程语言的体验开发者, 从仓颉编程语言上线公测后, 一直有使用, 练习时长0.4*两年半。
正如标题所说, 仓颉编程语言是一款面向全场景智能的新一代编程语言, 主打原生智能化、天生全场景、高性能、强安全。融入鸿蒙生态,为开发者提供良好的编程体验。全方位的编译优化和运行时实现、以及开箱即用的 IDE 工具链支持,为开发者打造友好开发体验和卓越程序性能。
它汲取了C语言的简洁高效、Java的面向对象特性、Go语言的并发处理优势、Rust的内存安全机制以及Python的易读易写风格。
当然, 新编程语言的诞生总免不了遭到谩骂唾弃, 但这是一款国产编程语言, 纵使其目前还不太行, 但作为一名Cn Coder, 没有理由不支持不尝试。
是的, “有剑不用”和”没有剑用” 两者的背后深意很大, 纵使”花厂”在互联网上的名声可能不太好, 但起码这家伙确确实实在一步一步的抬高国内计算机技术的竞争力, 这点不可否认。
初识 仓颉
编程语言
仓颉编程语言是一种面向全场景应用开发的编程语言, 可以兼顾开发效率和运行性能, 提供良好的编程体验。
举个栗子:
from net import http.ServerBuilder, http.FuncHandler
main() {
let server = ServerBuilder().addr("127.0.0.1").port(8080).build()
server.distributor.register("/index", FuncHandler({httpContext =>
httpContext.responseBuilder.body("Hello, Cangjie!")
}))
server.serve()
}
仓颉编程语言特性
- 语法简洁高效
- 多范式编程
- 类型安全
- 内存安全
- 高效并发
- 兼容语言生态
- 领域易拓展
- 助力UI开发
- 内置库功能丰富
环境配置及运行第一个仓颉程序
- 安装仓颉编程语言
- 访问 仓颉编程语言官网文档 下载最新版本的安装包。
- 根据操作系统的不同,选择相应的安装方式进行安装。
- 配置环境变量
- 将仓颉编程语言的安装目录添加到系统的环境变量中,以便在命令行中直接使用
cjc
命令。 - 在控制台中输入
cjc -v
命令,检查是否安装成功。 - 如果安装成功,控制台会输出仓颉编程语言的版本信息。
- 将仓颉编程语言的安装目录添加到系统的环境变量中,以便在命令行中直接使用
创建第一个仓颉程序
首先, 请在适当目录下新建一个名为hello.cj
的文本文件, 并向文件中写入以下仓颉代码
main() {
println("Hello, Cangjie!")
}
然后, 请在此目录下执行如下命令:
cjc hello.cj -o hello
这里仓颉编辑器会将hello.cj
中的源代码编译为此平台上的可执行文件hello
,在命令行环境中运行此文件, 您将看到程序输出了如下内容:
Hello, Cangjie!
温馨提示
以上编译命令是针对Linux/macOS 平台的, 如果在 Windows 平台上使用仓颉编程语言, 请将 hello
替换为 hello.exe
。
标识符
仓颉编程语言的标识符由字母、数字和下划线组成,且必须以字母或下划线开头。标识符是区分大小写的, 标识符分为普通标识符
和原始标识符
两种类型, 它们分别遵从不同的命名规则。
普通标识符
普通标识符
不能和仓颉关键字相同, 可以取自以下两类字符序列:
- 由英文字母开头, 后接零至多个英文字母、数字或下划线
- 由一至多个下划线
_
开头, 后接一个英文字母, 最后可接零至多个英文字母、数字或下划线_
合法的普通标识符
abc
_abc
abc_
a1b2c3
a_b_c
a1_b2_c3
不合法的普通标识符
ab&c // 使用了非法字符 "&"
_123 // 起始下划线 "_" 后不能接数字
3abc // 数字不能出现在头部
while // 不能使用仓颉关键字
原始标识符
原始标识符
是以反引号` ` 开头和结尾的字符序列, 反引号内的字符序列可以包含任何字符, 但不能包含反引号本身。
合法的原始标识符
`abc`
`_abc`
`a1b2c3`
`if`
`while`
不合法的原始标识符
`ab&c`
`_123`
`3abc`
变量
在仓颉编程语言中, 一个变量由对应的变量名、数据(值)和若干属性构成, 开发者通过变量名访问变量对应的数据, 但访问操作需要遵从相关属性的约束(如数据类型、可变性和可见性等)
变量定义的具体形式为: 修饰符
变量名
: 变量类型
= 初始值
其中修饰符
用于设置变量的各类属性, 可以有一个或多个, 常用的修饰符包括:
- 可变性修饰符: let 或 var, 分别对应不可变和可变属性, 可变性决定了变量被初始化后其值还能否改变, 仓颉变量也由此分为不可变变量和可变变量两类。
- 可见性修饰符: private 和 public 等, 影响全局变量和成员变量的可引用范围, 可见性修饰符决定了变量的作用域, 影响变量的可见性和访问权限。
- 静态性修饰符: static, 影响成员变量的存储和引用方式, 静态变量在类加载时被初始化, 其生命周期与类的生命周期相同, 而非静态变量在每次实例化对象时被初始化, 其生命周期与对象的生命周期相同。
在定义仓颉变量时, 可变性修饰符是必要的, 在此基础上, 还可以根据需要添加其他修饰符
变量名
应是一个合法的仓颉标识符。变量类型
指定了变量所持有数据的类型。当初始值具有明确类型时, 可以省略变量类型标注, 此时编译器可以自动推断出变量类型。初始值
是一个仓颉表达式, 用于初始化变量, 如果标注了变量类型, 需要保证初始值类型和变量类型一致。在定义全局变量或静态成员变量时, 必须指定初始值。在定义局部变量或实例成员变量时, 可以省略初始值, 但需要标注变量类型, 同时要在此变量被引用前完成初始化, 否则编译会错误。
来看一个例子: 定义了两个Int64类型的不可变变量a和可变变量b,随后修改了变量b的值, 并调用printlc函数打印a与b的值
main() {
let a: Int64 = 20
var b: Int64 = 12
b = 23
println("${a}${b}") // 2023
println("a = ${a}", ", b = ${b}") // a = 20, b = 23
}
如果尝试修改不可变变量, 编译时会报错
main() {
let pi: Float64 = 3.14159265
pi = 2.71828 // 错误: pi是不可变变量, 不能被修改
}
当初始值具有明确类型时, 可以省略变量类型标注
main() {
let a: Int64 = 2023
let b = a
println("a - b = ${a - b}")
}
// result: a - b = 0
其中变量b的类型可以由其初值a的类型自动推断为Int64, 所以此程序也可以被正常编译和运行, 将输出a - b = 0
在定义局部变量时, 可以不进行初始化, 但一定要在变量被引用前赋予初值
main() {
let text: String
text = "仓颉造字" // Cangjie
println(text)
}
// result: 仓颉造字
// TODO: 可能会出现乱码情况, 这是编码问题
在定义全局变量和静态成员变量时必须初始化, 否则编译会报错
// example.cj
let global: Int64 // Error, variable in top-level scope must be
if 表达式
if
表达式的基本形式为
if (条件 1) {
分支 1
} else if (条件 2) {
分支 2
} else {
分支 3
}
其中”条件”是布尔类型表达式, “分支 1”和”分支 2”是两个代码块。if
表达式将按如下规则执行
- 计算”条件 1”表达式, 如果值为
true
则执行”分支 1”, 值为false
则跳到 else if 判断计算”条件 2”或 else 判断 - 执行”分支 x”, 然后跳出
if
表达式 - 继续执行
if
表达式后面的代码
在一些场景中, 我们可能只关注条件成立时该做些什么, 所以else
和对应的代码块是允许省略的
from std import random.*
main() {
let number: Int8 = Random().nextInt8()
println(number)
if (number %2 == 0) {
println("even")
} else {
println("odd")
}
}
在这段程序中, 我们使用了仓颉标准库的random
包生成了一个随机数, 然后使用if
表达式判断这个整数是否能被2整除, 并在不同的条件分支中打印”偶数”或”奇数”
仓颉编程语言是强类型的, if
表达式的条件只能是布尔类型, 不能使用整数或浮点数等类型, 和C语言不同, 仓颉不以条件取值是否为0作为分支选择依据, 否则会报错
main() {
let number = 1
if (number) { // error: mismatched types
println("非零")
}
}
在许多场景中, 当一个条件不成立时, 我们可能还要判断另一个或多个条件、在执行对应的动作, 仓颉允许在else
之后跟随新的if
表达式, 由此支持多级条件判断和分支执行
from std import 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("脚踏实地, 仰望星空")
}
}
温馨提示
下列print、println、eprint、eprintln函数默认为UTF-8编码, windows环境需要手动执行命令chcp 65001
(将cmd更改为UTF-8编码)。
while 表达式
while
表达式的基本形式为
while (条件) {
循环体
}
其中”条件”是布尔类型表达式, “循环体”是一个代码块。while
表达式将按如下规则执行
- 计算”条件”表达式, 如果值为
true
则执行”循环体”, 值为false
则跳出while
表达式 - 执行”循环体”, 然后跳回第1步
- 继续执行
while
表达式后面的代码
以下程序使用while
表达式, 输出数字 0 到 10
main() {
var num: Int64 = 0
while (num <= 10) {
println(num)
num++
}
println("while执行完毕")
}
## do-while 表达式
`do-while` 表达式的基本形式为
```Cangjie
do {
循环体
} while (条件)
其中”条件”是布尔类型表达式, “循环体”是一个代码块。do-while
表达式将按如下规则执行
- 执行”循环体”, 转第2步
- 计算”条件”表达式, 如果值为
true
则跳回第1步, 值为false
则跳出do-while
表达式 - 继续执行
do-while
表达式后面的代码
以下程序使用do-while
表达式, 输出数字 0 到 10
main() {
var num: Int64 = 0
do {
println(num)
num++
} while (num <= 10)
println("do-while执行完毕")
}
for-in 表达式
for-in 表达式可以遍历那些扩展了迭代器接口iterable<T>的类型实例
for-in 表达式的基本形式为
for (迭代变量 in 序列) {
循环体
}
其中”循环体”是一个代码块, “迭代变量”用于绑定每轮遍历中由迭代器指向的数据, 可以作为”循环体”中的局部变量使用。“序列”是一个表达式, 目前我们理解为一组数据, 例如["张三", "李四", "王五"]
- 计算”序列”表达式, 将其值作为遍历对象, 并初始化遍历对象的迭代器。
- 更新迭代器, 如果迭代器终止, 转第4步, 否则转第3步
- 将当前迭代器指向的数据与”迭代变量”绑定, 并执行”循环体”, 转第2步。
- 结束循环, 继续执行for-in表达式后面的代码
[开始]
|
v
/ \
/ 遍历是 \
/ 否结束 \
| |
| YES | NO
v v
[结束] [print(i)]
|
------------|
|
v
回到判断条件
main() {
let userName = ["张三", "李四", "王五"]
for (name in userName) {
print(name)
}
}
// result: 张三李四王五
遍历区间
for-in 表达式可以遍历区间类型实例
区间的格式是 start..=end:step
1..=100 // 表示从1~100的数字
利用for-in表达式完成从0+…100的数字和
main() {
var num = 0
for (i in 1..=100) {
sum += i
}
println("sum = ${sum}")
}
迭代变量不可修改
在 for-in 表达式的循环体中, 不能修改迭代变量, 例如以下程序在编译时会报错:
main() {
for (i in 0..5){
i = i * 10 // error: cannot assign to immutable value
println(i)
}
}
where 条件
在部分循环遍历场景中, 对于特定取值的迭代变量, 我们可能需要直接跳过, 进入下一轮循环, 仓颉为此提供了更便捷的表达方式—可以在所遍历的”序列”之后用where关键字引导一个布尔表达式, 这样在每次将进入循环体执行前, 会先计算此表达式, 如果值为true则执行循环体, 反之直接进入下一轮循环
main() {
for (i in 0..8 where i % 2 == 1) { // i 为奇数时才会执行循环体
println("i = ${i}")
}
}
// result: 1 3 5 7
break 与 continue 表达式
在循环结构的程序中, 有时我们需要根据特定条件提前结束循环或跳过本轮循环, 为此仓颉引入了break
与continue
表达式, 它们可以出现在循环表达式的循环体中, break用于终止当前循环表达式的执行、转去执行循环表达式后面的代码, continue用于跳过本轮循环, 直接转去执行下一轮循环
例如, 以下程序使用for-in表达式和break表达式, 在给定的整数数组中, 找到第一个能被5整除的数字:
main() {
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
for (number in numbers) {
if (number % 5 == 0) {
println("第一个能被5整除的数字是: ${number}")
break
}
}
}
以下程序使用for-in表达式和continue表达式, 将给定的整数数组中的奇数打印出来:
main() {
let numbers = [12, 18, 25, 36, 49, 55]
for (number in numbers) {
if (number % 2 == 0) {
continue // 如果是偶数, 跳过本轮循环
}
println("奇数: ${number}")
}
}
// result: 奇数: 25 奇数: 49 奇数: 55
基本数据类型
仓颉中的基本数据类型, 包括: 整数类型、浮点类型、布尔类型、字符类型、字符串类型、Unit
类型、元组类型、区间类型、Nothing
类型
整数类型 Int&UInt
整数类型分为有符号 (signed) 整数类型和无符号 (unsigned) 整数类型
有符号整数类型: Int8
、Int16
、Int32
、Int64
无符号整数类型: UInt8
、UInt16
、UInt32
、UInt64
下表列出了所有整数类型的表示范围
类型 | 表示范围 |
---|---|
Int8 | -27 ~ 27-1 (-128 ~ 127) |
Int16 | -215 ~ 215-1 (-32,768 ~ 32,767) |
Int32 | -231 ~ 231-1 (-2,147,483,648 ~ 2,147,483,647) |
Int64 | -263 ~ 263-1 (-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807) |
UInt8 | 0 ~ 28-1 (0 ~ 255) |
UInt16 | 0 ~ 216-1 (0 ~ 65,535) |
UInt32 | 0 ~ 232-1 (0 ~ 4,294,967,295) |
UInt64 | 0 ~ 264-1 (0 ~ 18,446,744,073,709,551,615) |
程序具体使用哪种整数类型, 取决于该程序中需要处理的整数的性质和范围。在Int64
类型适合的情况下, 首选Int64
类型, 因为int64
的表示范围足够大, 并且整数字面量在没有类型上下文的情况下默认推断为Int64
类型, 可以避免不必要的类型转换
main() {
let a = 100
pring(a is Int64)
}
// result: true
整数类型字面量
整数类型字面量有 4 种进制表示形式: 二进制 (使用0b 或 0B 前缀)、八进制 (使用0o 或 0O 前缀)、十进制 (不使用前缀) 和十六进制 (使用0x 或 0X 前缀)。例如, 对于十进制数 24, 表示成二进制是 0b00011000 (或 0B00011000)、八进制是 0o30 (或 0O30)、十六进制是 0x18 (或 0X18)。
在使用整数类型时, 可以通过加入后缀来明确整数字面量的类型, 后缀于类型的对应为
整数类型 | Int8 | Int16 | Int32 | Int64 | UInt8 | UInt16 | UInt32 | UInt64 |
---|---|---|---|---|---|---|---|---|
字面量后缀 | i8 | i16 | i32 | i64 | u8 | u16 | u32 | u64 |
var x = 100i8 // x is 100 with type Int8
var y = 16u64 // y is 16 with type UInt64
var z = 282i32 // z is 282 with type Int32
浮点类型 Float
浮点类型包括 Float16、Float32 和 Float64
Float64 的精度约为小数点后 15 位, Float32 的精度约为小数点后 6位, Float16 的精度约为小数点后 3 位。使用哪种浮点类型, 取决于代码中需要处理的浮点数的性质和范围。在多种浮点类型都适合的情况下, 首选精度高的浮点类型, 因为精度低的浮点类型的累计计算误差很容易扩散, 并且它能精确表示的整数范围也很有限
浮点类型字面量
浮点类型字面量有两种进制表示形式: 十进制、十六进制。在十进制表示中, 一个浮点字面量至少要包含一个整数部分或一个小数部分, 没有小数部分时必须包含指数部分 (以 e 或 E 为前缀, 底数为 10)。在十六进制表示中, 一个浮点字面量除了至少要包含一个整数部分或一个小数部分 (以 0x 或 0X 为前缀), 同时必须包含指数部分 (以 p 或 P 为前缀, 底数为 2)。
let a: Float32 = 3.14
let b: Float32 = 2e3
let c: Float32 = 2.4e-1
let d: Float32 = 0x1p2
let e: Float64 = 0x2p4
在使用浮点数时, 可以通过加入后缀来明确浮点字面量的类型, 后缀与类型的对应为
后缀 | 类型 | 后缀 | 类型 | 后缀 | 类型 |
---|---|---|---|---|---|
f16 | Float16 | f32 | Float32 | f64 | Float64 |
let a = 3.14f16 // a is 3.14 with type Float16
let b = 2e3f32 // b is 2e3 with type Float32
let c = 2.4e-1f64 // c is 2.4e-1 with type Float64
布尔类型 Bool
布尔类型使用Bool
表示, 用来表示逻辑中的真和假
布尔类型字面量
布尔类型字面量有两个: true
和 false
。布尔类型的值可以通过逻辑运算符 (如与、或、非) 进行组合和计算
let a: Bool = true
let b: Bool = false
let c: Bool = a && b
let d: Bool = a || b
let e: Bool = !a
回顾if表达式
from std import random.*
main() {
let speed = Random(). nextFloat64() * 20.0
println("${speed} km/s")
println(speed > 16.7) // true or false
if (speed > 16.7) {
println("第三宇宙速度, 鹊桥相会")
} else if (speed > 11.2) {
println("第二宇宙速度, 嫦娥奔月")
} else if (speed > 7.9) {
println("第一宇宙速度, 腾云驾雾")
} else {
println("脚踏实地, 仰望星空")
}
}
回顾while表达式
main() {
var num: Int64 = 0
while (num <= 10) {
println(num)
num++
println(num <= 10) // true or false
}
println("while执行完毕")
}
字符类型 Rune
字符类型使用Char
表示, 可以表示 Unicode 字符集中的所有字符。
当前, 仓颉已经引入了Rune
, Rune
是Char
的类型别名, 定义为type Rune = Char
Rune
的语义与Char
相同。目前Rune
与Char
短期共存, 但是将来Char
将会被删除, 建议需要使用字符类型的地方使用Rune
温馨提示
Unicode源于一个很简单的想法: 将全世界所有的字符包含在一个集合里, 计算机只要支持这一个字符集, 就能显示所有的字符, 再也不会有乱码了
字符类型字面量
字符类型字面量有三种形式: 单个字符、转义字符和通用字符, 它们均使用一对单引号定义
单个字符
的字符字面量举例:
let a: Rune = 'a'
let b: Rune = 'b'
转义字符
是指在一个字符序列中对后面的字符进行另一种解释的字符。转义字符使用转义符号\
开头, 后面加需要转义的字符。 举例如下:
let slash: Rune = '\\'
let newline: Rune = '\n'
let tab: Rune = '\t'
通用字符
以\u
开头, 后面加上定义在一对花括号{}
中的 1~8个十六进制数, 即可表示对应的 Unicode 值代表的字符。 举例如下:
main() {
let a: Rune = '\u{4E2D}' // 中
let b: Rune = '\u{534E}' // 华
let c: Rune = '\u{6C11}' // 民
let d: Rune = '\u{65CF}' // 族
let result: String = String(a) + String(b) + String(c) + String(d)
print(result)
}
// result: 中华民族
字符串类型 String
字符串类型使用String
表示, 用于表达文本数据, 由一串 Unicode 字符组合而成
字符串类型字面量
字符串字面量分为三类: 单行字符串字面量、多行字符串字面量、多行原始字符串字面量
单行字符串字面量
的内容定义在一对双引号之内""
, 双引号中的内容可以是任意数量的 (除了非转义的双引号和单独出现的\
之外的) 任意字符。单行字符串字面量只能写在同一行,不能跨多行
let s1: String = ""
let s2 = "Hello Cangjie Lang"
let s3 = "\"Hello Cangjie Lang\""
let s4 = "Hello Cangjie Lang\n"
多行字符串字面量
以三个双引号开头"""
, 并以三个双引号结尾"""
,并且开头的三个双引号之后需要换行(否则编译错误)。字面量的内容从开头的三个双引号换行后的第一行开始, 到结尾的三个双引号之前结束, 之前的内容可以是任意数量的(除单个出现的\
之外的) 任意字符。不同于单行字符串字面量, 多行字符串字面量可以跨多行。
let s1: String = """
"""
let s2 = """
Hello,
Cangjie Lang"""
多行原始字符串字面量
以一个或多个井号#
加上一个双引号开始, 并以一个双引号加上和开始相同个数的#
结束。开始的双引号和结束的双引号之间的内容可以是任意数量的任意合法字符。不同于 (普通) 多行字符串字面量, 多行原始字符串字面量中的内容会维持原样 (转义字符不会被转义, 如下例中 s2 中的 \n 不是换行符, 而是由 \ 和 n 组成的字符串 \n)
let s1: String = #""#
let s2 = ##"\n"##
let s3 = ###"
Hello,
Canjie
Lang"###
插值字符串
插值字符串
是一种包含一个或多个插值表达式的字符串字面量 (不适用于多行原始字符串字面量), 通过将表达式插入到字符串中, 可以有效避免字符串拼接的问题。
插值表达式必须用花括号{}
包起来, 并在花括号前加上美元符号$
, 例如"${expression}"
。{}
中可以包含一个或者多个声明或表达式
main() {
let fruit = "apples"
let count = 10
let s = "There are ${count * count} ${fruit}"
println(s) // There are 100 apples
let r = 2.4
let area = "There area of a circle with redius ${r} is ${let pi = 3.14; pi * r * r}"
println(area) // There area of a circle with redius 2.400000 is 18.086400
}
数组类型 Array
使用Array
类型来构造单一元素类型, 有序序列的数据
仓颉使用Array<T>来表示Array类型。T 表示 Array 的元素类型, T 可以是任意类型
var a: Array<Int64> = ... // Array whose element type is Int64
var b: Array<String> = ... // Array whose element type is String
元素类型不相同的 Array 是不相同的类型, 所有它们之间不可以互相赋值
b = a // Type mismatch error
可以使用字面量来初始化一个 Array, 只需使用方括号将逗号分隔的值列表括起来即可
编译器会根据上下文自动推断 Array 字面量的类型
let b = [1, 2, 3, 4, 5] // Array<Int64>
let c = ["a", "b", "c"] // Array<String>
也可以使用构造函数的方式构造一个指定元素类型的 Array
let c = Array<Int64>(3, item: 0) // [0, 0, 0]
let d = Array<Int64>(3, {i => i + 1}) // [1, 2, 3]
⬆ `3` 表示 Array 的长度, `item` 表示 Array 中的每个元素的初始值, 也可以使用闭包来初始化 Array 中的每个元素
访问 Array 成员
当需要对 Array 的所有元素进行访问时, 可以使用 for-in 表达式遍历 Array 的所有元素
Array 是按元素插入顺序排列的, 因此对 Array 遍历的顺序总是恒定的
main() {
let arr = [0, 1, 2]
for (i in arr) {
println("The element is ${i}")
}
}
当需要知道某个 Array 包含的元素个数时, 可以使用 size 属性来获取。
main() {
let arr = [0, 1, 2]
println("The size of the array is ${arr.size}")
}
注意: 属性不用( ), 方法要用( )
当需要访问 Array 中的某个元素时, 可以使用下标运算符[]
来访问
非空 Array 的第一个元素总是从位置 0 开始
索引值不能使用负数
或者大于等于 Array 的长度, 当编译器能检查出索引值非法时, 会在编译时报错, 否则会在运行时抛异常。
main() {
let arr = [0, 1, 2]
let a = arr[0] // 0
let b = arr[1] // 1
println("The first element is ${a}")
println("The second element is ${b}")
let c = arr[-1] // error: index out of range
println("The last element is ${c}")
}
如果想获取某一段 Array 的元素, 可以在下标中传入 Range 类型的值, 就可以一次性取得 Range 对应范围的一段 Array
let arr1 = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
let arr2 = arr1[1..3] // [20, 30]
let arr3 = arr1[4..=7] // [50, 60, 70, 80]
修改 Array
Array 是一种长度不变的 Collection 类型, 因此 Array 没有提供添加和删除元素的成员函数
但 Array 允许对其中的元素进行修改
main() {
let arr = [0, 1, 2]
arr[0] = 10
println(arr[0]) // 10
}
Array 是引用类型, 因此 Array 在作为表达式使用时不会拷贝副本, 同一个 Array 实例的所有引用都会共享同样的数据。
因此对 Array 元素的修改会影响到该实例的所有引用
main() {
let arr1 = [0, 1, 2]
let arr2 = arr1
arr2[0] = 10
println(arr1) // [10, 1, 2]
println(arr2) // [10, 1, 2]
// ⬇ 值传递类型
var msg1 = 100
var msg2 = msg1
msg1 = 200
println(msg1) // 200
println(msg2) // 100
}
值类型数组 VArray
仓颉编程语言引入了值类型数组 VArray<T, $N>
, 其中T
表示该值类型数组的元素类型, $N
是一个固定的语法, 通过$
加上一个数值字面量表示这个值类型数组的长度
var a: VArray<Int64, $3> = [1, 2, 3]
同时, 它拥有两个构造函数
// VArray<T, $N>(initElement: (Int64) -> T)
let b = VArray<Int64, $5>({ i => i }) // [0, 1, 2, 3, 4]
println(b[3]) // 3
// VArray<T, $N>(item!: T)
let c = VArray<Int64, $5>(item: 0) // [0, 0, 0, 0, 0]
println(c[3]) // 0
除此之外, VArray<T, $N>
类型提供了两个成员方法
- 用于下标访问和修改的
[]
操作符方法:
var a: VArray<Int64, $3> = [1, 2, 3]
let i = a[1]
a[2] = 4
- 用于获取
VArray
长度的size
方法:
var a: VArray<Int64, $3> = [1, 2, 3]
let s = a.size
println(s) // 3
区间类型 Range
区间类型用于表示拥有固定步长的序列, 使用Range<T>
表示。当T
被实例化不同的类型时, 会得到不同的区间类型, 如最常用的Range<Int64>
用于表示整数区间
每个区间类型的实例都会包含start
(起始值)、end
(终止值)和step
(步长)三个属性, start
和end
的类型相同(即T
被实例化的类型), step
类型是Int64
区间类型字面量
区间字面量有两种形式: “左闭右开”区间和”左闭右闭”区间。其中, “左闭右开”区间的格式是start..end : step
, 表示一个从start
开始, 以step
为步长, 到end
(不包含end
)为止的区间; “左闭右闭”区间的格式是start..=end : step
, 表示一个从start
开始, 以step
为步长, 到end
(包含end
)为止的区间
let n = 10
let r1 = 0..10:1 // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r2 = 0..=n:1 // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
let r3 = n..0:-2 // 10, 8, 6, 4, 2
let r4 = n..=0:-2 // 10, 8, 6, 4, 2, 0
区间字面量中, 可以不写step
, 此时step
默认等于1
。
注意: step
不能为0, 另外区间也有可能是空的(即不包含任何元素的空序列)
let r5 = 0..10 // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r6 = 0..=10:0 // error: step cannot be 0
let r7 = 10..0:1 // empty ranges
let r8 = 0..10:-1 // empty ranges
let r9 = 10..=0:1 // empty ranges
let r10 = 0..=10:-1 // empty ranges
提示
表达式start..end:step
中, 当step > 0
且start >= end
, 或者step < 0
且start <= end
时, start..end:step
是一个空区间; 表达式start..=end:step
中, 当step > 0
且start > end
, 或者step < 0
且start < end
时, start..=end:step
是一个空区间
元组类型 Tuple
元组(Tuple)可以将多个不同的类型组合在一起, 成为一个新的类型。元组类型使用(T1, T2, …, Tn)表示, 其中T1~Tn可以是任意类型, 不同类型间使用逗号,
链接。元组至少是二元以上, 例如, (Int64, Float64)
表示一个二元组类型, (Int64, Float64, String)
表示一个三元组类型
元组的长度是固定的, 即一旦定义了一个元组类型的实例, 它的长度不能再更改
元组支持通过t[index]
的方式访问某个具体位置的元素, 其中t
是一个元组, index
是下标, 并且index
只能是从0
开始且小于元组元素个数的整数类型字面量, 否则, 编译报错。
下面的例子中, 使用pi[0]
和pi[1]
可以分别访问二元组pi
的第一个和第二个元素:
main() {
vat pi = (3.14, "PI")
println(pi[0]) // 3.14
println(pi[1]) // PI
}
元组类型是不可变类型, 即一旦定义一个元组类型的实例, 内容不再被更新
var tuple = (true, false)
tuple[0] = false // error: cannot assign to immutable value
元组类型的字面量
元组类型的字面量使用(E1, E2, ..., En)
表示, 其中E1~En是表达式, 多个表达式之间使用逗号分隔。
下面的例子中, 分别定义了一个(Int64, Float64)
类型的变量x, 以及一个(Int64, Float64, String)
类型的变量y, 并且使用元组类型的字面量初始化:
let x: (Int64, Float64) = (2, 3.14159)
let y: (Int64, Float64, String) = (2, 3.14159, "PI")
元组类型的类型参数
let tuple: (name: String, age: Int64) = ("apple", 18)
对于一个元组类型, 只允许统一写类型参数名, 或者统一不写类型参数名, 不能部分写类型参数名, 否则编译报错
let tuple: (String, age: Int64) = ("apple", 18) // error: type parameter name mismatch
其他类型
Unit 类型
对于那些只关心副作用而不关心值得表达式, 它们的类型是Unit
。
例如, print
函数、赋值表达式、复合赋值表达式、自增和自减表达式、循环表达式, 它们的类型都是Unit
Unit
类型只有一个值, 也就是它的字面量()
。除了赋值、判等和判不等外, Unit
类型不支持其它操作
Nothing 类型
Nothing
是一种特殊的类型, 它不包含任何值, 并且Nothing
类型是所有类型的子类型
break
、continue
、return
、throw
表达式的类型是Nothing
, 程序执行到这些表达式时, 它们之后的代码将不会被执行。其中break
、continue
只能在循环体中使用, return
只能在函数体中使用
注意
目前编译器还不允许在使用类型的地方显式使用Nothing
类型, 例如: let a: Nothing = ()
会报错, 但是可以在函数返回值的地方使用Nothing
类型, 例如: return break
运算符
算术运算符
算术运算符包含”加、减、乘、除、取余、自增和自减”
main() {
var add: Int64
var x = 10
var y = 20
add = x + y
println("x + y = ${add}")
}
余数运算符
余数运算符是比较常用的, 因为在逻辑思维上寻找规律, 余数运算符是很好用的
main() {
var result: Int64
var x = 101
var y = 5
result = x % y
println("x % y = ${result}")
}
自增和自减运算符
自增和自减运算符, 是一元运算符, 只需一个运算子。它们的作用是将运算子首先转为数值, 然后加上1或减去1
main() {
var x = 10
var y = 20
x++
y--
println("x++ = ${x}") // 11
println("y-- = ${y}") // 19
}
赋值运算符
赋值运算符(Assignment Operators)用于给变量赋值
最常见的赋值运算符, 是=
运算符, 它将右侧的值赋给左侧的变量
main() {
var x = 10
var y = x
println("y = ${y}") // 10
}
复合赋值运算符
赋值运算符还可以与其他运算符结合, 形成变体。
x += 1 // x = x + 1
x -= 1 // x = x - 1
x *= 2 // x = x * 2
x /= 2 // x = x / 2
x %= 2 // x = x % 2
比较运算符
比较运算符用于比较两个值的大小, 返回布尔值true
或false
, 表示是否满足指定的条件
比较运算符 | 描述 |
---|---|
< | 小于 |
> | 大于 |
<= | 小于等于 |
>= | 大于等于 |
== | 判等 |
!= | 判不等 |
from std import random.*
main() {
let number: Int8 = Random().nextInt8()
if (number % 2 == 0){
println("even")
} else {
println("odd")
}
}
布尔运算符
常用的布尔运算符有&&
(与)、||
(或)、!
(非)
取反运算符 (!
)
!true // false
!false // true
与运算符 (&&
)
多个条件都要满足
if (10 < 20 && 10 > 5) {
println("Yes")
} else {
println("No")
}
或运算符 (||
)
只要有一个条件满足即可
if (10 < 20 || 10 > 5) {
println("Yes")
} else {
println("No")
}
位运算符
位运算符直接处理每一个比特位(bit), 是非常底层的运算, 好处是速度极快, 缺点是很不直观, 许多场合不能使用它们, 否则会使代码难以理解和查错
快速计算位移方案 <<
>>
- 左移运算符就是*2的n次方 (n代表位移次数)
- 右移运算符就是/2的n次方 (n代表位移次数, 不同点, 出现小数时要取整)
位运算演算过程
字长
计算机的每个字包含的位数称为字长, 也称在同一时间中CPU一次操作处理二进制的位数
。大型计算机的字长为32-64位, 小型计算机为12-32位, 而微型计算机为4-16位。字长是衡量计算机性能的一个重要因素。现代计算机一般64位, 字长就是64位。
左移运算符 <<
5 << 2 // 20
↓
0000 0000 0000 0000 0000 0000 0000 0101 // 5
0000 0000 0000 0000 0000 0000 0001 0100 // 20 左移2位
右移运算符 >>
1000 >> 8 // 3
↓
0000 0000 0000 0000 0000 0011 1110 1000 // 1000
0000 0000 0000 0000 0000 0000 0000 0011 // 3 右移8位
十进制转为二进制
采用”除2取余, 逆序排列”的方法
例如: 10转为二进制
10 / 2 = 5 余数0
5 / 2 = 2 余数1
2 / 2 = 1 余数0
1 / 2 = 0 余数1
得到的余数逆序排列就是10的二进制数: 1010
函数
定义函数
仓颉使用关键字func
来表示函数定义的开始, func
之后依次是函数名、参数列表、可选的函数返回值类型、函数体。其中, 函数名可以是任意的合法标识符, 参数列表定义在一对圆括号内(多个参数间使用逗号分隔), 参数列表和函数返回值类型(如果存在)之间使用冒号间隔, 函数体定义在一对花括号内。
func add(a: Int64, b: Int64): Int64 {
return a + b
}
↑ 上例中定义了一个名为add
的函数, 其参数列表由两个Int64
类型的参数a
和b
组成, 函数返回值类型为Int64
, 函数体中使用return
语句返回两个参数的和
函数调用
函数调用的形式为funcName(arg1, arg2, ..., argn)
。其中, funcName
是要调用的函数名, arg1
~ argn
是n个调用时的参数(称为实参), 要求每个实参的类型必须是对应参数类型的子类型。实参可以有0个或多个, 当实参个数为0时, 调用方式为funcName()
参数列表
一个函数可以拥有0个或多个参数, 这些参数均定义在函数的参数列表中。根据函数调用时是否需要给定参数名, 可以将参数列表中的参数分为两类: 非命名参数
和命名参数
非命名参数的定义方式是p:T
, 其中p
是参数名, T
是参数p
的类型, 参数名和其类型间使用冒号链接。例如, 上例中add
函数的两个参数a
和b
均为非命名参数
命名参数的定义方式是p!:T
, 与非命名参数的不同是在参数名p
之后多了一个!
。可以将上例中add
函数的两个非命名参数修改为命名参数
func add(a!: Int64, b!: Int64): Int64 {
return a + b
}
命名参数还可以设置默认值, 通过p!:T = e
方式将参数p
的默认值设置为表达式e
的值。
例如, 可以将上述add
函数的两个参数的默认值都设置为1
:
func add(a!: Int64 = 1, b!: Int64 = 1): Int64 {
return a + b
}
非命名参数和命名参数的主要差异在于调用时的不同
根据函数定义时参数是非命名参数还是命名参数的不同, 函数调用时传实参的方式也有所不同: 对于非命名参数, 它对应的实参是一个表达式, 对于命名参数, 它对应的实参需要使用p:e
的形式, 其中p
是命名参数的名字, e
是表达式(即传递给参数p
的值)
func add(a!: Int64 = 1, b!: Int64 = 1): Int64 {
return a + b
}
add(a:10, b:20)
函数参数均为不可变变量, 在函数定义内不能对其赋值
func add(a: Int64, b: Int64): Int64 {
a = a + b // error: cannot assign to immutable value
return a
}
返回值
函数返回值类型
函数返回值类型是函数被调用后得到的值的类型, 函数定义时, 返回值类型是可选的: 可以显式地定义返回值类型(返回值类型定义在参数列表和函数体之间), 也可以不定义返回值类型, 交由编译器推导确定
当显式地定义了函数返回值类型时, 就要求函数体的类型、函数体中所有return e
表达式中e
的类型是返回值类型的子类型。
例如, 对于上述add
函数, 显示地定义了它的返回值类型为Int64
, 如果将函数体中地return a + b
修改为return (a, b)
, 则会因为类型不匹配而报错
// Error: the type of the expression after return does not match the return type of the function
func add(a: Int64, b: Int64): Int64 {
return (a, b) // error: type mismatch
}
在函数定义时如果未显式定义返回值类型, 编译器将根据函数体的类型以及函数体中所有地return
表达式来共同推导出函数的返回值类型。
例如, 下列add
函数的返回值类型虽然被省略, 但编译器可以根据return a + b
推导出add
函数的返回值类型是Int64
:
func add(a: Int64, b: Int64) {
return a + b // return type is Int64
}
提示
函数的返回值类型并不是任何情况下都可以被推导出来的, 如果返回值类型推导失败, 编译报错
函数体
函数体中定义了函数被调用时执行的操作, 通常包含一系列的变量定义和表达式, 也可以包含新的函数定义(即嵌套函数)。
如下add
函数的函数体中首先定义了Int64
类型的变量r
(初始值为0), 接着将a + b
的值赋于r
, 最后返回r
的值
func add(a: Int64, b: Int64): Int64 {
var r: Int64 = 0
r = a + b
return r
}
在函数体的任意位置都可以使用return
表达式来终止函数的执行并返回。return
表达式有两种形式:return
和return expr
( expr 是一种表达式)
对于return expr
, 要求expr
的类型与函数定义的返回值类型保持一致。
例如, 下例中会因为return 100
中100
类型(Int64
)和函数foo
的返回值类型String
不同而报错
// Error: the type of the expression after return does not match the return type of the function
func foo(): String {
return 100 // error: type mismatch
}
对于return
, 其等价于return ()
, 所以函数的返回值类型为Uint
func add(a: Int64, b: Int64) {
var r = 0
r = a + b
return r
}
func foo(): Unit{
add(1, 2)
return
}
提示
return
表达式作为一个整体, 其类型并不由后面跟随的表达式决定, 而是 Nothing 类型
在函数体内定义的变量属于局部变量的一种, 它的作用域从其定义之后开始到函数体结束
对于一个局部变量, 允许在其外层作用域中定义同名变量, 并且在此局部变量的作用域内, 局部变量会”遮盖”外层作用域的同名变量
let r = 0
func add(a: Int64, b: Int64) {
var r = 0
r = a + b
return r
}
👆上例, add
函数之前定义了Int64
类型的全局变量r
, 同时add
函数体内定义了同名的局部变量r
, 那么在函数体内, 所有使用变量r
的地方(如r = a + b
), 用到的将是局部变量r
, 即(在函数体内)局部变量r
”遮盖”了全局变量r
函数体也是有类型的, 函数体的类型是函数体内最后一”项”的类型: 若最后一项为表达式, 则函数体的类型是此表达式的类型, 若最后一项为变量定义或函数声明, 或函数体为空, 则函数体的类型为 Unit
func add(a: Int64, b: Int64): Int64 {
a + b
}
👆上例, 因为函数体的最后一”项”是Int64
类型的表达式(即a + b
), 所以函数体的类型也是Int64
, 与函数定义的返回值类型相匹配。
又如, 下例函数体的最后一项是print
函数调用, 所以函数体的类型是Unit
, 同样与函数定义的返回值类型相匹配
func foo(): Unit{
let s = "Hello"
println(s)
}
一等公民
仓颉编程语言中, 函数是一等公民(first-class citizens), 可以作为函数的参数或返回值, 也可以赋值给变量。因此函数本身也有类型, 称为函数类型
函数类型
函数类型
由函数的参数类型和返回类型组成, 参数类型和返回类型之间使用->
连接。参数类型使用圆括号()
括起来, 可以有0个或多个参数, 如果参数超过两个, 参数类型之间使用逗号,
分隔
func hello(): Unit{
println("Hello!")
}
👆示例定义了一个函数, 函数名为hello, 其类型是()->Unit
,表示该函数没有参数, 返回类型为Unit
func add(a: Int64, b: Int64): Int64 {
a + b
}
👆函数名为add
, 其类型是(Int64, Int64) -> Int64
, 表示该函数有两个参数, 两个参数类型均为Int64
, 返回类型为Int64
函数类型作为参数类型
示例: 函数名为printAdd
, 其类型是((Int64, Int64) -> Int64, Int64, Int64) -> Unit
, 表示该函数有三个参数, 参数类型分别为函数类型(Int64, Int64) -> Int64
和两个Int64
, 返回类型为Unit
func printAdd(add: (Int64, Int64) -> Int64, a: Int64, b: Int64 ): Unit {
println(add(a, b))
}
func demo(x: Int64, y: Int64): Int64{
return x * y
}
main() {
printAdd(demo, 10, 10)
}
函数类型作为返回类型
函数类型可以作为另一个函数的返回类型
👇示例, 函数名为returnAdd
, 其类型是() -> (Int64, Int64) -> Int64
, 表示该函数无参数, 返回类型为函数类型(Int64, Int64) -> Int64
。注意,->
是右结合的
func add(a: Int64, b: Int64): Int64 {
return a + b
}
func returnAdd(): (Int64, Int64) -> Int64 {
add
}
main() {
var a = returnAdd()
println(a(1, 2)) // 3
}
函数类型作为变量类型
函数名本身也是表达式, 它的类型为对应的函数类型
func add(p1: Int64, p2: Int64): Int64 {
p1 + p2
}
let f: (Int64, Int64) -> Int64 = add
嵌套函数
定义在源文件顶层的函数被称为全局函数。定义在函数体内的函数被称为嵌套函数
👇示例, 函数foo
内定义了一个嵌套函数nestAdd
, 可以在foo
内调用该嵌套函数nestAdd
, 也可以将嵌套函数nestAdd
作为返回值返回, 在foo
外对其进行调用:
func foo() {
func nestAdd(a: Int64, b: Int64): Unit{
a + b + 3
}
println(nestAdd(1, 2)) // 6
return nestAdd
}
main() {
let f = foo()
let x = f(1, 2)
println("result: ${x}") // 6
}
Lambda 表达式
Lambda 表达式定义
Lambda 表达式的语法形式: {p1: T1, ..., pn:Tn => expressions | declarations}
其中, =>
之前为参数列表, 多个参数之间使用,
分隔, 每个参数名和参数类型之间使用:
分隔。=>
之前也可以没有参数。=>
之后为 Lambda 表达式体, 是一组表达式或声明序列
let f1 = { a: Int64, b: Int64 => a + b }
var display = { => println("Hello") } // Parameterless Lambda
main() {
println(f1(10, 20)) // 30
display() // Hello
}
Lambda 表达式中参数的类型标注可缺省。以下情形中, 若参数类型省略, 编译器会尝试进行类型推断
- Lambda 表达式赋值给变量时, 其参数类型根据变量类型推断
- Lambda 表达式作为函数调用表达式的实参使用时, 其参数类型根据函数的形参类型推断
var sum: (Int64, Int64) -> Int64 = { a, b => a + b }
func f(fn: (Int64) -> Int64): Int64{
fn(1)
}
main() {
println(sum(10, 20)) // 30
println(f({ num => num + 10 })) // 11
}
Lambda 表达式声明
Lambda 表达式支持立即调用
let r1 = { a: Int64, b: Int64 => a + b }(1, 2)
let r2 = { => 123}()
Lambda 表达式也可以赋值给一个变量, 使用变量名进行调用
func f() {
var g = { x: Int64 => println("x = ${x}") }
g(2)
}
闭包
一个函数或 Lambda 从定义它的静态作用域中捕获了变量, 函数或 Lambda 和捕获的变量一起被称为一个闭包, 这样即使脱离了闭包定义所在的作用域, 闭包也能正常运行
示例1: 闭包add
, 捕获了let
声明的局部变量num
, 之后通过返回值返回到num
定义的作用域之外, 调用add
时仍可正常访问num
func returnAddNum(): (Int64) -> Int64 {
let num: Int64 = 10
func add(a: Int64) {
return a + num
}
add
}
main() {
let f = returnAddNum()
println(f(10)) // 20
}
示例2: 捕获的变量必须在闭包定义时可见
func f() {
let x = 99
func f1() {
println(x)
}
let f2 = { =>
println(y) // error: cannot capture 'y' which is not in scope
}
let y = 100
f1() // Print 99.
f2()
}
示例3: 捕获的变量必须在闭包定义前完成初始化
func f() {
let x: Int64
func f1() {
println(x) // Error: x is not initialized yet.
}
x = 99
f1()
}
需要注意的是, 捕获具有传递性, 如果一个函数f
调用了捕获var
变量的函数g
, 且存在g
捕获的var
变量不在函数f
内定义, 那么函数f
同样捕获了var
变量, 此时, f
也不能作为一等公民使用
👇示例, g
捕获了var
声明的变量x
, f
调用了g
, 且g
捕获的x
不在f
内定义, f
同样不能作为一等公民使用
func h(): Unit{
var x = 1
func g() { x } // captured a mutable variable
func f() {
g() // invoked g
}
return f // error
}