语法简明高效
多范式编程
类型安全
内存安全
高效并发
兼容语言生态
领域易扩展
助力 UI 开发
内置库功能丰富
普通标识符
原始标识符是在普通标识符或仓颉关键字的外面加上一对反引号,主要用于将仓颉关键字作为标识符的场景。
在扩展名为 .cj 的文本文件中编写仓颉程序,这些程序和文件也被称为源代码和源文件,在程序开发的最后阶段,这些源代码将被编译为特定格式的二进制文件。
如果要将仓颉程序编译为可执行文件,您需要在顶层作用域中定义一个 main 函数作为程序入口,它可以有 Array 类型的参数,也可以没有参数,它的返回值类型可以是整数类型或 Unit 类型。
顶层作用域可以定义一系列的变量、函数和自定义类型(如 struct、class、enum 和 interface 等),其中的变量和函数分别被称为全局变量和全局函数。
非顶层作用域中不能定义上述自定义类型,但可以定义变量和函数,称之为局部变量和局部函数。特别地,对于定义在自定义类型中的变量和函数,称之为成员变量和成员函数。
修饰符用于设置变量的各类属性,可以有一个或多个
不可变变量
可变变量
可变性决定了变量被初始化后其值还能否改变
影响全局变量和成员变量的可引用范围,
private
public
影响成员变量的存储和引用方式
static
一个合法的仓颉标识符
指定了变量所持有数据的类型
当初始值具有明确类型时,可以省略变量类型标注,此时编译器可以自动推断出变量类型。
是一个仓颉表达式,用于初始化变量
程序在运行阶段,只有指令流转和数据变换,仓颉程序中的各种标识符已不复存在。
编译器使用了一些机制,将这些名字和编程所取用的数据实体/存储空间绑定起来
从编译器实现层面看,任何变量总会关联一个值(一般是通过内存地址/寄存器关联),只是在使用时,对有些变量,我们将直接取用这个值本身,这被称为值类型变量,而对另一些变量,我们把这个值作为索引、取用这个索引指示的数据,这被称为引用类型变量。
值类型变量通常在线程栈上分配,每个变量都有自己的数据副本;
从编译器实现层面看,使用时,对变量将直接取用这个值本身
从语言层面看,值类型变量对它所绑定的数据/存储空间是独占的
在给值类型变量赋值时,一般会产生拷贝操作,且原来绑定的数据/存储空间被覆写。
引用类型变量通常在进程堆中分配,多个变量可引用同一数据对象,对一个变量执行的操作可能会影响其他变量。
从编译器实现层面看,使用时,把这个值作为索引、取用这个索引指示的数据
从语言层面看,引用类型变量所绑定的数据/存储空间可以和其他引用类型变量共享
在给引用类型变量赋值时,只是改变了引用关系,原来绑定的数据/存储空间不会被覆写。
将名字和程序元素的绑定关系限制在一定范围里。不同作用域之间可以是并列或无关的,也可以是嵌套或包含关系。一个作用域将明确我们能用哪些名字访问哪些程序元素
在仓颉编程语言中,用一对大括号“{}”包围一段仓颉代码,即构造了一个新的作用域,其中可以继续使用大括号“{}”包围仓颉代码,由此产生了嵌套作用域
在仓颉编程语言中,简化并延伸了表达式的传统定义——凡是可求值的语言元素都是表达式。
在仓颉程序中,由一对大括号“{}”包围起来的一组表达式,被称为“代码块”,它将作为程序的一个顺序执行流,其中的表达式将按编码顺序依次执行。
如果代码块中有至少一个表达式,规定此代码块的值与类型等于其中最后一个表达式的值与类型,如果代码块中没有表达式,规定这种空代码块的类型为 Unit、值为 ()。
顺序结构
if 表达式
if-let 表达式
for-in 表达式可以遍历那些扩展了迭代器接口 Iterable 的类型实例。
遍历区间
遍历元组构成的序列
迭代变量不可修改
使用通配符 _ 代替迭代变量
等同于使用 if 表达式和 continue 表达式在循环体中实现这一逻辑
while 表达式
do-while 表达式
while-let 表达式
break 用于终止当前循环表达式的执行、转去执行循环表达式之后的代码
continue 用于提前结束本轮循环、进入下一轮循环
break 与 continue 表达式的类型都是 Nothing。
Int8、Int16、Int32、Int64 和 IntNative,分别用于表示编码长度为 8-bit、16-bit、32-bit、64-bit 和平台相关大小的有符号整数值的类型。
UInt8、UInt16、UInt32、UInt64 和 UIntNative,分别用于表示编码长度为 8-bit、16-bit、32-bit、64-bit 和平台相关大小的无符号整数值的类型。
整数类型字面量有 4 种进制表示形式:二进制(使用 0b 或 0B 前缀)、八进制(使用 0o 或 0O 前缀)、十进制(没有前缀)、十六进制(使用 0x 或 0X 前缀)。
仓颉编程语言支持字符字节字面量,以方便使用 ASCII 码表示 UInt8 类型的值。字符字节字面量由字符 b、一对标识首尾的单引号、以及一个 ASCII 字符组成
整数类型支持的操作
浮点类型包括 Float16、 Float32 和 Float64,分别用于表示编码长度为 16-bit、 32-bit 和 64-bit 的浮点数(带小数部分的数字,如 3.14159、8.24 和 0.1 等)的类型。
Float64 的精度约为小数点后 15 位,Float32 的精度约为小数点后 6 位,Float16 的精度约为小数点后 3 位。
浮点类型字面量有两种进制表示形式:十进制、十六进制。
浮点类型支持的操作
布尔类型使用 Bool 表示,用来表示逻辑中的真和假。
布尔类型字面量
布尔类型支持的操作
字符类型使用 Rune 表示,可以表示 Unicode 字符集中的所有字符。
字符类型字面量有三种形式:单个字符、转义字符和通用字符。
一个 Rune 字面量由字符 r 开头,后跟一个由一对单引号或双引号包含的字符。
字符类型支持的操作
字符串类型使用 String 表示,用于表达文本数据,由一串 Unicode 字符组合而成。
字符串字面量分为三类:单行字符串字面量,多行字符串字面量,多行原始字符串字面量。
单行字符串字面量的内容定义在一对单引号或一对双引号之内,引号中的内容可以是任意数量的(除了非转义的双引号和单独出现的 \ 之外的)任意字符。单行字符串字面量只能写在同一行,不能跨越多行。
多行字符串字面量开头结尾需各存在三个双引号(""")或三个单引号(''')。
多行原始字符串字面量以一个或多个井号(#)和一个单引号(')或双引号(")开头,后跟任意数量的合法字符,直到出现与字符串开头相同的引号和与字符串开头相同数量的井号为止。
插值表达式必须用花括号 {} 包起来,并在 {} 之前加上 $ 前缀。{} 中可以包含一个或者多个声明或表达式。
字符串类型支持的操作
元组(Tuple)可以将多个不同的类型组合在一起,成为一个新的类型。元组类型使用 (T1, T2, ..., TN) 表示,其中 T1 到 TN 可以是任意类型,不同类型间使用逗号(,)连接。元组至少是二元,例如,(Int64, Float64) 表示一个二元组类型,(Int64, Float64, String) 表示一个三元组类型。
元组的长度是固定的,即一旦定义了一个元组类型的实例,它的长度不能再被更改。
元组类型是不可变类型,即一旦定义了一个元组类型的实例,它的内容不能再被更新。
元组类型的字面量
元组类型的类型参数
以使用 Array 类型来构造单一元素类型,有序序列的数据。
仓颉使用 Array 来表示 Array 类型。T 表示 Array 的元素类型,T 可以是任意类型。
访问 Array 成员
Array 是一种长度不变的 Collection 类型,因此 Array 没有提供添加和删除元素的成员函数。
值类型数组 VArray ,其中 T 表示该值类型数组的元素类型,$N 是一个固定的语法,通过 $ 加上一个 Int64 类型的数值字面量表示这个值类型数组的长度。需要注意的是,VArray 不能省略 ,且使用类型别名时,不允许拆分 VArray 关键字与其泛型参数。
区间类型用于表示拥有固定步长的序列,区间类型是一个泛型,使用 Range 表示。
每个区间类型的实例都会包含 start、end 和 step 三个值。其中,start 和 end 分别表示序列的起始值和终止值,step 表示序列中前后两个元素之间的差值(即步长);
“左闭右开”区间的格式是 start..end : step,它表示一个从 start 开始,以 step 为步长,到 end(不包含 end)为止的区间;
“左闭右闭”区间的格式是 start..=end : step,它表示一个从 start 开始,以 step 为步长,到 end(包含 end)为止的区间。
对于那些只关心副作用而不关心值的表达式,它们的类型是 Unit。例如,print 函数、赋值表达式、复合赋值表达式、自增和自减表达式、循环表达式,它们的类型都是 Unit。
Unit 类型只有一个值,也是它的字面量:()。除了赋值、判等和判不等外,Unit 类型不支持其他操作。
Nothing 是一种特殊的类型,它不包含任何值,并且 Nothing 类型是所有类型的子类型。
break、continue、return 和 throw 表达式的类型是 Nothing,程序执行到这些表达式时,它们之后的代码将不会被执行。
仓颉使用关键字 func 来表示函数定义的开始,func 之后依次是函数名、参数列表、可选的函数返回值类型、函数体。
根据函数调用时是否需要给定参数名,可以将参数列表中的参数分为两类:非命名参数和命名参数。
命名参数的定义方式是 p!: T,与非命名参数的不同是在参数名 p 之后多了一个 !。
命名参数还可以设置默认值,通过 p!: T = e 方式将参数 p 的默认值设置为表达式 e 的值。
非命名参数和命名参数的主要差异在于调用时的不同
函数返回值类型是函数被调用后得到的值的类型。
函数体中定义了函数被调用时执行的操作,通常包含一系列的变量定义和表达式,也可以包含新的函数定义(即嵌套函数)。
在函数体的任意位置都可以使用 return 表达式来终止函数的执行并返回。return 表达式有两种形式:return 和 return expr(expr 是一个表达式)。
函数返回值类型中我们提到函数体也是有类型的,函数体的类型是函数体内最后一“项”的类型:若最后一项为表达式,则函数体的类型是此表达式的类型,若最后一项为变量定义或函数声明,或函数体为空,则函数体的类型为 Unit。
函数调用的形式为 f(arg1, arg2, ..., argn)。其中,f 是要调用的函数的名字,arg1 到 argn 是 n 个调用时的参数(称为实参),要求每个实参的类型必须是对应参数类型的子类型。
对于非命名参数,它对应的实参是一个表达式,对于命名参数,它对应的实参需要使用 p: e 的形式,其中 p 是命名参数的名字,e 是表达式(即传递给参数 p 的值)
仓颉编程语言中,函数是一等公民(first-class citizens),可以作为函数的参数或返回值,也可以赋值给变量。因此函数本身也有类型,称之为函数类型。
函数类型由函数的参数类型和返回类型组成,参数类型和返回类型之间使用 -> 连接。参数类型使用圆括号 () 括起来,可以有 0 个或多个参数,如果参数超过一个,参数类型之间使用逗号(,)分隔。
对于一个函数类型,只允许统一写类型参数名,或者统一不写类型参数名,不能交替存在。
函数类型作为参数类型
函数类型作为返回类型
函数类型作为变量类型
定义在源文件顶层的函数被称为全局函数。定义在函数体内的函数被称为嵌套函数。
Lambda 表达式的语法为如下形式: { p1: T1, ..., pn: Tn => expressions | declarations }。
=> 之前为参数列表,多个参数之间使用 , 分隔,每个参数名和参数类型之间使用 : 分隔。
=> 之前也可以没有参数。
=> 之后为 lambda 表达式体,是一组表达式或声明序列。
Lambda 表达式不管有没有参数,都不可以省略 =>,除非其作为尾随 lambda。
Lambda 表达式支持立即调用
一个函数或 lambda 从定义它的静态作用域中捕获了变量,函数或 lambda 和捕获的变量一起被称为一个闭包,这样即使脱离了闭包定义所在的作用域,闭包也能正常运行。
函数或 lambda 的定义中对于以下几种变量的访问,称为变量捕获 函数的参数缺省值中访问了本函数之外定义的局部变量;
函数或 lambda 内访问了本函数或本 lambda 之外定义的局部变量;
class/struct 内定义的不是成员函数的函数或 lambda 访问了实例成员变量或 this。
对定义在本函数或本 lambda 内的局部变量的访问;
对本函数或本 lambda 的形参的访问;
对全局变量和静态成员变量的访问;
对实例成员变量在实例成员函数或属性中的访问。由于实例成员函数或属性将 this 作为参数传入,在实例成员函数或属性内通过 this 访问所有实例成员变量。
当函数最后一个形参是函数类型,并且函数调用对应的实参是 lambda 时,我们可以使用尾随 lambda 语法,将 lambda 放在函数调用的尾部,圆括号外面。
流操作符包括两种:表示数据流向的中缀操作符 |> (称为 pipeline)和表示函数组合的中缀操作符 ~> (称为 composition)。
pipeline 表达式的语法形式如下:e1 |> e2。等价于如下形式的语法糖:let v = e1; e2(v) 。
其中 e2 是函数类型的表达式,e1 的类型是 e2 的参数类型的子类型。
composition 表达式表示两个单参函数的组合。composition 表达式语法如下: f ~> g。等价于如下形式: { x => g(f(x)) }。
其中 f,g 均为只有一个参数的函数类型的表达式。
f 和 g 组合,则要求 f(x) 的返回类型是 g(...) 的参数类型的子类型。
变长参数是一种特殊的函数调用语法糖。当形参最后一个非命名参数是 Array 类型时,实参中对应位置可以直接传入参数序列代替 Array 字面量(参数个数可以是 0 个或多个)
变长参数可以出现在全局函数、静态成员函数、实例成员函数、局部函数、构造函数、函数变量、lambda、函数调用操作符重载、索引操作符重载的调用处。不支持其他操作符重载、composition、pipeline 这几种调用方式。
在仓颉编程语言中,如果一个作用域中,一个函数名对应多个函数定义,这种现象称为函数重载。
函数名相同,函数参数不同(是指参数个数不同,或者参数个数相同但参数类型不同)的两个函数构成重载。
对于两个同名泛型函数,如果重命名一个函数的泛型形参后,其非泛型部分与另一个函数的非泛型部分函数参数不同,则两个函数构成重载,否则这两个泛型函数构成重复定义错误(类型变元的约束不参与判断)
同一个类内的两个构造函数参数不同,构成重载。
同一个类内的主构造函数和 init 构造函数参数不同,构成重载(认为主构造函数和 init 函数具有相同的名字)。
两个函数定义在不同的作用域,在两个函数可见的作用域中构成重载。
两个函数分别定义在父类和子类中,在两个函数可见的作用域中构成重载。
函数调用时,所有可被调用的函数(是指当前作用域可见且能通过类型检查的函数)构成候选集,候选集中有多个函数,究竟选择候选集中哪个函数,需要进行函数重载决议 优先选择作用域级别高的作用域内的函数。在嵌套的表达式或函数中,越是内层作用域级别越高。
如果作用域级别相对最高的仍有多个函数,则需要选择最匹配的函数(对于函数 f 和 g 以及给定的实参,如果 f 可以被调用时 g 也总是可以被调用的,但反之不然,则我们称 f 比 g 更匹配)。
子类和父类认为是同一作用域。
如果需要在某个类型上重载某个操作符,可以通过为类型定义一个函数名为此操作符的函数的方式实现,这样,在该类型的实例使用该操作符时,就会自动调用此操作符函数。
定义操作符函数时需要在 func 关键字前面添加 operator 修饰符;
操作符函数的参数个数需要匹配对应操作符的要求(详见附录操作符);
操作符函数只能定义在 class、interface、struct、enum 和 extend 中;
操作符函数具有实例成员函数的语义,所以禁止使用 static 修饰符;
操作符函数不能为泛型函数。
操作符重载函数定义和使用
可以被重载的操作符
常量求值允许某些特定形式的表达式在编译时求值,可以减少程序运行时需要的计算。
const 变量是一种特殊的变量,它以关键字 const 修饰,定义在编译时完成求值,并且在运行时不可改变的变量。
const 变量可以是全局变量,局部变量,静态成员变量。但是 const 变量不能在扩展中定义。const 变量可以访问对应类型的所有实例成员,也可以调用对应类型的所有非 mut 实例成员函数。
const 上下文是指 const 变量初始化表达式,这些表达式始终在编译时求值。因此需要对 const 上下文中允许的表达式加以限制,避免修改全局状态、I/O 等副作用,确保其可以在编译时求值。
const 表达式具备了可以在编译时求值的能力。满足如下规则的表达式是 const 表达式 数值类型、Bool、Unit、Rune、String 类型的字面量(不包含插值字符串)。
所有元素都是 const 表达式的 Array 字面量(不能是 Array 类型,可以使用 VArray 类型),tuple 字面量。
const 变量,const 函数形参,const 函数中的局部变量。
const 函数,包含使用 const 声明的函数名、符合 const 函数要求的 lambda、以及这些函数返回的函数表达式。
const 函数调用(包含 const 构造函数),该函数的表达式必须是 const 表达式,所有实参必须都是 const 表达式。
所有参数都是 const 表达式的 enum 构造器调用,和无参数的 enum 构造器。
数值类型、Bool、Unit、Rune、String 类型的算术表达式、关系表达式、位运算表达式,所有操作数都必须是 const 表达式。
if、match、try、控制转移表达式(包含 return、break、continue、throw)、is、as。这些表达式内的表达式必须都是 const 表达式。
const 表达式的成员访问(不包含属性的访问),tuple 的索引访问。
const init 和 const 函数中的 this 和 super 表达式。
const 表达式的 const 实例成员函数调用,且所有实参必须都是 const 表达式。
const 函数是一类特殊的函数,这些函数具备了可以在编译时求值的能力。在 const 上下文中调用这种函数时,这些函数会在编译时执行计算。而在其它非 const 上下文,const 函数会和普通函数一样在运行时执行。
如果一个 struct 或 class 定义了 const 构造器,那么这个 struct/class 实例可以用在 const 表达式中
struct 类型的定义以关键字 struct 开头,后跟 struct 的名字,接着是定义在一对花括号中的 struct 定义体。struct 定义体中可以定义一系列的成员变量、成员属性(参见属性)、静态初始化器、构造函数和成员函数。
struct 只能定义在源文件顶层。
struct 成员变量分为实例成员变量和静态成员变量(使用 static 修饰符修饰,且必须有初值),二者访问上的区别在于实例成员变量只能通过 struct 实例(我们说 a 是 T 类型的实例,指的是 a 是一个 T 类型的值)访问,静态成员变量只能通过 struct 类型名访问。
struct 支持定义静态初始化器,并在静态初始化器中通过赋值表达式来对静态成员变量进行初始化。
静态初始化器以关键字组合 static init 开头,后跟无参参数列表和函数体,且不能被访问修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。
struct 支持两类构造函数:普通构造函数和主构造函数。
普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成对所有未初始化的实例成员变量的初始化(如果参数名和成员变量名无法区分,可以在成员变量前使用 this 加以区分,this 表示 struct 的当前实例),否则编译报错。
struct 内还可以定义(最多)一个主构造函数。主构造函数的名字和 struct 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 let 或 var),成员变量形参同时扮演定义成员变量和构造函数参数的功能。
struct 成员函数分为实例成员函数和静态成员函数(使用 static 修饰符修饰),二者的区别在于:实例成员函数只能通过 struct 实例访问,静态成员函数只能通过 struct 类型名访问;静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。
struct 的成员(包括成员变量、成员属性、构造函数、成员函数、操作符函数(详见操作符重载章节))用 4 种访问修饰符修饰:private、internal、protected 和 public,缺省的修饰符是 internal。
private 表示在 struct 定义内可见。
internal 表示仅当前包及子包(包括子包的子包,详见包章节)内可见。
protected 表示当前模块(详见包章节)可见。
public 表示模块内外均可见。
递归和互递归定义的 struct 均是非法的
定义了 struct 类型后,即可通过调用 struct 的构造函数来创建 struct 实例。
struct 类型是值类型,其实例成员函数无法修改实例本身。
mut 函数是一种可以修改 struct 实例本身的特殊的实例成员函数。在 mut 函数内部,this 的语义是特殊的,这种 this 拥有原地修改字段的能力。
只允许在 interface、struct 和 struct 的扩展内定义 mut 函数(class 是引用类型,实例成员函数不需要加 mut 也可以修改实例成员变量,所以禁止在 class 中定义 mut 函数)。
mut 函数与普通的实例成员函数相比,多一个 mut 关键字来修饰。
mut 只能修饰实例成员函数,不能修饰静态成员函数。
mut 函数中的 this 不能被捕获,也不能作为表达式。不能在 mut 函数中对 struct 的实例成员变量进行捕获。
接口中的实例成员函数,也可以使用 mut 修饰。
因为 struct 是值类型,所以如果一个变量是 struct 类型且使用 let 声明,那么不能通过这个变量访问该类型的 mut 函数。
为避免逃逸,如果一个变量的类型是 struct 类型,那么这个变量不能将该类型使用 mut 修饰的函数作为一等公民来使用,只能调用这些 mut 函数。
为避免逃逸,非 mut 的实例成员函数(包括 lambda 表达式)不能直接访问所在类型的 mut 函数,反之可以。
enum 类型提供了通过列举一个类型的所有可能取值来定义此类型的方式。
定义 enum 时需要把它所有可能的取值一一列出,称这些值为 enum 的构造器(或者 constructor)。
enum 类型的定义以关键字 enum 开头,接着是 enum 的名字,之后是定义在一对花括号中的 enum 体,enum 体中定义了若干构造器,多个构造器之间使用 | 进行分隔(第一个构造器之前的 | 是可选的)。
enum 只能定义在源文件顶层。
当 enum 和 struct 类型存在互递归关系时,且 enum 类型作为 Option 的类型参数,可能存在编译错误。
定义了 enum 类型之后,就可以创建此类型的实例(即 enum 值),enum 值只能取 enum 类型定义中的一个构造器。enum 没有构造函数,可以通过 类型名.构造器,或者直接使用构造器的方式来构造一个 enum 值(对于有参构造器,需要传实参)。
Option 类型使用 enum 定义,它包含两个构造器:Some 和 None。其中,Some 会携带一个参数,表示有值,None 不带参数,表示无值。当需要表示某个类型可能有值,也可能没有值的时候,可选择使用 Option 类型。
Option 类型还有一种简单的写法:在类型名前加 ?。也就是说,对于任意类型 Ty,?Ty 等价于 Option。例如,?Int64 等价于 Option,?String 等价于 Option 等等。
常量模式可以是整数字面量、浮点数字面量、字符字面量、布尔字面量、字符串字面量(不支持字符串插值)、Unit 字面量。
通配符模式使用下划线 _ 表示,可以匹配任意值。通配符模式通常作为最后一个 case 中的模式,用来匹配其他 case 未覆盖到的情况
绑定模式使用 id 表示,id 是一个合法的标识符。与通配符模式相比,绑定模式同样可以匹配任意值,但绑定模式会将匹配到的值与 id 进行绑定,在 => 之后可以通过 id 访问其绑定的值。
Tuple 模式用于 tuple 值的匹配,它的定义和 tuple 字面量类似:(p_1, p_2, ..., p_n),区别在于这里的 p_1 到 p_n(n 大于等于 2)是模式(可以是本章节中介绍的任何模式,多个模式间使用逗号分隔)而不是表达式。
类型模式用于判断一个值的运行时类型是否是某个类型的子类型。类型模式有两种形式:_: Type(嵌套一个通配符模式 _)和 id: Type(嵌套一个绑定模式 id),它们的差别是后者会发生变量绑定,而前者并不会。
enum 模式用于匹配 enum 类型的实例,它的定义和 enum 的构造器类似:无参构造器 C 或有参构造器 C(p_1, p_2, ..., p_n),构造器的类型前缀可以省略,区别在于这里的 p_1 到 p_n(n 大于等于 1)是模式。
Tuple 模式和 enum 模式可以嵌套任意模式。
模式可以分为两类:refutable 模式和 irrefutable 模式。
在类型匹配的前提下,当一个模式有可能和待匹配值不匹配时,称此模式为 refutable 模式;反之,当一个模式总是可以和待匹配值匹配时,称此模式为 irrefutable 模式。
仓颉支持两种 match 表达式,第一种是包含待匹配值的 match 表达式,第二种是不含待匹配值的 match 表达式。
case 之后是一个模式或多个由 | 连接的相同种类的模式(如上例中的 1、0、_ 都是模式,详见模式概述章节);模式之后可以接一个可选的 pattern guard,表示本条 case 匹配成功后额外需要满足的条件;
与包含待匹配值的 match 表达式相比,关键字 match 之后并没有待匹配的表达式,并且 case 之后不再是 pattern(模式),而是类型为 Bool 的表达式(上述代码中的 x > 0 和 x < 0)或者 _(表示 true),当然,case 中也不再有 pattern guard。
在上下文有明确的类型要求时,要求每个 case 分支中 => 之后的代码块的类型是上下文所要求的类型的子类型;
在上下文没有明确的类型要求时,match 表达式的类型是每个 case 分支中 => 之后的代码块的类型的最小公共父类型;
当 match 表达式的值没有被使用时,其类型为 Unit,不要求各分支的类型有最小公共父类型。
if-let 表达式首先对条件中 <- 右侧的表达式进行求值,如果此值能匹配 <- 左侧的模式,则执行 if 分支,否则执行 else 分支(可省略)。
while-let 表达式首先对条件中 <- 右侧的表达式进行求值,如果此值能匹配 <- 左侧的模式,则执行循环体,然后重复执行此过程。如果模式匹配失败,则结束循环,继续执行 while-let 表达式之后的代码
其他使用模式的地方
class 与 struct 的主要区别在于:class 是引用类型,struct 是值类型,它们在赋值或传参时行为是不同的;class 之间可以继承,但 struct 之间不能继承。
class 定义体中可以定义一系列的成员变量、成员属性(参见属性)、静态初始化器、构造函数、成员函数和操作符函数
class 只能定义在源文件顶层。
class 成员变量分为实例成员变量和静态成员变量,静态成员变量使用 static 修饰符修饰,必须有初值,只能通过类型名访问
实例成员变量定义时可以不设置初值(但必须标注类型),也可以设置初值,只能通过对象(即类的实例)访问
静态初始化器以关键字组合 static init 开头,后跟无参参数列表和函数体,且不能被访问修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。
和 struct 一样,class 中也支持定义普通构造函数和主构造函数。
普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成所有未初始化实例成员变量的初始化,否则编译报错。
class 内还可以定义(最多)一个主构造函数。主构造函数的名字和 class 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 let 或 var),成员变量形参同时具有定义成员变量和构造函数参数的功能。
创建类的实例时调用的构造函数,将根据以下顺序执行类中的表达式 先初始化主构造函数之外定义的有缺省值的变量;
如果构造函数体内未显式调用父类构造函数或本类其它构造函数,则调用父类的无参构造函数 super(),如果父类没有无参构造函数,则报错;
执行构造函数体内的代码。
如果 class 定义中不存在自定义构造函数(包括主构造函数),并且所有实例成员变量都有初值,则会自动为其生成一个无参构造函数(调用此无参构造函数会创建一个所有实例成员变量的值均等于其初值的对象);否则,不会自动生成此无参构造函数。
class 支持定义终结器,这个函数在类的实例被垃圾回收的时候被调用。终结器的函数名固定为 ~init。终结器一般被用于释放系统资源
终结器没有参数,没有返回类型,没有泛型类型参数,没有任何修饰符,也不可以被显式调用。
带有终结器的类不可被 open 修饰,只有非 open 的类可以拥有终结器。
一个类最多只能定义一个终结器。
终结器不可以定义在扩展中。
终结器被触发的时机是不确定的。
终结器可能在任意一个线程上执行。
多个终结器的执行顺序是不确定的。
终结器向外抛出未捕获异常属于未定义行为。
终结器中创建线程或者使用线程同步功能属于未定义行为。
终结器执行结束之后,如果这个对象还可以被继续访问,则属于未定义行为。
如果对象在初始化过程中抛出异常,这样未完整初始化的对象的终结器不会执行。
class 成员函数同样分为实例成员函数和静态成员函数(使用 static 修饰符修饰),实例成员函数只能通过对象访问,静态成员函数只能通过 class 类型名访问;静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。
根据有没有函数体,实例成员函数又可以分为抽象成员函数和非抽象成员函数。抽象成员函数没有函数体,只能定义在抽象类或接口(详见接口章节)中。
对于 class 的成员(包括成员变量、成员属性、构造函数、成员函数),可以使用的访问修饰符有 4 种访问修饰符修饰:private、internal、protected 和 public,缺省的含义是 internal。 private 表示在 class 定义内可见。
internal 表示仅当前包及子包(包括子包的子包,详见包章节)内可见。
protected 表示当前模块(详见包章节)及当前类的子类可见。
public 表示模块内外均可见。
在类内部,我们支持 This 类型占位符,代指当前类的类型。它只能被作为实例成员函数的返回类型来使用,当使用子类对象调用在父类中定义的返回 This 类型的函数时,该函数调用的类型会被识别为子类类型,而非定义所在的父类类型。
定义了 class 类型后,即可通过调用其构造函数来创建对象(通过 class 类型名调用构造函数)。
如果类 B 继承类 A,则我们称 A 为父类,B 为子类。子类将继承父类中除 private 成员和构造函数以外的所有成员。
抽象类总是可被继承的,故抽象类定义时的 open 修饰符是可选的,也可以使用 sealed 修饰符修饰抽象类,表示该抽象类只能在本包被继承。
但非抽象的类可被继承是有条件的:定义时必须使用修饰符 open 修饰。当带 open 修饰的实例成员被 class 继承时,该 open 的修饰符也会被继承。当非 open 修饰的类中存在 open 修饰的成员时,编译器会给出告警。
可以在子类定义处通过 <: 指定其继承的父类,但要求父类必须是可继承的。
class 仅支持单继承
sealed 修饰符只能修饰抽象类,表示被修饰的类定义只能在本定义所在的包内被其他类继承。sealed 已经蕴含了 public/open 的语义,因此定义 sealed abstract class 时若提供 public/open 修饰符,编译器将会告警。sealed 的子类可以不是 sealed 类,仍可被 open/sealed 修饰,或不使用任何继承性修饰符。若 sealed 类的子类被 open 修饰,则其子类可在包外被继承。sealed 的子类可以不被 public 修饰。
子类的 init 构造函数可以使用 super(args) 的形式调用父类构造函数,或使用 this(args) 的形式调用本类其它构造函数,但两者之间只能调用一个。如果调用,必须在构造函数体内的第一个表达式处,在此之前不能有任何表达式或声明。
子类中可以覆盖(override)父类中的同名非抽象实例成员函数,即在子类中为父类中的某个实例成员函数定义新的实现。覆盖时,要求父类中的成员函数使用 open 修饰,子类中的同名函数使用 override 修饰,其中 override 是可选的。
对于被覆盖的函数,调用时将根据变量的运行时类型(由实际赋给该变量的对象决定)确定调用的版本(即所谓的动态派发)。
对于静态函数,子类中可以重定义父类中的同名非抽象静态函数,即在子类中为父类中的某个静态函数定义新的实现。重定义时,要求子类中的同名静态函数使用 redef 修饰,其中 redef 是可选的。
如果抽象函数或 open 修饰的函数有命名形参,那么实现函数或 override 修饰的函数也需要保持同样的命名形参。
接口用来定义一个抽象类型,它不包含数据,但可以定义类型的行为。一个类型如果声明实现某接口,并且实现了该接口中所有的成员,就被称为实现了该接口。
成员函数
操作符重载函数
成员属性
这些成员都是抽象的,要求实现类型必须拥有对应的成员实现。
接口使用关键字 interface 声明
因为 interface 默认具有 open 语义,所以 interface 定义时的 open 修饰符是可选的
interface 也可以使用 sealed 修饰符表示只能在 interface 定义所在的包内继承、实现或扩展该 interface。
当我们想为一个类型实现多个接口,可以在声明处使用 & 分隔多个接口,实现的接口之间没有顺序要求。
仓颉所有的类型都可以实现接口,包括数值类型、Rune、String、struct、class、enum、Tuple、函数以及其它类型。
Any 类型是一个内置的接口
仓颉中所有接口都默认继承它,所有非接口类型都默认实现它,因此所有类型都可以作为 Any 类型的子类型使用。
属性(Properties)提供了一个 getter 和一个可选的 setter 来间接获取和设置值。
使用属性的时候与普通变量无异,我们只需要对数据操作,对内部的实现无感知,可以更便利地实现访问控制、数据监控、跟踪调试、数据绑定等机制
属性可以在 interface、class、struct、enum、extend 中定义。
属性的 getter 和 setter 分别对应两个不同的函数 getter 函数类型是 () -> T,T 是该属性的类型,当使用该属性作为表达式时会执行 getter 函数。
setter 函数类型是 (T) -> Unit,T 是该属性的类型,形参名需要显式指定,当对该属性赋值时会执行 setter 函数。
和成员函数一样,成员属性也支持 open、override、redef 修饰,所以我们也可以在子类型中覆盖/重定义父类型属性的实现。
子类型覆盖父类型的属性时,如果父类型属性带有 mut 修饰符,则子类型属性也需要带有 mut 修饰符,同时也必须保持一样的类型。
类似于抽象函数,我们在 interface 和抽象类中也可以声明抽象属性,这些抽象属性没有实现。
属性分为实例成员属性和静态成员属性。成员属性的使用和成员变量的使用方式一样
无 mut 修饰符的属性类似 let 声明的变量,不可以被赋值。
带有 mut 修饰符的属性类似 var 声明的变量,可以取值也可以被赋值。
继承 class 带来的子类型关系
实现接口带来的子类型关系
元组类型的子类型关系
函数类型的子类型关系
永远成立的子类型关系
传递性带来的子类型关系
泛型类型的子类型关系
仓颉不支持不同类型之间的隐式转换(子类型天然是父类型,所以子类型到父类型的转换不是隐式类型转换),类型转换必须显式地进行。
对于数值类型(包括:Int8,Int16,Int32,Int64,IntNative,UInt8,UInt16,UInt32,UInt64,UIntNative,Float16,Float32,Float64),仓颉支持使用 T(e) 的方式得到一个值等于 e,类型为 T 的值。其中,表达式 e 的类型和 T 可以是上述任意数值类型。
Rune 到 UInt32 和整数类型到 Rune 的转换 Rune 到 UInt32 的转换使用 UInt32(e) 的方式,其中 e 是一个 Rune 类型的表达式,UInt32(e) 的结果是 e 的 Unicode scalar value 对应的 UInt32 类型的整数值。
整数类型到 Rune 的转换使用 Rune(num) 的方式,其中 num 的类型可以是任意的整数类型,且仅当 num 的值落在 [0x0000, 0xD7FF] 或 [0xE000, 0x10FFFF] (即 Unicode scalar value)中时,返回对应的 Unicode scalar value 表示的字符,否则,编译报错(编译时可确定 num 的值)或运行时抛异常。
仓颉支持使用 is 操作符来判断某个表达式的类型是否是指定的类型(或其子类型)。具体而言,对于表达式 e is T(e 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e is T 的值为 true,否则 e is T 的值为 false。
as 操作符可以用于将某个表达式的类型转换为指定的类型。因为类型转换有可能会失败,所以 as 操作返回的是一个 Option 类型。具体而言,对于表达式 e as T(e 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e as T 的值为 Option.Some(e),否则 e as T 的值为 Option.None。
在仓颉编程语言中,泛型指的是参数化类型,参数化类型是一个在声明时未知并且需要在使用时指定的类型。
类型形参:一个类型或者函数声明可能有一个或者多个需要在使用处被指定的类型,这些类型就被称为类型形参。在声明形参时,需要给定一个标识符,以便在声明体中引用。
类型变元:在声明类型形参后,当我们通过标识符来引用这些类型时,这些标识符被称为类型变元。
类型实参:当我们在使用泛型声明的类型或函数时指定了泛型参数,这些参数被称为类型实参。
类型构造器:一个需要零个、一个或者多个类型作为实参的类型称为类型构造器。
如果一个函数声明了一个或多个类型形参,则将其称为泛型函数。语法上,类型形参紧跟在函数名后,并用 <> 括起,如果有多个类型形参,则用“,”分离。
全局泛型函数
局部泛型函数
泛型成员函数
静态泛型函数
泛型接口
泛型类
泛型结构体
泛型枚举
泛型类型的子类型关系
类型别名的定义以关键字 type 开头,接着是类型的别名(如上例中的 I64),然后是等号 =,最后是原类型
只能在源文件顶层定义类型别名,并且原类型必须在别名定义处可见。
类型别名也是可以声明类型形参的,但是不能对其形参使用 where 声明约束
泛型约束的作用是在函数、class、enum、struct 声明时明确泛型形参所具备的操作与能力。
约束大致分为接口约束与子类型约束。语法为在函数、类型的声明体之前使用 where 关键字来声明,对于声明的泛型形参 T1, T2,可以使用 where T1 <: Interface, T2 <: Type 这样的方式来声明泛型约束,同一个类型变元的多个约束可以使用 & 连接。
扩展可以为在当前 package 可见的类型(除函数、元组、接口)添加新功能。
当不能破坏被扩展类型的封装性,但希望添加额外的功能时,可以使用扩展。
添加成员函数
添加操作符重载函数
添加成员属性
实现接口
扩展虽然可以添加额外的功能,但不能变更被扩展类型的封装性,因此扩展不支持以下功能 扩展不能增加成员变量。
扩展的函数和属性必须拥有实现。
扩展的函数和属性不能使用 open、override、 redef修饰。
扩展不能访问被扩展类型中 private 修饰的成员。
根据扩展有没有实现新的接口,扩展可以分为 直接扩展 和 接口扩展 两种用法,直接扩展即不包含额外接口的扩展;接口扩展即包含接口的扩展,接口扩展可以用来为现有的类型添加新功能并实现接口,增强抽象灵活性。
直接扩展
接口扩展
扩展本身不能使用修饰符修饰。
扩展成员可使用的修饰符有:static、public、protected、internal、private、mut。 使用 private 修饰的成员只能在本扩展内使用,外部不可见。
使用 internal 修饰的成员可以在当前包及子包(包括子包的子包)内使用,这是默认行为。
使用 protected 修饰的成员在本模块内可以被访问(受导出规则限制)。当被扩展类型是 class 时,该 class 的子类定义体内也能访问。
使用 static 修饰的成员,只能通过类型名访问,不能通过实例对象访问。
对 struct 类型的扩展可以定义 mut 函数。
扩展内的成员定义不支持使用 open、override、redef 修饰。
为一个其它 package 的类型实现另一个 package 的接口,可能造成理解上的困扰。
为了防止一个类型被意外实现不合适的接口,仓颉不允许定义孤儿扩展,指的是既不与接口(包含接口继承链上的所有接口)定义在同一个包中,也不与被扩展类型定义在同一个包中的接口扩展。
扩展的实例成员与类型定义处一样可以使用 this,this 的功能保持一致。同样也可以省略 this 访问成员。扩展的实例成员不能使用 super。
扩展不能访问被扩展类型中 private 修饰的成员。
扩展不能遮盖被扩展类型的任何成员。
扩展也不允许遮盖其它扩展增加的任何成员。
在同一个包内,对同一类型可以扩展多次,并且在扩展中可以直接调用被扩展类型的其他扩展中非 private 修饰的函数。
扩展也是可以被导入和导出的,但是扩展本身不能使用 public 修饰,扩展的导出有一套特殊的规则。
对于直接扩展,只有当扩展与被扩展的类型在同一个包中,并且被扩展的类型和扩展中添加的成员都使用 public 或 protected 修饰时,扩展的功能才会被导出。
除此以外的直接扩展均不能被导出,只能在当前包使用。
如果接口扩展和被扩展类型在同一个包,但接口是来自导入的,只有当被扩展类型使用 public 修饰时,扩展的功能才会被导出。
如果接口扩展与接口在同一个包,则只有当接口是使用 public 修饰时,扩展的功能才会被导出。
如果我们不需要增加和删除元素,但需要修改元素,就应该使用它。
如果我们需要频繁对元素增删查改,就应该使用它。
如果我们希望每个元素都是唯一的,就应该使用它。
如果我们希望存储一系列的映射关系,就应该使用它。
使用 ArrayList 类型需要导入 collection 包。
使用 ArrayList 表示 ArrayList 类型,T 表示 ArrayList 的元素类型,T 可以是任意类型。
ArrayList 具备非常好的扩容能力,适合于需要频繁增加和删除元素的场景
ArrayList 是引用类型,ArrayList 在作为表达式使用时不会拷贝副本,同一个 ArrayList 实例的所有引用都会共享同样的数据。
可以使用下标语法对某个位置的元素进行修改。
将单个元素添加到 ArrayList 的末尾,请使用 append 函数。如果希望同时添加多个元素到末尾,可以使用 appendAll 函数
可以通过 insert 和 insertAll 函数将指定的单个元素或相同元素类型的 Collection 值插入到我们指定索引的位置
可以使用 remove 函数,需要指定删除的索引。
当我们需要对 ArrayList 的所有元素进行访问时,可以使用 for-in 循环遍历 ArrayList 的所有元素。
当我们需要知道某个 ArrayList 包含的元素个数时,可以使用 size 属性获得对应信息。
增加 ArrayList 的大小
使用 HashSet 类型需要导入 collection 包
使用 HashSet 类型来构造只拥有不重复元素的 Collection。
仓颉使用 HashSet 表示 HashSet 类型,T 表示 HashSet 的元素类型,T 必须是实现了 Hashable 和 Equatable 接口的类型
可以使用 for-in 循环遍历 HashSet 的所有元素。
当我们需要知道某个 HashSet 包含的元素个数时,可以使用 size 属性获得对应信息。
当我们想判断某个元素是否被包含在某个 HashSet 中时,可以使用 contains 函数。
HashSet 是一种可变的引用类型,HashSet 类型提供了添加元素、删除元素的功能。
如果需要将单个元素添加到 HashSet 的末尾,请使用 put 函数。
如果希望同时添加多个元素,可以使用 putAll 函数,这个函数可以接受另一个相同元素类型的 Collection 类型(例如 Array)。
使用 HashMap 类型需要导入 collection 包
HashMap 是一种哈希表,提供对其包含的元素的快速访问。表中的每个元素都使用其键作为标识,我们可以使用键来访问相应的值。
仓颉使用 HashMap 表示 HashMap 类型,K 表示 HashMap 的键类型,K 必须是实现了 Hashable 和 Equatable 接口的类型,例如数值或 String。V 表示 HashMap 的值类型,V 可以是任意类型。
当我们需要对 HashMap 的所有元素进行访问时,可以使用 for-in 循环遍历 HashMap 的所有元素。
需要注意的是,HashMap 并不保证按插入元素的顺序排列,因此遍历的顺序和插入的顺序可能不同。
当我们需要知道某个 HashMap 包含的元素个数时,可以使用 size 属性获得对应信息。
当我们想判断某个键是否被包含 HashMap 中时,可以使用 contains 函数。如果该键存在会返回 true,否则返回 false。
当我们想访问指定键对应的元素时,可以使用下标语法访问(下标的类型必须是键类型)。
HashMap 是一种可变的引用类型,HashMap 类型提供了修改元素、添加元素、删除元素的功能。
如果需要将单个键值对添加到 HashMap 的末尾,请使用 put 函数。
如果希望同时添加多个键值对,可以使用 putAll 函数。
从 HashMap 中删除元素,可以使用 remove 函数,需要指定删除的键。
Range、Array、ArrayList 其实都是通过 Iterable 来支持 for-in 语法的。
Array、ArrayList、HashSet、HashMap 类型都实现了 Iterable,因此我们都可以将其用在 for-in 或者 while-let 中。
可以将源代码根据功能进行分组,并将不同功能的代码分开管理,每组独立管理的代码会生成一个输出文件。在使用时,通过导入对应的输出文件使用相应的功能,或者通过不同功能的交互与组合实现更加复杂的特性,使得项目管理更加高效。
在仓颉编程语言中,包是编译的最小单元,每个包可以单独输出 AST 文件、静态库文件、动态库文件等产物。每个包有自己的名字空间,在同一个包内不允许有同名的顶层定义或声明(函数重载除外)。一个包中可以包含多个源文件。
模块是若干包的集合,是第三方开发者发布的最小单元。
一个模块的程序入口只能在其根目录下,它的顶层最多只能有一个作为程序入口的 main ,该 main 没有参数或参数类型为 Array,返回类型为整数类型或 Unit 类型。
在仓颉编程语言中,包声明以关键字 package 开头,后接 root 包至当前包由 . 分隔路径上所有包的包名。包名必须是合法的普通标识符(不含原始标识符)。
包所在的文件夹名必须与包名一致。
源码根目录默认名为 src。
源码根目录下的包可以没有包声明,此时编译器将默认为其指定包名 default。
仓颉的包名需反映当前源文件相对于项目源码根目录 src 的路径,并将其中的路径分隔符替换为小数点。例如包的源代码位于 src/directory_0/directory_1 下,root 包名为 pkg 则其源代码中的包声明应为 package pkg.directory_0.directory_1。
仓颉有 4 种访问修饰符:private、internal、protected、public,在修饰顶层元素时不同访问修饰符的语义如下 private 表示仅当前文件内可见。不同的文件无法访问这类成员。
internal 表示仅当前包及子包(包括子包的子包)内可见。同一个包内可以不导入就访问这类成员,当前包的子包(包括子包的子包)内可以通过导入来访问这类成员。
protected 表示仅当前模块内可见。同一个包的文件可以不导入就访问这类成员,不同包但是在同一个模块内的其它包可以通过导入访问这些成员,不同模块的包无法访问这些成员。
public 表示模块内外均可见。同一个包的文件可以不导入就访问这类成员,其它包可以通过导入访问这些成员。
pacakge 支持使用 internal、protected、public,默认修饰符为 public。
import 支持使用全部访问修饰符,默认修饰符为 private。
其他顶层声明支持使用全部访问修饰符,默认修饰符为 internal。
在仓颉编程语言中,可以通过 import fullPackageName.itemName 的语法导入其他包中的一个顶层声明或定义,fullPackageName 为完整路径包名,itemName 为声明的名字。导入语句在源文件中的位置必须在包声明之后,其他声明或定义之前。
如果要导入的多个 itemName 同属于一个 fullPackageName,可以使用 import fullPackageName.{itemName[, itemName]*} 语法
诸如 String、Range 等类型能直接使用,并不是因为这些类型是内置类型,而是因为编译器会自动为源码隐式的导入 core 包中所有的 public 修饰的声明。
不同包的名字空间是分隔的,因此在不同的包之间可能存在同名的顶层声明。在导入不同包的同名顶层声明时,我们支持使用 import packageName.name as newName 的方式进行重命名来避免冲突。没有名字冲突的情况下仍然可以通过 import as 来重命名导入的内容。
重导出一个导入的名字
仓颉程序入口为 main,源文件根目录下的包的顶层最多只能有一个 main。
如果模块采用生成可执行文件的编译方式,编译器只在源文件根目录下的顶层查找 main。
如果没有找到,编译器将会报错;如果找到 main,编译器会进一步对其参数和返回值类型进行检查。
需要注意的是,main 不可被访问修饰符修饰,当一个包被导入时,包中定义的 main 不会被导入。
在仓颉中,异常类有 Error 和 Exception Error 类描述仓颉语言运行时,系统内部错误和资源耗尽错误,应用程序不应该抛出这种类型错误,如果出现内部错误,只能通知给用户,尽量安全终止程序。
Exception 类描述的是程序运行时的逻辑错误或者 IO 错误导致的异常,例如数组越界或者试图打开一个不存在的文件等,这类异常需要在程序中捕获处理。
由于异常是 class 类型,只需要按 class 对象的构建方式去创建异常即可。
仓颉语言提供 throw 关键字,用于抛出异常。用 throw 来抛出异常时,throw 之后的表达式必须是 Exception 的子类型(同为异常的 Error 不可以手动 throw )
throw 关键字抛出的异常需要被捕获处理。若异常没有被捕获,则由系统调用默认的异常处理函数。
不涉及资源自动管理的普通 try 表达式;
会进行资源自动管理 try-with-resources 表达式。
普通 try 表达式包括三个部分:try 块,catch 块和 finally 块。
try 块,以关键字 try 开始,后面紧跟一个由表达式与声明组成的块(用一对花括号括起来,定义了新的局部作用域,可以包含任意表达式和声明,后简称“块”),try 后面的块内可以抛出异常,并被紧随的 catch 块所捕获并处理(如果不存在 catch 块或未被捕获,则在执行完 finally 块后,该异常继续被抛出)。
catch 块,一个普通 try 表达式可以包含零个或多个 catch 块(当没有 catch 块时必须有 finally 块)。每个 catch 块以关键字 catch 开头,后跟一条 catchPattern 和一个块,catchPattern 通过模式匹配的方式匹配待捕获的异常。一旦匹配成功,则交由其后跟随的块进行处理,并且忽略它后面的其他 catch 块。当某个 catch 块可捕获的异常类型均可被定义在它前面的某个 catch 块所捕获时,会在此 catch 块处报“catch 块不可达”的 warning。
finally 块,以关键字 finally 开始,后面紧跟一个块。原则上,finally 块中主要实现一些“善后”的工作,如释放资源等,且要尽量避免在 finally 块中再抛异常。并且无论异常是否发生(即无论 try 块中是否抛出异常),finally 块内的内容都会被执行(若异常未被处理,执行完 finally 块后,继续向外抛出异常)。一个 try 表达式在包含 catch 块时可以不包含 finally 块,否则必须包含 finally 块。
Try-with-resources 表达式主要是为了自动释放非内存资源。不同于普通 try 表达式,try-with-resources 表达式中的 catch 块和 finally 块均是可选的,并且 try 关键字其后的块之间可以插入一个或者多个 ResourceSpecification 用来申请一系列的资源(ResourceSpecification 并不会影响整个 try 表达式的类型)。
需要说明的是,try-with-resources 表达式中一般没有必要再包含 catch 块和 finally 块,也不建议用户再手动释放资源。因为 try 块执行的过程中无论是否发生异常,所有申请的资源都会被自动释放,并且执行过程中产生的异常均会被向外抛出。
有时也需要所有异常做统一处理(如此处不该出现异常,出现了就统一报错),这时可以使用 CatchPattern 的通配符模式来处理。
Identifier: ExceptionClass。
Identifier: ExceptionClass_1 | ExceptionClass_2 | ... | ExceptionClass_n。此格式可以通过连接符 | 将多个异常类进行拼接,连接符 | 表示“或”的关系:可以捕获类型为 ExceptionClass_1 及其子类的异常,或者捕获类型为 ExceptionClass_2 及其子类的异常,依次类推,或捕获类型为 ExceptionClass_n 及其子类的异常(假设 n 大于 1)。
ConcurrentModificationException 并发修改产生的异常
IllegalArgumentException 传递不合法或不正确参数时抛出的异常
NegativeArraySizeException 创建大小为负的数组时抛出的异常
NoneValueException 值不存在时产生的异常,如 Map 中不存在要查找的 key
OverflowException 算术运算溢出异常
因为 Option 类型可以同时表示有值和无值两种状态,而无值在某些情况下也可以理解为一种错误,所以 Option 类型也可以用作错误处理。
因为 Option 类型是一种 enum 类型,所以可以使用上文提到的 enum 的模式匹配来实现对 Option 值的解构。
对于 ?T 类型的表达式 e1,如果希望 e1 的值等于 None 时同样返回一个 T 类型的值 e2,可以使用 ?? 操作符。对于表达式 e1 ?? e2,当 e1 的值等于 Some(v) 时返回 v 的值,否则返回 e2 的值。
? 需要和 . 或 () 或 [] 或 {}(特指尾随 lambda 调用的场景)一起使用,用以实现 Option 类型对 .,(),[] 和 {} 的支持。以 . 为例((),[] 和 {}同理),对于 ?T1 类型的表达式 e,当 e 的值等于 Some(v) 时,e?.b 的值等于 Option.Some(v.b),否则 e?.b 的值等于 Option.None,其中 T2 是 v.b 的类型。
对于 ?T 类型的表达式 e,可以通过调用 getOrThrow 函数实现解构。当 e 的值等于 Some(v) 时,getOrThrow() 返回 v 的值,否则抛出异常。
仓颉编程语言提供抢占式的线程模型作为并发编程机制。
语言线程是编程语言中并发模型的基本执行单位,语言线程的目的是屏蔽底层实现细节。例如,仓颉编程语言希望给开发者提供一个友好、高效、统一的并发编程界面,让开发者无需关心操作系统线程、用户态线程等差异,因此提供仓颉线程的概念。开发者在大多数情况下只需面向仓颉线程编写并发代码。
native 线程指语言实现中所使用到的线程(一般是操作系统线程),他们作为语言线程的具体实现载体。不同编程语言会以不同的方式实现语言线程。例如,一些编程语言直接通过操作系统调用来创建线程,这意味着每个语言线程对应一个 native 线程,这种实现方案一般被称之为 1:1 线程模型。此外,另有一些编程语言提供特殊的线程实现,他们允许多个语言线程在多个 native 线程上切换执行,这种也被称为 M:N 线程模型,即 M 个语言线程在 N 个 native 线程上调度执行,其中 M 和 N 不一定相等。当前,仓颉语言的实现同样采用 M:N 线程模型;因此,仓颉线程本质上是一种用户态的轻量级线程,支持抢占且相比操作系统线程更轻量化。
仓颉线程本质上是用户态的轻量级线程,每个仓颉线程都受到底层 native 线程的调度执行,并且多个仓颉线程可以由一个 native 线程执行。每个 native 线程会不断地选择一个就绪的仓颉线程完成执行,如果仓颉线程在执行过程中发生阻塞(例如等待互斥锁的释放),那么 native 线程会将当前的仓颉线程挂起,并继续选择下一个就绪的仓颉线程。发生阻塞的仓颉线程在重新就绪后会继续被 native 线程调度执行。
在进行跨语言编程时,开发者需要谨慎调用可能发生阻塞的 foreign 函数,例如 IO 相关的操作系统调用等。
当开发者希望并发执行某一段代码时,只需创建一个仓颉线程即可
创建一个新的仓颉线程,可以使用关键字 spawn 并传递一个无形参的 lambda 表达式,该 lambda 表达式即为在新线程中执行的代码。
通过 spawn 表达式的返回值,来等待线程执行结束。
spawn 表达式的返回类型是 Future,其中 T 是类型变元,其类型与 lambda 表达式的返回类型一致。当我们调用 Future 的 get() 成员函数时,它将等待它的线程执行完成。 get(): T:阻塞等待线程执行结束,并返回执行结果,如果该线程已经结束,则直接返回执行结果。
get(ns: Int64): Option:阻塞等待该 Future 所代表的线程执行结束,并返回执行结果,当到达超时时间 ns 时,如果该线程还没有执行结束,将会返回 Option.None。如果 ns <= 0,其行为与 get() 相同。
Future 除了可以用于阻塞等待线程执行结束以外,还可以获取线程执行的结果。
每个 Future 对象都有一个对应的仓颉线程,以 Thread 对象为表示。Thread 类主要被用于访问线程的属性信息,例如线程标识等。需要注意的是,Thread 无法直接被实例化构造对象,仅能从 Future 的 thread 成员属性获取对应的 Thread 对象,或是通过 Thread 的静态成员属性 currentThread 得到当前正在执行线程对应的 Thread 对象。
可以通过 Future 的 cancel() 方法向对应的线程发送终止请求,该方法不会停止线程执行。开发者需要使用 Thread 的 hasPendingCancellation 属性来检查线程是否存在终止请求。
一般而言,如果线程存在终止请求,那么开发者可以实施相应的线程终止逻辑。因此,如何终止线程都交由开发者自行处理,如果开发者忽略终止请求,那么线程继续执行直到正常结束。
在并发编程中,如果缺少同步机制来保护多个线程共享的变量,很容易会出现数据竞争问题(data race)。
仓颉编程语言提供三种常见的同步机制来确保数据的线程安全:原子操作、互斥锁和条件变量。
仓颉提供整数类型、Bool 类型和引用类型的原子操作。
整数类型的原子操作支持基本的读写、交换以及算术运算操作 load 读取
store 写入
swap 交换,返回交换前的值
compareAndSwap 比较再交换,交换成功返回 true,否则返回 false
fetchAdd 加法,返回执行加操作之前的值
fetchSub 减法,返回执行减操作之前的值
fetchAnd 与,返回执行与操作之前的值
fetchOr 或,返回执行或操作之前的值
fetchXor 异或,返回执行异或操作之前的值
Bool 类型和引用类型的原子操作只提供读写和交换操作 load 读取
store 写入
swap 交换,返回交换前的值
compareAndSwap 比较再交换,交换成功返回 true,否则返回 false
可重入互斥锁的作用是对临界区加以保护,使得任意时刻最多只有一个线程能够执行临界区的代码。当一个线程试图获取一个已被其他线程持有的锁时,该线程会被阻塞,直到锁被释放,该线程才会被唤醒,可重入是指线程获取该锁后可再次获得该锁。
在访问共享数据之前,必须尝试获取锁;
处理完共享数据后,必须进行解锁,以便其他线程可以获得锁。
Monitor 是一个内置的数据结构,它绑定了互斥锁和单个与之相关的条件变量(也就是等待队列)。Monitor 可以使线程阻塞并等待来自另一个线程的信号以恢复执行。这是一种利用共享变量进行线程同步的机制
调用 Monitor 对象的 wait、notify 或 notifyAll 方法前,需要确保当前线程已经持有对应的 Monitor 锁。
添加当前线程到该 Monitor 对应的等待队列中;
阻塞当前线程,同时完全释放该 Monitor 锁,并记录锁的重入次数;
等待某个其它线程使用同一个 Monitor 实例的 notify 或 notifyAll 方法向该线程发出信号;
当前线程被唤醒后,会自动尝试重新获取 Monitor 锁,且持有锁的重入状态与第 2 步记录的重入次数相同;但是如果尝试获取 Monitor 锁失败,则当前线程会阻塞在该 Monitor 锁上。
MultiConditionMonitor 是一个内置的数据结构,它绑定了互斥锁和一组与之相关的动态创建的条件变量。该类应仅当在 Monitor 类不足以满足复杂的线程间同步的场景下使用。
互斥锁 ReentrantMutex 提供了一种便利灵活的加锁的方式,同时因为它的灵活性,也可能引起忘了解锁,或者在持有互斥锁的情况下抛出异常不能自动释放持有的锁的问题。因此,仓颉编程语言提供一个 synchronized 关键字,搭配 ReentrantMutex 一起使用,可以在其后跟随的作用域内自动进行加锁解锁操作,用来解决类似的问题。
使用 core 包中的 ThreadLocal 可以创建并使用线程局部变量,每一个线程都有它独立的一个存储空间来保存这些线程局部变量,因此,在每个线程可以安全地访问他们各自的线程局部变量,而不受其他线程的影响。
sleep 函数会阻塞当前运行的线程,该线程会主动睡眠一段时间,之后再恢复执行,其参数类型为 Duration 类型。
仓颉编程语言将与应用程序外部载体交互的操作称为 I/O 操作。I 对应输入(Input),O 对应输出(Output)。
仓颉编程语言所有的 I/O 机制都是基于数据流进行输入输出,这些数据流表示了字节数据的序列。数据流是一串连续的数据集合,它就像承载数据的管道,在管道的一端输入数据,在管道的另一端就可以输出数据。
将数据从外存中读取到内存中的称为输入流(InputStream),输入端可以一段一段地向管道中写入数据,这些数据段会按先后顺序形成一个长的数据流。
将数据从内存写入外存中的称为输出流(OutputStream),输出端也可以一段一段地从管道中读出数据,每次可以读取其中的任意长度的数据(不需要跟输入端匹配),但只能读取先输入的数据,再读取后输入的数据。
仓颉编程语言将标准输入输出、文件操作、网络数据流、字符串流、加密流、压缩流等等形式的操作,统一用 Stream 描述。
Stream 主要面向处理原始二进制数据,Stream 中最小的数据单元是 Byte。
程序从输入流读取数据源(数据源包括外界的键盘、文件、网络等),即输入流是将数据源读入到程序的通信通道。
程序向输出流写入数据。输出流是将程序中的数据输出到外界(显示器、打印机、文件、网络等)的通信通道。
按照数据流职责上的差异,可以将 Stream 简单分成两类: 节点流:直接提供数据源,节点流的构造方式通常是依赖某种直接的外部资源(即文件、网络等)。
处理流:只能代理其它数据流进行处理,处理流的构造方式通常是依赖其它的流。
节点流是指直接提供数据源的流,节点流的构造方式通常是依赖某种直接的外部资源(即文件、网络等)。
仓颉编程语言中常见的节点流包含标准流(StdIn、StdOut、StdErr)、文件流(File)、网络流(Socket)等。
标准流包含了标准输入流(stdin)、标准输出流(stdout)和标准错误输出流(stderr)。
标准流是程序与外部数据交互的标准接口。程序运行的时候从输入流读取数据,作为程序的输入,程序运行过程中输出的信息被传送到输出流,类似的,错误信息被传送到错误流。
默认情况下标准输入流来源于键盘输入的信息,例如在命令行界面中输入的文本。
输出流分为标准输出流和标准错误流,默认情况下,它们都会输出到屏幕,例如在命令行界面中看到的文本。
仓颉编程语言提供了 fs 包来支持通用文件系统任务。虽然不同的操作系统对于文件系统提供的接口有所不同,但是仓颉编程语言抽象出以下一些共通的功能,通过统一的功能接口,屏蔽不同操作系统之间的差异,来简化使用。
这些常规操作任务包括:创建文件/目录、读写文件、重命名或移动文件/目录、删除文件/目录、复制文件/目录、获取文件/目录元数据、检查文件/目录是否存在。
如果要检查某个路径对应的文件是否存在,可以使用 exists 函数。当 exists 函数返回 true 时表示文件存在,反之不存在。
移动文件、拷贝文件和删除文件也非常简单,File 同样提供了对应的静态函数 move、copy、delete。
如果需要直接将文件的所有数据读出来,或者一次性将数据写入文件里,可以使用 File 提供的 readFrom、writeTo 函数直接读写文件。
File 提供了两种构造方式,一种是通过两个方便的静态函数 openRead/create 直接打开文件或创建新文件的实例,另一种是通过构造函数传入完整的打开文件选项来构造新实例。
处理流是指代理其它数据流进行处理的流。
仓颉编程语言中常见的处理流包含 BufferedInputStream、BufferedOutputStream、StringReader、StringWriter、ChainedInputStream 等。
由于涉及磁盘的 I/O 操作相比内存的 I/O 操作要慢很多,所以对于高频次且小数据量的读写操作来说,不带缓冲的数据流效率很低,每次读取和写入数据都会带来大量的 I/O 耗时。
带缓冲的数据流,可以多次读写数据,但不触发磁盘 I/O 操作,只是先放到内存里。等凑够了缓冲区大小的时候再一次性操作磁盘,这种方式可以显著减少磁盘操作次数,从而提升性能表现。
仓颉编程语言标准库提供了 BufferedInputStream 和 BufferedOutputStream 这两个类型用来提供缓冲功能。
由于仓颉编程语言的输入流和输出流是基于字节数据来抽象的(拥有更好的性能),在部分以字符串为主的场景中使用起来不太友好,例如往文件里写入大量的文本内容时,需要将文本内容转换成字节数据,再写入文件。
子主题2