仓颉语言用户指南
版本号:0.51.4
发布时间:2024-05-06
华为技术有限公司

简介

仓颉编程语言是一种面向全场景应用开发的通用编程语言,可以兼顾开发效率和运行性能,并提供良好的编程体验,主要具有如下特点:

  • 语法简明高效:仓颉编程语言提供了一系列简明高效的语法,旨在减少冗余书写、提升开发效率,例如插值字符串、主构造函数、Flow 表达式、matchif-letwhile-let 和重导出等语法,让开发者可以用较少编码表达相关逻辑。
  • 多范式编程:仓颉编程语言支持函数式、命令式和面向对象等多范式编程,融合了高阶函数、代数数据类型、模式匹配、泛型等函数式语言的先进特性,还有封装、接口、继承、子类型多态等支持模块化开发的面向对象语言特性,以及值类型、全局函数等简洁高效的命令式语言特性。开发者可以根据开发偏好或应用场景,选用不同的编程范式。
  • 类型安全:仓颉编程语言是静态强类型语言,通过编译时类型检查尽早识别程序错误,降低运行时风险,也便于代码维护。同时,仓颉编译器提供了强大的类型推断能力,可以减少类型标注工作,提高开发效率。
  • 内存安全:仓颉编程语言支持自动内存管理,并在运行时进行数组下标越界检查、溢出检查等,确保运行时内存安全。
  • 高效并发:仓颉编程语言提供了用户态轻量化线程(原生协程),以及简单易用的并发编程机制,保证并发场景的高效开发和运行。
  • 兼容语言生态:仓颉编程语言支持和 C 等主流编程语言的互操作,并采用便捷的声明式编程范式,可实现对其他语言库的高效复用和生态兼容。
  • 领域易扩展:仓颉编程语言提供了基于词法宏的元编程能力,支持在编译时变换代码,此外,还提供了尾随 lambda、属性、操作符重载、部分关键字可省略等特性,开发者可由此深度定制程序的语法和语义,有利于内嵌式领域专用语言(Embedded Domain Specific Languages,EDSL)的构建。
  • 助力 UI 开发:UI 开发是构建端侧应用的重要环节,基于仓颉编程语言的元编程和尾随 lambda 等特性,可以搭建声明式 UI 开发框架,提升 UI 开发效率和体验。
  • 助力 AI 开发:AI 是当今重要的研究与应用领域,仓颉编程语言为此提供了原生自动微分支持,可有效减少 AI 开发中数学运算相关的编码,结合元编程等能力,开发者还能快速搭建 AI 开发框架,可参考仓颉 AI 项目。
  • 内置库功能丰富:仓颉编程语言提供了功能丰富的内置库,涉及数据结构、常用算法、数学计算、正则匹配、系统交互、文件操作、网络通信、数据库访问、日志打印、解压缩、编解码、加解密和序列化等功能。

仓颉编程语言的特性较为丰富繁多,但却易于入门和上手,适合渐进式学习与实践。本手册的各章节内容如下:

  • 第一章:仓颉编程语言简介。
  • 第二章:介绍如何安装仓颉编译器工具链,并指导读者编译运行第一个仓颉程序。
  • 第三章:介绍仓颉编程语言的基本概念,包括变量、表达式、作用域和程序基本结构等。
  • 第四章:介绍基础数据类型,包括整数、浮点数、字符串、元组和区间等类型。
  • 第五章:介绍自定义类型,包括 enum 类型、 struct 类型、class 类型和 interface 类型,以及子类型关系、类型转换和类型别名。
  • 第六章:介绍常用的 Collection 数据类型,包括 ArrayArrayListHashSetHashMap 等。
  • 第七章:介绍错误处理,包括异常类和 try 表达式,以及 Option 类型在错误处理中的应用。
  • 第八章:介绍函数,包括函数的基本概念、函数作为一等公民的相关特性、lambda 表达式、闭包、函数调用语法糖、函数重载、操作符重载和 mut 函数等。
  • 第九章:介绍模式匹配,包括 match 表达式和六种基本模式,以及模式的 Refutability 属性。
  • 第十章:介绍泛型编程,包括泛型参数、自定义泛型类型、泛型接口、泛型函数和泛型约束等。
  • 第十一章:介绍属性,包括属性的定义和使用。
  • 第十二章:介绍扩展,包括扩展的定义、扩展的孤儿规则、扩展的访问与遮盖、扩展的导入导出。
  • 第十三章:介绍并发,包括仓颉线程的创建和执行,以及三种常用的同步机制(原子操作、互斥锁和条件变量)。
  • 第十四章:介绍元编程,包括 Tokens 相关类型、quote 表达式和仓颉宏的相关语法,并结合实例介绍仓颉宏的编译和使用方式。
  • 第十五章:介绍自动微分,包括可微类型和不可微类型,以及可微函数和微分表达式等。
  • 第十六章:介绍包管理,包括包与模块的概念,以及包的声明与导入方式等。
  • 第十七章:介绍跨语言互操作,目前只涉及仓颉与 C 语言的互操作,包括类型映射、声明与调用 C 函数、CType 类型约束、CFunc 类型函数和 inout 引用传参等特性。
  • 第十八章:介绍仓颉编译器 cjc 的使用方法及编译选项。
  • 第十九章:介绍仓颉运行时相关的环境变量。
  • 第二十章:介绍条件编译,包括如何使用内置条件和自定义条件等。
  • 第二十一章:介绍常量求值,即仓颉允许某些特定形式的表达式在编译时求值,可以提高程序运行时效率。
  • 第二十二章:介绍注解,即仓颉针对某些特殊场景给开发者提供的内置属性宏。
  • 第二十三章:介绍仓颉动态特性,包括反射和动态加载特性。
  • 第二十四章:本章为附录,介绍仓颉中的关键字、操作符和 TokenKind 类型等。

入门指南

跟随本章的指导,您将学会如何安装仓颉工具链,并尝试编译运行第一个仓颉程序。

安装仓颉工具链

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

目前仓颉工具链已适配部分版本的 Linux 和 Windows 平台,但是仅针对部分 Linux 发行版做了完整功能测试,详情可参阅附录“Linux 版本工具链的支持与安装”章节,在暂未进行过完整功能测试的平台上,仓颉工具链的功能完整性不受到保证。此外,当前 Windows 平台上的仓颉编译器基于 MinGW 实现,相较于 Linux 版本的仓颉编译器,功能会有部分欠缺,二者的具体差异请见版本 Release Note。

Linux

环境准备

Linux

Linux 版仓颉工具链的系统环境要求如下:

架构环境要求
x86_64glibc 2.22,Linux Kernel 4.12 或更高版本,系统安装 libstdc++ 6.0.24 或更高版本
aarch64glibc 2.27,Linux Kernel 4.15 或更高版本,系统安装 libstdc++ 6.0.24 或更高版本

除此之外,对于不同的 Linux 发行版,还需要安装相应的依赖软件包:

Ubuntu 18.04

$ apt-get install binutils libc-dev libc++-dev libgcc-7-dev

EulerOS R11

$ yum install binutils glibc-devel gcc

此外,仓颉工具链还依赖 OpenSSL 3 组件,由于该组件可能无法从以上发行版的默认软件源直接安装,因此你需要自行手动安装,安装方式请参考附录[编译安装依赖工具]小节。

其他 Linux 发行版

您可以在附录“Linux 版本工具链的支持与安装”章节找到更多 Linux 发行版的依赖安装命令。

安装指导

首先请前往仓颉官方发布渠道,下载适配您平台架构的安装包:

  • Cangjie-x.y.z-linux_x64.tar.gz:适用于 x86_64 架构 Linux 系统的仓颉工具链
  • Cangjie-x.y.z-linux_aarch64.tar.gz:适用于 aarch64 架构 Linux 系统的仓颉工具链

假设这里选择了 Cangjie-x.y.z-linux_x64.tar.gz,下载到本地后,请执行如下命令解压:

tar xvf Cangjie-x.y.z-linux_x64.tar.gz

解压完成,您会在当前工作路径下看到一个名为 cangjie 的目录,其中存放了仓颉工具链的全部内容,请执行如下命令完成仓颉工具链的安装配置:

source cangjie/envsetup.sh

为了验证是否安装成功,可以执行如下命令:

cjc -v

其中 cjc 是仓颉编译器的可执行文件名,如果您在命令行中看到了仓颉编译器版本信息,那么恭喜您,已经成功安装了仓颉工具链。值得说明的是,envsetup.sh 脚本只是在当前 shell 环境中配置了工具链相关的环境变量,所以仓颉工具链仅在当前 shell 环境中可用,在新的 shell 环境中,您需要重新执行 envsetup.sh 脚本配置环境。

卸载与更新

在 Linux 平台,删除上述仓颉工具链的安装包目录,同时移除上述环境变量(最简单的,您可以新开一个 shell 环境),即可完成卸载。

$ rm -rf <path>/<to>/cangjie

若需要更新仓颉工具链,您需要先卸载当前版本,然后按上述指导重新安装最新版本的仓颉工具链。

Windows

本节以 Windows 10 平台为例,介绍仓颉工具链的安装方式。

安装指导

在 Windows 平台上,我们为开发者提供了 exezip 两种格式的安装包,请前往仓颉官方发布渠道,选择和下载适配您平台架构的 Windows 版安装包。

如果您选择了 exe 格式的安装包(例如 Cangjie-x.y.z-windows_x64.exe),请直接执行此文件,跟随安装向导点击操作,即可完成安装。

如果您选择了 zip 格式的安装包(例如 Cangjie-x.y.z-windows_x64.zip),请将它解压到适当目录,在安装包中,我们为开发者提供了三种不同格式的安装脚本,分别是 envsetup.batenvsetup.ps1envsetup.sh,您可以根据使用习惯及环境配置,选择一种执行:

若使用 Windows 命令提示符(CMD)环境,请执行:

path\to\cangjie\envsetup.bat

若使用 PowerShell 环境,请执行:

. path\to\cangjie\envsetup.ps1

若使用 MSYS shell、bash 等环境,请执行:

source path/to/cangjie/envsetup.sh

为了验证是否安装成功,请在以上命令环境中继续执行 cjc -v 命令,如果输出了仓颉编译器版本信息,那么恭喜您,已经成功安装了仓颉工具链。

注意:基于 zip 安装包和执行脚本的安装方式,类似于 Linux 平台,即 envsetup 脚本所配置的环境变量,只在当前命令行环境中有效,如果打开新的命令行窗口,需要重新执行 envsetup 脚本配置环境。

卸载与更新

运行仓颉安装目录下的 unins000.exe 可执行文件,跟随卸载向导点击操作,即可完成卸载。

若需要更新仓颉工具链,您需要先卸载当前版本,然后按上述指导重新安装最新版本的仓颉工具链。

运行第一个仓颉程序

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

首先,请在适当目录下新建一个名为 hello.cj 的文本文件,并向文件中写入以下仓颉代码:

// hello.cj
main() {
    println("你好,仓颉")
}

在这段代码中,使用了仓颉的注释语法,您可以在 // 符号之后写单行注释,也可以在一对 /**/ 符号之间写多行注释,这与 C/C++ 等语言的注释语法相同。注释内容不影响程序的编译和运行。

然后,请在此目录下执行如下命令:

cjc hello.cj -o hello

这里仓颉编译器会将 hello.cj 中的源代码编译为此平台上的可执行文件 hello,在命令行环境中运行此文件,您将看到程序输出了如下内容:

你好,仓颉

以上编译命令是针对 Linux 平台的,如果您使用 Windows 平台,只需要将编译命令改为 cjc hello.cj -o 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

变量

在仓颉编程语言中,一个变量由对应的变量名、数据(值)和若干属性构成,开发者通过变量名访问变量对应的数据,但访问操作需要遵从相关属性的约束(如数据类型、可变性和可见性等)。当程序被编译运行时,一个变量可以具化为一份可操作的存储空间。

因此,在定义一个仓颉变量时,就需要指定变量名、初始值和相关属性。变量定义的具体形式为:

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

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

  • 可变性修饰符:letvar,分别对应不可变和可变属性,可变性决定了变量被初始化后其值还能否改变,仓颉变量也由此分为不可变变量和可变变量两类。
  • 可见性修饰符:privatepublic 等,影响全局变量和成员变量的可引用范围,详见后续章节的相关介绍。
  • 静态性修饰符:static,影响成员变量的存储和引用方式,详见后续章节的相关介绍。

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

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

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

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

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

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
}

当初始值具有明确类型时,可以省略变量类型标注,例如:

main() {
    let a: Int64 = 2023
    let b = a
    println("a - b = ${a - b}")
}

其中变量 b 的类型可以由其初值 a 的类型自动推断为 Int64,所以此程序也可以被正常编译和运行,将输出:

a - b = 0

在定义局部变量时,可以不进行初始化,但一定要在变量被引用前赋予初值,例如:

main() {
    let text: String
    text = "仓颉造字"
    println(text)
}

编译运行此程序,将输出:

仓颉造字

在定义全局变量和静态成员变量时必须初始化,否则编译会报错,例如:

// example.cj
let global: Int64 // error: variable in top-level scope must be initialized
// example.cj
class Player {
    static let score: Int32 // error: static variable 'score' needs to be initialized when declaring
}

值类型和引用类型变量

程序在运行阶段,只有指令流转和数据变换,仓颉程序中的各种标识符已不复存在。由此可见,编译器使用了一些机制,将这些名字和编程所取用的数据实体/存储空间绑定起来。

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

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

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

  1. 在给值类型变量赋值时,一般会产生拷贝操作,且原来绑定的数据/存储空间被覆写。在给引用类型变量赋值时,只是改变了引用关系,原来绑定的数据/存储空间不会被覆写。
  2. let 定义的变量,要求变量被初始化后都不能再赋值。对于引用类型,这只是限定了引用关系不可改变,但是所引用的数据是可以被修改的。

在仓颉编程语言中,基础数据类型和 struct 等类型属于值类型,而 classArray 等类型属于引用类型。

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

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

由此可以看出,对于值类型的 Copy 类型变量,在赋值时总是获取 Copy 实例的拷贝,如 c2 = c1,随后对 c2 成员的修改并不影响 c1。对于引用类型的 Share 类型变量,在赋值时将建立变量和实例之间的引用关系,如 s2 = s1,随后对 s2 成员的修改会影响 s1

如果将以上程序中的 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
}

表达式

在一些传统编程语言中,一个表达式由一个或多个操作数(operand)通过零个或多个操作符(operator)组合而成,表达式总是隐含着一个计算过程,因此每个表达式都会有一个计算结果,对于只有操作数而没有操作符的表达式,其计算结果就是操作数自身,对于包含操作符的表达式,计算结果是对操作数执行操作符定义的计算而得到的值。在这种定义下的表达式也被称为算术运算表达式。

在仓颉编程语言中,我们简化并延伸了表达式的传统定义——凡是可求值的语言元素都是表达式。因此,仓颉不仅有传统的算术运算表达式,还有条件表达式、循环表达式和 try 表达式等,它们都可以被求值,并作为值去使用,如作为变量定义的初值和函数实参等。此外,因为仓颉是强类型的编程语言,所以仓颉表达式不仅可求值,还有确定的类型。

仓颉编程语言的各种表达式将在后续章节中逐一介绍,本节只介绍最常用的条件表达式和循环表达式。

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

在仓颉编程语言中,条件表达式分为 if 表达式和 if-let 表达式两种,它们的值与类型需要根据使用场景来确定。循环表达式有四种:for-in 表达式、while 表达式、do-while 表达式和 while-let 表达式,它们的类型都是 Unit、值为 ()。其中 if-let 表达式和 while-let 表达式都与模式匹配相关,请参见模式匹配章节,本节只介绍以上提及的其他几种表达式。

在仓颉程序中,由一对大括号“{}”包围起来的一组表达式,被称为“代码块”,它将作为程序的一个顺序执行流,其中的表达式将按编码顺序依次执行。如果代码块中有至少一个表达式,我们规定此代码块的值与类型等于其中最后一个表达式的值与类型,如果代码块中没有表达式,规定这种空代码块的类型为 Unit、值为 ()。请注意,代码块本身不是一个表达式,不能被单独使用,它将依附于函数、条件表达式和循环表达式等执行和求值。

if 表达式

if 表达式的基本形式为:

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

其中“条件”是布尔类型表达式,“分支 1”和“分支 2”是两个代码块。if 表达式将按如下规则执行:

  1. 计算“条件”表达式,如果值为 true 则转到第 2 步,值为 false 则转到第 3 步。
  2. 执行“分支 1”,转到第 4 步。
  3. 执行“分支 2”,转到第 4 步。
  4. 继续执行 if 表达式后面的代码。

在一些场景中,我们可能只关注条件成立时该做些什么,所以 else 和对应的代码块是允许省略的。

如下程序演示了 if 表达式的基本用法:

from std import random.*

main() {
    let number: Int8 = Random().nextInt8()
    println(number)
    if (number % 2 == 0) {
        println("偶数")
    } else {
        println("奇数")
    }
}

在这段程序中,我们使用仓颉标准库的 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("脚踏实地,仰望星空")
    }
}

if 表达式的值与类型,需要根据使用形式与场景来确定:

  1. 当含 else 分支的 if 表达式被求值时,需要根据求值上下文确定 if 表达式的类型:

    • 如果上下文明确要求值类型为 T,则 if 表达式各分支代码块的类型必须是 T 的子类型,这时 if 表达式的类型被确定为 T,如果不满足子类型约束,编译会报错。

    • 如果上下文没有明确的类型要求,则 if 表达式的类型是其各分支代码块类型的最小公共父类型,如果最小公共父类型不存在,编译会报错。

    如果编译通过,则 if 表达式的值就是所执行分支代码块的值。

  2. 如果含 else 分支的 if 表达式没有被求值,在这种场景里,开发者一般只想在不同分支里做不同操作,不会关注各分支最后一个表达式的值与类型,为了不让上述类型检查规则影响这一思维习惯,仓颉规定这种场景下的 if 表达式类型为 Unit、值为 (),且各分支不参与上述类型检查。

  3. 对于不含 else 分支的 if 表达式,由于 if 分支也可能不被执行,所以我们规定这类 if 表达式的类型为 Unit、值为 ()

例如,以下程序基于 if 表达式求值,模拟一次简单的模数转换过程:

main() {
    let zero: Int8 = 0
    let one: Int8 = 1
    let voltage = 5.0
    let bit = if (voltage < 2.5) {
        zero
    } else {
        one
    }
}

在以上程序中,if 表达式作为变量定义的初值使用,由于变量 bit 没有被标注类型、需要从初值中推导,所以 if 表达式的类型取为两个分支代码块类型的最小公共父类型,根据前文对“代码块”的介绍,可知两个分支代码块类型都是 Int8,所以 if 表达式的类型被确定为 Int8,其值为所执行分支即 else 分支代码块的值,所以变量 bit 的类型为 Int8、值为 1。

while 表达式

while 表达式的基本形式为:

while (条件) {
  循环体
}

其中“条件”是布尔类型表达式,“循环体”是一个代码块。while 表达式将按如下规则执行:

  1. 计算“条件”表达式,如果值为 true 则转第 2 步,值为 false 转第 3 步。
  2. 执行“循环体”,转第 1 步。
  3. 结束循环,继续执行 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 表达式的基本形式为:

do {
  循环体
} while (条件)

其中“条件”是布尔类型表达式,“循环体”是一个代码块。do-while 表达式将按如下规则执行:

  1. 执行“循环体”,转第 2 步。
  2. 计算“条件”表达式,如果值为 true 则转第 1 步,值为 false 转第 3 步。
  3. 结束循环,继续执行 do-while 表达式后面的代码。

例如,以下程序使用 do-while 表达式,基于蒙特卡洛算法,近似计算圆周率的值:

from std import random.*

main() {
    let random = Random()
    var totalPoints = 0
    var hitPoints = 0

    do {
        // 在 ((0, 0), (1, 1)) 这个正方形中随机取点
        let x = random.nextFloat64()
        let y = random.nextFloat64()
        // 判断是否落在正方形内接圆里
        if ((x - 0.5) ** 2 + (y - 0.5) ** 2 < 0.25) {
            hitPoints++
        }
        totalPoints++
    } while (totalPoints < 1000000)

    let pi = 4.0 * Float64(hitPoints) / Float64(totalPoints)
    println("圆周率近似值为:${pi}")
}

运行以上程序,将输出:

圆周率近似值为:3.141872

由于算法涉及随机数,所以每次运行程序输出的数值可能都不同,但都会约等于 3.14。

for-in 表达式

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

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

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

  1. 计算“序列”表达式,将其值作为遍历对象,并初始化遍历对象的迭代器。
  2. 更新迭代器,如果迭代器终止,转第 4 步,否则转第 3 步。
  3. 将当前迭代器指向的数据与“迭代变量”绑定,并执行“循环体”,转第 2 步。
  4. 结束循环,继续执行 for-in 表达式后面的代码。

仓颉内置的区间和数组等类型已经扩展了 Iterable<T> 接口。

例如,以下程序使用 for-in 表达式,遍历中国地支字符构成的数组 noumenonArray,输出农历 2024 年各月的干支纪法:

main() {
    let metaArray = ['甲', '乙', '丙', '丁', '戊',
        '己', '庚', '辛', '壬', '癸']
    let noumenonArray = ['寅', '卯', '辰', '巳', '午', '未',
        '申', '酉', '戌', '亥', '子', '丑']
    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
    }
}

运行以上程序,将输出:

农历 2024 年各月干支:
丙寅 丁卯 戊辰 己巳 庚午 辛未 壬申 癸酉 甲戌 乙亥 丙子 丁丑

遍历区间

for-in 表达式可以遍历区间类型实例,例如:

main() {
    var sum = 0
    for (i in 1..=100) {
        sum += i
    }
    println(sum)
}

运行以上程序,将输出:

5050

关于区间类型的详细内容,请参阅“基本数据类型”章节。

遍历元组构成的序列

如果一个序列的元素是元组类型,则使用 for-in 表达式遍历时,“迭代变量”可以写成元组形式,以此实现对序列元素的解构,例如:

main() {
    let array = [(1, 2), (3, 4), (5, 6)]
    for ((x, y) in array) {
        println("${x}, ${y}")
    }
}

运行以上程序,将输出:

1, 2
3, 4
5, 6

迭代变量不可修改

for-in 表达式的循环体中,不能修改迭代变量,例如以下程序在编译时会报错:

main() {
    for (i in 0..5) {
        i = i * 10 // error: cannot assign to value which is an initialized 'let' constant
        println(i)
    }
}

使用通配符_代替迭代变量

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

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

运行以上程序,将输出:

4294967296

在这种场景下,如果您使用普通的标识符定义迭代变量,编译会输出“unused variable”告警,使用通配符 _ 则可以避免这一告警。

where 条件

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

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

运行以上程序,将输出:

1
3
5
7

break 与 continue 表达式

在循环结构的程序中,有时我们需要根据特定条件提前结束循环或跳过本轮循环,为此仓颉引入了 breakcontinue 表达式,它们可以出现在循环表达式的循环体中,break 用于终止当前循环表达式的执行、转去执行循环表达式之后的代码,continue 用于提前结束本轮循环、进入下一轮循环。breakcontinue 表达式的类型都是 Nothing

例如,以下程序使用 for-in 表达式和 break 表达式,在给定的整数数组中,找到第一个能被 5 整除的数字:

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

for-in 迭代至 numbers 数组的第三个数 25 时,由于 25 可以被 5 整除,所以将执行 if 分支中的 printlnbreakbreak 将终止 for-in 循环,numbers中的后续数字不会被遍历到,因此运行以上程序,将输出:

25

以下程序使用 for-in 表达式和 continue 表达式,将给定整数数组中的奇数打印出来:

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

在循环迭代中,当 number 是偶数时,continue 将被执行,这会提前结束本轮循环、进入下一轮循环,println 不会被执行,因此运行以上程序,将输出:

25
49
55

作用域

在前文中,我们初步介绍了如何给仓颉程序元素命名,实际上,除了变量,我们还可以给函数和自定义类型等命名,在程序中将使用这些名字访问对应的程序元素。

但在实际应用中,需要考虑一些特殊情况:

  • 当程序规模较大时,那些简短的名字很容易重复,即产生命名冲突。
  • 结合运行时考虑,在有些代码片段中,另一些程序元素是无效的,对它们的引用会导致运行时错误。
  • 在某些逻辑构造中,为了表达元素之间的包含关系,不应通过名字直接访问子元素,而是要通过其父元素名间接访问。

为了应对这些问题,现代编程语言引入了“作用域”的概念及设计,将名字和程序元素的绑定关系限制在一定范围里。不同作用域之间可以是并列或无关的,也可以是嵌套或包含关系。一个作用域将明确我们能用哪些名字访问哪些程序元素,具体规则是:

  1. 当前作用域中定义的程序元素与名字的绑定关系,在当前作用域和其内层作用域中是有效的,可以通过此名字直接访问对应的程序元素。
  2. 内层作用域中定义的程序元素与名字的绑定关系,在外层作用域中无效。
  3. 内层作用域可以使用外层作用域中的名字重新定义绑定关系,根据规则 1,此时内层作用域中的命名相当于遮盖了外层作用域中的同名定义,对此我们称内层作用域的级别比外层作用域的级别高。

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

用大括号“{}”包围代码构造作用域时,其中不限于使用表达式,还可以定义函数和自定义类型等,这不同于前文中提到的“代码块”概念,当然“代码块”也是一个作用域。

例如在以下名为 test.cj 的仓颉源文件里,在顶层作用域中定义了名字 element,它和字符串“仓颉”绑定,而 mainif 引导的代码块中也定义了名字 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

程序结构

通常,我们都会在扩展名为 .cj 的文本文件中编写仓颉程序,这些程序和文件也被称为源代码和源文件,在程序开发的最后阶段,这些源代码将被编译为特定格式的二进制文件。

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

定义main函数时,不需要写 func 修饰符。此外,如果需要获取程序启动时的命令行参数,可以声明和使用 Array<String> 类型参数。

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

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

main() {
    println(a)
}

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

enuminterface 中仅支持定义成员函数。

例如在以下程序中,我们在顶层作用域定义了全局函数 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

基本数据类型

本章介绍仓颉中的基本数据类型以及它们支持的基本操作,包括:整数类型、浮点类型、布尔类型、字符类型、字符串类型、Unit 类型、元组类型、区间类型、Nothing 类型。

整数类型

整数类型分为有符号(signed)整数类型和无符号(unsigned)整数类型。

有符号整数类型包括 Int8Int16Int32Int64IntNative,分别用于表示编码长度为 8-bit16-bit32-bit64-bit 和平台相关大小的有符号整数值的类型。

无符号整数类型包括 UInt8UInt16UInt32UInt64UIntNative,分别用于表示编码长度为 8-bit16-bit32-bit64-bit 和平台相关大小的无符号整数值的类型。

对于编码长度为 N 的有符号整数类型,其表示范围为:$$-2^{N-1} \sim 2^{N-1}-1$$;对于编码长度为 N 的无符号整数类型,其表示范围为:$$0 \sim 2^{N}-1$$。下表列出了所有整数类型的表示范围:

类型表示范围
Int8$$-2^7 \sim 2^7-1 (-128 \sim 127)$$
Int16$$-2^{15} \sim 2^{15}-1 (-32,768 \sim 32,767)$$
Int32$$-2^{31} \sim 2^{31}-1 (-2,147,483,648 \sim 2,147,483,647)$$
Int64$$-2^{63} \sim 2^{63}-1 (-9,223,372,036,854,775,808 \sim 9,223,372,036,854,775,807)$$
IntNativeplatform dependent
UInt8$$0 \sim 2^8-1 (0 \sim 255)$$
UInt16$$0 \sim 2^{16}-1 (0 \sim 65,535)$$
UInt32$$0 \sim 2^{32}-1 (0 \sim 4,294,967,295)$$
UInt64$$0 \sim 2^{64}-1 (0 \sim 18,446,744,073,709,551,615)$$
UIntNativeplatform dependent

程序具体使用哪种整数类型,取决于该程序中需要处理的整数的性质和范围。在 Int64 类型适合的情况下,首选 Int64 类型,因为 Int64 的表示范围足够大,并且整数字面量(在下一节介绍)在没有类型上下文的情况下默认推断为 Int64 类型,可以避免不必要的类型转换。

整数类型字面量

整数类型字面量有 4 种进制表示形式:二进制(使用 0b0B 前缀)、八进制(使用 0o0O 前缀)、十进制(没有前缀)、十六进制(使用 0x0X 前缀)。例如,对于十进制数 24,表示成二进制是 0b00011000(或 0B00011000),表示成八进制是 0o30(或 0O30),表示成十六进制是 0x18(或 0X18)。

在各进制表示中,可以使用下划线 _ 充当分隔符的作用,方便识别数值的位数,如 0b0001_1000

对于整数类型字面量,如果它的值超出了上下文要求的整数类型的表示范围,编译器将会报错。

let x: Int8 = 128          // Error: 128 out of the range of Int8
let y: UInt8 = 256         // Error: 256 out of the range of UInt8
let z: Int32 = 0x8000_0000 // Error: 0x8000_0000 out of the range of Int32

在使用整数类型字面量时,可以通过加入后缀来明确整数字面量的类型,后缀与类型的对应为:

后缀类型后缀类型
i8Int8u8UInt8
i16Int16u16UInt16
i32Int32u32UInt32
i64Int64u64UInt64

加入了后缀的整数字面量可以像下面的方式来使用:

var x = 100i8  // x is 100 with type Int8
var y = 0x10u64 // y is 16 with type UInt64
var z = 0o432i32  // z is 282 with type Int32

整数类型支持的操作

整数类型默认支持的操作符包括:算术操作符、位操作符、关系操作符、自增和自减操作符、赋值操作符、复合赋值操作符。各操作符的优先级和用法参见附录中的[操作符]。

  1. 算术操作符包括:一元负号(-)、加法(+)、减法(-)、乘法(*)、除法(/)、取模(%)、幂运算(**)。

    注意:

    • 除了一元负号(-)和幂运算(**),其他操作符要求左右操作数是相同的类型。

    • */+- 的操作数可以是整数类型或浮点类型。

    • % 的操作数只支持整数类型。

    • ** 的左操作数只能为 Int64 类型或 Float64 类型,并且:

      • 当左操作数类型为 Int64 时,右操作数只能为 UInt64 类型,表达式的类型为 Int64
      • 当左操作数类型为 Float64 时,右操作数只能为 Int64 类型或 Float64 类型,表达式的类型为 Float64

    幂运算的使用,见如下示例:

    let p1 = 2 ** 3               // p1 = 8
    let p2 = 2 ** UInt64(3 ** 2)  // p2 = 512
    let p3 = 2.0 ** 3.0           // p3 = 8.0
    let p4 = 2.0 ** 3 ** 2        // p4 = 512.0
    let p5 = 2.0 ** 3.0           // p5 = 8.0
    let p6 = 2.0 ** 3.0 ** 2.0    // p6 = 512.0
    
  2. 位操作符包括:按位求反(!)、左移(<<)、右移(>>)、按位与(&)、按位异或(^)、按位或(|)。注意,按位与、按位异或和按位或操作符要求左右操作数是相同的整数类型。

  3. 关系操作符包括:小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、相等(==)、不等(!=)。要求关系操作符的左右操作数是相同的整数类型。

  4. 自增和自减操作符包括:自增(++)和自减(--)。注意,仓颉中的自增和自减操作符只能作为一元后缀操作符使用。

  5. 赋值操作符即 =,复合赋值操作符包括:+=-=*=/=%=**=<<=>>=&=^=|=

注:本章中我们所提及的某个类型支持的操作,均是指在没有[操作符重载]的前提下。

字符字节字面量

仓颉编程语言引入了字符字节字面量来更为方便地用 ASCII 码来表示 UInt8 类型的值,由字符 b 和被单引号引用的值组成。例如:

var a = b'x' // a is 120 with type UInt8
var b = b'\n' // b is 10 with type UInt8
var c = b'\u{78}' // c is 120 with type UInt8

对于 \u 转义的字符不支持 Unicode,所以内部最多有两位 16 进制数。

浮点类型

浮点类型包括 Float16Float32Float64,分别用于表示编码长度为 16-bit32-bit64-bit 的浮点数(带小数部分的数字,如 3.14159、8.24 和 0.1 等)的类型。Float16Float32Float64 分别对应 IEEE 754 中的半精度格式(即 binary16)、单精度格式(即 binary32)和双精度格式(即 binary64)。

Float64 的精度约为小数点后 15 位,Float32 的精度约为小数点后 6 位,Float16 的精度约为小数点后 3 位。使用哪种浮点类型,取决于代码中需要处理的浮点数的性质和范围。在多种浮点类型都适合的情况下,首选精度高的浮点类型,因为精度低的浮点类型的累计计算误差很容易扩散,并且它能精确表示的整数范围也很有限。

浮点类型字面量

浮点类型字面量有两种进制表示形式:十进制、十六进制。在十进制表示中,一个浮点字面量至少要包含一个整数部分或一个小数部分,没有小数部分时必须包含指数部分(以 eE 为前缀,底数为 10)。在十六进制表示中,一个浮点字面量除了至少要包含一个整数部分或小数部分(以 0x0X 为前缀),同时必须包含指数部分(以 pP 为前缀,底数为 2)。

下面的例子展示了浮点字面量的使用:

let a: Float32 = 3.14
let b: Float32 = 2e3
let c: Float32 = 2.4e-1
let d: Float64 = .123e2
let e: Float64 = 0x1.1p0
let f: Float64 = 0x1p2
let g: Float64 = 0x.2p4

在使用十进制浮点数字面量时,可以通过加入后缀来明确浮点数字面量的类型,后缀与类型的对应为:

后缀类型
f16Float16
f32Float32
f64Float64

加入了后缀的浮点数字面量可以像下面的方式来使用:

let a = 3.14f32   // a is 3.14 with type Float32
let b = 2e3f32    // b is 2e3 with type Float32
let c = 2.4e-1f64 // c is 2.4e-1 with type Float64
let d = .123e2f64 // d is .123e2 with type Float64

浮点类型支持的操作

浮点类型默认支持的操作符包括:算术操作符、关系操作符、赋值操作符、复合赋值操作符。浮点类型不支持自增和自减操作符。

布尔类型

布尔类型使用 Bool 表示,用来表示逻辑中的真和假。

布尔类型字面量

布尔类型只有两个字面量:truefalse

下面的例子展示了布尔字面量的使用:

let a: Bool = true
let b: Bool = false

布尔类型支持的操作

布尔类型支持的操作符包括:逻辑操作符(逻辑非 !,逻辑与 &&,逻辑或 ||)、部分关系操作符(==!=)、赋值操作符、部分复合赋值操作符(&&=||=)。

字符类型

字符类型使用 Char 表示,可以表示 Unicode 字符集中的所有字符。

当前,仓颉已经引入了 RuneRuneChar 的 类型别名,定义为 type Rune = Char。

Rune 的语义与 Char 相同,都是 Unicode Scalar Value。目前 RuneChar 短期共存,但是将来 Char 将会被删除,建议需要使用字符类型的地方使用 Rune

字符类型字面量

字符类型字面量有三种形式:单个字符、转义字符和通用字符,它们均使用一对单引号定义。

单个字符的字符字面量举例:

let a: Char = 'a'
let b: Char = 'b'

转义字符是指在一个字符序列中对后面的字符进行另一种解释的字符。转义字符使用转义符号 \ 开头,后面加需要转义的字符。举例如下:

let slash: Char = '\\'
let newLine: Char = '\n'
let tab: Char = '\t'

通用字符以 \u 开头,后面加上定义在一对花括号中的 1~8 个十六进制数,即可表示对应的 Unicode 值代表的字符。举例如下:

main() {
    let he: Char = '\u{4f60}'
    let llo: Char = '\u{597d}'
    print(he)
    print(llo)
}

编译并执行上述代码,输出结果为:

你好

字符类型支持的操作

字符类型仅支持关系操作符:小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、相等(==)、不等(!=)。比较的是字符的 Unicode 值。

字符串类型

字符串类型使用 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,
    Cangjie
    Lang"###

插值字符串

插值字符串是一种包含一个或多个插值表达式的字符串字面量(不适用于多行原始字符串字面量),通过将表达式插入到字符串中,可以有效避免字符串拼接的问题。虽然我们直到现在才介绍它,但其实它早已经出现在之前的示例代码中,因为我们经常在 println 函数中输出非字符串类型的变量值,例如 println("${x}")

插值表达式必须用花括号 {} 包起来,并在 {} 之前加上 $ 前缀。{} 中可以包含一个或者多个声明或表达式。

当插值字符串求值时,每个插值表达式所在位置会被 {} 中的最后一项的值替换,整个插值字符串最终仍是一个字符串。

下面是插值字符串的简单示例:

main() {
    let fruit = "apples"
    let count = 10
    let s = "There are ${count * count} ${fruit}"
    println(s)

    let r = 2.4
    let area = "The area of a circle with radius ${r} is ${let PI = 3.141592; PI * r * r}"
    println(area)
}

编译并执行上述代码,输出结果为:

There are 100 apples
The area of a circle with radius 2.400000 is 18.095570

字符串类型支持的操作

字符串类型支持使用关系操作符进行比较,支持使用 + 进行拼接。下面的例子展示了字符串类型的判等和拼接:

main() {
    let s1 = "abc"
    var s2 = "ABC"
    let r1 = s1 == s2
    println("The result of 'abc' == 'ABC' is: ${r1}")
    let r2 = s1 + s2
    println("The result of 'abc' + 'ABC' is: ${r2}")
}

编译并执行上述代码,输出结果为:

The result of 'abc' == 'ABC' is: false
The result of 'abc' + 'ABC' is: abcABC

字符串还支持其他常见操作,例如拆分、替换等,可参见标准库文档。

字节数组字面量

仓颉编程语言引入了字节数组字面量来方便地表达 Array<UInt8> 类型的值,由字符 b 、一对用双引号引用的 ASCII 字符串组成。例如:

var a = b"hello"

表示一个长度为 5,类型为 Array<UInt8> 的数组,其中的每个元素的值为对应字符的 ASCII 的数值,即 [104, 101, 108, 108, 111]

同时也支持转义字符,例如:

var b = b"\u{78}\n\r"

表示一个长度为 3,类型为 Array<UInt8> 的数组,其中的元素的值为 [120, 10, 13]

这里 \u 转义的字符和字符字节字面量相同,也不支持 Unicode,所以内部最多有两位 16 进制数。

值类型数组 VArray

仓颉编程语言引入了值类型数组 VArray<T, $N> ,其中 T 表示该值类型数组的元素类型,$N 是一个固定的语法,通过 $ 加上一个数值字面量表示这个值类型数组的长度。需要注意的是,VArray<T, $N> 不能省略 <T, $N>,且使用类型别名时,不允许拆分 VArray 关键字与其泛型参数。

type varr1 = VArray<Int64, $3> // ok
type varr2 = VArray // error

注意: 由于运行时后端限制,当前 VArray<T, $N> 的元素类型 TT 的成员不能包含引用类型、枚举类型、Lambda 表达式(CFunc 除外)以及未实例化的泛型类型。

VArray 可以由一个数组的字面量来进行初始化,左值 a 必须标识出 VArray 的实例化类型:

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]
// VArray<T, $N>(item!: T)
let c = VArray<Int64, $5>(item: 0) // [0, 0, 0, 0, 0]

除此之外,VArray<T, $N> 类型提供了两个成员方法:

1、用于下标访问和修改的 [] 操作符方法:

var a: VArray<Int64, $3> = [1, 2, 3]
let i = a[1] // i is 2
a[2] = 4 // a is [1, 2, 4]

2、用于获取 VArray 长度的 size 成员:

var a: VArray<Int64, $3> = [1, 2, 3]
let s = a.size // s is 3

Unit 类型

对于那些只关心副作用而不关心值的表达式,它们的类型是 Unit。例如,print 函数、赋值表达式、复合赋值表达式、自增和自减表达式、循环表达式,它们的类型都是 Unit

Unit 类型只有一个值,也是它的字面量:()。除了赋值、判等和判不等外,Unit 类型不支持其他操作。

元组类型

元组(Tuple)可以将多个不同的类型组合在一起,成为一个新的类型。元组类型使用 (T1, T2, ..., TN) 表示,其中 T1TN 可以是任意类型,不同类型间使用逗号(,)连接。元组至少是二元以上,例如,(Int64, Float64) 表示一个二元组类型,(Int64, Float64, String) 表示一个三元组类型。

元组的长度是固定的,即一旦定义了一个元组类型的实例,它的长度不能再被更改。

元组类型是不可变类型,即一旦定义了一个元组类型的实例,它的内容不能再被更新。例如

var tuple = (true, false)
tuple[0] = false // Error: 'tuple element' can not be assigned

元组类型的字面量

元组类型的字面量使用 (e1, e2, ..., eN) 表示,其中 e1eN 是表达式,多个表达式之间使用逗号分隔。下面的例子中,分别定义了一个 (Int64, Float64) 类型的变量 x,以及一个 (Int64, Float64, String) 类型的变量 y,并且使用元组类型的字面量为它们定义了初值:

let x: (Int64, Float64) = (3, 3.141592)
let y: (Int64, Float64, String) = (3, 3.141592, "PI")

元组支持通过 t[index] 的方式访问某个具体位置的元素,其中 t 是一个元组,index 是下标,并且 index 只能是从 0 开始且小于元组元素个数的整数类型字面量,否则,编译报错。下面的例子中,使用 pi[0]pi[1] 可以分别访问二元组 pi 的第一个元素和第二个元素。

main() {
    var pi = (3.14, "PI")
    println(pi[0])
    println(pi[1])
}

编译并执行上述代码,输出结果为:

3.140000
PI

在赋值表达式中,可使用元组字面量对表达式的右值进行解构,这要求赋值表达式等号左边必须是一个元组字面量,这个元组字面量里面的元素必须都是左值(左值即出现在赋值操作符左边的,可保存值的表达式,具体参见各章节对赋值操作的描述)或者一个元组字面量,当元组字面量中出现 _ 时,表示忽略等号右侧 tuple 对应位置处的求值结果(意味着这个位置处的类型检查总是可以通过的),等号右边的表达式也必须是 tuple 类型,右边 tuple 每个元素的类型必须是对应位置左值类型的子类型。注意,复合赋值不支持这种解构方式。求值顺序上先计算等号右边表达式的值,再对左值部分从左往右逐个赋值,例如

var a: Int64
var b: String
var c: Unit
var f = { => ((1, "abc"), ())}
((a, b), c) = f() // value of a is 1, value of b is "abc", value of c is '()'
((a, b), _) = ((2, "def"), 3.0) // value of a is 2, value of b is "def", 3.0 is ignored

元组类型的类型参数

可以为元组类型标记显式的类型参数名,下面例子中的 nameprice 就是 类型参数名

func getFruitPrice (): (name: String, price: Int64) {
    return ("banana", 10)
}

对于一个元组类型,只允许统一写类型参数名,或者统一不写类型参数名,不允许交替存在。

let c: (name: String, Int64) = ("banana", 5)   // error

区间类型

区间类型用于表示拥有固定步长的序列,区间类型是一个泛型(详见泛型章节),使用 Range<T> 表示。当 T 被实例化不同的类型时(要求此类型必须支持关系操作符,并且可以和 Int64 类型的值做加法),会得到不同的区间类型,如最常用的 Range<Int64> 用于表示整数区间。

每个区间类型的实例都会包含 startendstep 三个值。其中,startend 分别表示序列的起始值和终止值,step 表示序列中前后两个元素之间的差值(即步长);startend 的类型相同(即 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   // r1 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r2 = 0..=n : 1   // r2 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
let r3 = n..0 : -2   // r3 contains 10, 8, 6, 4, 2
let r4 = 10..=0 : -2 // r4 contains 10, 8, 6, 4, 2, 0

区间字面量中,可以不写 step,此时 step 默认等于 1,但是注意,step 的值不能等于 0。另外,区间也有可能是空的(即不包含任何元素的空序列),举例如下:

let r5 = 0..10   // the step of r5 is 1, and it contains 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 // r7 to r10 are empty ranges
let r8 = 0..10 : -1
let r9 = 10..=0 : 1
let r10 = 0..=10 : -1

注:表达式 start..end : step 中,当 step > 0start >= end,或者 step < 0start <= end 时,start..end : step 是一个空区间;表达式 start..=end : step 中,当 step > 0start > end,或者 step < 0start < end 时,start..=end : step 是一个空区间。

Nothing 类型

Nothing 是一种特殊的类型,它不包含任何值,并且 Nothing 类型是所有类型的子类型。

breakcontinuereturnthrow 表达式的类型是 Nothing,程序执行到这些表达式时,它们之后的代码将不会被执行。其中 breakcontinue 只能在循环体中使用,return 只能在函数体中使用。

包围着的循环体”无法穿越“函数边界。在下面的例子中,break 出现在函数 f 中,外层的 while 循环体不被视作包围着它的循环体;continue 出现在 lambda 表达式 中,外层的 while 循环体不被视作包围着它的循环体。

while (true) {
    func f() {
        break // Error: break must be used directly inside a loop
    }
    let g = { =>
        continue // Error: continue must be used directly inside a loop
    }
}

由于函数的形参和其默认值不属于该函数的函数体,所以下面例子中的 return 表达式缺少包围它的函数体——它既不属于外层函数 f(因为内层函数定义 g 已经开始),也不在内层函数 g 的函数体中:

func f() {
    func g(x!: Int64 = return) { // Error: return must be used inside a function body
        0
    }
    1
}

注:目前编译器还不允许在使用类型的地方显式地使用 Nothing 类型。

自定义类型

struct 类型

struct 是一种自定义类型,可以将若干不同类型的值组合在一起,成为一个新类型。本节依次介绍如何定义 struct 类型,如何创建 struct 实例,以及 struct 中的 mut 函数。

定义 struct 类型

struct 类型的定义以关键字 struct 开头,后跟 struct 的名字,接着是定义在一对花括号中的 struct 定义体。struct 定义体中可以定义一系列的成员变量、成员属性(参见[属性])、静态初始化器、构造函数和成员函数。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

上例中定义了名为 Rectanglestruct 类型,它有两个 Int64 类型的成员变量 widthheight,一个有两个 Int64 类型参数的构造函数(使用关键字 init 定义,函数体中通常是对成员变量的初始化),以及一个成员函数 area(返回 widthheight 的乘积)。

注:struct 只能定义在源文件顶层。

struct 成员变量

struct 成员变量分为实例成员变量和静态成员变量(使用 static 修饰符修饰,且必须有初值),二者的区别在于实例成员变量只能通过 struct 实例(我们说 aT 类型的实例,指的是 a 是一个 T 类型的值)访问,静态成员变量只能通过 struct 类型名访问。

实例成员变量定义时可以不设置初值(但必须标注类型,如上例中的 widthheight),也可以设置初值,例如:

struct Rectangle {
    let width = 10
    let height = 20
}

struct 静态初始化器

struct 支持定义静态初始化器,并在静态初始化器中通过赋值表达式来对静态成员变量进行初始化。

静态初始化器以关键字组合 static init 开头,后跟无参参数列表和函数体,且不能被可见性修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。

struct Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
}

一个 struct 中最多允许定义一个静态初始化器,否则报重定义错误。

struct Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
    static init() { // Error: redefinition with the previous static init function
        degree = 180
    }
}

struct 构造函数

struct 支持两类构造函数:普通构造函数和主构造函数。

普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成对所有未初始化的实例成员变量的初始化(如果参数名和成员变量名无法区分,可以在成员变量前使用 this 加以区分,this 表示 struct 的当前实例),否则编译报错。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) { // Error: 'height' is not initialized in the constructor
        this.width = width
    }
}

一个 struct 中可以定义多个普通构造函数,但它们必须构成重载(参见[函数重载]),否则报重定义错误。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64) {
        this.width = width
        this.height = width
    }

    public init(width: Int64, height: Int64) { // Ok: overloading with the first init function
        this.width = width
        this.height = height
    }

    public init(height: Int64) { // Error: redefinition with the first init function
        this.width = height
        this.height = height
    }
}

除了可以定义若干普通的以 init 为名字的构造函数外,struct 内还可以定义(最多)一个主构造函数。主构造函数的名字和 struct 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 letvar),成员变量形参同时扮演定义成员变量和构造函数参数的功能。

使用主构造函数通常可以简化 struct 的定义,例如,上述包含一个 init 构造函数的 Rectangle 可以简化为如下定义:

struct Rectangle {
    public Rectangle(let width: Int64, let height: Int64) {}
}

主构造函数的参数列表中也可以定义普通形参,例如:

struct Rectangle {
    public Rectangle(name: String, let width: Int64, let height: Int64) {}
}

如果 struct 定义中不存在自定义构造函数(包括主构造函数),并且所有实例成员变量都有初始值,则会自动为其生成一个无参构造函数(调用此无参构造函数会创建一个所有实例成员变量的值均等于其初值的对象);否则,不会自动生成此无参构造函数。例如,对于如下 struct 定义,注释中给出了自动生成的无参构造函数:

struct Rectangle {
    let width: Int64 = 10
    let height: Int64 = 10
    /* Auto-generated memberwise constructor:
    public init() {
    }
    */
}

struct 成员函数

struct 成员函数分为实例成员函数和静态成员函数(使用 static 修饰符修饰),二者的区别在于:实例成员函数只能通过 struct 实例访问,静态成员函数只能通过 struct 类型名访问;静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。

下例中,area 是实例成员函数,typeName 是静态成员函数。

struct Rectangle {
    let width: Int64 = 10
    let height: Int64 = 20

    public func area() {
        this.width * this.height
    }

    public static func typeName(): String {
        "Rectangle"
    }
}

实例成员函数中可以通过 this 访问实例成员变量,例如:

struct Rectangle {
    let width: Int64 = 1
    let height: Int64 = 1

    public func area() {
        this.width * this.height
    }
}

struct 成员的可见修饰符

struct 的成员(包括成员变量、成员属性、构造函数、成员函数、操作符函数(详见操作符重载章节))用两种可见性修饰符修饰:publicprivate,缺省的含义是仅包内可见。

使用 public 修饰的成员在 struct 定义内部和外部均可见;使用 private 修饰的成员仅在 struct 定义内部可见,外部无法访问。

下面的例子中,widthpublic 修饰的成员,在类外可以访问,height 是缺省可见修饰符的成员,仅在本包可见,包外部无法访问。

package a
public struct Rectangle {
    public var width: Int64
    var height: Int64
    private var area: Int64
    ...
}

func samePkgFunc() {
    var r = Rectangle(10, 20)
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // Ok: 'height' has no modifier and can be accessed here
    r.area = 30               // Error: private 'area' can't be accessed here
}
package b
import a.*
main() {
    var r = Rectangle(10, 20)
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // Error: no modifier 'height' can't be accessed here
    r.area = 30               // Error: private 'area' can't be accessed here
}

禁止递归 struct

递归和互递归定义的 struct 均是非法的。例如:

struct R1 { // Error: 'R1' recursively references itself
    let other: R1
}
struct R2 { // Error: 'R2' and 'R3' are mutually recursive
    let other: R3
}
struct R3 { // Error: 'R2' and 'R3' are mutually recursive
    let other: R2
}

使用 struct 构造函数创建 struct 实例

定义了 struct 类型后,即可通过调用 struct 的构造函数来创建 struct 实例。在 struct 定义之外,通过 struct 类型名调用构造函数。例如,下例中定义了一个 Rectangle 类型的变量 r

let r = Rectangle(10, 20)

创建了 struct 实例之后,可以通过实例访问它的(public 修饰的)实例成员变量和实例成员函数。例如,下例中通过 r.widthr.height 可分别访问 rwidthheight 的值,通过 r.area() 可以调用 r 的成员函数 area

let r = Rectangle(10, 20)
let width = r.width   // width = 10
let height = r.height // height = 20
let a = r.area()      // a = 200

如果希望通过 struct 实例去修改成员变量的值,需要将 struct 类型的变量定义为可变变量,并且被修改的成员变量也必须是可变成员变量(使用 var 定义)。举例如下:

struct Rectangle {
    public var width: Int64
    public var height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

main() {
    var r = Rectangle(10, 20) // r.width = 10, r.height = 20
    r.width = 8               // r.width = 8
    r.height = 24             // r.height = 24
    let a = r.area()          // a = 192
}

在赋值或传参时,会对 struct 实例进行复制,生成新的实例,对其中一个实例的修改并不会影响另外一个实例。以赋值为例,下面的例子中,将 r1 赋值给 r2 之后,修改 r1widthheight 的值,并不会影响 r2widthheight 值。

struct Rectangle {
    public var width: Int64
    public var height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

main() {
    var r1 = Rectangle(10, 20) // r1.width = 10, r1.height = 20
    var r2 = r1                // r2.width = 10, r2.height = 20
    r1.width = 8               // r1.width = 8
    r1.height = 24             // r1.height = 24
    let a1 = r1.area()         // a1 = 192
    let a2 = r2.area()         // a2 = 200
}

struct 中的 mut 函数

默认情况下,struct 中的实例成员函数是无法修改它的实例成员变量和实例成员属性的。如果需要修改,可以在定义实例成员函数时使用关键字 mut 修饰,使之成为 mut 函数。mut 函数是一种特殊的实例成员函数,mut 函数内部可以“原地”修改当前实例,以实现 struct 通过调用函数修改自身的目的。

语法上,定义 mut 实例成员函数是在 func 关键字之前加上 mut 修饰符。例如,可以在 Rectangle 中定义一个用于修改 width 值的 mut 函数 setW

struct Rectangle {
    public var width: Int64
    public var height: Int64

    public init(width!: Int64, height!: Int64) {
        this.width = width
        this.height = height
    }

    public mut func setW(v: Int64): Unit {
        width = v
    }

    public func area() {
        width * height
    }
}

调用 mut 函数时,mut 函数允许修改当前实例的成员值,例如,下例中通过调用 setWrwidth 值设置为 8

main() {
    var r = Rectangle(width: 10, height: 20) // r.width = 10, r.height = 20
    r.setW(8)                 // r.width = 8
    let a = r.area()          // a = 160
}

注意,上例中 r 如果使用 let 定义,则不能调用 Rectangle 中的 mut 函数。

enum 类型

本节介绍仓颉中的 enum 类型。enum 类型提供了通过列举一个类型的所有可能取值来定义此类型的方式。

在很多语言中都有 enum 类型(或者称枚举类型),但是不同语言中的 enum 类型的使用方式和表达能力均有所差异,仓颉中的 enum 类型可以理解为函数式编程语言中的代数数据类型(Algebraic Data Types)。

接下来,首先介绍如何定义和使用 enum,然后介绍如何使用模式匹配使得 enum 取不同值时执行不同的操作,最后介绍一个名为 Option 的常用 enum 类型,用于表示某个类型的实例要么有值要么没值。

enum 的定义和使用

定义 enum 时需要把它所有可能的取值一一列出,我们称这些值为 enum 的构造器(或者 constructor)。

enum RGBColor {
    | Red | Green | Blue
}

enum 类型的定义以关键字 enum 开头,接着是 enum 的名字,之后是定义在一对花括号中的 enum 体,enum 体中定义了若干构造器,多个构造器之间使用 | 进行分隔(第一个构造器之前的 | 是可选的)。上例中定义了一个名为 RGBColorenum 类型,它有 3 个构造器:RedGreenBlue,分别表示 RGB 色彩模式中的红色、绿色和蓝色。

上述 enum 中的构造器还可以携带若干(至少一个)参数,称为有参构造器。例如,可以为 RedGreenBlue 设置一个 UInt8 的类型的参数,用来表示每个颜色的亮度级别:

enum RGBColor {
    | Red(UInt8) | Green(UInt8) | Blue(UInt8)
}

仓颉支持同一个 enum 中定义多个同名构造器,但是要求这些构造器的参数个数不同(认为没有参数的构造器的参数个数等于 0),例如:

enum RGBColor {
    | Red | Green | Blue
    | Red(UInt8) | Green(UInt8) | Blue(UInt8)
}

enum 支持递归定义,例如,下面的例子中使用 enum 定义了一种表达式(即 Expr),此表达式只能有 3 种形式:单独的一个数字 Num(携带一个 Int64 类型的参数)、加法表达式 Add(携带两个 Expr 类型的参数)、减法表达式 Sub(携带两个 Expr 类型的参数)。对于 AddSub 这两个构造器,其参数中递归地使用到了 Expr 自身。

enum Expr {
    | Num(Int64)
    | Add(Expr, Expr)
    | Sub(Expr, Expr)
}

另外,在 enum 体中还可以定义一系列成员函数、操作符函数(详见操作符重载章节)和成员属性(详见属性章节),但是要求构造器、成员函数、成员属性之间不能重名。例如,下面的例子在 RGBColor 中定义了一个名为 printType 的函数,它会输出字符串 RGBColor

enum RGBColor {
    | Red | Green | Blue

    public static func printType() {
        print("RGBColor")
    }
}

注:enum 只能定义在源文件顶层。

使用限制:当 enumstruct 类型存在互递归关系时,且 enum 类型作为 Option 的类型参数, 可能存在编译错误。

enum 值

定义了 enum 类型之后,就可以创建此类型的实例(即 enum 值),enum 值只能取 enum 类型定义中的一个构造器。enum 没有构造函数,可以通过 类型名.构造器,或者直接使用构造器的方式来构造一个 enum 值(对于有参构造器,需要传实参)。

下例中,RGBColor 中定义了三个构造器,其中有两个无参构造器(RedGreen)和一个有参构造器(Blue(UInt8)),main 中定义了三个 RGBColor 类型的变量 rgb,其中,r 的值使用 RGBColor.Red 进行初始化,g 的值直接使用 Green 进行初始化,b 的值使用 Blue(100) 进行初始化:

enum RGBColor {
    | Red | Green | Blue(UInt8)
}

main() {
    let r = RGBColor.Red
    let g = Green
    let b = Blue(100)
}

当省略类型名时,enum 构造器的名字可能和类型名、变量名、函数名发生冲突。此时必须加上 enum 类型名来使用 enum 构造器,否则只会选择同名的类型、变量、函数定义。

下面的例子中,只有构造器 Blue(UInt8) 可以不带类型名使用,RedGreen(UInt8) 皆会因为名字冲突而不能直接使用,必须加上类型名 RGBColor

let Red = 1

func Green(g: UInt8) {
    return g
}

enum RGBColor {
    | Red | Green(UInt8) | Blue(UInt8)
}

let r1 = Red                 // Will choose 'let Red'
let r2 = RGBColor.Red        // Ok: constructed by enum type name

let g1 = Green(100)          // Will choose 'func Green'
let g2 = RGBColor.Green(100) // Ok: constructed by enum type name

let b = Blue(100)            // Ok: can be uniquely identified as an enum constructor

如下的例子中,只有构造器 Blue 会因为名称冲突而不能直接使用,必须加上类型名 RGBColor

class Blue {}

enum RGBColor {
    | Red | Green(UInt8) | Blue(UInt8)
}

let r = Red                 // Ok: constructed by enum type name

let g = Green(100)          // Ok: constructed by enum type name

let b = Blue(100)           // Will choose constructor of 'class Blue' and report an error

enum 的模式匹配

对于一个 enum 值,我们通常希望它是不同的构造器时执行不同的操作,在仓颉中,可以通过模式匹配来实现。本节只是对 enum 的模式匹配做一个简单的介绍,关于模式匹配的更多内容,会在之后的[模式匹配]章节中详细说明。

对于如下使用 enum 定义的 RGBColor,如果希望 enum 值是不同构造器时分别输出其字符串表示,则可以使用 match 表达式和 enum 模式实现(关于 match 表达式和模式的详细介绍,参见[模式匹配]):

enum RGBColor {
    | Red | Green | Blue
}

main() {
    let c = Green
    let cs = match (c) {
        case Red => "Red"
        case Green => "Green"  // Matched
        case Blue => "Blue"
    }
    print(cs)
}

上例中,RedGreenBlue 分别用于匹配 RGBColor 中不同的构造器,上述代码的执行结果为:

Green

对于 enum 中的有参构造器,同样可以使用 match 表达式来匹配,并且可以解构出有参构造器中参数的值,举例如下:

enum RGBColor {
    | Red(UInt8) | Green(UInt8) | Blue(UInt8)
}

main() {
    let c = Green(100)
    let cs = match (c) {
        case Red(r) => "Red = ${r}"
        case Green(g) => "Green = ${g}"  // Matched
        case Blue(b) => "Blue = ${b}"
    }
    print(cs)
}

上例中,RGBColor 的三个构造器均有一个 UInt8 类型的参数,在匹配变量 c 的值时,case 之后使用 enum 模式来匹配不同的构造器,并将参数值分别与变量 rgb 进行绑定,一旦某条 case 匹配成功(本例中会与第二条 case 匹配),则返回对应的字符串。

上述代码的执行结果为:

Green = 100

Option 类型

Option 类型使用 enum 定义,它包含两个构造器:SomeNone。其中,Some 会携带一个参数,表示有值,None 不带参数,表示无值。当需要表示某个类型可能有值,也可能没有值的时候,可选择使用 Option 类型。

Option 类型被定义为一个泛型 enum 类型,定义如下(这里我们仅需要知道尖括号中的 T 是一个类型形参,当 T 为不同类型时会得到不同的 Option 类型即可。关于泛型的详细介绍,可参见[泛型]。):

enum Option<T> {
    | Some(T)
    | None
}

其中,Some 构造器的参数类型就是类型形参 T,当 T 被实例化为不同的类型时,会得到不同的 Option 类型,例如:Option<Int64>Option<String>等。

Option 类型还有一种简单的写法:在类型名前加 ?。也就是说,对于任意类型 Ty?Ty 等价于 Option<Ty>。例如,?Int64 等价于 Option<Int64>?String 等价于 Option<String> 等等。

下面的例子展示了如何定义 Option 类型的变量:

let a: Option<Int64> = Some(100)
let b: ?Int64 = Some(100)
let c: Option<String> = Some("Hello")
let d: ?String = None

另外,虽然 TOption<T> 是不同的类型,但是当明确知道某个位置需要的是 Option<T> 类型的值时,可以直接传一个 T 类型的值,编译器会用 Option<T> 类型的 Some 构造器将 T 类型的值封装成 Option<T> 类型的值(注意:这里并不是类型转换)。例如,下面的定义是合法的(等价于上例中变量 abc 的定义):

let a: Option<Int64> = 100
let b: ?Int64 = 100
let c: Option<String> = "100"

在上下文没有明确的类型要求时,无法使用 None 直接构造出想要的类型,此时应使用 None<T> 这样的语法来构造 Option<T> 类型的数据,例如

let a = None<Int64> // a: Option<Int64>
let b = None<Bool> // b: Option<Bool>

最后,关于 Option 的使用,请参见“错误处理”中“Option 类型用于错误处理”的内容。

class 类型

class 类型是面向对象编程中的经典概念,仓颉中同样支持使用 class 来实现面向对象编程。class 与上面介绍的 struct 的主要区别在于:class 是引用类型,struct 是值类型,它们在赋值或传参时行为是不同的(下文会有介绍);class 之间可以继承,但 struct 之间不能继承。

本节依次介绍如何定义 class 类型,如何创建对象,以及 class 的继承。

定义 class 类型

class 类型的定义以关键字 class 开头,后跟 class 的名字,接着是定义在一对花括号中的 class 定义体。class 定义体中可以定义一系列的成员变量、成员属性(参见[属性])、静态初始化器、构造函数、成员函数和操作符函数(详见操作符重载章节)。

class Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

上例中定义了名为 Rectangleclass 类型,它有两个 Int64 类型的成员变量 widthheight,一个有两个 Int64 类型参数的构造函数,以及一个成员函数 area(返回 widthheight 的乘积)。

注:class 只能定义在源文件顶层。

class 成员变量

class 成员变量分为实例成员变量和静态成员变量(使用 static 修饰符修饰,且必须有初值)。实例成员变量只能通过对象(即 class 的实例)访问,静态成员变量只能通过类型名访问。

class Rectangle {
    let width = 10
    static let height = 20
}

let l = Rectangle.height // l = 20

实例成员变量定义时可以不设置初值(但必须标注类型),也可以设置初值,例如:

class Rectangle {
    let width = 10
    let height = 20
}

class 静态初始化器

class 支持定义静态初始化器,并在静态初始化器中通过赋值表达式来对静态成员变量进行初始化。

静态初始化器以关键字组合 static init 开头,后跟无参参数列表和函数体,且不能被可见性修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。

class Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
}

一个 class 中最多允许定义一个静态初始化器,否则报重定义错误。

class Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
    static init() { // Error: redefinition with the previous static init function
        degree = 180
    }
}

class 构造函数

struct 一样,class 中也支持定义普通构造函数和主构造函数。

普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成所有未初始化实例成员变量的初始化,否则编译报错。

class Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) { // Error: 'height' is not initialized in the constructor
        this.width = width
    }
}

一个 class 中可以定义多个普通构造函数,但它们必须构成重载(参见[函数重载]),否则报重定义错误。

class Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64) {
        this.width = width
        this.height = width
    }

    public init(width: Int64, height: Int64) { // Ok: overloading with the first init function
        this.width = width
        this.height = height
    }

    public init(height: Int64) { // Error: redefinition with the first init function
        this.width = height
        this.height = height
    }
}

除了可以定义若干普通的以 init 为名字的构造函数外,class 内还可以定义(最多)一个主构造函数。主构造函数的名字和 class 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 letvar),成员变量形参同时具有定义成员变量和构造函数参数的功能。

使用主构造函数通常可以简化 class 的定义,例如,上述包含一个 init 构造函数的 Rectangle 可以简化为如下定义:

class Rectangle {
    public Rectangle(let width: Int64, let height: Int64) {}
}

主构造函数的参数列表中也可以定义普通形参,例如:

class Rectangle {
    public Rectangle(name: String, let width: Int64, let height: Int64) {}
}

如果 class 定义中不存在自定义构造函数(包括主构造函数),并且所有实例成员变量都有初始值,则会自动为其生成一个无参构造函数(调用此无参构造函数会创建一个所有实例成员变量的值均等于其初值的对象);否则,不会自动生成此无参构造函数。例如,对于如下 class 定义,编译器会为其自动生成一个无参构造函数:

class Rectangle {
    let width = 10
    let height = 20

    /* Auto-generated parameterless constructor:
    public init() {

    }
    */
}

// Invoke the auto-generated parameterless constructor
let r = Rectangle() // r.width = 10,r.height = 20

class 终结器

class 支持定义终结器,这个函数在类的实例被垃圾回收的时候被调用。终结器的函数名固定为 ~init。终结器一般被用于释放系统资源:

class C {
    var p: CString

    init(s: String) {
        p = unsafe { LibC.mallocCString(s) }
        println(s)
    }
    ~init() {
        unsafe { LibC.free(p) }
    }
}

使用终结器有些限制条件,需要开发者注意:

  1. 终结器没有参数,没有返回类型,没有泛型类型参数,没有任何修饰符,也不可以被显式调用。
  2. 带有终结器的类不可被 open 修饰,只有非 open 的类可以拥有终结器。
  3. 一个类最多只能定义一个终结器。
  4. 终结器不可以定义在扩展中。
  5. 终结器被触发的时机是不确定的。
  6. 终结器可能在任意一个线程上执行。
  7. 多个终结器的执行顺序是不确定的。
  8. 终结器向外抛出未捕获异常属于未定义行为。
  9. 终结器中创建线程或者使用线程同步功能属于未定义行为。
  10. 终结器执行结束之后,如果这个对象还可以被继续访问,则属于未定义行为。

class 成员函数

class 成员函数同样分为实例成员函数和静态成员函数(使用 static 修饰符修饰),实例成员函数只能通过对象访问,静态成员函数只能通过 class 类型名访问;静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。

下例中,area 是实例成员函数,typeName 是静态成员函数。

class Rectangle {
    let width: Int64 = 10
    let height: Int64 = 20

    public func area() {
        this.width * this.height
    }

    public static func typeName(): String {
        "Rectangle"
    }
}

根据有没有函数体,实例成员函数又可以分为抽象成员函数和非抽象成员函数。抽象成员函数没有函数体,只能定义在抽象类或接口(详见接口章节)中。例如,下例中在抽象类 AbRectangle(使用关键字 abstract 修饰)中定义了抽象函数 foo

abstract class AbRectangle {
    public func foo(): Unit
}

需要注意的是,抽象实例成员函数默认具有 open 的语义,open 修饰符是可选的,且必须使用 publicprotected 进行修饰。

非抽象函数必须有函数体,在函数体中可以通过 this 访问实例成员变量,例如:

class Rectangle {
    let width: Int64 = 10
    let height: Int64 = 20

    public func area() {
        this.width * this.height
    }
}

class 成员的可见修饰符

对于 class 的成员(包括成员变量、成员属性、构造函数、成员函数),可以使用的可见性修饰符有三种:publicprotectedprivate,缺省的含义是仅包内可见。

使用 public 修饰的成员在 class 定义内部和外部均可见,成员变量、成员属性和成员函数在 class 外部可以通过对象访问;使用 protected 修饰的成员在本包、本 class 及其子类中可见,外部无法访问;使用 private 修饰的成员仅在本 class 定义内部可见,外部无法访问;缺省可见修饰符的成员仅在本包可见,外部无法访问。

package a
public open class Rectangle {
    public var width: Int64
    protected var height: Int64
    private var area: Int64
    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
        this.area = this.width * this.height
    }
    init(width: Int64, height: Int64, multiple: Int64) {
        this.width = width
        this.height = height
        this.area = width * height * multiple
    }
}

func samePkgFunc() {
    var r = Rectangle(10, 20) // Ok: constructor 'Rectangle' can be accessed here
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // Ok: protected 'height' can be accessed here
    r.area = 30               // Error: private 'area' cannot be accessed here
}
package b
import a.*
public class Cuboid <: Rectangle {
    private var length: Int64
    public init(width: Int64, height: Int64, length: Int64) {
        super(width, height)
        this.length = length
    }
    public func volume() {
        this.width * this.height * this.length // Ok: protected 'height' can be accessed here
    }
}

main() {
    var r = Rectangle(10, 20, 2) // Error: Rectangle has no `public` constructor with three parameters
    var c = Cuboid(20, 20, 20)
    c.width = 8               // Ok: public 'width' can be accessed here
    c.height = 24             // Error: protected 'height' cannot be accessed here
    c.area = 30               // Error: private 'area' cannot be accessed here
}

This 类型

在类内部,我们支持 This 类型占位符,代指当前类的类型。它只能被作为实例成员函数的返回类型来使用,当使用子类对象调用在父类中定义的返回 This 类型的函数时,该函数调用的类型会被识别为子类类型,而非定义所在的父类类型。

如果实例成员函数没有声明返回类型,并且只存在返回 This 类型表达式时,当前函数的返回类型会推断为 This。示例如下:

open class C1 {
    func f(): This {  // its type is `() -> C1`
        return this
    }

    func f2() { // its type is `() -> C1`
        return this
    }

    public open func f3(): C1 {
        return this
    }
}
class C2 <: C1 {
    // member function f is inherited from C1, and its type is `() -> C2` now
    public override func f3(): This { // ok
        return this
    }
}

var obj1: C2 = C2()
var obj2: C1 = C2()

var x = obj1.f()    // During compilation, the type of x is C2
var y = obj2.f()    // During compilation, the type of y is C1

创建对象

定义了 class 类型后,即可通过调用 class 的构造函数来创建对象(通过 class 类型名调用构造函数)。例如,下例中通过 Rectangle(10, 20) 创建 Rectangle 类型的对象并赋值给变量 r

let r = Rectangle(10, 20)

创建对象之后,可以通过对象访问(public 修饰的)实例成员变量和实例成员函数。例如,下例中通过 r.widthr.height 可分别访问 rwidthheight 的值,通过 r.area() 可以调用成员函数 area

let r = Rectangle(10, 20) // r.width = 10, r.height = 20
let width = r.width       // width = 10
let height = r.height     // height = 20
let a = r.area()          // a = 200

如果希望通过对象去修改成员变量的值(不鼓励这种方式,最好还是通过成员函数去修改),需要将 class 类型中的成员变量定义为可变成员变量(即使用 var 定义)。举例如下:

class Rectangle {
    public var width: Int64
    public var height: Int64

    ...
}

main() {
    let r = Rectangle(10, 20) // r.width = 10, r.height = 20
    r.width = 8               // r.width = 8
    r.height = 24             // r.height = 24
    let a = r.area()          // a = 192
}

不同于 struct,对象在赋值或传参时,不会将对象进行复制,多个变量指向的是同一个对象,通过一个变量去修改对象中成员的值,其他变量中对应的成员变量也会被修改。以赋值为例,下面的例子中,将 r1 赋值给 r2 之后,修改 r1widthheight 的值,r2widthheight 值也同样会被修改。

main() {
    var r1 = Rectangle(10, 20) // r1.width = 10, r1.height = 20
    var r2 = r1                // r2.width = 10, r2.height = 20
    r1.width = 8               // r1.width = 8
    r1.height = 24             // r1.height = 24
    let a1 = r1.area()         // a1 = 192
    let a2 = r2.area()         // a2 = 192
}

class 的继承

像大多数支持 class 的编程语言一样,仓颉中的 class 同样支持继承。如果 class B 继承 class A,则我们称 A 为父类,B 为子类。子类将继承父类中除 private 成员和构造函数以外的所有成员。

抽象 class 总是可被继承的,故抽象类定义时的 open 修饰符是可选的,也可以使用 sealed 修饰符修饰抽象类,表示该抽象类只能在本包被继承。但非抽象的 class 可被继承是有条件的:定义时必须使用修饰符 open 修饰。当带 open 修饰的实例成员被 class 继承时,该 open 的修饰符也会被继承。当非 open 修饰的类中存在 open 修饰的成员时,编译器会给出告警。

可以在子类定义处通过 <: 指定其继承的父类,但要求父类必须是可继承的。例如,下面的例子中,class A 使用 open 修饰,是可以被 class B 继承的,但是因为 class B 是不可继承的,所以 C 在继承 B 的时候会报错。

open class A {
    let a: Int64 = 10
}

class B <: A { // Ok: 'B' Inheritance 'A'
    let b: Int64 = 20
}

class C <: B { // Error: 'B' is not inheritable
    let c: Int64 = 30
}

class 仅支持单继承,因此下面这样一个 class 继承两个 class 的代码是不合法的(&class 实现多个接口时的语法,详见接口章节)。

open class A {
    let a: Int64 = 10
}

open class B {
    let b: Int64 = 20
}

class C <: A & B { // Error: 'C' can only inherit one class
    let c: Int64 = 30
}

因为 class 是单继承的,所以任何 class 都最多只能有一个直接父类。对于定义时指定了父类的 class,它的直接父类就是定义时指定的类,对于定义时未指定父类的 class,它的直接父类是 Object 类型。Object 是所有 class 的父类(注意,Object 没有直接父类,并且 Object 中不包含任何成员)。

因为子类是继承自父类的,所以子类的对象天然可以当做父类的对象使用,但是反之不然。例如,下例中 B 是 A 的子类,那么 B 类型的对象可以赋值给 A 类型的变量,但是 A 类型的对象不能赋值给 B 类型的变量。

open class A {
    let a: Int64 = 10
}

class B <: A {
    let b: Int64 = 20
}

let a: A = B() // Ok: subclass objects can be assigned to superclass variables
open class A {
    let a: Int64 = 10
}

class B <: A {
    let b: Int64 = 20
}

let b: B = A() // Error: superclass objects can not be assigned to subclass variables

class 定义的类型不允许继承类型本身。

class A <: A {}  // Error, 'A' inherits itself.

sealed 修饰符只能修饰抽象类,表示被修饰的 class 定义只能在本定义所在的包内被其他 class 继承。sealed 已经蕴含了 public/open 的语义,因此定义 sealed abstract class 时若提供 public/open 修饰符,编译器将会告警。sealed 的子类可以不是 sealed 类,仍可被 open/sealed 修饰,或不使用任何继承性修饰符。若 sealed 类的子类被 open 修饰,则其子类可在包外被继承。sealed 的子类可以不被 public 修饰。

package A
public sealed abstract class C1 {}   // Warning, redundant modifier, 'sealed' implies 'public'
sealed open abstract class C2 {}     // Warning, redundant modifier, 'sealed' implies 'open'
sealed abstract class C3 {}          // OK, 'public' is optional when 'sealed' is used

class S1 <: C1 {}  // OK
public open class S2 <: C1 {}   // OK
public sealed abstract class S3 <: C1 {}  // OK
open class S4 <: C1 {}   // OK
package B
import A.*

class SS1 <: S2 {}  // OK
class SS2 <: S3 {}  // Error, S3 is sealed class, cannot be inherited here.
sealed class SS3 {} // Error, 'sealed' cannot be used on non-abstract class.

父类构造函数调用

子类的 init 构造函数可以使用 super(args) 的形式调用父类构造函数,或使用 this(args) 的形式调用本类其它构造函数,但两者之间只能调用一个。如果调用,必须在构造函数体内的第一个表达式处,在此之前不能有任何表达式或声明。

open class A {
    A(let a: Int64) {}
}

class B <: A {
    let b: Int64
    init(b: Int64) {
        super(30)
        this.b = b
    }

    init() {
        this(20)
    }
}

子类的主构造函数中,可以使用 super(args) 的形式调用父类构造函数,但不能使用 this(args) 的形式调用本类其它构造函数。

如果子类的构造函数没有显式调用父类构造函数,也没有显式调用其他构造函数,编译器会在该构造函数体的开始处插入直接父类的无参构造函数的调用。如果此时父类没有无参构造函数,则会编译报错;

open class A {
    let a: Int64
    init() {
        a = 100
    }
}

open class B <: A {
    let b: Int64
    init(b: Int64) {
        // OK, `super()` added by compiler
        this.b = b
    }
}

open class C <: B {
    let c: Int64
    init(c: Int64) {  // Error, there is no non-parameter constructor in super class
        this.c = c
    }
}

覆盖和重定义

子类中可以覆盖(override)父类中的同名非抽象实例成员函数,即在子类中为父类中的某个实例成员函数定义新的实现。覆盖时,要求父类中的成员函数使用 open 修饰,子类中的同名函数使用 override 修饰,其中 override 是可选的。例如,下面的例子中,子类 B 中的函数 f 覆盖了父类 A 中的函数 f

open class A {
    public open func f(): Unit {
        println("I am superclass")
    }
}

class B <: A {
    public override func f(): Unit {
        println("I am subclass")
    }
}

main() {
    let a: A = A()
    let b: A = B()
    a.f()
    b.f()
}

对于被覆盖的函数,调用时将根据变量的运行时类型(由实际赋给该变量的对象决定)确定调用的版本(即所谓的动态派发)。例如,上例中 a 的运行时类型是 A,因此 a.f() 调用的是父类 A 中的函数 fb 的运行时类型是 B(编译时类型是 A),因此 b.f() 调用的是子类 B 中的函数 f。所以程序会输出:

I am superclass
I am subclass

对于静态函数,子类中可以重定义父类中的同名非抽象静态函数,即在子类中为父类中的某个静态函数定义新的实现。重定义时,要求子类中的同名静态函数使用 redef 修饰,其中 redef 是可选的。例如,下面的例子中,子类 D 中的函数 foo 重定义了父类 C 中的函数 foo

open class C {
    public static func foo(): Unit {
        println("I am class C")
    }
}

class D <: C {
    public redef static func foo(): Unit {
        println("I am class D")
    }
}

main() {
    C.foo()
    D.foo()
}

对于被重定义的函数,调用时将根据 class 的类型决定调用的版本。例如,上例中 C.foo() 调用的是父类 C 中的函数 fooD.foo() 调用的是子类 D 中的函数 foo

I am class C
I am class D

如果抽象函数或 open 修饰的函数有命名形参,那么实现函数或 override 修饰的函数也需要保持同样的命名形参。

open class A {
    public open func f(a!: Int32): Int32 {
        a + 1
    }
}

class B <: A {
    public override func f(a!: Int32): Int32 { // ok
        a + 2
    }
}

class C <: A {
    public override func f(b!: Int32): Int32 { // error
        b + 3
    }
}

main() {
    B().f(a: 0)
    C().f(b: 0)
}

还需要注意的是,当实现或重定义的函数为泛型函数时,子类型函数的类型变元约束需要比父类型中对应函数更宽松或相同。

open class A {}
open class B <: A {}
open class C <: B {}

open class Base {
    static func f<T>(a: T): Unit where T <: B {}
    static func g<T>(): Unit where T <: B {}
}

class D <: Base {
    redef static func f<T>(a: T): Unit where T <: C {} // Error: stricter constraint
    redef static func g<T>(): Unit where T <: C {} // Error: stricter constraint
}

class E <: Base {
    redef static func f<T>(a: T): Unit where T <: A {} // OK: looser constraint
    redef static func g<T>(): Unit where T <: A {} // OK: looser constraint
}

class F <: Base {
    redef static func f<T>(a: T): Unit where T <: B {} // OK: same constraint
    redef static func g<T>(): Unit where T <: B {} // OK: same constraint
}

接口

接口用来定义一个抽象类型,它不包含数据,但可以定义类型的行为。一个类型如果声明实现某接口,并且实现了该接口中所有的成员,就被称为实现了该接口。

接口的成员可以包含:

  • 成员函数
  • 操作符重载函数
  • 成员属性

这些成员都是抽象的,要求实现类型必须拥有对应的成员实现。

接口的定义

一个简单的接口定义如下:

/*'open' is optional */ interface I {
    func f(): Unit
}

接口使用关键字 interface 声明,其后是接口的标识符 I 和接口的成员。接口成员可被 open 修饰符修饰,并且 open 修饰符是可选的。

当接口 I 声明了一个成员函数 f 之后,当我们要为一个类型实现 I 时,就必须要在该类型中实现一个对应的 f 函数。

因为 interface 默认具有 open 语义,所以 interface 定义时的 open 修饰符是可选的。

如下面的代码所示,我们定义了一个 class Foo,使用 Foo <: I 的形式声明了 Foo 实现 I 接口。

Foo 中必须包含 I 声明的所有成员的实现,即需要定义一个相同签名的 f,否则会由于没有实现接口而编译报错。

class Foo <: I {
    public func f(): Unit {
        println("Foo")
    }
}

main() {
    let a = Foo()
    let b: I = a
    b.f() // "Foo"
}

当某个类型实现了某个接口之后,该类型就会成为该接口的子类型。

对于上面的例子,我们就可以认为 FooI 的子类型,因此任何一个 Foo 类型的实例,都可以当作 I 类型的实例使用。

main 中我们将一个 Foo 类型的变量 a,赋值给一个 I 类型的变量 b。然后我们再调用 b 中的函数 f,就会打印出 Foo 实现的 f 版本。程序的输出结果为:

Foo

interface 也可以使用 sealed 修饰符表示只能在 interface 定义所在的包内继承、实现或扩展该 interfacesealed 已经蕴含了 public/open 的语义,因此定义 sealed interface 时若提供 public/open 修饰符,编译器将会告警。继承 sealed 接口的子接口或实现 sealed 接口的类仍可被 sealed 修饰或不使用 sealed 修饰。若 sealed 接口的子接口被 public 修饰,且不被 sealed 修饰,则其子接口可在包外被继承、实现或扩展。继承、实现 sealed 接口的类型可以不被 public 修饰。

package A
public interface I1 {}
sealed interface I2 {}         // OK
public sealed interface I3 {}  // Warning, redundant modifier, 'sealed' implies 'public'
sealed open interface I4 {}    // Warning, redundant modifier, 'sealed' implies 'open'

class C1 <: I1 {}
public open class C2 <: I1 {}
sealed class C3 <: I2 {}
extend Int64 <: I2 {}
package B
import A.*

class S1 <: I1 {}  // OK
class S2 <: I2 {}  // Error, I2 is sealed interface, cannot be inherited here.

通过接口的这种约束能力,我们可以对一系列的类型约定共同的功能,达到对功能进行抽象的目的。

例如下面的代码,我们可以定义一个 Flyable 接口,并且让其他具有 Flyable 属性的类实现它。

interface Flyable {
    func fly(): Unit
}

class Bird <: Flyable {
    public func fly(): Unit {
        println("Bird flying")
    }
}

class Bat <: Flyable {
    public func fly(): Unit {
        println("Bat flying")
    }
}

class Airplane <: Flyable {
    public func fly(): Unit {
        println("Airplane flying")
    }
}

func fly(item: Flyable): Unit {
    item.fly()
}

main() {
    let bird = Bird()
    let bat = Bat()
    let airplane = Airplane()
    fly(bird)
    fly(bat)
    fly(airplane)
}

编译并执行上面的代码,我们会看到如下输出:

Bird flying
Bat flying
Airplane flying

接口的成员可以是实例的或者静态的,以上的例子已经展示过实例成员函数的作用,接下来我们来看看静态成员函数的作用。

静态成员函数和实例成员函数类似,都要求实现类型提供实现。

例如下面的例子,我们定义了一个 NamedType 接口,这个接口含有一个静态成员函数 typename 用来获得每个类型的字符串名称。

这样其它类型在实现 NamedType 接口时就必须实现 typename 函数,之后我们就可以安全地在 NamedType 的子类型上获得类型的名称。

interface NamedType {
    static func typename(): String
}

class A <: NamedType {
    public static func typename(): String {
        "A"
    }
}

class B <: NamedType {
    public static func typename(): String {
        "B"
    }
}

main() {
    println("the type is ${ A.typename() }")
    println("the type is ${ B.typename() }")
}

程序输出结果为:

the type is A
the type is B

接口中的静态成员函数(或属性)可以没有默认实现,也可以拥有默认实现。

当其没有默认实现时,将无法通过接口类型名对其进行访问。例如下面的代码,直接访问 NamedTypetypename 函数会发生编译报错,因为 NamedType 不具有 typename 函数的实现。

main() {
    NamedType.typename() // error
}

接口中的静态成员函数(或属性)也可以拥有默认实现,当另一个类型继承拥有默认静态函数(或属性)实现的接口时,该类型可以不再实现这个静态成员函数(或属性),该函数(或属性)可以通过接口名和该类型名直接访问。如下用例,NamedType 的成员函数 typename 拥有默认实现,且在 A 中都可以不用再重新实现它,同时,也可以通过接口名和该类型名对其进行直接访问。

interface NamedType {
    static func typename(): String {
        "interface NamedType"
    }
}

class A <: NamedType {}

main() {
    println(NamedType.typename())
    println(A.typename())
    0
}

程序输出结果为:

interface NamedType
interface NamedType

通常我们会通过泛型约束,在泛型函数中使用这类静态成员。

例如下面的 printTypeName 函数,当我们约束泛型变元 TNamedType 的子类型时,我们需要保证 T 的实例化类型中所有的静态成员函数(或属性)都必须拥有实现,以保证可以使用 T.typename 的方式访问泛型变元的实现,达到了我们对静态成员抽象的目的。详见泛型章节。

interface NamedType {
    static func typename(): String
}

interface I <: NamedType {
    static func typename(): String {
        f()
    }
    static func f(): String
}

class A <: NamedType {
    public static func typename(): String {
        "A"
    }
}

class B <: NamedType {
    public static func typename(): String {
        "B"
    }
}

func printTypeName<T>() where T <: NamedType {
    println("the type is ${ T.typename() }")
}

main() {
    printTypeName<A>() // Ok
    printTypeName<B>() // Ok
    printTypeName<I>() // Error: 'I' must implement all static function. Otherwise, an unimplemented 'f' is called, causing problems.
}

需要注意的是,接口的成员默认就被 public 修饰,不可以声明额外的访问控制修饰符,同时也要求实现类型必须使用 public 实现。

interface I {
    func f(): Unit
}

open class C <: I {
    protected func f() {} // Compiler error, f needs to be public semantics
}

接口继承

当我们想为一个类型实现多个接口,可以在声明处使用 & 分隔多个接口,实现的接口之间没有顺序要求。

例如下面的例子,我们可以让 MyInt 同时实现 Addable 和 Subtractable 两个接口。

interface Addable {
    func add(other: Int64): Int64
}

interface Subtractable {
    func sub(other: Int64): Int64
}

class MyInt <: Addable & Subtractable {
    var value = 0
    public func add(other: Int64): Int64 {
        value + other
    }
    public func sub(other: Int64): Int64 {
        value - other
    }
}

接口可以继承一个或多个接口,但不能继承类。与此同时,接口继承的时候可以添加新的接口成员。

例如下面的例子,Calculable 接口继承了 Addable 和 Subtractable 两个接口,并且增加了乘除两种运算符重载。

interface Addable {
    func add(other: Int64): Int64
}

interface Subtractable {
    func sub(other: Int64): Int64
}

interface Calculable <: Addable & Subtractable {
    func mul(other: Int64): Int64
    func div(other: Int64): Int64
}

这样实现类型实现 Calculable 接口时就必须同时实现加减乘除四种运算符重载,不能缺少任何一个成员。

class MyInt <: Calculable {
    var value = 0
    public func add(other: Int64): Int64 {
        value + other
    }
    public func sub(other: Int64): Int64 {
        value - other
    }
    public func mul(other: Int64): Int64 {
        value * other
    }
    public func div(other: Int64): Int64 {
        value / other
    }
}

MyInt 实现 Calculable 的同时,也同时实现了 Calculable 继承的所有接口,因此 MyInt 也实现了 Addable 和 Subtractable,即同时是它们的子类型。

main() {
    let myInt = MyInt()
    let add: Addable = myInt
    let sub: Subtractable = myInt
    let calc: Calculable = myInt
}

对于 interface 的继承,子接口如果继承了父接口中有默认实现的函数或属性,则在子接口中不允许仅写此函数或属性的声明(即没有默认实现),而是必须要给出新的默认实现,并且函数定义前的 override 修饰符(或 redef 修饰符)是可选的;子接口如果继承了父接口中没有默认实现的函数或属性,则在子接口中允许仅写此函数或属性的声明(当然也允许定义默认实现),并且函数声明或定义前的 override 修饰符(或 redef 修饰符)是可选的。

interface I1 {
   func f(a: Int64) {
        a
   }
   static func g(a: Int64) {
        a
   }
   func f1(a: Int64): Unit
   static func g1(a: Int64): Unit
}

interface I2 <: I1 {
    /*'override' is optional*/ func f(a: Int64) {
       a + 1
    }
    override func f(a: Int32) {} // error: override function 'f' does not have an overridden function from its supertypes
    static /*'redef' is optional*/ func g(a: Int64) {
       a + 1
    }
    /*'override' is optional*/ func f1(a: Int64): Unit {}
    static /*'redef' is optional*/ func g1(a: Int64): Unit {}
}

接口实现

仓颉所有的类型都可以实现接口,包括数值类型、Char、String、struct、class、enum、Tuple、函数以及其它类型。

一个类型实现接口有三种途径:

  1. 在定义类型时就声明实现接口,在以上的内容中我们已经见过相关例子。
  2. 通过扩展实现接口,这种方式详见扩展章节。
  3. 由语言内置实现,具体详见仓颉内置类型及库的相关文档。

实现类型声明实现接口时,需要实现接口中要求的所有成员,为此需要满足下面一些规则。

  1. 对于成员函数和操作符重载函数,要求实现类型提供的函数实现与接口对应的函数名称相同、参数列表相同、返回类型相同。
  2. 对于成员属性,要求是否被 mut 修饰保持一致,并且属性的类型相同。

所以大部分情况都如同上面的例子,我们需要让实现类型中包含与接口要求的一样的成员的实现。

但有个地方是个例外,如果接口中的成员函数或操作符重载函数的返回值类型是 class 类型,那么允许实现函数的返回类型是其子类型。

例如下面这个例子,I 中的 f 返回类型是一个 class 类型 Base,因此 C 中实现的 f 返回类型可以是 Base 的子类型 Sub

open class Base {}
class Sub <: Base {}

interface I {
    func f(): Base
}

class C <: I {
    public func f(): Sub {
        Sub()
    }
}

除此以外,接口的成员还可以为 class 类型提供默认实现。拥有默认实现的接口成员,当实现类型是 class 的时候,class 可以不提供自己的实现而继承接口的实现。

需要注意的是,默认实现只对类型是 class 的实现类型有效,对其它类型无效。

例如下面的代码中,SayHi 中的 say 拥有默认实现,因此 A 实现 SayHi 时可以继承 say 的实现,而 B 也可以选择提供自己的 say 实现。

interface SayHi {
    func say() {
        "hi"
    }
}

class A <: SayHi {}

class B <: SayHi {
    public func say() {
        "hi, B"
    }
}

特别地,如果一个类型在实现多个接口时,多个接口中包含同一个成员的默认实现,这时会发生多重继承的冲突,语言无法选择最适合的实现,因此这时接口中的默认实现也会失效,需要实现类型提供自己的实现。

例如下面的例子,SayHiSayHello 中都包含了 say 的实现,Foo 在实现这两个接口时就必须提供自己的实现,否则会出现编译错误。

interface SayHi {
    func say() {
        "hi"
    }
}

interface SayHello {
    func say() {
        "hello"
    }
}

class Foo <: SayHi & SayHello {
    public func say() {
        "Foo"
    }
}

struct、enum 和 class 在实现接口时,函数或属性定义前的 override 修饰符(或 redef 修饰符)是可选的,无论接口中的函数或属性是否存在默认实现。

interface I {
    func foo(): Int64 {
        return 0
    }
}
enum E <: I{
    elem
    public override func foo(): Int64 {
        return 1
    }
}
struct S <: I {
    public override func foo(): Int64 {
        return 1
    }
}

Any 类型

Any 类型是一个内置的接口,它的定义如下面。

interface Any {}

仓颉中所有接口都默认继承它,所有非接口类型都默认实现它,因此所有类型都可以作为 Any 类型的子类型使用。

如下面的代码,我们可以将一系列不同类型的变量赋值给 Any 类型的变量。

main() {
    var any: Any = 1
    any = 2.0
    any = "hello, world!"
}

子类型关系

与其他面向对象语言一样,仓颉语言提供子类型关系和子类型多态。举例说明(不限于下述用例):

  • 假设函数的形参是类型 T,则函数调用时传入的参数的实际类型既可以是 T 也可以是 T 的子类型(严格地说,T 的子类型已经包括 T 自身,下同)。
  • 假设赋值表达式 = 左侧的变量的类型是 T,则 = 右侧的表达式的实际类型既可以是 T 也可以是 T 的子类型。
  • 假设函数定义中用户标注的返回类型是 T,则函数体的类型(以及函数体内所有 return 表达式的类型)既可以是 T 也可以是 T 的子类型。

那么如何判定两个类型是否存在子类型关系呢?下面我们对此展开说明。

继承 class 带来的子类型关系

继承 class 后,子类即为父类的子类型。如下代码中, Sub 即为 Super 的子类型。

open class Super { }
class Sub <: Super { }

实现接口带来的子类型关系

实现接口(含扩展实现)后,实现接口的类型即为接口的子类型。如下代码中,I3I1I2 的子类型, CI1 的子类型, Int64I2 的子类型:

interface I1 { }
interface I2 { }

interface I3 <: I1 & I2 { }

class C <: I1 { }

extend Int64 <: I2 { }

需要注意的是,部分跨扩展类型赋值后的类型向下转换场景(isas)支持不完善,可能出现判断失败,计划在未来版本修复,见如下示例:

// file1.cj
package p1

public class A{}

public func get(): Any {
    return A()
}

// =====================
// file2.cj
import p1.*

interface I0 {}

extend A <: I0 {}

main() {
    let v: Any = get()
    println(v is I0) // 无法正确判断类型,打印内容不确定
}

元组类型的子类型关系

仓颉语言中的元组类型也有子类型关系。直观的,如果一个元组 t1 的每个元素的类型都是另一个元组 t2 的对应位置元素类型的子类型,那么元组 t1 的类型也是元组 t2 的类型的子类型。例如下面的代码中,由于 C2 <: C1C4 <: C3,因此也有 (C2, C4) <: (C1, C3) 以及 (C4, C2) <: (C3, C1)

open class C1 { }
class C2 <: C1 { }

open class C3 { }
class C4 <: C3 { }

let t1: (C1, C3) = (C2(), C4()) // OK
let t2: (C3, C1) = (C4(), C2()) // OK

函数类型的子类型关系

仓颉语言中,函数是一等公民,而函数类型亦有子类型关系:给定两个函数类型 (U1) -> S2(U2) -> S1(U1) -> S2 <: (U2) -> S1 当且仅当 U2 <: U1S2 <: S1(注意顺序)。例如下面的代码定义了两个函数 f : (U1) -> S2g : (U2) -> S1,且 f 的类型是 g 的类型的子类型。由于 f 的类型是 g 的子类型,所以代码中使用到 g 的地方都可以换为 f

open class U1 { }
class U2 <: U1 { }

open class S1 { }
class S2 <: S1 { }


func f(a: U1): S2 { S2() }
func g(a: U2): S1 { S1() }

func call1() {
    g(U2()) // OK.
    f(U2()) // OK.
}

func h(lam: (U2) -> S1): S1 {
    lam(U2())
}

func call2() {
    h(g) // OK.
    h(f) // OK.
}

对于上面的规则,S2 <: S1 部分很好理解:函数调用产生的结果数据会被后续程序使用,函数 g 可以产生 S1 类型的结果数据,函数 f 可以产生 S2 类型的结果,而 g 产生的结果数据的应当能被 f 产生的结果数据替代,因此要求 S2 <: S1

对于 U2 <: U1 的部分,可以这样理解:在函数调用产生结果前,它本身应当能够被调用,函数调用的实参类型固定不变,同时形参类型要求更宽松时,依然可以被调用,而形参类型要求更严格时可能无法被调用——例如给定上述代码中的定义 g(U2()) 可以被换为 f(U2()),正是因为实参类型 U2 的要求更严格于形参类型 U1

永远成立的子类型关系

仓颉语言中,有些预设的子类型关系是永远成立的:

  • 一个类型 T 永远是自身的子类型,即 T <: T
  • Nothing 类型永远是其他任意类型 T 的子类型,即 Nothing <: T
  • 任意类型 T 都是 Any 类型的子类型,即 T <: Any
  • 任意 class 定义的类型都是 Object 的子类型,即如果有 class C {},则 C <: Object

传递性带来的子类型关系

子类型关系具有传递性。如下代码中,虽然只描述了 I2 <: I1C <: I2,以及 Bool <: I2,但根据子类型的传递性,也隐式存在 C <: I1 以及 Bool <: I1 这两个子类型关系。

interface I1 { }
interface I2 <: I1 { }

class C <: I2 { }

extend Bool <: I2 { }

泛型类型的子类型关系

泛型类型间也有子类型关系,详见泛型章节。

类型转换

仓颉不支持不同类型之间的隐式转换(我们认为子类型天然是父类型,所以子类型到父类型的转换不是隐式类型转换),类型转换必须显式地进行。下面将依次介绍数值类型之间的转换,CharUInt32 和整数类型到 Char 的转换,以及 isas 操作符。

数值类型之间的转换

对于数值类型(包括:Int8Int16Int32Int64IntNativeUInt8UInt16UInt32UInt64UIntNativeFloat16Float32Float64),仓颉支持使用 T(e) 的方式得到一个值等于 e,类型为 T 的值。其中,表达式 e 的类型和 T 可以是上述任意数值类型。

下面的例子展示了数值类型之间的类型转换:

main() {
    let a: Int8 = 10
    let b: Int16 = 20
    let r1 = Int16(a)
    println("The type of r1 is 'Int16', and r1 = ${r1}")
    let r2 = Int8(b)
    println("The type of r2 is 'Int8', and r2 = ${r2}")

    let c: Float32 = 1.0
    let d: Float64 = 1.123456789
    let r3 = Float64(c)
    println("The type of r3 is 'Float64', and r3 = ${r3}")
    let r4 = Float32(d)
    println("The type of r4 is 'Float32', and r4 = ${r4}")

    let e: Int64 = 1024
    let f: Float64 = 1024.1024
    let r5 = Float64(e)
    println("The type of r5 is 'Float64', and r5 = ${r5}")
    let r6 = Int64(f)
    println("The type of r6 is 'Int64', and r6 = ${r6}")
}

上述代码的执行结果为:

The type of r1 is 'Int16', and r1 = 10
The type of r2 is 'Int8', and r2 = 20
The type of r3 is 'Float64', and r3 = 1.000000
The type of r4 is 'Float32', and r4 = 1.123457
The type of r5 is 'Float64', and r5 = 1024.000000
The type of r6 is 'Int64', and r6 = 1024

CharUInt32 和整数类型到 Char 的转换

CharUInt32 的转换使用 UInt32(e) 的方式,其中 e 是一个 Char 类型的表达式,UInt32(e) 的结果是 e 的 Unicode scalar value 对应的 UInt32 类型的整数值。

整数类型到 Char 的转换使用 Char(num) 的方式,其中 num 的类型可以是任意的整数类型,且仅当 num 的值落在 [0x0000, 0xD7FF][0xE000, 0x10FFFF] (即 Unicode scalar value)中时,返回对应的 Unicode scalar value 表示的字符,否则,编译报错(编译时可确定 num 的值)或运行时抛异常。

下面的例子展示了 CharUInt32 之间的类型转换:

main() {
    let x: Char = 'a'
    let y: UInt32 = 65
    let r1 = UInt32(x)
    let r2 = Char(y)
    println("The type of r1 is 'UInt32', and r1 = ${r1}")
    println("The type of r2 is 'Char', and r2 = ${r2}")
}

上述代码的执行结果为:

The type of r1 is 'UInt32', and r1 = 97
The type of r2 is 'Char', and r2 = A

isas 操作符

仓颉支持使用 is 操作符来判断某个表达式的类型是否是指定的类型(或其子类型)。具体而言,对于表达式 e is Te 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e is T 的值为 true,否则 e is T 的值为 false

下面的例子展示了 is 操作符的使用:

open class Base {
    var name: String = "Alice"
}
class Derived <: Base {
    var age: UInt8 = 18
}

main() {
    let a = 1 is Int64
    println("Is the type of 1 'Int64'? ${a}")
    let b = 1 is String
    println("Is the type of 1 'String'? ${b}")

    let b1: Base = Base()
    let b2: Base = Derived()
    var x = b1 is Base
    println("Is the type of b1 'Base'? ${x}")
    x = b1 is Derived
    println("Is the type of b1 'Derived'? ${x}")
    x = b2 is Base
    println("Is the type of b2 'Base'? ${x}")
    x = b2 is Derived
    println("Is the type of b2 'Derived'? ${x}")
}

上述代码的执行结果为:

Is the type of 1 'Int64'? true
Is the type of 1 'String'? false
Is the type of b1 'Base'? true
Is the type of b1 'Derived'? false
Is the type of b2 'Base'? true
Is the type of b2 'Derived'? true

as 操作符可以用于将某个表达式的类型转换为指定的类型。因为类型转换有可能会失败,所以 as 操作返回的是一个 Option 类型。具体而言,对于表达式 e as Te 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e as T 的值为 Option<T>.Some(e),否则 e as T 的值为 Option<T>.None

下面的例子展示了 as 操作符的使用(注释中标明了 as 操作的结果):

open class Base {
    var name: String = "Alice"
}
class Derived <: Base {
    var age: UInt8 = 18
}

let a = 1 as Int64     // a = Option<Int64>.Some(1)
let b = 1 as String    // b = Option<String>.None

let b1: Base = Base()
let b2: Base = Derived()
let d: Derived = Derived()
let r1 = b1 as Base    // r1 = Option<Base>.Some(b1)
let r2 = b1 as Derived // r2 = Option<Derived>.None
let r3 = b2 as Base    // r3 = Option<Base>.Some(b2)
let r4 = b2 as Derived // r4 = Option<Derived>.Some(b2)
let r5 = d as Base     // r5 = Option<Base>.Some(d)
let r6 = d as Derived  // r6 = Option<Derived>.Some(d)

类型别名

当某个类型的名字比较复杂或者在特定场景中不够直观时,可以选择使用类型别名的方式为此类型设置一个别名。

type I64 = Int64

类型别名的定义以关键字 type 开头,接着是类型的别名(如上例中的 I64),然后是等号 =,最后是原类型(即被取别名的类型,如上例中的 Int64)。

只能在源文件顶层定义类型别名,并且原类型必须在别名定义处可见。例如,下例中 Int64 的别名定义在 main 中将报错,LongNameClassB 类型在为其定义别名时不可见,同样报错。

main() {
    type I64 = Int64 // Error: type aliases can only be defined at the top level of the source file
}

class LongNameClassA { }
type B = LongNameClassB // Error: type 'LongNameClassB' is not defined

一个(或多个)类型别名定义中禁止出现(直接或间接的)循环引用。

type A = (Int64, A) // Error: 'A' refered itself

type B = (Int64, C) // Error: 'B' and 'C' are circularly refered
type C = (B, Int64)

类型别名并不会定义一个新的类型,它仅仅是为原类型定义了另外一个名字,它有如下几种使用场景:

  1. 作为类型使用,例如:

    type A = B
    class B {}
    var a: A = B() // Use typealias A as type B
    
  2. 当类型别名实际指向的类型为 class、struct 时,可以作为构造器名称使用:

    type A = B
    class B {}
    func foo() { A() }  // Use type alias A as constructor of B
    
  3. 当类型别名实际指向的类型为 class、interface、struct 时,可以作为访问内部静态成员变量或函数的类型名:

    type A = B
    class B {
        static var b : Int32 = 0;
        static func foo() {}
    }
    func foo() {
        A.foo() // Use A to access static method in class B
        A.b
    }
    
  4. 当类型别名实际指向的类型为 enum 时,可以作为 enum 声明的构造器的类型名:

    enum TimeUnit {
        Day | Month | Year
    }
    type Time = TimeUnit
    var a = Time.Day  
    var b = Time.Month   // Use type alias Time to access constructors in TimeUnit
    

需要注意的是,当前用户自定义的类型别名暂不支持在类型转换表达式中使用,参考如下示例:

type MyInt = Int32
MyInt(0)  // Error: no matching function for operator '()' function call

基础 Collection 类型

本章我们来看看仓颉中常用的几种基础 Collection 类型,包含 Array、ArrayList、HashSet、HashMap。

我们可以在不同的场景中选择适合我们业务的类型:

  • Array:如果我们不需要增加和删除元素,但需要修改元素,就应该使用它。
  • ArrayList:如果我们需要频繁对元素增删查改,就应该使用它。
  • HashSet:如果我们希望每个元素都是唯一的,就应该使用它。
  • HashMap:如果我们希望存储一系列的映射关系,就应该使用它。

下表是这些类型的基础特性:

类型名称元素可变增删元素元素唯一性有序序列
Array<T>YNNY
ArrayList<T>YYNY
HashSet<T>NYYN
HashMap<K, V>K: N, V: YYK: Y, V: NN

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

我们可以轻松使用字面量来初始化一个 Array,只需要使用方括号将逗号分隔的值列表括起来即可。

编译器会根据上下文自动推断 Array 字面量的类型。

let a: Array<String> = [] // Created an empty Array whose element type is String
let b = [1, 2, 3, 3, 2, 1] // Created a Array whose element type is Int64, containing elements 1, 2, 3, 3, 2, 1

也可以使用构造函数的方式构造一个指定元素类型的 Array。

需要注意的是,当通过 item 指定的初始值初始化 Array 时,该构造函数不会拷贝 item,如果 item 是一个引用类型,构造后数组的每一个元素都将指向相同的引用。

let a = Array<Int64>() // Created an empty Array whose element type is Int64
let b = Array<Int64>(a) // Use another Array to initialize b
let c = Array<Int64>(3, item: 0) // Created an Array whose element type is Int64, length is 3 and all elements are initialized as 0
let d = Array<Int64>(3, {i => i + 1}) /* Created an Array whose element type is Int64, length is 3 and all elements are initialized by the initialization function */

访问 Array 成员

当我们需要对 Array 的所有元素进行访问时,可以使用 for-in 循环遍历 Array 的所有元素。

Array 是按元素插入顺序排列的,因此对 Array 遍历的顺序总是恒定的。

main() {
    let arr = [0, 1, 2]
    for (i in arr) {
        println("The element is ${i}")
    }
}

编译并执行上面的代码,会输出:

The element is 0
The element is 1
The element is 2

当我们需要知道某个 Array 包含的元素个数时,可以使用 size 属性获得对应信息。

main() {
    let arr = [0, 1, 2]
    if (arr.size == 0) {
        println("This is an empty array")
    } else {
        println("The size of array is ${arr.size}")
    }
}

编译并执行上面的代码,会输出:

The size of array is 3

当我们想访问单个指定位置的元素时,可以使用下标语法访问(下标的类型必须是 Int64)。非空 Array 的第一个元素总是从位置 0 开始的。我们可以从 0 开始访问 Array 的任意一个元素,直到最后一个位置(Array 的 size - 1)。索引值不能使用负数或者大于等于 size,当编译期能检查出索引值非法时,会在编译时报错,否则会在运行时抛异常。

main() {
    let arr = [0, 1, 2]
    let a = arr[0] // a == 0
    let b = arr[1] // b == 1
    let c = arr[-1] // array size is '3', but access index is '-1', which would overflow
}

如果我们想获取某一段 Array 的元素,可以在下标中传入 Range 类型的值,就可以一次性取得 Range 对应范围的一段 Array。

let arr1 = [0, 1, 2, 3, 4, 5, 6]
let arr2 = arr1[0..5]
// arr2 contains the elements 0, 1, 2, 3, 4

当 Range 字面量在下标语法中使用时,我们可以省略 start 或 end。

当省略 start 时,Range 会从 0 开始;当省略 end 时,Range 的 end 会延续到最后一位。

let arr1 = [0, 1, 2, 3, 4, 5, 6]
let arr2 = arr1[..3]
// arr2 contains elements 0, 1, 2
let arr3 = arr1[2..]
// arr3 contains elements 2, 3, 4, 5, 6

修改 Array

Array 是一种长度不变的 Collection 类型,因此 Array 没有提供添加和删除元素的成员函数。

但是 Array 允许我们对其中的元素进行修改,同样使用下标语法。

main() {
    let arr = [0, 1, 2, 3, 4, 5]
    arr[0] = 3
    println("The first element is ${arr[0]}")
}

编译并执行上面的代码,会输出:

The first element is 3

Array 是引用类型,因此 Array 在作为表达式使用时不会拷贝副本,同一个 Array 实例的所有引用都会共享同样的数据。

因此对 Array 元素的修改会影响到该实例的所有引用。

let arr1 = [0, 1, 2]
let arr2 = arr1
arr2[0] = 3
// arr1 contains elements 3, 1, 2
// arr2 contains elements 3, 1, 2

ArrayList

使用 ArrayList 类型需要导入 collection 包:

from std import collection.*

仓颉使用 ArrayList<T> 表示 ArrayList 类型,T 表示 ArrayList 的元素类型,T 可以是任意类型。

ArrayList 具备非常好的扩容能力,适合于需要频繁增加和删除元素的场景。

相比 Array,ArrayList 既可以原地修改元素,也可以原地增加和删除元素。

ArrayList 的可变性是一个非常有用的特征,我们可以让同一个 ArrayList 实例的所有引用都共享同样的元素,并且对它们统一进行修改。

var a: ArrayList<Int64> = ... // ArrayList whose element type is Int64
var b: ArrayList<String> = ... // ArrayList whose element type is String

元素类型不相同的 ArrayList 是不相同的类型,所以它们之间不可以互相赋值。

因此以下例子是不合法的。

b = a // Type mismatch

仓颉中可以使用构造函数的方式构造一个指定的 ArrayList。

let a = ArrayList<String>() // Created an empty ArrayList whose element type is String
let b = ArrayList<String>(100) // Created an ArrayList whose element type is String, and allocate a space of 100
let c = ArrayList<Int64>([0, 1, 2]) // Created an ArrayList whose element type is Int64, containing elements 0, 1, 2
let d = ArrayList<Int64>(c) // Use another Collection to initialize an ArrayList
let e = ArrayList<String>(2, {x: Int64 => x.toString()}) // Created an ArrayList whose element type is String and size is 2. All elements are initialized by specified rule function

访问 ArrayList 成员

当我们需要对 ArrayList 的所有元素进行访问时,可以使用 for-in 循环遍历 ArrayList 的所有元素。

from std import collection.*

main() {
    let list = ArrayList<Int64>([0, 1, 2])
    for (i in list) {
        println("The element is ${i}")
    }
}

编译并执行上面的代码,会输出:

The element is 0
The element is 1
The element is 2

当我们需要知道某个 ArrayList 包含的元素个数时,可以使用 size 属性获得对应信息。

from std import collection.*

main() {
    let list = ArrayList<Int64>([0, 1, 2])
    if (list.size == 0) {
        println("This is an empty arraylist")
    } else {
        println("The size of arraylist is ${list.size}")
    }
}

编译并执行上面的代码,会输出:

The size of arraylist is 3

当我们想访问单个指定位置的元素时,可以使用下标语法访问(下标的类型必须是 Int64)。非空 ArrayList 的第一个元素总是从位置 0 开始的。我们可以从 0 开始访问 ArrayList 的任意一个元素,直到最后一个位置(ArrayList 的 size - 1)。使用负数或大于等于 size 的索引会触发运行时异常。

let a = list[0] // a == 0
let b = list[1] // b == 1
let c = list[-1] // Runtime exceptions

ArrayList 也支持下标中使用 Range 的语法,详见 Array 章节。

修改 ArrayList

我们可以使用下标语法对某个位置的元素进行修改。

let list = ArrayList<Int64>([0, 1, 2])
list[0] = 3

ArrayList 是引用类型,ArrayList 在作为表达式使用时不会拷贝副本,同一个 ArrayList 实例的所有引用都会共享同样的数据。

因此对 ArrayList 元素的修改会影响到该实例的所有引用。

let list1 = ArrayList<Int64>([0, 1, 2])
let list2 = list1
list2[0] = 3
// list1 contains elements 3, 1, 2
// list2 contains elements 3, 1, 2

如果需要将单个元素添加到 ArrayList 的末尾,请使用 append 函数。如果希望同时添加多个元素到末尾,可以使用 appendAll 函数,这个函数可以接受其它相同元素类型的 Collection 类型(例如 Array)。

from std import collection.*

main() {
    let list = ArrayList<Int64>()
    list.append(0) // list contains element 0
    list.append(1) // list contains elements 0, 1
    let li = [2, 3]
    list.appendAll(li) // list contains elements 0, 1, 2, 3
}

我们可以通过 insert 和 insertAll 函数将指定的单个元素或相同元素类型的 Collection 值插入到我们指定索引的位置。该索引处的元素和后面的元素会被挪后以腾出空间。

let list = ArrayList<Int64>([0, 1, 2]) // list contains elements 0, 1, 2
list.insert(1, 4) // list contains elements 0, 4, 1, 2

从 ArrayList 中删除元素,可以使用 remove 函数,需要指定删除的索引。该索引处后面的元素会挪前以填充空间。

let list = ArrayList<String>(["a", "b", "c", "d"]) // list contains the elements "a", "b", "c", "d"
list.remove(1) // Delete the element at subscript 1, now the list contains elements "a", "c", "d"

增加 ArrayList 的大小

每个 ArrayList 都需要特定数量的内存来保存其内容。当我们向 ArrayList 添加元素并且该 ArrayList 开始超出其保留容量时,该 ArrayList 会分配更大的内存区域并将其所有元素复制到新内存中。这种增长策略意味着触发重新分配内存的添加操作具有性能成本,但随着 ArrayList 的保留内存变大,它们发生的频率会越来越低。

如果我们知道大约需要添加多少个元素,可以在添加之前预备足够的内存以避免中间重新分配,这样可以提升性能表现。

from std import collection.*

main() {
    let list = ArrayList<Int64>(100) // Allocate space at once
    for (i in 0..100) {
        list.append(i) // Does not trigger reallocation of space
    }
    list.reserve(100) // Prepare more space
    for (i in 0..100) {
        list.append(i) // Does not trigger reallocation of space
    }
}

Iterable 和 Collections

前面我们已经了解过 Range、Array、ArrayList,它们都可以使用 for-in 进行遍历操作

那么对一个用户自定义类型,能不能实现类似的遍历操作呢?

答案是可以的。

Range、Array、ArrayList 其实都是通过 Iterable 来支持 for-in 语法的。

Iterable 是如下形式(只展示了核心代码)的一个内置 interface。

interface Iterable<T> {
    func iterator(): Iterator<T>
    ...
}

iterator 函数要求返回的 Iterator 类型是如下形式(只展示了核心代码)的另一个内置 interface。

interface Iterator<T> <: Iterable<T> {
    mut func next(): Option<T>
    ...
}

我们可以使用 for-in 语法来遍历任何一个实现了 Iterable 接口类型的实例。

假设有这样一个 for-in 代码。

let list = [1, 2, 3]
for (i in list) {
    println(i)
}

那么它等价于如下形式的 while 代码。

let list = [1, 2, 3]
var it = list.iterator()
while (true) {
    match (it.next()) {
        case Some(i) => println(i)
        case None => break
    }
}

另外一种常见的遍历 Iterable 类型的方法是使用 while-let,比如上面 while 代码的另一种等价写法是:

let list = [1, 2, 3]
var it = list.iterator()
while (let Some(i) <- it.next()) {
    println(i)
}

Array、ArrayList、HashSet、HashMap 类型都实现了 Iterable,因此我们都可以将其用在 for-in 或者 while-let 中。

HashSet

使用 HashSet 类型需要导入 collection 包:

from std import collection.*

我们可以使用 HashSet 类型来构造只拥有不重复元素的 Collection。

仓颉使用 HashSet<T> 表示 HashSet 类型,T 表示 HashSet 的元素类型,T 必须是实现了 Hashable 和 Equatable<T> 接口的类型,例如数值或 String。

var a: HashSet<Int64> = ... // HashSet whose element type is Int64
var b: HashSet<String> = ... // HashSet whose element type is String

元素类型不相同的 HashSet 是不相同的类型,所以它们之间不可以互相赋值。

因此以下例子是不合法的。

b = a // Type mismatch

仓颉中可以使用构造函数的方式构造一个指定的 HashSet。

let a = HashSet<String>() // Created an empty HashSet whose element type is String
let b = HashSet<String>(100) // Created a HashSet whose capacity is 100
let c = HashSet<Int64>([0, 1, 2]) // Created a HashSet whose element type is Int64, containing elements 0, 1, 2
let d = HashSet<Int64>(c) // Use another Collection to initialize a HashSet
let e = HashSet<Int64>(10, {x: Int64 => (x * x)}) // Created a HashSet whose element type is Int64 and size is 10. All elements are initialized by specified rule function

访问 HashSet 成员

当我们需要对 HashSet 的所有元素进行访问时,可以使用 for-in 循环遍历 HashSet 的所有元素。

需要注意的是,HashSet 并不保证按插入元素的顺序排列,因此遍历的顺序和插入的顺序可能不同。

from std import collection.*

main() {
    let mySet = HashSet<Int64>([0, 1, 2])
    for (i in mySet) {
        println("The element is ${i}")
    }
}

编译并执行上面的代码,有可能会输出:

The element is 0
The element is 1
The element is 2

当我们需要知道某个 HashSet 包含的元素个数时,可以使用 size 属性获得对应信息。

from std import collection.*

main() {
    let mySet = HashSet<Int64>([0, 1, 2])
    if (mySet.size == 0) {
        println("This is an empty hashset")
    } else {
        println("The size of hashset is ${mySet.size}")
    }
}

编译并执行上面的代码,会输出:

The size of hashset is 3

当我们想判断某个元素是否被包含在某个 HashSet 中时,可以使用 contains 函数。如果该元素存在会返回 true,否则返回 false。

let mySet = HashSet<Int64>([0, 1, 2])
let a = mySet.contains(0) // a == true
let b = mySet.contains(-1) // b == false

修改 HashSet

HashSet 是一种可变的引用类型,HashSet 类型提供了添加元素、删除元素的功能。

HashSet 的可变性是一个非常有用的特征,我们可以让同一个 HashSet 实例的所有引用都共享同样的元素,并且对它们统一进行修改。

如果需要将单个元素添加到 HashSet,请使用 put 函数。如果希望同时添加多个元素,可以使用 putAll 函数,这个函数可以接受另一个相同元素类型的 Collection 类型(例如 Array)。当元素不存在时,put 函数会执行添加的操作,当 HashSet 中存在相同元素时,put 函数将不会有效果。

let mySet = HashSet<Int64>()
mySet.put(0) // mySet contains elements 0
mySet.put(0) // mySet contains elements 0
mySet.put(1) // mySet contains elements 0, 1
let li = [2, 3]
mySet.putAll(li) // mySet contains elements 0, 1, 2, 3

HashSet 是引用类型,HashSet 在作为表达式使用时不会拷贝副本,同一个 HashSet 实例的所有引用都会共享同样的数据。

因此对 HashSet 元素的修改会影响到该实例的所有引用。

let set1 = HashSet<Int64>([0, 1, 2])
let set2 = set1
set2.put(3)
// set1 contains elements 0, 1, 2, 3
// set2 contains elements 0, 1, 2, 3

从 HashSet 中删除元素,可以使用 remove 函数,需要指定删除的元素。

let mySet = HashSet<Int64>([0, 1, 2, 3])
mySet.remove(1) // mySet contains elements 0, 2, 3

HashMap

使用 HashMap 类型需要导入 collection 包:

from std import collection.*

我们可以使用 HashMap 类型来构造元素为键值对的 Collection。

HashMap 是一种哈希表,提供对其包含的元素的快速访问。表中的每个元素都使用其键作为标识,我们可以使用键来访问相应的值。

仓颉使用 HashMap<K, V> 表示 HashMap 类型,K 表示 HashMap 的键类型,K 必须是实现了 Hashable 和 Equatable<K> 接口的类型,例如数值或 String。V 表示 HashMap 的值类型,V 可以是任意类型。

var a: HashMap<Int64, Int64> = ... // HashMap whose key type is Int64 and value type is Int64
var b: HashMap<String, Int64> = ... // HashMap whose key type is String and value type is Int64

元素类型不相同的 HashMap 是不相同的类型,所以它们之间不可以互相赋值。

因此以下例子是不合法的。

b = a // Type mismatch

仓颉中可以使用构造函数的方式构造一个指定的 HashMap。

let a = HashMap<String, Int64>() // Created an empty HashMap whose key type is String and value type is Int64
let b = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)]) // whose key type is String and value type is Int64, containing elements ("a", 0), ("b", 1), ("c", 2)
let c = HashMap<String, Int64>(b) // Use another Collection to initialize a HashMap
let d = HashMap<String, Int64>(10) // Created a HashMap whose key type is String and value type is Int64 and capacity is 10
let e = HashMap<Int64, Int64>(10, {x: Int64 => (x, x * x)}) // Created a HashMap whose key and value type is Int64 and size is 10. All elements are initialized by specified rule function

访问 HashMap 成员

当我们需要对 HashMap 的所有元素进行访问时,可以使用 for-in 循环遍历 HashMap 的所有元素。

需要注意的是,HashMap 并不保证按插入元素的顺序排列,因此遍历的顺序和插入的顺序可能不同。

from std import collection.*

main() {
    let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
    for ((k, v) in map) {
        println("The key is ${k}, the value is ${v}")
    }
}

编译并执行上面的代码,有可能会输出:

The key is a, the value is 0
The key is b, the value is 1
The key is c, the value is 2

当我们需要知道某个 HashMap 包含的元素个数时,可以使用 size 属性获得对应信息。

from std import collection.*

main() {
    let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
    if (map.size == 0) {
        println("This is an empty hashmap")
    } else {
        println("The size of hashmap is ${map.size}")
    }
}

编译并执行上面的代码,会输出:

The size of hashmap is 3

当我们想判断某个键是否被包含 HashMap 中时,可以使用 contains 函数。如果该键存在会返回 true,否则返回 false。

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
let a = map.contains("a") // a == true
let b = map.contains("d") // b == false

当我们想访问指定键对应的元素时,可以使用下标语法访问(下标的类型必须是键类型)。使用不存在的键作为索引会触发运行时异常。

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
let a = map["a"] // a == 0
let b = map["b"] // b == 1
let c = map["d"] // Runtime exceptions

修改 HashMap

HashMap 是一种可变的引用类型,HashMap 类型提供了修改元素、添加元素、删除元素的功能。

HashMap 的可变性是一个非常有用的特征,我们可以让同一个 HashMap 实例的所有引用都共享同样的元素,并且对它们统一进行修改。

我们可以使用下标语法对某个键对应的值进行修改。

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
map["a"] = 3

HashMap 是引用类型,HashMap 在作为表达式使用时不会拷贝副本,同一个 HashMap 实例的所有引用都会共享同样的数据。

因此对 HashMap 元素的修改会影响到该实例的所有引用。

let map1 = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
let map2 = map1
map2["a"] = 3
// map1 contains the elements ("a", 3), ("b", 1), ("c", 2)
// map2 contains the elements ("a", 3), ("b", 1), ("c", 2)

如果需要将单个键值对添加到 HashMap,请使用 put 函数。如果希望同时添加多个键值对,可以使用 putAll 函数。当键不存在时,put 函数会执行添加的操作,当键存在时,put 函数会将新的值覆盖旧的值。

let map = HashMap<String, Int64>()
map.put("a", 0) // map contains the element ("a", 0)
map.put("b", 1) // map contains the elements ("a", 0), ("b", 1)
let map2 = HashMap<String, Int64>([("c", 2), ("d", 3)])
map.putAll(map2) // map contains the elements ("a", 0), ("b", 1), ("c", 2), ("d", 3)

除了使用 put 函数以外,我们也可以使用赋值的方式直接将新的键值对添加到 HashMap。

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2)])
map["d"] = 3 // map contains the elements ("a", 0), ("b", 1), ("c", 2), ("d", 3)

从 HashMap 中删除元素,可以使用 remove 函数,需要指定删除的键。

let map = HashMap<String, Int64>([("a", 0), ("b", 1), ("c", 2), ("d", 3)])
map.remove("d") // map contains the elements ("a", 0), ("b", 1), ("c", 2)

错误处理

异常

异常是一类特殊的可以被程序员捕获并处理的错误,是程序执行时出现的一系列不正常行为的统称,例如,数组越界、除零错误、计算溢出、非法输入等。为了保证系统的正确性和健壮性,很多软件系统中都包含大量的代码用于错误检测和错误处理。

异常不属于程序的正常功能,一旦发生异常,要求程序必须立即处理,即将程序的控制权从正常功能的执行处转移至处理异常的部分。仓颉编程语言提供异常处理机制用于处理程序运行时可能出现的各种异常情况。

定义异常

在仓颉中,异常类有 ExceptionError

  • Error 类描述仓颉语言运行时,系统内部错误和资源耗尽错误,应用程序不应该抛出这种类型错误,如果出现内部错误,只能通知给用户,尽量安全终止程序。
  • Exception 类描述的是程序运行时的逻辑错误或者 IO 错误导致的异常,例如数组越界或者试图打开一个不存在的文件等,这类异常需要在程序中捕获处理。

用户不可以通过继承仓颉语言内置的 Error 或其子类类来自定义异常,但是可以继承内置的 Exception 或其子类来自定义异常,例如

open class FatherException <: Exception {
    public open func printException() {
        print("I am a FatherException")
    }
}

class ChildException <: FatherException {
    public override func printException() {
        print("I am a ChildException")
    }
}

下面列表展示了 Exception 的主要函数及其说明

函数种类函数及说明
构造函数init() 默认构造函数
构造函数init(message: String) 可以设置异常消息的构造函数
成员属性open prop message: String 返回发生异常的详细信息。该消息在异常类构造函数中初始化,默认空字符串。
成员函数open func toString(): String 返回异常类型名以及异常的详细信息,其中,异常的详细信息会默认调用 message。
成员函数func printStackTrace(): Unit 打印堆栈信息至标准错误流。

下面列表展示了 Error 的主要函数及其说明

函数种类函数及说明
成员属性open prop message: String 返回发生错误的详细信息。该消息在错误发生时,内部初始化,默认空字符串。
成员函数open func toString(): String 返回错误类型名以及错误的详细信息,其中,错误的详细信息会默认调用 message。
成员函数func printStackTrace(): Unit 打印堆栈信息至标准错误流。

创建和抛出异常

上文介绍了如何自定义异常,接下来我们学习如何创建和抛出异常。

  • 由于异常是 class 类型,只需要按 class 对象的构建方式去创建异常即可。如表达式 FatherException() 即创建了一个类型为 FatherException 的异常。
  • 仓颉语言提供 throw 关键字,用于抛出异常。用 throw 来抛出异常时,throw 之后的表达式必须是 Exception 的子类型(同为异常的 Error 不可以手动 throw ),如 throw ArithmeticException("I am an Exception!") (被执行到时)会抛出一个算术运算异常。
  • throw 关键字抛出的异常需要被捕获处理。若异常没有被捕获,则由系统调用默认的异常处理函数。

具体的创建、处理示例,请参考下一小节 [异常处理]。

异常处理

异常处理由 try 表达式完成,可分为:

  • 不涉及资源自动管理的普通 try 表达式;
  • 会进行资源自动管理 try-with-resources 表达式。

普通 try 表达式

普通 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 后面紧跟的块以及每个 catch 块的的作用域互相独立。

下面是一个只有 try 块和 catch 块的简单示例:

main() {
    try {
        throw NegativeArraySizeException("I am an Exception!")
    } catch (e: NegativeArraySizeException) {
        println(e)
        println("NegativeArraySizeException is caught!")
    }
    println("This will also be printed!")
}

执行结果为

NegativeArraySizeException: I am an Exception!
NegativeArraySizeException is caught!
This will also be printed!

(catchPattern) 中引入的变量作用域级别与 catch 后面的块中变量作用域级别相同,在 catch 块中再次引入相同名字会触发重定义错。例如:

main() {
    try {
        throw NegativeArraySizeException("I am an Exception!")
    } catch (e: NegativeArraySizeException) {
        println(e)
        let e = 0 // Error, redefinition
        println(e)
        println("NegativeArraySizeException is caught!")
    }
    println("This will also be printed!")
}

下面是带有 finally 块的 try 表达式的简单示例:

main() {
    try {
        throw NegativeArraySizeException("NegativeArraySizeException")
    } catch (e: NegativeArraySizeException) {
        println("Exception info: ${e}.")
    } finally {
        println("The finally block is executed.")
    }
}

执行结果为

Exception info: NegativeArraySizeException: NegativeArraySizeException.
The finally block is executed.
普通 try 表达式的类型

Try 表达式可以出现在任何允许使用表达式的地方。Try 表达式的类型的确定方式,与 ifmatch 表达式等多分支语法结构的类型的确定方式相似(参见[if 表达式的类型],[match 表达式的类型]),即为 finally 分支除外的所有分支的类型的最小公共父类型。例如下面代码中的 try 表达式和变量 x 的类型均为 E 和 D 的最小公共父类型 D;finally 分支中的 C() 并不参与公共父类型的计算(若参与,则最小公共父类型会变为 C)。 另外,当 Try 表达式的值没有被使用时,其类型为 Unit,不要求各分支的类型有最小公共父类型。

open class C { }
open class D <: C { }
class E <: D { }
main () {
    let x = try {
        E()
    } catch (e: Exception) {
        D()
    } finally {
        C()
    }
    0
}

Try-with-resources 表达式

Try-with-resources 表达式主要是为了自动释放非内存资源。不同于普通 try 表达式,try-with-resources 表达式中的 catch 块和 finally 块均是可选的,并且 try 关键字其后的块之间可以插入一个或者多个 ResourceSpecification 用来申请一系列的资源(ResourceSpecification 并不会影响整个 try 表达式的类型)。这里所讲的资源对应到语言层面即指对象,因此 ResourceSpecification 其实就是实例化一系列的对象(多个实例化之间使用“,”分隔)。使用 try-with-resources 表达式的例子如下所示:

class R <: Resource {
    public func isClosed(): Bool {
        true
    }
    public func close(): Unit {
        print("R is closed")
    }
}

main() {
    try (r = R()) {
        println("Get the resource")
    }
}

程序输出结果为:

Get the resource

try 关键字和 {} 之间引入的名字,其作用域与 {} 中引入的变量作用域级别相同,在 {} 中再次引入相同名字会触发重定义错。

class R <: Resource {
    public func isClosed(): Bool {
        true
    }
    public func close(): Unit {
        print("R is closed")
    }
}

main() {
    try (r = R()) {
        println("Get the resource")
        let r = 0 // Error, redefinition
        println(r)
    }
}

Try-with-resources 表达式中的 ResourceSpecification 的类型必须实现 Resource 接口,并且尽量保证其中的 isClosed 函数不要再抛异常:

interface Resource {
    func isClosed(): Bool
    func close(): Unit
}

需要说明的是,try-with-resources 表达式中一般没有必要再包含 catch 块和 finally 块,也不建议用户再手动释放资源。因为 try 块执行的过程中无论是否发生异常,所有申请的资源都会被自动释放,并且执行过程中产生的异常均会被向外抛出。但是,如果需要显式地捕获 try 块或资源申请和释放过程中可能抛出的异常并处理,仍可在 try-with-resources 表达式中包含 catch 块和 finally 块:

class R <: Resource {
    public func isClosed(): Bool {
        true
    }
    public func close(): Unit {
        print("R is closed")
    }
}

main() {
    try (r = R()) {
        println("Get the resource")
    } catch (e: Exception) {
        println("Exception happened when executing the try-with-resources expression")
    } finally {
        println("End of the try-with-resources expression")
    }
}

程序输出结果如下:

Get the resource
End of the try-with-resources expression

Try-with-resources 表达式的类型是 Unit

CatchPattern 进阶介绍

大多数时候,我们只想捕获某一类型和其子类型的异常,这时候我们使用 CatchPattern 的类型模式来处理;但有时也需要所有异常做统一处理(如此处不该出现异常,出现了就统一报错),这时可以使用 CatchPattern 的通配符模式来处理。

类型模式在语法上有两种格式:

  • Identifier: ExceptionClass。此格式可以捕获类型为 ExceptionClass 及其子类的异常,并将捕获到的异常实例转换成 ExceptionClass,然后与 Identifier 定义的变量进行绑定,接着就可以在 catch 块中通过 Identifier 定义的变量访问捕获到的异常实例。
  • Identifier: ExceptionClass_1 | ExceptionClass_2 | ... | ExceptionClass_n。此格式可以通过连接符|将多个异常类进行拼接,连接符 | 表示“或”的关系:可以捕获类型为 ExceptionClass_1 及其子类的异常,或者捕获类型为 ExceptionClass_2 及其子类的异常,依次类推,或捕获类型为 ExceptionClass_n 及其子类的异常(假设 n 大于 1)。当待捕获异常的类型属于上述“或”关系中的任一类型或其子类型时,此异常将被捕获。但是由于无法静态地确定被捕获异常的类型,所以被捕获异常的类型会被转换成由|连接的所有类型的最小公共父类,并将异常实例与 Identifier 定义的变量进行绑定。因此在此类模式下,catch 块内只能通过 Identifier 定义的变量访问 ExceptionClass_i(1 <= i <= n) 的最小公共父类中的成员变量和成员函数。当然,也可以使用通配符代替类型模式中的 Identifier,差别仅在于通配符不会进行绑定操作。

示例如下:

main(): Int64 {
    try {
        throw IllegalArgumentException("This is an Exception!")
    } catch (e: OverflowException) {
        println(e.message)
        println("OverflowException is caught!")
    } catch (e: IllegalArgumentException | NegativeArraySizeException) {
        println(e.message)
        println("IllegalArgumentException or NegativeArraySizeException is caught!")
    } finally {
        println("finally is executed!")
    }
    return 0
}

执行结果:

This is an Exception!
IllegalArgumentException or NegativeArraySizeException is caught!
finally is executed!

关于“被捕获异常的类型是由|连接的所有类型的最小公共父类”的示例:

open class Father <: Exception {
    var father: Int32 = 0
}

class ChildOne <: Father {
    var childOne: Int32 = 1
}

class ChildTwo <: Father {
    var childTwo: Int32 = 2
}

main() {
    try {
        throw ChildOne()
    } catch (e: ChildTwo | ChildOne) {
        println("ChildTwo or ChildOne?")
    }
}

通配符模式的语法是 _,它可以捕获同级 try 块内抛出的任意类型的异常,等价于类型模式中的 e: Exception,即捕获 Exception 子类所定义的异常。示例:

// Catch with wildcardPattern.
try {
    throw OverflowException()
} catch (_) {
    println("catch an exception!")
}

常见运行时异常

在仓颉语言中内置了最常见的异常类,开发人员可以直接使用。

异常描述
ConcurrentModificationException并发修改产生的异常
IllegalArgumentException传递不合法或不正确参数时抛出的异常
NegativeArraySizeException创建大小为负的数组时抛出的异常
NoneValueException值不存在时产生的异常,如 Map 中不存在要查找的 key
OverflowException算术运算溢出异常

Option 类型用于错误处理

在[Option 类型]中我们介绍了 Option 类型的定义,因为 Option 类型可以同时表示有值和无值两种状态,而无值在某些情况下也可以理解为一种错误,所以 Option 类型也可以用作错误处理。

例如,在下例中,如果函数 getOrThrow 的参数值等于 Some(v) 则将 v 的值返回,如果参数值等于 None 则抛出异常。

func getOrThrow(a: ?Int64) {
    match (a) {
        case Some(v) => v
        case None => throw NoneValueException()
    }
}

因为 Option 是一种非常常用的类型,所以仓颉为其提供了多种解构方式,以方便 Option 类型的使用,具体包括:模式匹配、getOrThrow 函数、coalescing 操作符(??),以及问号操作符(?)。下面将对这些方式逐一介绍。

  1. 模式匹配:因为 Option 类型是一种 enum 类型,所以可以使用上文提到的 enum 的模式匹配来实现对 Option 值的解构。例如,下例中函数 getString 接受一个 ?Int64 类型的参数,当参数是 Some 值时,返回其中数值的字符串表示,当参数是 None 值时,返回字符串 "none"

    func getString(p: ?Int64): String{
        match (p) {
            case Some(x) => "${x}"
            case None => "none"
        }
    }
    main() {
        let a = Some(1)
        let b: ?Int64 = None
        let r1 = getString(a)
        let r2 = getString(b)
        println(r1)
        println(r2)
    }
    

    上述代码的执行结果为:

    1
    none
    
  2. coalescing 操作符(??):对于 ?T 类型的表达式 e1,如果希望 e1 的值等于 None 时同样返回一个 T 类型的值 e2,可以使用 ?? 操作符。对于表达式 e1 ?? e2,当 e1 的值等于 Some(v) 时返回 v 的值,否则返回 e2 的值。举例如下:

    main() {
        let a = Some(1)
        let b: ?Int64 = None
        let r1: Int64 = a ?? 0
        let r2: Int64 = b ?? 0
        println(r1)
        println(r2)
    }
    

    上述代码的执行结果为:

    1
    0
    
  3. 问号操作符(?):? 需要和 .()[]{}(特指尾随 lambda 调用的场景)一起使用,用以实现 Option 类型对 .()[]{} 的支持。以 . 为例(()[]{}同理),对于 ?T1 类型的表达式 e,当 e 的值等于 Some(v) 时,e?.b 的值等于 Option<T2>.Some(v.b),否则 e?.b 的值等于 Option<T2>.None,其中 T2v.b 的类型。举例如下:

    struct R {
        public var a: Int64
        public init(a: Int64) {
            this.a = a
        }
    }
    
    let r = R(100)
    let x = Some(r)
    let y = Option<R>.None
    let r1 = x?.a   // r1 = Option<Int64>.Some(100)
    let r2 = y?.a   // r2 = Option<Int64>.None
    

    问号操作符(?)支持多层访问,以 a?.b.c?.d 为例(()[]{}同理)。表达式 a 的类型需要是某个 Option<T1>T1 包含实例成员 bb 的类型中包含实例成员变量 cc 的类型是某个 Option<T2>T2 包含实例成员 d;表达式 a?.b.c?.d 的类型为 Option<T3>,其中 T3T2 的实例成员 d 的类型;当 a 的值等于 Some(va)va.b.c 的值等于 Some(vc) 时,a?.b.c?.d 的值等于 Option<T3>.Some(vc.d);当 a 的值等于 Some(va)va.b.c 的值等于 None 时,a?.b.c?.d 的值等于 Option<T3>.Noned 不会被求值);当 a 的值等于 None 时,a?.b.c?.d 的值等于 Option<T3>.Nonebcd 都不会被求值)。

    struct A {
        let b: B = B()
    }
    
    struct B {
        let c: Option<C> = C()
        let c1: Option<C> = Option<C>.None
    }
    
    struct C {
        let d: Int64 = 100
    }
    
    let a = Some(A())
    let a1 = a?.b.c?.d // a1 = Option<Int64>.Some(100)
    let a2 = a?.b.c1?.d // a2 = Option<Int64>.None
    
  4. getOrThrow 函数:对于 ?T 类型的表达式 e,可以通过调用 getOrThrow 函数实现解构。当 e 的值等于 Some(v) 时,getOrThrow() 返回 v 的值,否则抛出异常。举例如下:

    main() {
        let a = Some(1)
        let b: ?Int64 = None
        let r1 = a.getOrThrow()
        println(r1)
        try {
            let r2 = b.getOrThrow()
        } catch (e: NoneValueException) {
            println("b is None")
        }
    }
    

    上述代码的执行结果为:

    1
    b is None
    

函数

函数定义

仓颉使用关键字 func 来表示函数定义的开始,func 之后依次是函数名、参数列表、可选的函数返回值类型、函数体。其中,函数名可以是任意的合法标识符,参数列表定义在一对圆括号内(多个参数间使用逗号分隔),参数列表和函数返回值类型(如果存在)之间使用冒号分隔,函数体定义在一对花括号内。

函数定义举例:

func add(a: Int64, b: Int64): Int64 {
    return a + b
}

上例中定义了一个名为 add 的函数,其参数列表由两个 Int64 类型的参数 ab 组成,函数返回值类型为 Int64,函数体中将 ab 相加并返回。

下面依次对函数定义中的参数列表、函数返回值类型和函数体作进一步介绍。

参数列表

一个函数可以拥有 0 个或多个参数,这些参数均定义在函数的参数列表中。根据函数调用时是否需要给定参数名,可以将参数列表中的参数分为两类:非命名参数和命名参数。

非命名参数的定义方式是 p: T,其中 p 表示参数名,T 表示参数 p 的类型,参数名和其类型间使用冒号连接。例如,上例中 add 函数的两个参数 ab 均为非命名参数。

命名参数的定义方式是 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
}

注:只能为命名参数设置默认值,不能为非命名参数设置默认值。

参数列表中可以同时定义非命名参数和命名参数,但是需要注意的是,非命名参数只能定义在命名参数之前,也就意味着命名参数之后不能再出现非命名参数。例如,下例中 add 函数的参数列表定义是不合法的:

func add(a!: Int64, b: Int64): Int64 { // Error: named parameter 'a' must be defined after non-named parameter 'b'
    return a + b
}

非命名参数和命名参数的主要差异在于调用时的不同,具体可参见下文[函数调用]中的介绍。

函数参数均为不可变变量,在函数定义内不能对其赋值。

func add(a: Int64, b: Int64): Int64 {
    a = a + b // error
    return a
}

函数参数作用域从定义处起至函数体结束:

func add(a: Int64, b: Int64): Int64 {
    var a_ = a // OK
    var b = b  // Error: redefinition of declaration 'b'
    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)
}

在函数定义时如果未显式定义返回值类型,编译器将根据函数体的类型以及函数体中所有的 return 表达式来共同推导出函数的返回值类型。例如,下例中 add 函数的返回值类型虽然被省略,但编译器可以根据 return a + b 推导出 add 函数的返回值类型是 Int64:

func add(a: Int64, b: Int64) {
    return a + b
}

注:函数的返回值类型并不是任何情况下都可以被推导出来的,如果返回值类型推导失败,编译器会报错。

函数体

函数体中定义了函数被调用时执行的操作,通常包含一系列的变量定义和表达式,也可以包含新的函数定义(即嵌套函数)。如下 add 函数的函数体中首先定义了 Int64 类型的变量 r(初始值为 0),接着将 a + b 的值赋值给 r,最后将 r 的值返回:

func add(a: Int64, b: Int64) {
    var r = 0
    r = a + b
    return r
}

在函数体的任意位置都可以使用 return 表达式来终止函数的执行并返回。return 表达式有两种形式:returnreturn exprexpr 是一个表达式)。

对于 return expr,要求 expr 的类型与函数定义中的返回值类型保持一致。例如,下例中会因为 return 100100 类型(Int64)和函数 foo 的返回值类型(String)不同而报错。

// error: cannot convert an integer literal to type 'Struct-String'
func foo(): String {
    return 100
}

对于 return,其等价于 return (),所以要求函数的返回值类型为 Unit

func add(a: Int64, b: Int64) {
    var r = 0
    r = a + b
    return r
}

func foo(): Unit {
    add(1, 2)
    return
}

注:return 表达式作为一个整体,其类型并不由后面跟随的表达式决定,而是 Nothing 类型。

在函数体内定义的变量属于局部变量的一种(如上例中的 r 变量),它的作用域从其定义之后开始到函数体结束。

对于一个局部变量,允许在其外层作用域中定义同名变量,并且在此局部变量的作用域内,局部变量会“遮盖”外层作用域的同名变量。例如:

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"
    print(s)
}

函数调用

函数调用的形式为 f(arg1, arg2, ..., argn)。其中,f 是要调用的函数的名字,arg1argnn 个调用时的参数(称为实参),要求每个实参的类型必须是对应参数类型的子类型。实参可以有 0 个或多个,当实参个数为 0 时,调用方式为 f()

根据函数定义时参数是非命名参数还是命名参数的不同,函数调用时传实参的方式也有所不同:对于非命名参数,它对应的实参是一个表达式,对于命名参数,它对应的实参需要使用 p: e 的形式,其中 p 是命名参数的名字,e 是表达式(即传递给参数 p 的值)。

非命名参数调用举例:

func add(a: Int64, b: Int64) {
    return a + b
}

main() {
    let x = 1
    let y = 2
    let r = add(x, y)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 3

命名参数调用举例:

func add(a: Int64, b!: Int64) {
    return a + b
}

main() {
    let x = 1
    let y = 2
    let r = add(x, b: y)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 3

对于多个命名参数,调用时的传参顺序可以和定义时的参数顺序不同。例如,下例中调用 add 函数时 b 可以出现在 a 之前:

func add(a!: Int64, b!: Int64) {
    return a + b
}

main() {
    let x = 1
    let y = 2
    let r = add(b: y, a: x)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 3

对于拥有默认值的命名参数,调用时如果没有传实参,那么此参数将使用默认值作为实参的值。例如,下例中调用 add 函数时没有为参数 b 传实参,那么参数 b 的值等于其定义时的默认值 2

func add(a: Int64, b!: Int64 = 2) {
    return a + b
}

main() {
    let x = 1
    let r = add(x)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 3

对于拥有默认值的命名参数,调用时也可以为其传递新的实参,此时命名参数的值等于新的实参的值,即定义时的默认值将失效。例如,下例中调用 add 函数时为参数 b 传了新的实参值 20,那么参数 b 的值就等于 20

func add(a: Int64, b!: Int64 = 2) {
    return a + b
}

main() {
    let x = 1
    let r = add(x, b: 20)
    println("The sum of x and y is ${r}")
}

执行结果为:

The sum of x and y is 21

函数是一等公民

仓颉编程语言中,函数是一等公民(first-class citizens),可以作为函数的参数或返回值,也可以赋值给变量。因此函数本身也有类型,称之为函数类型。

函数类型

函数类型由函数的参数类型和返回类型组成,参数类型和返回类型之间使用 -> 连接。参数类型使用圆括号 () 括起来,可以有 0 个或多个参数,如果参数超过两个,参数类型之间使用逗号 , 分隔。

例如:

func hello(): Unit {
    println("Hello!")
}

上述示例定义了一个函数,函数名为 hello,其类型是 () -> Unit,表示该函数没有参数,返回类型为 Unit

以下给出另一些示例:

  • 示例:函数名为 display,其类型是 (Int64) -> Unit,表示该函数有一个参数,参数类型为 Int64,返回类型为 Unit

    func display(a: Int64): Unit {
        println(a)
    }
    
  • 示例:函数名为 add,其类型是 (Int64, Int64) -> Int64,表示该函数有两个参数,两个参数类型均为 Int64,返回类型为 Int64

    func add(a: Int64, b: Int64): Int64 {
        a + b
    }
    
  • 示例:函数名为 returnTuple,其类型是 (Int64, Int64) -> (Int64, Int64),两个参数类型均为 Int64, 返回类型为元组类型:(Int64, Int64)

    func returnTuple(a: Int64, b: Int64): (Int64, Int64) {
        (a, b)
    }
    

函数类型的类型参数

可以为函数类型标记显式的类型参数名,下面例子中的 nameprice 就是 类型参数名

main() {
    let fruitPriceHandler: (name: String, price: Int64) -> Unit
    fruitPriceHandler = {n, p => println("fruit: ${n} price: ${p} yuan")}
    fruitPriceHandler("banana", 10)
}

另外对于一个函数类型,只允许统一写类型参数名,或者统一不写类型参数名,不能交替存在。

let handler: (name: String, Int64) -> Int64   // error

函数类型作为参数类型

  • 示例:函数名为 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))
    }
    

函数类型作为返回类型

函数类型可以作为另一个函数的返回类型。

如下示例中,函数名为 returnAdd,其类型是 () -> (Int64, Int64) -> Int64,表示该函数无参数,返回类型为函数类型 (Int64, Int64) -> Int64。注意,-> 是右结合的。

func add(a: Int64, b: Int64): Int64 {
    a + b
}

func returnAdd(): (Int64, Int64) -> Int64 {
    add
}

main() {
    var a = returnAdd()
    println(a(1,2))
}

函数类型作为变量类型

函数名本身也是表达式,它的类型为对应的函数类型。

func add(p1: Int64, p2: Int64): Int64 {
    p1 + p2
}

let f: (Int64, Int64) -> Int64 = add

上述示例中,函数名是 add,其类型为 (Int64, Int64) -> Int64。变量 f 的类型与 add 类型相同, add 被用来初始化 f

若一个函数在当前作用域中被重载(见[函数重载])了,那么直接使用该函数名作为表达式可能产生歧义,如果产生歧义编译器会报错,例如:

func add(i: Int64, j: Int64) {
    i + j
}

func add(i: Float64, j: Float64) {
    i + j
}

main() {
    var f = add   // Error: ambiguous function 'add'
    var plus: (Int64, Int64) -> Int64 = add  // OK
}

嵌套函数

定义在源文件顶层的函数被称为全局函数。定义在函数体内的函数被称为嵌套函数。

示例,函数 foo 内定义了一个嵌套函数 nestAdd,可以在 foo 内调用该嵌套函数 nestAdd,也可以将嵌套函数 nestAdd 作为返回值返回,在 foo 外对其进行调用:

func foo() {
    func nestAdd(a: Int64, b: Int64) {
        a + b + 3
    }

    println(nestAdd(1, 2))  // 6

    return nestAdd
}

main() {
    let f = foo()
    let x = f(1, 2)
    println("result: ${x}")
}

程序会输出:

6
result: 6

Lambda 表达式

Lambda 表达式定义

Lambda 表达式的语法为如下形式:

{ p1: T1, ..., pn: Tn => expressions | declarations }

其中,=> 之前为参数列表,多个参数之间使用 , 分隔,每个参数名和参数类型之间使用 : 分隔。=> 之前也可以没有参数。=> 之后为 lambda 表达式体,是一组表达式或声明序列。Lambda 表达式的参数名的作用域与函数的相同,为 lambda 表达式的函数体部分,其作用域级别可视为与 lambda 表达式的函数体内定义的变量等同。

let f1 = { a: Int64, b: Int64 => a + b }

var display = { => println("Hello") }   // Parameterless lambda expression.

Lambda 表达式不管有没有参数,都不可以省略 =>,除非其作为尾随 lambda。例如:

var display = { => println("Hello") }

func f2(lam: () -> Unit) { }
let f2Res = f2{ println("World") } // OK to omit the =>

Lambda 表达式中参数的类型标注可缺省。以下情形中,若参数类型省略,编译器会尝试进行类型推断,当编译器无法推断出类型时会编译报错:

  • Lambda 表达式赋值给变量时,其参数类型根据变量类型推断;
  • Lambda 表达式作为函数调用表达式的实参使用时,其参数类型根据函数的形参类型推断。
// The parameter types are inferred from the type of the variable sum1
var sum1: (Int64, Int64) -> Int64 = { a, b => a + b }

var sum2: (Int64, Int64) -> Int64 = { a: Int64, b => a + b }

func f(a1: (Int64) -> Int64): Int64 {
    a1(1)
}

main(): Int64 {
    // The parameter type of lambda is inferred from the type of function f
    f({ a2 => a2 + 10 })
}

Lambda 表达式中不支持声明返回类型,其返回类型总是从上下文中推断出来,若无法推断则报错。

  • 若上下文明确指定了 lambda 表达式的返回类型,则其返回类型为上下文指定的类型。

    • Lambda 表达式赋值给变量时,其返回类型根据变量类型推断返回类型:
    let f: () -> Unit = { ... }
    
    • Lambda 表达式作为参数使用时,其返回类型根据使用处所在的函数调用的形参类型推断:
    func f(a1: (Int64) -> Int64): Int64 {
        a1(1)
    }
    
    main(): Int64 {
        f({ a2: Int64 => a2 + 10 })
    }
    
    • Lambda 表达式作为返回值使用时,其返回类型根据使用处所在函数的返回类型推断:
    func f(): (Int64) -> Int64 {
        { a: Int64 => a }
    }
    
  • 若上下文中类型未明确,与推导函数的返回值类型类似,编译器会根据 lambda 表达式体中所有 return 表达式 'return xxx' 中 xxx 的类型,以及 lambda 表达式体的类型,来共同推导出 lambda 表达式的返回类型。

    => 右侧的内容与普通函数体的规则一样。

    let sum1 = { a: Int64, b: Int64 => a + b }
    
  • => 的右侧为空,返回类型为 Unit

    let f = { => }
    

Lambda 表达式调用

Lambda 表达式支持立即调用,例如:

let r1 = { a: Int64, b: Int64 => a + b }(1, 2) // r1 = 3
let r2 = { => 123 }()                          // r2 = 123

Lambda 表达式也可以赋值给一个变量,使用变量名进行调用,例如:

func f() {
    var g = { x: Int64 => println("x = ${x}") }
    g(2)
}

闭包

一个函数或 lambda 从定义它的静态作用域中捕获了变量,函数或 lambda 和捕获的变量一起被称为一个闭包,这样即使脱离了闭包定义所在的作用域,闭包也能正常运行。

函数或 lambda 的定义中对于以下几种变量的访问,称为变量捕获:

  • 函数的参数缺省值中访问了本函数之外定义的局部变量;

  • 函数或 lambda 内访问了本函数或本 lambda 之外定义的局部变量;

  • class/struct 内定义的不是成员函数的函数或 lambda 访问了实例成员变量或 this;

以下情形的变量访问不是变量捕获:

  • 对定义在本函数或本 lambda 内的局部变量的访问;

  • 对本函数或本 lambda 的形参的访问;

  • 对全局变量和静态成员变量的访问;

  • 对实例成员变量在实例成员函数或属性中的访问。由于实例成员函数或属性将 this 作为参数传入,在实例成员函数或属性内通过 this 访问所有实例成员变量。

变量的捕获发生在闭包定义时,因此变量捕获有以下规则:

  • 被捕获的变量必须在闭包定义时可见,否则编译报错;

  • 被捕获的变量必须在闭包定义时已经完成初始化,否则编译报错;

示例 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 defined yet
    }
    let y = 88
    f1()          // Print 99.
    f2()
}

示例 3:捕获的变量必须在闭包定义前完成初始化

func f() {
    let x: Int64
    func f1() {
        println(x)    // Error: x is not initialized yet.
    }
    x = 99
    f1()
}

如果捕获的变量是引用类型,可修改其可变实例成员变量的值。

class C {
    public var num: Int64 = 0
}

func returnIncrementer(): () -> Unit {
    let c: C = C()

    func incrementer() {
        c.num++
    }

    incrementer
}

main() {
    let f = returnIncrementer()
    f() // c.num increases by 1
}

为了防止捕获了 var 声明变量的闭包逃逸,这类闭包只能被调用,不能作为一等公民使用,包括不能赋值给变量,不能作为实参或返回值使用,不能直接将闭包的名字作为表达式使用。

func f() {
    var x = 1
    let y = 2

    func g() {
        println(x)  // OK, captured a mutable variable.
    }
    let b = g  // Error, g cannot be assigned to a variable

    g  // Error, g cannot be used as an expression
    g()  // OK, g can be invoked

    g  // Error, g cannot be used as a return value.
}

需要注意的是,捕获具有传递性,如果一个函数 f 调用了捕获 var 变量的函数 g,且存在 g 捕获的 var 变量不在函数 f 内定义,那么函数 f 同样捕获了 var 变量,此时,f 也不能作为一等公民使用。

以下示例中,g 捕获了 var 声明的变量 xf 调用了 g,且 g 捕获的 x 不在 f 内定义,f 同样不能作为一等公民使用:

func h(){
    var x = 1

    func g() {  x }   // captured a mutable variable

    func f() {
        g()      // invoked g
    }
    return f // error
}

以下示例中,g 捕获了 var 声明的变量 xf 调用了 g。但 g 捕获的 xf 内定义,f 没有捕获其它 var 声明的变量。因此,f 仍作为一等公民使用:

func h(){
    func f() {
        var x = 1
        func g() { x }   // captured a mutable variable

        g()
    }
    return f // ok
}

静态成员变量和全局变量的访问,不属于变量捕获,因此访问了 var 修饰的全局变量、静态成员变量的函数或 lambda 仍可作为一等公民使用。

class C {
    static public var a: Int32 = 0
    static public func foo() {
        a++       // OK
        return a
    }
}

var globalV1 = 0

func countGlobalV1() {
    globalV1++
    C.a = 99
    let g = C.foo  // OK
}

func g(){
    let f = countGlobalV1 // OK
    f()
}

函数调用语法糖

尾随 lambda

尾随 lambda 可以使函数的调用看起来像是语言内置的语法一样,增加语言的可扩展性。

当函数最后一个形参是函数类型,并且函数调用对应的实参是 lambda 时,我们可以使用尾随 lambda 语法,将 lambda 放在函数调用的尾部,圆括号外面。

例如,下例中我们定义了一个 myIf 函数,它的第一个参数是 Bool 类型,第二个参数是函数类型。当第一个参数的值为 true 时,返回第二个参数调用后的值,否则返回 0。调用 myIf 时可以像普通函数一样调用,也可以使用尾随 lambda 的方式调用。

func myIf(a: Bool, fn: () -> Int64) {
    if(a) {
        fn()
    } else {
        0
    }
}

func test() {
    myIf(true, { => 100 }) // General function call

    myIf(true) {        // Trailing closure call
        100
    }
}

当函数调用有且只有一个 lambda 实参时,我们还可以省略 (),只写 lambda。

示例:

func f(fn: (Int64) -> Int64) { fn(1) }

func test() {
    f { i => i * i }
}

Flow 表达式

流操作符包括两种:表示数据流向的中缀操作符 |> (称为 pipeline)和表示函数组合的中缀操作符 ~> (称为 composition)。

Pipeline 表达式

当需要对输入数据做一系列的处理时,可以使用 pipeline 表达式来简化描述。 pipeline 表达式的语法形式如下:

e1 |> e2

等价于如下形式的语法糖:

let v = e1; e2(v)

其中 e2 是函数类型的表达式,e1 的类型是 e2 的参数类型的子类型;

示例:

func inc(x: Array<Int64>): Array<Int64> { // Increasing the value of each element in the array by '1'
    let s = x.size
    var i = 0
    for (e in x where i < s) {
        x[i] = e + 1
        i++
    }
    x
}

func sum(y: Array<Int64>): Int64 { // Get the sum of elements in the array.
    var s = 0
    for (j in y) {
        s += j
    }
    s
}

let arr: Array<Int64> = Array<Int64>([1, 3, 5])
let res = arr |> inc |> sum // res = 12

Composition 表达式

composition 表达式表示两个单参函数的组合。composition 表达式语法如下:

f ~> g

等价于如下形式:

{ x => g(f(x)) }

其中 fg 均为只有一个参数的函数类型的表达式。

fg 组合,则要求 f(x) 的返回类型是 g(...) 的参数类型的子类型。

示例 1:

func f(x: Int64): Float64 {
    Float64(x)
}
func g(x: Float64): Float64 {
    x
}

var fg = f ~> g // The same as { x: Int64 => g(f(x)) }

示例 2:

func f(x: Int64): Float64 {
    Float64(x)
}

let lambdaComp = ({x: Int64 => x}) ~> f // The same as { x: Int64 => f({x: Int64 => x}(x)) }

示例 3:

func h1<T>(x: T): T { x }
func h2<T>(x: T): T { x }
var hh = h1<Int64> ~> h2<Int64> // The same as { x: Int64 => h2<Int64>(h1<Int64>(x)) }

注:表达式 f ~> g 中,会先对 f 求值,然后对 g 求值,最后才会进行函数的组合。

需要注意的是,流操作符不能与无默认值的命名形参函数直接一同使用,这是因为无默认值的命名形参函数必须给出命名实参才可以调用。例如:

func f(a!: Int64): Unit {}

var a = 1 |> f  // error

如果需要使用,用户可以通过 lambda 表达式传入 f 函数的命名实参:

func f(a!: Int64): Unit {}

var x = 1 |>  { x: Int64 => f(a: x) } // ok

由于相同的原因,当 f 的参数有默认值时,直接与流运算符一起使用也是错误的,例如:

func f(a!: Int64 = 2): Unit {}

var a = 1 |> f // error

但是当命名形参都存在默认值时,不需要给出命名实参也可以调用该函数,函数仅需要传入非命名形参,那么这种函数是可以同流运算符一起使用的,例如:

func f(a: Int64, b!: Int64 = 2): Unit {}

var a = 1 |> f  // ok

当然,如果想要在调用f时,为参数 b 传入其他参数,那么也需要借助 lambda 表达式:

func f(a: Int64, b!: Int64 = 2): Unit {}

var a = 1 |> {x: Int64 => f(x,  b: 3)}  // ok

变长参数

变长参数是一种特殊的函数调用语法糖。当形参最后一个非命名参数是 Array 类型时,实参中对应位置可以直接传入参数序列代替 Array 字面量(参数个数可以是 0 个或多个)。示例如下:

func sum(arr: Array<Int64>) {
    var total = 0
    for (x in arr) {
        total += x
    }
    return total
}

main() {
    println(sum())
    println(sum(1, 2, 3))
}

程序输出:

0
6

需要注意,只有最后一个非命名参数可以作为变长参数,命名参数不能使用这个语法糖。

func length(arr!: Array<Int64>) {
    return arr.size
}

main() {
    println(length())        // error: expected 1 argument, found 0
    println(length(1, 2, 3)) // error: expected 1 argument, found 3
}

变长参数可以出现在全局函数、静态成员函数、实例成员函数、局部函数、构造函数、函数变量、lambda、函数调用操作符重载、索引操作符重载的调用处。不支持其他操作符重载、compose、pipeline 这几种调用方式。示例如下:

class Counter {
    var total = 0
    init(data: Array<Int64>) { total = data.size }
    operator func ()(data: Array<Int64>) { total += data.size }
}

main() {
    let counter = Counter(1, 2)
    println(counter.total)
    counter(3, 4, 5)
    println(counter.total)
}

程序输出:

2
5

函数重载决议总是会优先考虑不使用变长参数就能匹配的函数,只有在所有函数都不能匹配,才尝试使用变长参数解析。示例如下:

func f<T>(x: T) where T <: ToString {
    println("item: ${x}")
}

func f(arr: Array<Int64>) {
    println("array: ${arr}")
}

main() {
    f()
    f(1)
    f(1, 2)
}

程序输出:

array: []
item: 1
array: [1, 2]

当编译器无法决议时会报错:

func f(arr: Array<Int64>) { arr.size }
func f(first: Int64, arr: Array<Int64>) { first + arr.size }

main() {
    println(f(1, 2, 3)) // error
}

函数重载

函数重载定义

在仓颉编程语言中,如果一个作用域中,一个函数名对应多个函数定义,这种现象称为函数重载。

  • 函数名相同,函数参数不同(是指参数个数不同,或者参数个数相同但参数类型不同)的两个函数构成重载。示例如下:
// Scenario 1
func f(a: Int64): Unit {
}

func f(a: Float64): Unit {
}

func f(a: Int64, b: Float64): Unit {
}
  • 对于两个同名泛型函数,如果重命名一个函数的泛型形参后,其非泛型部分与另一个函数的非泛型部分函数参数不同,则两个函数构成重载,否则这两个泛型函数构成重复定义错误(类型变元的约束不参与判断)。示例如下:
interface I1{}
interface I2{}

func f1<X, Y>(a: X, b: Y) {}
func f1<Y, X>(a: X, b: Y) {} // Ok: after rename generic type parameter, it will be 'func f1<X, Y>(a: Y, b: X)'

func f2<T>(a: T) where T <: I1 {}
func f2<T>(a: T) where T <: I2 {} // Error: not overloading
  • 同一个类内的两个构造函数参数不同,构成重载。示例如下:
// Scenario 2
class C {
    var a: Int64
    var b: Float64

    public init(a: Int64, b: Float64) {
        this.a = a
        this.b = b
    }

    public init(a: Int64) {
        b = 0.0
        this.a = a
    }
}
  • 同一个类内的主构造函数和 init 构造函数参数不同,构成重载(认为主构造函数和 init 函数具有相同的名字)。示例如下:
// Scenario 3
class C {
    C(var a!: Int64, var b!: Float64) {
        this.a = a
        this.b = b
    }

    public init(a: Int64) {
        b = 0.0
        this.a = a
    }
}
  • 两个函数定义在不同的作用域,在两个函数可见的作用域中构成重载。示例如下:
// Scenario 4
func f(a: Int64): Unit {
}

func g() {
    func f(a: Float64): Unit {
    }
}
  • 两个函数分别定义在父类和子类中,在两个函数可见的作用域中构成重载。示例如下:
// Scenario 5
open class Base {
    public func f(a: Int64): Unit {
    }
}

class Sub <: Base {
    public func f(a: Float64): Unit {
    }
}

只允许函数声明引入的函数重载,但是以下情形不构成重载,不构成重载的两个名字不能定义或声明在同一个作用域内:

  • class、interface、struct 类型的静态成员函数和实例成员函数之间不能重载
  • enum 类型的 constructor、静态成员函数和实例成员函数之间不能重载

如下示例,两个变量均为函数类型且函数参数类型不同,但由于它们不是函数声明所以不能重载,如下示例将编译报错(重定义错):

main() {
    var f: (Int64) -> Unit
    var f: (Float64) -> Unit
}

如下示例,虽然变量 f 为函数类型,但由于变量和函数之间不能同名,如下示例将编译报错(重定义错):

main() {
    var f: (Int64) -> Unit

    func f(a: Float64): Unit {   // Error: functions and variables cannot have the same name.
    }
}

如下示例,静态成员函数 f 与实例成员函数 f 的参数类型不同,但由于类内静态成员函数和实例成员函数之间不能重载,如下示例将编译报错:

class C {
    public static func f(a: Int64): Unit {
    }
    public func f(a: Float64): Unit {
    }
}

函数重载决议

函数调用时,所有可被调用的函数(是指当前作用域可见且能通过类型检查的函数)构成候选集,候选集中有多个函数,究竟选择候选集中哪个函数,需要进行函数重载决议,有如下规则:

  • 优先选择作用域级别高的作用域内的函数。在嵌套的表达式或函数中,越是内层作用域级别越高。

    如下示例中在 inner 函数体内调用 g(Sub()) 时,候选集包括 inner 函数内定义的函数 ginner 函数外定义的函数 g,函数决议选择作用域级别更高的 inner 函数内定义的函数 g

    open class Base {}
    class Sub <: Base {}
    
    func outer() {
        func g(a: Sub) {
            print("1")
        }
    
        func inner() {
            func g(a: Base) {
                print("2")
            }
    
            g(Sub())   // Output: 2
        }
    }
    
  • 如果作用域级别相对最高的仍有多个函数,则需要选择最匹配的函数(对于函数 f 和 g 以及给定的实参,如果 f 可以被调用时 g 也总是可以被调用的,但反之不然,则我们称 f 比 g 更匹配)。如果不存在唯一最匹配的函数,则报错。

    如下示例中,两个函数 g 定义在同一作用域,选择更匹配的函数 g(a: Sub): Unit

    open class Base {}
    class Sub <: Base {}
    
    func outer() {
        func g(a: Sub) {
            print("1")
        }
        func g(a: Base) {
            print("2")
        }
    
        g(Sub())   // Output: 1
    
    }
    
  • 子类和父类认为是同一作用域。如下示例中,一个函数 g 定义在父类中,另一个函数 g 定义在子类中,在调用 s.g(Sub()) 时,两个函数 g 当成同一作用域级别决议,则选择更匹配的父类中定义的函数 g(a: Sub): Unit

    open class Base {
        public func g(a: Sub) { print("1") }
    }
    
    class Sub <: Base {
        public func g(a: Base) {
            print("2")
        }
    }
    
    func outer() {
        let s: Sub = Sub()
        s.g(Sub())   // Output: 1
    }
    

操作符重载

如果希望在某个类型上支持此类型默认不支持的操作符,可以使用操作符重载实现。

如果需要在某个类型上重载某个操作符,可以通过为类型定义一个函数名为此操作符的函数的方式实现,这样,在该类型的实例使用该操作符时,就会自动调用此操作符函数。

操作符函数定义与普通函数定义相似,区别如下:

  • 定义操作符函数时需要在 func 关键字前面添加 operator 修饰符;
  • 操作符函数的参数个数需要匹配对应操作符的要求(详见附录[操作符]);
  • 操作符函数只能定义在 class、interface、struct、enum 和 extend 中;
  • 操作符函数具有实例成员函数的语义,所以禁止使用 static 修饰符;
  • 操作符函数不能为泛型函数。

另外,需要注意:

  • 被重载后的操作符不改变它们固有的优先级和结合性(详见附录[操作符])。

操作符重载函数定义和使用

定义操作符函数有两种方式:

  1. 对于可以直接包含函数定义的类型 (包括 structenumclassinterface ),可以直接在其内部定义操作符函数的方式实现操作符的重载。
  2. 使用 extend 的方式为其添加操作符函数,从而实现操作符在这些类型上的重载。对于无法直接包含函数定义的类型(是指除 structclassenuminterface 之外其他的类型)或无法改变其实现的类型,比如第三方定义的 structclassenuminterface,只能采用这种方式(参见[扩展的定义]);

操作符函数对参数类型的约定如下:

  1. 对于一元操作符,操作符函数没有参数,对返回值的类型没有要求。

  2. 对于二元操作符,操作符函数只有一个参数,对返回值的类型没有要求。

    如下示例中介绍了一元操作符和二元操作符的定义和使用:

    - 实现对一个 Point 实例中两个成员变量 xy 取负值,然后返回一个新的 Point 对象,+ 实现对两个 Point 实例中两个成员变量 xy 分别求和,然后返回一个新的 Point 对象。

    open class Point {
        var x: Int64 = 0
        var y: Int64 = 0
        public init (a: Int64, b: Int64) {
            x = a
            y = b
        }
    
        public operator func -(): Point {
            Point(-x, -y)
        }
        public operator func +(right: Point): Point {
            Point(this.x + right.x, this.y + right.y)
        }
    }
    

    接下来,就可以在 Point 的实例上直接使用一元 - 操作符和二元 + 操作符:

    main() {
        let p1 = Point(8, 24)
        let p2 = -p1      // p2 = Point(-8, -24)
        let p3 = p1 + p2  // p3 = Point(0, 0)
    }
    
  3. 索引操作符([])分为取值 let a = arr[i] 和赋值 arr[i] = a 两种形式,它们通过是否存在特殊的命名参数 value 来区分不同的重载。索引操作符重载不要求同时重载两种形式,可以只重载赋值不重载取值,反之亦可。

    索引操作符取值形式 [] 内的参数序列对应操作符重载的非命名参数,可以是 1 个或多个,可以是任意类型。不可以有其它命名参数。返回类型可以是任意类型。

    class A {
        operator func [](arg1: Int64, arg2: String): Int64 {
            return 0
        }
    }
    
    func f() {
        let a = A()
        let b: Int64 = a[1, "2"]
        // b == 0
    }
    

    索引操作符赋值形式 [] 内的参数序列对应操作符重载的非命名参数,可以是 1 个或多个,可以是任意类型。= 右侧的表达式对应操作符重载的命名参数,有且只能有一个命名参数,该命名参数的名称必须是 value, 不能有默认值,value 可以是任意类型。返回类型必须是 Unit 类型。

    需要注意的是,value 只是一种特殊的标记,在索引操作符赋值时并不需要使用命名参数的形式调用。

    class A {
        operator func [](arg1: Int64, arg2: String, value!: Int64): Unit {
            return
        }
    }
    
    func f() {
        let a = A()
        a[1, "2"] = 0
    }
    

    特别的,除 enum 外的不可变类型不支持重载索引操作符赋值形式。

  4. 函数调用操作符(())重载函数,输入参数和返回值类型可以是任意类型。示例如下:

    open class A {
        public init() {}
    
        public operator func ()(): Unit {}
    }
    
    func test1() {
        let a = A() // ok, A() is call the constructor of A.
        a() // ok, a() is to call the operator () overloading function.
    }
    

    不能使用 thissuper 调用 () 操作符重载函数。示例如下:

    open class A {
        public init() {}
        public init(x: Int64) {
            this() // ok, this() calls the constructor of A.
        }
    
        public operator func ()(): Unit {}
    
        public func foo() {
            this()  // error, this() calls the constructor of A.
            super() // error
        }
    }
    
    class B <: A {
        public init() {
            super() // ok, super()  calls the constuctor of the super class.
        }
    
        public func goo() {
            super() // error
        }
    }
    

    对于枚举类型,当构造器形式和 () 操作符重载函数形式都满足时,优先匹配构造器形式。示例如下:

    enum E {
        Y | X | X(Int64)
    
        public operator func ()(p: Int64) {}
        public operator func ()(p: Float64) {}
    }
    
    main() {
        let e = X(1) // ok, X(1) is to call the constructor X(Int64).
        X(1.0) // ok, X(1.0) is to call the operator () overloading function.
        let e1 = X
        e1(1) // ok, e1(1) is to call the operator () overloading function.
        Y(1) // oK, Y(1) is to call the operator () overloading function.
    }
    

可以被重载的操作符

下表列出了所有可以被重载的操作符(优先级从高到低):

OperatorDescription
()Function call
[]Indexing
!NOT
-Negative
**Power
*Multiply
/Divide
%Remainder
+Add
-Subtract
<<Bitwise left shift
>>Bitwise right shift
<Less than
<=Less than or equal
>Greater than
>=Greater than or equal
==Equal
!=Not equal
&Bitwise AND
^Bitwise XOR
|Bitwise OR

需要注意的是:

  1. 一旦在某个类型上重载了除关系操作符(<<=>>===!=)之外的其他二元操作符,并且操作符函数的返回类型与左操作数的类型一致或是其子类型,那么此类型支持对应的复合赋值操作符。当操作符函数的返回类型与左操作数的类型不一致且不是其子类型时,在使用对应的复合赋值符号时将报类型不匹配错误;
  2. 仓颉编程语言不支持自定义操作符,即不允许定义除上表中所列 operator 之外的其他操作符函数。
  3. 对于类型 T, 如果 T 已经默认支持了上述若干可重载操作符,那么通过扩展的方式再次为其实现同签名的操作符函数时将报重定义错误。例如,为数值类型重载其已支持的同签名算术操作符、位操作符或关系操作符等操作符时,为 Char 重载同签名的关系操作符时,为 Bool 类型重载同签名的逻辑操作符、判等或不等操作符时,等等这些情况,均会报重定义错。

Mut 函数

Struct 类型是值类型,其实例成员函数无法修改实例本身。例如,下例中,成员函数 g 中不能修改成员变量 i 的值。

struct Foo {
    var i = 0

    public func g() {
        i += 1  // Error: the value of a instance member variable cannot be modified in an instance member function
    }
}

Mut 函数是一种可以修改 struct 实例本身的特殊的实例成员函数。在 mut 函数内部,this 的语义是特殊的,这种 this 拥有原地修改字段的能力。

只允许在 interface、struct 和 struct 的扩展内定义 mut 函数(class 是引用类型,实例成员函数不需要加 mut 也可以修改实例成员变量,所以禁止在 class 中定义 mut 函数)。

Mut 函数定义

Mut 函数与普通的实例成员函数相比,多一个 mut 关键字来修饰。

例如,下例中在函数 g 之前增加 mut 修饰符之后,即可在函数体内修改成员变量 i 的值。

struct Foo {
    var i = 0

    public mut func g() {
        i += 1  // ok
    }
}

mut 只能修饰实例成员函数,不能修饰静态成员函数。

struct A {
    public mut func f(): Unit {} // ok
    public mut operator func +(rhs: A): A { // ok
        A()
    }
    public mut static func g(): Unit {} // Error: static member functions cannot be modified with 'mut'
}

Mut 函数中的 this 不能被捕获,也不能作为表达式。不能在 mut 函数中对 struct 的实例成员变量进行捕获。

示例:

struct Foo {
    var i = 0

    public mut func f(): Foo {
        let f1 = { => this } // Error: 'this' in mut functions cannot be captured
        let f2 = { => this.i = 2 } // Error: instance member variables in mut functions cannot be captured
        let f3 = { => this.i } // Error: instance member variables in mut functions cannot be captured
        let f4 = { => i } // Error: instance member variables in mut functions cannot be captured
        this // Error: 'this' in mut functions cannot be used as expressions
    }
}

接口中的 mut 函数

接口中的实例成员函数,也可以使用 mut 修饰。

struct 类型在实现 interface 的函数时必须保持一样的 mut 修饰。struct 以外的类型实现 interface 的函数时不能使用 mut 修饰。

示例:

interface I {
    mut func f1(): Unit
    func f2(): Unit
}

struct A <: I {
    public mut func f1(): Unit {} // Ok: as in the interface, the 'mut' modifier is used
    public func f2(): Unit {} // Ok: as in the interface, the 'mut' modifier is not used
}

struct B <: I {
    public func f1(): Unit {} // Error: 'f1' is modified with 'mut' in interface, but not in struct
    public mut func f2(): Unit {} // Error: 'f2' is not modified with 'mut' in interface, but did in struct
}

class C <: I {
    public func f1(): Unit {} // ok
    public func f2(): Unit {} // ok
}

struct 的实例赋值给 interface 类型时是拷贝语义,因此 interfacemut 函数并不能修改 struct 实例的值。

示例:

interface I {
    mut func f(): Unit
}
struct Foo <: I {
    public var v = 0
    public mut func f(): Unit {
        v += 1
    }
}
main() {
    var a = Foo()
    var b: I = a  
    b.f()  // Calling 'f' via 'b' cannot modify the value of 'a'
    println(a.v) // 0
}

程序输出结果为:

0

Mut 函数的使用限制

因为 struct 是值类型,所以如果一个变量是 struct 类型且使用 let 声明,那么不能通过这个变量访问该类型的 mut 函数。

示例:

interface I {
    mut func f(): Unit
}
struct Foo <: I {
    public var i = 0
    public mut func f(): Unit {
        i += 1
    }
}
main() {
    let a = Foo()
    a.f() // Error: 'a' is of type struct and is declared with 'let', the 'mut' function cannot be accessed via 'a'
    var b = Foo()
    b.f() // ok
    let c: I = Foo()
    c.f() // ok
}

为避免逃逸,如果一个变量的类型是 struct 类型,那么这个变量不能将该类型使用 mut 修饰的函数作为一等公民来使用,只能调用这些 mut 函数。

示例:

interface I {
    mut func f(): Unit
}

struct Foo <: I {
    var i = 0

    public mut func f(): Unit {
        i += 1
    }
}

main() {
    var a = Foo()
    var fn = a.f // Error: mut function 'f' of 'a' cannot be used as a first class citizen.
    var b: I = Foo()
    fn = b.f // ok
}

为避免逃逸,非 mut 的实例成员函数(包括 lambda 表达式)不能直接访问所在类型的 mut 函数,反之可以。

示例:

struct Foo {
    var i = 0

    public mut func f(): Unit {
        i += 1
        g() // ok
    }

    public func g(): Unit {
        f() // Error: mut functions cannot be invoked in non-mut functions
    }
}

interface I {
    mut func f(): Unit {
        g() // ok
    }

    func g(): Unit {
        f() // Error: mut functions cannot be invoked in non-mut functions
    }
}

模式匹配

本章主要介绍仓颉中的模式匹配(pattern matching),首先介绍 match 表达式和模式,然后介绍模式的 refutability(即某个模式是否一定能匹配成功),最后介绍模式匹配在 match 表达式之外的使用。

match 表达式

仓颉支持两种 match 表达式,第一种是包含待匹配值的 match 表达式,第二种是不含待匹配值的 match 表达式。

含匹配值的 match 表达式举例:

main() {
    let x = 0
    match (x) {
        case 1 => let r1 = "x = 1"
                  print(r1)
        case 0 => let r2 = "x = 0" // Matched.
                  print(r2)
        case _ => let r3 = "x != 1 and x != 0"
                  print(r3)
    }
}

match 表达式以关键字 match 开头,后跟要匹配的值(如上例中的 xx 可以是任意表达式),接着是定义在一对花括号内的若干 case 分支。

每个 case 分支以关键字 case 开头,case 之后是一个模式或多个由 | 连接的相同种类的模式(如上例中的 10_ 都是模式,详见模式章节);模式之后可以接一个可选的 pattern guard,表示本条 case 匹配成功后额外需要满足的条件;接着是一个 =>=> 之后即本条 case 分支匹配成功后需要执行的操作,可以是一系列表达式、变量和函数定义(新定义的变量或函数的作用域从其定义处开始到下一个 case 之前结束),如上例中的变量定义和 print 函数调用。

match 表达式执行时依次将 match 之后的表达式与每个 case 中的模式进行匹配,一旦匹配成功(如果有 pattern guard,也需要 where 之后的表达式的值为 true;如果 case 中有多个由 | 连接的模式,只要待匹配值和其中一个模式匹配则认为匹配成功)则执行 => 之后的代码然后退出 match 表达式的执行(意味着不会再去匹配它之后的 case),如果匹配不成功则继续与它之后的 case 中的模式进行匹配,直到匹配成功(match 表达式可以保证一定存在匹配的 case 分支)。

上例中,因为 x 的值等于 0,所以会和第二条 case 分支匹配(此处使用的是常量模式,匹配的是值是否相等,详见常量模式章节),最后输出 x = 0

编译并执行上述代码,输出结果为:

x = 0

match 表达式要求所有匹配必须是穷尽(exhaustive)的,意味着待匹配表达式的所有可能取值都应该被考虑到。当 match 表达式非穷尽,或者编译器判断不出是否穷尽时,均会编译报错,换言之,所有 case 分支(包含 pattern guard)所覆盖的取值范围的并集,应该包含待匹配表达式的所有可能取值。常用的确保 match 表达式穷尽的方式是在最后一个 case 分支中使用通配符模式 _,因为 _ 可以匹配任何值。

match 表达式的穷尽性保证了一定存在和待匹配值相匹配的 case 分支。下面的例子将编译报错,因为所有的 case 并没有覆盖 x 的所有可能取值:

func nonExhaustive(x: Int64) {
    match (x) {
        case 0 => print("x = 0")
        case 1 => print("x = 1")
        case 2 => print("x = 2")
    }
}

case 分支的模式之后,可以使用 pattern guard 进一步对匹配出来的结果进行判断。pattern guard 使用 where cond 表示,要求表达式 cond 的类型为 Bool

在下面的例子中(使用到了 enum 模式,详见 Enum 模式章节),当 RGBColor 的构造器的参数值大于等于 0 时,输出它们的值,当参数值小于 0 时,认为它们的值等于 0

enum RGBColor {
    | Red(Int16) | Green(Int16) | Blue(Int16)
}
main() {
    let c = RGBColor.Green(-100)
    let cs = match (c) {
        case Red(r) where r < 0 => "Red = 0"
        case Red(r) => "Red = ${r}"
        case Green(g) where g < 0 => "Green = 0" // Matched.
        case Green(g) => "Green = ${g}"
        case Blue(b) where b < 0 => "Blue = 0"
        case Blue(b) => "Blue = ${b}"
    }
    print(cs)
}

编译执行上述代码,输出结果为:

Green = 0

没有匹配值的 match 表达式举例:

main() {
    let x = -1
    match {
        case x > 0 => print("x > 0")
        case x < 0 => print("x < 0") // Matched.
        case _ => print("x = 0")
    }
}

与包含待匹配值的 match 表达式相比,关键字 match 之后并没有待匹配的表达式,并且 case 之后不再是 pattern,而是类型为 Bool 的表达式(上述代码中的 x > 0x < 0)或者 _(表示 true),当然,case 中也不再有 pattern guard

无匹配值的 match 表达式执行时依次判断 case 之后的表达式的值,直到遇到值为 truecase 分支;一旦某个 case 之后的表达式值等于 true,则执行此 case=> 之后的代码,然后退出 match 表达式的执行(意味着不会再去判断该 case 之后的其他 case)。

上例中,因为 x 的值等于 -1,所以第二条 case 分支中的表达式(即 x < 0)的值等于 true,执行 print("x < 0")

编译并执行上述代码,输出结果为:

x < 0

match 表达式的类型

对于 match 表达式(无论是否有匹配值),

  • 在上下文有明确的类型要求时,要求每个 case 分支中 => 之后的代码块的类型是上下文所要求的类型的子类型;

  • 在上下文没有明确的类型要求时,match 表达式的类型是每个 case 分支中 => 之后的代码块的类型的最小公共父类型。

  • match 表达式的值没有被使用时,其类型为 Unit,不要求各分支的类型有最小公共父类型。

下面分别举例说明。

let x = 2
let s: String = match (x) {
    case 0 => "x = 0"
    case 1 => "x = 1"
    case _ => "x != 0 and x != 1" // Matched.
}

上面的例子中,定义变量 s 时,显式地标注了其类型为 String,属于上下文类型信息明确的情况,因此要求每个 case=> 之后的代码块的类型均是 String 的子类型,显然上例中 => 之后的字符串类型的字面量均满足要求。

再来看一个没有上下文类型信息的例子:

let x = 2
let s = match (x) {
    case 0 => "x = 0"
    case 1 => "x = 1"
    case _ => "x != 0 and x != 1" // Matched.
}

上例中,定义变量 s 时,未显式标注其类型,因为每个 case=> 之后的代码块的类型均是 String,所以 match 表达式的类型是 String,进而可确定 s 的类型也是 String

模式

对于包含匹配值的 match 表达式,case 之后支持哪些模式决定了 match 表达式的表达能力,本节中我们将依次介绍仓颉支持的模式,包括:常量模式、通配符模式、绑定模式、tuple 模式、类型模式和 enum 模式。

常量模式

常量模式可以是整数字面量、浮点数字面量、字符字面量、布尔字面量、字符串字面量(不支持字符串插值)、Unit 字面量。

在包含匹配值的 match 表达式中使用常量模式时,要求常量模式表示的值的类型与待匹配值的类型相同,匹配成功的条件是待匹配的值与常量模式表示的值相等。

下面的例子中,根据 score 的值(假设 score 只能取 0100 间被 10 整除的值),输出考试成绩的等级:

main() {
    let score = 90
    let level = match (score) {
        case 0 | 10 | 20 | 30 | 40 | 50 => "D"
        case 60 => "C"
        case 70 | 80 => "B"
        case 90 | 100 => "A" // Matched.
        case _ => "Not a valid score"
    }
    println(level)
}

编译执行上述代码,输出结果为:

A

通配符模式

通配符模式使用下划线 _ 表示,可以匹配任意值。通配符模式通常作为最后一个 case 中的模式,用来匹配其他 case 未覆盖到的情况,如上节中匹配 score 值的示例中,最后一个 case 中使用 _ 来匹配无效的 score 值。

绑定模式

绑定模式使用 id 表示,id 是一个合法的标识符。与通配符模式相比,绑定模式同样可以匹配任意值,但绑定模式会将匹配到的值与 id 进行绑定,在 => 之后可以通过 id 访问其绑定的值。

下面的例子中,最后一个 case 中使用了绑定模式,用于绑定非 0 值:

main() {
    let x = -10
    let y = match (x) {
        case 0 => "zero"
        case n => "x is not zero and x = ${n}" // Matched.
    }
    println(y)
}

编译执行上述代码,输出结果为:

x is not zero and x = -10

使用 | 连接多个模式时不能使用绑定模式,也不可嵌套出现在其它模式中,否则会报错:

main() {
    let opt = Some(0)
    match (opt) {
        case x | x => {} // Error: variable cannot be introduced in patterns connected by '|'
        case Some(x) | Some(x) => {} // Error: variable cannot be introduced in patterns connected by '|'
        case x: Int64 | x: String => {} // Error: variable cannot be introduced in patterns connected by '|'
    }
}

绑定模式 id 相当于新定义了一个名为 id 的不可变变量(其作用域从引入处开始到该 case 结尾处),因此在 => 之后无法对 id 进行修改。例如,下例中最后一个 case 中对 n 的修改是不允许的。

main() {
    let x = -10
    let y = match (x) {
        case 0 => "zero"
        case n => n = n + 0 // Error: 'n' cannot be modified.
                  "x is not zero"
    }
    println(y)
}

对于每个 case 分支,=> 之后变量作用域级别与 case=> 前引入的变量作用域级别相同,在 => 之后再次引入相同名字会触发重定义错。例如:

main() {
    let x = -10
    let y = match (x) {
        case 0 => "zero"
        case n => let n = 0 // Error, redefinition
                  println(n)
                  "x is not zero"
    }
    println(y)
}

注:当模式的 identifier 为 enum 构造器时,该模式会被当成 enum 模式进行匹配,而不是绑定模式(关于 enum 模式,详见 Enum 模式章节)。

enum RGBColor {
    | Red | Green | Blue
}

main() {
    let x = Red
    let y = match (x) {
        case Red => "red" // The 'Red' is enum mode here.
        case _ => "not red"
    }
    println(y)
}

编译执行上述代码,输出结果为:

red

Tuple 模式

Tuple 模式用于 tuple 值的匹配,它的定义和 tuple 字面量类似:(p_1, p_2, ..., p_n),区别在于这里的 p_1p_nn 大于等于 2)是模式(可以是模式章节中介绍的任何模式,多个模式间使用逗号分隔)而不是表达式。

例如,(1, 2, 3) 是一个包含三个常量模式的 tuple 模式,(x, y, _) 是一个包含两个绑定模式,一个通配符模式的 tuple 模式。

给定一个 tuple 值 tv 和一个 tuple 模式 tp,当且仅当 tv 每个位置处的值均能与 tp 中对应位置处的模式相匹配,才称 tp 能匹配 tv。例如,(1, 2, 3) 仅可以匹配 tuple 值 (1, 2, 3)(x, y, _) 可以匹配任何三元 tuple 值。

下面的例子中,展示了 tuple 模式的使用:

main() {
    let tv = ("Alice", 24)
    let s = match (tv) {
        case ("Bob", age) => "Bob is ${age} years old"
        case ("Alice", age) => "Alice is ${age} years old" // Matched, "Alice" is a constant pattern, and 'age' is a variable pattern.
        case (name, 100) => "${name} is 100 years old"
        case (_, _) => "someone"
    }
    println(s)
}

编译执行上述代码,输出结果为:

Alice is 24 years old

同一个 tuple 模式中不允许引入多个名字相同的绑定模式。例如,下例中最后一个 case 中的 case (x, x) 是不合法的。

main() {
    let tv = ("Alice", 24)
    let s = match (tv) {
        case ("Bob", age) => "Bob is ${age} years old"
        case ("Alice", age) => "Alice is ${age} years old"
        case (name, 100) => "${name} is 100 years old"
        case (x, x) => "someone" // Error: Cannot introduce a variable pattern with the same name, which will be a redefinition error.
    }
    println(s)
}

类型模式

类型模式用于判断一个值的运行时类型是否是某个类型的子类型。类型模式有两种形式:_: Type(嵌套一个通配符模式 _)和 id: Type(嵌套一个绑定模式 id),它们的差别是后者会发生变量绑定,而前者并不会。

对于待匹配值 v 和类型模式 id: Type(或 _: Type),首先判断 v 的运行时类型是否是 Type 的子类型,若成立则视为匹配成功,否则视为匹配失败;如匹配成功,则将 v 的类型转换为 Type 并与 id 进行绑定(对于 _: Type,不存在绑定这一操作)。

假设有如下两个类,BaseDerived,并且 DerivedBase 的子类,Base 的无参构造函数中将 a 的值设置为 10Derived 的无参构造函数中将 a 的值设置为 20

open class Base {
    var a: Int64
    public init() {
        a = 10
    }
}

class Derived <: Base {
    public init() {
        a = 20
    }
}

下面的代码展示了使用类型模式并匹配成功的例子:

main() {
    var d = Derived()
    var r = match (d) {
        case b: Base => b.a // Matched.
        case _ => 0
    }
    println("r = ${r}")
}

编译执行上述代码,输出结果为:

r = 20

下面的代码展示了使用类型模式但类型模式匹配失败的例子:

open class Base {
    var a: Int64
    public init() {
        a = 10
    }
}

class Derived <: Base {
    public init() {
        a = 20
    }
}

main() {
    var b = Base()
    var r = match (b) {
        case d: Derived => d.a // Type pattern match failed.
        case _ => 0 // Matched.
    }
    println("r = ${r}")
}

编译执行上述代码,输出结果为:

r = 0

Enum 模式

Enum 模式用于匹配 enum 类型的实例,它的定义和 enum 的构造器类似:无参构造器 C 或有参构造器 C(p_1, p_2, ..., p_n),构造器的类型前缀可以省略,区别在于这里的 p_1p_nn 大于等于 1)是模式。例如,Some(1) 是一个包含一个常量模式的 enum 模式,Some(x) 是一个包含一个绑定模式的 enum 模式。

给定一个 enum 实例 ev 和一个 enum 模式 ep,当且仅当 ev 的构造器名字和 ep 的构造器名字相同,且 ev 参数列表中每个位置处的值均能与 ep 中对应位置处的模式相匹配,才称 ep 能匹配 ev。例如,Some("one") 仅可以匹配 Option<String> 类型的Some 构造器 Option<String>.Some("one")Some(x) 可以匹配任何 Option 类型的 Some 构造器。

下面的例子中,展示了 enum 模式的使用,因为 x 的构造器是 Year,所以会和第一个 case 匹配:

enum TimeUnit {
    | Year(UInt64)
    | Month(UInt64)
}

main() {
    let x = Year(2)
    let s = match (x) {
        case Year(n) => "x has ${n * 12} months" // Matched.
        case TimeUnit.Month(n) => "x has ${n} months"
    }
    println(s)
}

编译执行上述代码,输出结果为:

x has 24 months

使用 | 连接多个 enum 模式:

enum TimeUnit {
    | Year(UInt64)
    | Month(UInt64)
}

main() {
    let x = Year(2)
    let s = match (x) {
        case Year(0) | Year(1) | Month(_) => "ok" // Ok
        case Year(2) | Month(m) => "invalid" // Error: Variable cannot be introduced in patterns connected by '|'
        case Year(n: UInt64) | Month(n: UInt64) => "invalid" // Error: Variable cannot be introduced in patterns connected by '|'
    }
    println(s)
}

使用 match 表达式匹配 enum 值时,要求 case 之后的模式要覆盖待匹配 enum 类型中的所有构造器,如果未做到完全覆盖,编译器将报错:

enum RGBColor {
    | Red | Green | Blue
}

main() {
    let c = Green
    let cs = match (c) { // Error: Not all constructors of RGBColor are covered.
        case Red => "Red"
        case Green => "Green"
    }
    println(cs)
}

我们可以通过加上 case Blue 来实现完全覆盖,也可以在 match 表达式的最后通过使用 case _ 来覆盖其他 case 未覆盖的到的情况,如:

enum RGBColor {
    | Red | Green | Blue
}

main() {
    let c = Blue
    let cs = match (c) {
        case Red => "Red"
        case Green => "Green"
        case _ => "Other" // Matched.
    }
    println(cs)
}

上述代码的执行结果为:

Other

模式的嵌套组合

Tuple 模式和 enum 模式可以嵌套任意模式。下面的代码展示了不同模式嵌套组合使用:

enum TimeUnit {
    | Year(UInt64)
    | Month(UInt64)
}

enum Command {
    | SetTimeUnit(TimeUnit)
    | GetTimeUnit
    | Quit
}

main() {
    let command = SetTimeUnit(Year(2022))
    match (command) {
        case SetTimeUnit(Year(year)) => println("Set year ${year}")
        case SetTimeUnit(Month(month)) => println("Set month ${month}")
        case _ => ()
    }
}

编译执行上述代码,输出结果为:

Set year 2022

模式的 Refutability

模式可以分为两类:refutable 模式和 irrefutable 模式。在类型匹配的前提下,当一个模式有可能和待匹配值不匹配时,称此模式为 refutable 模式;反之,当一个模式总是可以和待匹配值匹配时,称此模式为 irrefutable 模式。

对于上述介绍的各种模式,规定如下:

常量模式是 refutable 模式。例如,下例中第一个 case 中的 1 和第二个 case 中的 2 都有可能和 x 的值不相等。

func constPat(x: Int64) {
    match (x) {
        case 1 => "one"
        case 2 => "two"
        case _ => "_"
    }
}

通配符模式是 irrefutable 模式。例如,下例中无论 x 的值是多少,_ 总能和其匹配。

func wildcardPat(x: Int64) {
    match (x) {
        case _ => "_"
    }
}

绑定模式是 irrefutable 模式。例如,下例中无论 x 的值是多少,绑定模式 a 总能和其匹配。

func varPat(x: Int64) {
    match (x) {
        case a => "x = ${a}"
    }
}

Tuple 模式是 irrefutable 模式,当且仅当其包含的每个模式都是 irrefutable 模式。例如,下例中 (1, 2)(a, 2) 都有可能和 x 的值不匹配,所以它们是 refutable 模式,而 (a, b) 可以匹配任何 x 的值,所以它是 irrefutable 模式。

func tuplePat(x: (Int64, Int64)) {
    match (x) {
        case (1, 2) => "(1, 2)"
        case (a, 2) => "(${a}, 2)"
        case (a, b) => "(${a}, ${b})"
    }
}

类型模式是 refutable 模式。例如,下例中(假设 BaseDerived 的父类,并且 Base 实现了接口 I),x 的运行时类型有可能既不是 Base 也不是 Derived,所以 a: Derivedb: Base 均是 refutable 模式。

interface I {}
open class Base <: I {}
class Derived <: Base {}

func typePat(x: I) {
    match (x) {
        case a: Derived => "Derived"
        case b: Base => "Base"
        case _ => "Other"
    }
}

Enum 模式是 irrefutable 模式,当且仅当它对应的 enum 类型中只有一个有参构造器,且 enum 模式中包含的其他模式也是 irrefutable 模式。例如,对于下例中的 E1E2 定义,函数 enumPat1 中的 A(1)refutable 模式,A(a)irrefutable 模式;而函数 enumPat2 中的 B(b)C(c) 均是 refutable 模式。 ​

enum E1 {
    A(Int64)
}

enum E2 {
    B(Int64) | C(Int64)
}

func enumPat1(x: E1) {
    match (x) {
        case A(1) => "A(1)"
        case A(a) => "A(${a})"
    }
}

func enumPat2(x: E2) {
    match (x) {
        case B(b) => "B(${b})"
        case C(c) => "C(${c})"
    }
}

if-let 与 while-let 表达式

在一些应用场景中,我们只关注一个表达式是否为某种特定模式,是则将其解构,取出对应值做相关操作。虽然可以用 match 表达式实现这一逻辑,但书写上可能比较冗长,为此,仓颉提供了更便捷的表达方式——允许在 if 表达式和 while 表达式的条件部分,使用 let 修饰符匹配和解构模式,这时它们被称为 if-let 表达式和 while-let 表达式。

在条件部分使用 let 匹配模式的形式为:

let 模式 <- 表达式

这里的“模式”可以是常量模式、通配符模式、绑定模式、Tuple 模式和 enum 模式,如果模式中含有占位标识符,则此处等同于定义了一个不可变变量(这正是使用 let 表达此语义的原因),如果模式被匹配,这个变量就会与解构后的值绑定,可以作为 if 分支或 while 循环体中的局部变量使用。

if-let 表达式

if-let 表达式首先对条件中 let 等号右侧的表达式进行求值,如果此值能匹配 let 等号左侧的模式,则执行 if 分支,否则执行 else 分支(可省略)。例如:

main() {
    let result = Option<Int64>.Some(2023)

    if (let Some(value) <- result) {
        println("操作成功,返回值为:${value}")
    } else {
        println("操作失败")
    }
}

运行以上程序,将输出:

操作成功,返回值为:2023

对于以上程序,如果将 result 的初始值修改为 Option<Int64>.None,则 if-let 的模式匹配会失败,将执行 else 分支:

main() {
    let result = Option<Int64>.None

    if (let Some(value) <- result) {
        println("操作成功,返回值为:${value}")
    } else {
        println("操作失败")
    }
}

运行以上程序,将输出:

操作失败

while-let 表达式

while-let 表达式首先对条件中 let 等号右侧的表达式进行求值,如果此值能匹配 let 等号左侧的模式,则执行循环体,然后重复执行此过程。如果模式匹配失败,则结束循环,继续执行 while-let 表达式之后的代码。例如:

from std import random.*

// 此函数模拟在通信中接收数据,获取数据可能失败
func recv(): Option<UInt8> {
    let number = Random().nextUInt8()
    if (number < 128) {
        return Some(number)
    }
    return None
}

main() {
    // 模拟循环接收通信数据,如果失败就结束循环
    while (let Some(data) <- recv()) {
        println(data)
    }
    println("receive failed")
}

运行以上程序,可能的输出为:

73
94
receive failed

其他使用模式的地方

模式除了可以在 match 表达式中使用外,还可以使用在变量定义(等号左侧是个模式)和 for in 表达式(for 关键字和 in 关键字之间是个模式)中。

但是,并不是所有的模式都能使用在变量定义和 for in 表达式中,只有 irrefutable 的模式才能在这两处被使用,所以只有通配符模式、绑定模式、irrefutable tuple 模式和 irrefutable enum 模式是允许的。

  1. 变量定义和 for in 表达式中使用通配符模式的例子如下:

    main() {
        let _ = 100
        for (_ in 1..5) {
            println("0")
        }
    }
    

    上例中,变量定义时使用了通配符模式,表示定义了一个没有名字的变量(当然此后也就没办法对其进行访问),for in 表达式中使用了通配符模式,表示不会将 1..5 中的元素与某个变量绑定(当然循环体中就无法访问 1..5 中元素值)。编译执行上述代码,输出结果为:

    0
    0
    0
    0
    
  2. 变量定义和 for in 表达式中使用绑定模式的例子如下:

    main() {
        let x = 100
        println("x = ${x}")
        for (i in 1..5) {
            println(i)
        }
    }
    

    上例中,变量定义中的 x 以及 for in 表达式中的 i 都是绑定模式。编译执行上述代码,输出结果为:

    x = 100
    1
    2
    3
    4
    
  3. 变量定义和 for in 表达式中使用 irrefutable tuple 模式的例子如下:

    main() {
        let (x, y) = (100, 200)
        println("x = ${x}")
        println("y = ${y}")
        for ((i, j) in [(1, 2), (3, 4), (5, 6)]) {
            println("Sum = ${i + j}")
        }
    }
    
    

    上例中,变量定义时使用了 tuple 模式,表示对 (100, 200) 进行解构并分别和 xy 进行绑定,效果上相当于定义了两个变量 xyfor in 表达式中使用了 tuple 模式,表示依次将 [(1, 2), (3, 4), (5, 6)] 中的 tuple 类型的元素取出,然后解构并分别和 ij 进行绑定,循环体中输出 i + j 的值。编译执行上述代码,输出结果为:

    x = 100
    y = 200
    Sum = 3
    Sum = 7
    Sum = 11
    
  4. 变量定义和 for in 表达式中使用 irrefutable enum 模式的例子如下:

    enum RedColor {
        Red(Int64)
    }
    main() {
        let Red(red) = Red(0)
        println("red = ${red}")
        for (Red(r) in [Red(10), Red(20), Red(30)]) {
            println("r = ${r}")
        }
    }
    

    上例中,变量定义时使用了 enum 模式,表示对 Red(0) 进行解构并将构造器的参数值(即 0)与 red 进行绑定。for in 表达式中使用了 enum 模式,表示依次将 [Red(10), Red(20), Red(30)] 中的元素取出,然后解构并将构造器的参数值与 r 进行绑定,循环体中输出 r 的值。编译执行上述代码,输出结果为:

    red = 0
    r = 10
    r = 20
    r = 30
    

泛型

在仓颉编程语言中,泛型指的是参数化类型,参数化类型是一个在声明时未知并且需要在使用时指定的类型。类型声明与函数声明可以是泛型的。最为常见的例子就是 Array<T>Set<T> 等容器类型。以数组类型为例,当使用数组类型 Array 时,会需要其中存放的是不同的类型,我们不可能定义所有类型的数组,通过在类型声明中声明类型形参,在应用数组时再指定其中的类型,这样就可以减少在代码上的重复。

术语

为了方便讨论我们先定义以下几个常用的术语:

  • 类型形参:一个类型或者函数声明可能有一个或者多个需要在使用处被指定的类型,这些类型就被称为类型形参。在声明形参时,需要给定一个标识符,以便在声明体中引用。
  • 类型变元:在声明类型形参后,当我们通过标识符来引用这些类型时,这些标识符被称为类型变元。
  • 类型实参:当我们在使用泛型声明的类型或函数时指定了泛型参数,这些参数被称为类型实参。
  • 类型构造器:一个需要零个、一个或者多个类型作为实参的类型称为类型构造器。

类型形参在声明时一般在类型名称的声明或者函数名称的声明后,使用尖括号 <...> 括起来。例如泛型列表可声明为:

class List<T> {
    var elem: Option<T> = None
    var tail: Option<List<T>> = None
}

func sumInt(a: List<Int64>) {  }

其中 List<T> 中的 T 被称为类型形参。对于 elem: Option<T> 中对 T 的引用称为类型变元,同理 tail: Option<List<T>> 中的 T 也称为类型变元。函数 sumInt 的参数中 List<Int64>Int64 被称为 List 的类型实参。 List 就是类型构造器,List<Int64> 通过 Int64 类型实参构造出了一个类型 Int64 的列表类型。

泛型接口

泛型可以用来定义泛型接口,以标准库中定义的 Iterable 为例,它需要返回一个 Iterator 类型,这一类型是一个容器的遍历器。 Iterator 是一个泛型接口,Iterator 内部有一个从容器类型中返回下一个元素的 next 成员函数,next 成员函数返回的类型是一个需要在使用时指定的类型,所以 Iterator 需要声明泛型参数。

public interface Iterable<E> {
    func iterator(): Iterator<E>
}

public interface Iterator<E> <: Iterable<E> {
    func next(): Option<E>
}

public interface Collection<T> <: Iterable<T> {
     prop size: Int64
     func isEmpty(): Bool
}

泛型类型

在仓颉编程语言中,class、struct 与 enum 的声明都可以声明类型形参,也就是说它们都可以是泛型的。

泛型 class

[泛型接口]中介绍了泛型接口的定义和使用,本节我们介绍泛型类的定义和使用。如 Map 的键值对就是使用泛型类来定义的:

可以看一下 Map 类型中的键值对 Node 类型就可以使用泛型类来定义:

public open class Node<K, V> where K <: Hashable & Equatable<K> {
    public var key: Option<K> = Option<K>.None
    public var value: Option<V> = Option<V>.None

    public init() {}

    public init(key: K, value: V) {
        this.key = Option<K>.Some(key)
        this.value = Option<V>.Some(value)
    }
}

由于键与值的类型有可能不相同,且可以为任意满足条件的类型,所以 Node 需要两个类型形参 KVK <: Hashable, K <: Equatable<K> 是对于键类型的约束,意为 K 要实现 HashableEquatable<K> 接口,也就是 K 需要满足的条件。对于泛型约束,详见泛型约束章节。

泛型 struct

struct 类型的泛型与 class 是类似的,下面我们可以使用 struct 定义一个类似于二元元组的类型:

struct Pair<T, U> {
    let x: T
    let y: U
    public init(a: T, b: U) {
        x = a
        y = b
    }
    public func first(): T {
        return x
    }
    public func second(): U {
        return y
    }
}

main() {
    var a: Pair<String, Int64> = Pair<String, Int64>("hello", 0)
    println(a.first())
    println(a.second())
}

程序输出的结果为:

hello
0

Pair 中我们提供了 firstsecond 两个函数来取得元组的第一个与第二个元素。

泛型 enum

在仓颉编程语言中,泛型 enum 声明的类型里被使用得最广泛的例子之一就是 Option 类型了,关于 Option 详细描述可以详见 Option 类型章节。 Option 类型是用来表示在某一类型上的值可能是个空的值。这样,Option 就可以用来表示在某种类型上计算的失败。这里是何种类型上的失败是不确定的,所以很明显,Option 是一个泛型类型,需要声明类型形参。

package core // `Option` is defined in core.

public enum Option<T> {
      Some(T)
    | None

    public func getOrThrow(): T {
        match (this) {
            case Some(v) => v
            case None => throw NoneValueException()
        }
    }
    ...
}

可以看到,Option<T> 分成两种情况,一种是 Some(T),用来表示一个正常的返回结果,另一种是 None 用来表示一个空的结果。其中的 getOrThrow 函数会是将 Some(T) 内部的值返回出来的函数,返回的结果就是 T 类型,而如果参数是 None,那么直接抛出异常。

例如:如果我们想定义一个安全的除法,因为在除法上的计算是可能失败的。如果除数为 0,那么返回 None ,否则返回一个用 Some 包装过的结果:

func safeDiv(a: Int64, b: Int64): Option<Int64> {
    var res: Option<Int64> = match (b) {
                case 0 => None
                case _ => Some(a/b)
            }
    return res
}

这样,在除数为 0 时,程序运行的过程中不会因除以 0 而抛出算术运算异常。

泛型类型的子类型关系

实例化后的泛型类型间也有子类型关系。例如当我们写出下列代码时,

interface I<X, Y> { }

class C<Z> <: I<Z, Z> { }

根据第 3 行,便知 C<Bool> <: I<Bool, Bool> 以及 C<D> <: I<D, D> 等。这里的第 3 行可以解读为“于所有的(不含类型变元的) Z 类型,都有 C<Z> <: I<Z, Z> 成立”。

但是对于下列代码

open class C { }
class D <: C { }

interface I<X> { }

I<D> <: I<C> 是不成立的(即使 D <: C 成立),这是因为在仓颉语言中,用户定义的类型构造器在其类型参数处是不型变的。

型变的具体定义为:如果 AB 是(实例化后的)类型,T 是类型构造器,设有一个类型参数 X(例如 interface T<X>),那么

  • 如果 T(A) <: T(B) 当且仅当 A = B,则 T不型变的。
  • 如果 T(A) <: T(B) 当且仅当 A <: B ,则 TX 处是协变的。
  • 如果 T(A) <: T(B) 当且仅当 B <: A ,则 TX 处是逆变的。

因为现阶段的仓颉中,所有用户自定义的泛型类型在其所有的类型变元处都是不变的,所以给定 interface I<X> 和类型 AB,只有 A = B,我们才能得到 I<A> <: I<B>;反过来,如果知道了 I<A> <: I<B>,也可推出 A = B(内建类型除外:内建的元组类型对其每个元素类型来说,都是协变的;内建的函数类型在其入参类型处是逆变的,在其返回类型处是协变的。)

不型变限制了一些语言的表达能力,但也避免了一些安全问题,例如“协变数组运行时抛异常”的问题(Java 便有这个问题)。

泛型的类型别名

类型别名也是可以声明类型形参的,但是不能对其形参使用 where 声明约束,对于泛型变元的约束我们会在后面给出解释。

当一个泛型类型的名称过长时,我们就可以使用类型别名来为其声明一个更短的别名。例如,有一个类型为 RecordData ,我们可以把他用类型别名简写为 RD

struct RecordData<T> {
    var a: T
    public init(x: T){
        a = x
    }
}

type RD<T> = RecordData<T>

main(): Int64 {
    var struct1: RD<Int32> = RecordData<Int32>(2)
    return 1
}

在使用时就可以用 RD<Int32> 来代指 RecordData<Int32> 类型。

泛型函数

如果一个函数声明了一个或多个类型形参,则将其称为泛型函数。语法上,类型形参紧跟在函数名后,并用 <> 括起,如果有多个类型形参,则用 , 分离。

全局泛型函数

在声明全局泛型函数时,只需要在函数名后使用尖括号声明类型形参,然后就可以在函数形参、返回类型及函数体中对这一类型形参进行引用。例如 id 函数定义为:

func id<T>(a: T): T {
    return a
}

其中 (a: T) 是函数声明的形参,其中使用到了 id 函数声明的类型形参 T,并且在 id 函数的返回类型使用。

再比如另一个复杂的例子,定义如下一个泛型函数 composition,该函数声明了 3 个类型形参,分别是 T1, T2, T3,其功能是把两个函数 f: (T1) -> T2, g: (T2) -> T3 复合成类型为 (T1) -> T3 的函数。

func composition<T1, T2, T3>(f: (T1) -> T2, g: (T2) -> T3): (T1) -> T3 {
    return {x: T1 => g(f(x))}
}

因为被用来复合的函数可以是任意类型,例如可以是 (Int32) -> Bool, (Bool) -> Int64 的复合,也可以是 (Int64) -> Char, (Char) -> Int8 的复合,所以才需要使用泛型函数。

func times2(a: Int64): Int64 {
    return a * 2
}

func plus10(a: Int64): Int64 {
    return a + 10
}

func times2plus10(a: Int64) {
    return composition<Int64, Int64, Int64>(times2, plus10)(a)
}

main() {
  println(times2plus10(9))
  return 0
}

这里,我们复合两个 (Int64) -> Int64 的函数,将 9 先乘以 2,再加 10,结果会是 28。

28

局部泛型函数

局部函数也可以是泛型函数。例如泛型函数 id 可以嵌套定义在其它函数中:

func foo(a: Int64) {
    func id<T>(a: T): T { a }

    func double(a: Int64): Int64 { a + a }

    return (id<Int64> ~> double)(a) == (double ~> id<Int64>)(a)
}

main() {
    println(foo(1))
    return 0
}

这里由于 id 的单位元性质,函数 id<Int64> ~> doubledouble ~> id<Int64> 是等价的,结果是 true

true

泛型成员函数

class、struct 与 enum 的成员函数可以是泛型的。例如:

class A {
    func foo<T>(a: T): Unit where T <: ToString {
        println("${a}")
    }
}

struct B {
    func bar<T>(a: T): Unit where T <: ToString {
        println("${a}")
    }
}

enum C {
    | X | Y

    func coo<T>(a: T): Unit where T <: ToString {
        println("${a}")
    }
}

main() {
    var a = A()
    var b = B()
    var c = C.X
    a.foo<Int64>(10)
    b.bar<String>("abc")
    c.coo<Bool>(false)
    return 0
}

程序输出的结果为:

10
abc
false

这里需要注意的是,class 中声明的泛型成员函数不能被 open 修饰,如果被 open 修饰则会报错,例如:

class A {
    public open func foo<T>(a: T): Unit where T <: ToString { // Error: open generic function is not allowed
        println("${a}")
    }
}

在为类型使用 extend 声明进行扩展时,扩展中的函数也可以是泛型的,例如我们可以为 Int64 类型增加一个泛型成员函数:

extend Int64 {
    func printIntAndArg<T>(a: T) where T <: ToString {
        println(this)
        println("${a}")
    }
}

main() {
    var a: Int64 = 12
    a.printIntAndArg<String>("twelve")
}

程序输出的结果将为:

12
twelve

静态泛型函数

interface、class、struct、enum 与 extend 中可以定义静态泛型函数,例如下例 ToPair class 中从 ArrayList 中返回一个元组:

from std import collection.*

class ToPair {
    public static func fromArray<T>(l: ArrayList<T>): (T, T) {
        return (l[0], l[1])
    }
}

main() {
    var res: ArrayList<Int64> = ArrayList([1,2,3,4])
    var a: (Int64, Int64) = ToPair.fromArray<Int64>(res)
    return 0
}

泛型约束

泛型约束的作用是在函数、class、enum、struct 声明时明确泛型形参所具备的操作与能力。只有声明了这些约束才能调用相应的成员函数。在很多场景下泛型形参是需要加以约束的。以 id 函数为例:

func id<T>(a: T) {
    return a
}

我们唯一能做的事情就是将函数形参 a 这个值返回,而不能进行 a + 1println("${a}") 等操作,因为它可能是一个任意的类型,比如 (Bool) -> Bool,这样就无法与整数相加,同样因为是函数类型,也不能通过 println 函数来输出在命令行上。而如果这一泛型形参上有了约束,那么就可以做更多操作了。

约束大致分为接口约束与子类型约束。语法为在函数、类型的声明体之前使用 where 关键字来声明,对于声明的泛型形参 T1, T2,可以使用 where T1 <: Interface, T2 <: Type 这样的方式来声明泛型约束,同一个类型变元的多个约束可以使用 & 连接。例如:where T1 <: Interface1 & Interface2

例如,仓颉中的 println 函数能接受类型为字符串的参数,如果我们需要把一个泛型类型的变量转为字符串后打印在命令行上,可以对这个泛型类型变元加以约束,这个约束是 core 中定义的 ToString 接口,显然它是一个接口约束:

package core // `ToString` is defined in core.

public interface ToString {
    func toString(): String
}

这样我们就可以利用这个约束,定义一个名为 genericPrint 的函数:

func genericPrint<T>(a: T) where T <: ToString {
    println(a)
}

main() {
    genericPrint<Int64>(10)
    return 0
}

结果为:

10

如果 genericPrint 函数的类型实参没有实现 ToString 接口,那么编译器会报错。例如我们传入一个函数做为参数时:

func genericPrint<T>(a: T) where T <: ToString {
    println(a)
}

main() {
    genericPrint<(Int64) -> Int64>({ i => 0 })
    return 0
}

如果我们对上面的文件进行编译,那么编译器会抛出泛型类型参数与满足约束的错误。因为 genericPrint 函数的泛型的类型实参不满足约束 (Int64) -> Int64 <: ToString

除了上述通过接口来表示约束,还可以使用子类型来约束一个泛型类型变元。例如:当我们要声明一个动物园类型 Zoo<T>,但是我们需要这里声明的类型形参 T 受到约束,这个约束就是 T 需要是动物类型 Animal 的子类型, Animal 类型中声明了 run 成员函数。这里我们声明两个子类型 DogFox 都实现了 run 成员函数,这样在 Zoo<T> 的类型中,我们就可以对于 animals 数组列表中存放的动物实例调用 run 成员函数:

from std import collection.*

abstract class Animal {
    public func run(): String
}

class Dog <: Animal {
    public func run(): String {
        return "dog run"
    }
}

class Fox <: Animal {
    public func run(): String {
        return "fox run"
    }
}

class Zoo<T> where T <: Animal {
    var animals: ArrayList<Animal> = ArrayList<Animal>()
    public func addAnimal(a: T) {
        animals.append(a)
    }

    public func allAnimalRuns() {
        for(a in animals) {
            println(a.run())
        }
    }
}

main() {
    var zoo: Zoo<Animal> = Zoo<Animal>()
    zoo.addAnimal(Dog())
    zoo.addAnimal(Fox())
    zoo.allAnimalRuns()
    return 0
}

程序的输出为:

dog run
fox run

属性

属性(Properties)提供了一个 getter 和一个可选的 setter 来间接检索和设置值。

使用属性的时候与普通变量无异,我们只需要对数据操作,对内部的实现无感知,可以更便利地实现访问控制、数据监控、跟踪调试、数据绑定等机制。

属性在使用时可以作为表达式或被赋值。

以下是一个简单的例子,b 是一个典型的属性,封装了外部对 a 的访问:

class Foo {
    private var a = 0

    public mut prop b: Int64 {
        get() {
            println("get")
            a
        }
        set(value) {
            println("set")
            a = value
        }
    }
}

main() {
    var x = Foo()
    let y = x.b + 1 // get
    x.b = y // set
}

此处 Foo 提供了一个名为 b 的属性,针对 getter/setter 这两个功能,我们提供了 get 和 set 两种语法来定义。当一个类型为 Foo 的变量 x 在访问 b 时,会调用 b 的 get 操作返回类型为 Int64 的值,因此可以用来与 1 相加;而当 x 在对 b 进行赋值时,会调用 b 的 set 操作,将 y 的值传给 set 的 value,最终将 value 的值赋值给 a。

通过属性 b,外部对 Foo 的成员变量 a 完全不感知,但却可以通过 b 做到同样的访问和修改操作,实现了有效的封装性。所以程序的输出如下:

get
set

定义属性

属性可以在 interface,class,struct,enum,extend 中定义。

一个典型的属性语法结构如下:

class Foo {
    public prop a: Int64 {
        get() { 0 }
    }
    public mut prop b: Int64 {
        get() { 0 }
        set(v) {}
    }
}

其中使用 prop 声明的 a 和 b 都是属性,a 和 b 的类型都是 Int64。a 是无 mut 修饰符的属性,这类属性有且仅有定义 getter(对应取值)实现。b 是使用 mut 修饰的属性,这类属性必须分别定义 getter(对应取值)和 setter(对应赋值)的实现。

属性的 getter 和 setter 分别对应两个不同的函数。

  1. getter 函数类型是 () -> T,T 是该属性的类型,当使用该属性作为表达式时会执行 getter 函数。
  2. setter 函数类型是 (T) -> Unit,T 是该属性的类型,形参名需要显式指定,当对该属性赋值时会执行 setter 函数。

getter 和 setter 的实现中可以和函数体一样包含声明和表达式,与函数体的规则一样,详见函数体章节。

setter 中的参数对应的是赋值时传入的值。

class Foo {
    private var j = 0
    public mut prop i: Int64 {
        get() {
            j
        }
        set(v) {
            j = v
        }
    }
}

需要注意的是,在属性的 getter 和 setter 中访问属性自身属于递归调用,与函数调用一样可能会出现死循环的情况。

修饰符

我们可以在 prop 前面声明需要的修饰符。

class Foo {
    public prop a: Int64 {
        get() {
            0
        }
    }
    private prop b: Int64 {
        get() {
            0
        }
    }
}

和成员函数一样,成员属性也支持 openoverrideredef 修饰,所以我们也可以在子类型中 override/redef 父类型属性的实现。

子类型覆盖父类型的属性时,如果父类型属性带有 mut 修饰符,则子类型属性也需要带有 mut 修饰符,同时也必须保持一样的类型。

如下代码所示,A 中定义了 x 和 y 两个属性,B 中可以分别对 x 和 y 进行 override/redef

open class A {
    private var valueX = 0
    private static var valueY = 0

    public open prop x: Int64 {
        get() { valueX }
    }

    public static mut prop y: Int64 {
        get() { valueY }
        set(v) {
            valueY = v
        }
    }
}
class B <: A {
    private var valueX2 = 0
    private static var valueY2 = 0

    public override prop x: Int64 {
        get() { valueX2 }
    }

    public redef static mut prop y: Int64 {
        get() { valueY2 }
        set(v) {
            valueY2 = v
        }
    }
}

抽象属性

类似于抽象函数,我们在 interface 和抽象类中也可以声明抽象属性,这些抽象属性没有实现。

interface I {
    prop a: Int64
}

abstract class C {
    public prop a: Int64
}

当实现类型实现 interface 或者非抽象子类继承抽象类时,必须要实现这些抽象属性。

与覆盖的规则一样,实现类型或子类在实现这些属性时,如果父类型属性带有 mut 修饰符,则子类型属性也需要带有 mut 修饰符,同时也必须保持一样的类型。

interface I {
    prop a: Int64
    mut prop b: Int64
}
class C <: I {
    private var value = 0

    public prop a: Int64 {
        get() { value }
    }

    public mut prop b: Int64 {
        get() { value }
        set(v) {
            value = v
        }
    }
}

通过抽象属性,我们可以让接口和抽象类对一些数据操作能以更加易用的方式进行约定,相比函数的方式要更加直观。

如下代码所示,如果我们要对一个 size 值的获取和设置进行约定,使用属性的方式 (I1) 相比使用函数的方式 (I2) 代码更少,也更加符合对数据操作的意图。

interface I1 {
    mut prop size: Int64
}

interface I2 {
    func getSize(): Int64
    func setSize(value: Int64): Unit
}

class C <: I1 & I2 {
    private var mySize = 0

    public mut prop size: Int64 {
        get() {
            mySize
        }
        set(value) {
            mySize = value
        }
    }

    public func getSize() {
        mySize
    }

    public func setSize(value: Int64) {
        mySize = value
    }
}

main() {
    let a: I1 = C()
    a.size = 5
    println(a.size)

    let b: I2 = C()
    b.setSize(5)
    println(b.getSize())
}
5
5

使用属性

属性分为实例成员属性和静态成员属性。成员属性的使用和成员变量的使用方式一样,详见成员变量章节。

class A {
    public prop x: Int64 {
        get() {
            123
        }
    }
    public static prop y: Int64 {
        get() {
            321
        }
    }
}

main() {
    var a = A()
    println(a.x) // 123
    println(A.y) // 321
}

结果为:

123
321

mut 修饰符的属性类似 let 声明的变量,不可以被赋值。

class A {
    private let value = 0
    public prop i: Int64 {
        get() {
            value
        }
    }
}

main() {
    var x = A()
    println(x.i) // OK
    x.i = 1 // Error
}

带有 mut 修饰符的属性类似 var 声明的变量,可以取值也可以被赋值。

class A {
    private var value: Int64 = 0
    public mut prop i: Int64 {
        get() {
            value
        }
        set(v) {
            value = v
        }
    }
}

main() {
    var x = A()
    println(x.i) // OK
    x.i = 1 // OK
}
0

扩展

扩展可以为在当前 package 可见的类型(除函数、元组、接口)添加新功能。

当我们不能破坏原有类型的封装性,但希望添加额外的功能时,可以使用扩展。

可以添加的功能包括:

  • 添加成员函数
  • 添加操作符重载函数
  • 添加成员属性
  • 实现接口

扩展虽然可以添加额外的功能,但不能变更原有类型的封装性,因此扩展不支持以下功能:

  1. 扩展不能增加成员变量。
  2. 扩展的函数和属性必须拥有实现。
  3. 扩展的函数和属性不能使用 openoverrideredef修饰。
  4. 扩展不能访问原类型 private 的成员。

扩展的定义

一个简单的扩展语法结构示例如下:

extend String {
    public func printSize() {
        println("the size is ${this.size}")
    }
}

如上例所示,扩展使用 extend 关键字声明,其后跟着被扩展的类型 String 和扩展的功能。

当为 String 扩展了 printSize 函数之后,我们就能在当前 package 内对 String 的实例访问该函数,就像是 String 本身具备该函数。

main() {
    let a = "123"
    a.printSize() // the size is 3
}

编译执行上述代码,输出结果为:

the size is 3

被扩展类型处的泛型变元的名称不需要与原来定义处的相同(但个数要相同)。被扩展类型中的类型变元会隐式引入被扩展类型定义时的泛型约束。

例如下面所示的 MyList<T>

class MyList<T> {
    public let data: Array<T> = Array<T>()
}

extend MyList<T> {} // OK
extend MyList<R> {} // OK
extend MyList {} // error
extend MyList<T, R> {} // error

对于泛型类型的扩展,我们可以在其中声明额外的泛型约束,来实现一些有限情况下才能使用的函数。

例如我们可以定义一个叫 Pair 的类型,这个类型可以让我们方便的存储两个元素(类似于 Tuple)。

我们希望 Pair 类型可以容纳任何类型,因此两个泛型变元不应该有任何约束,这样才能保证 Pair 能容纳所有类型。

但同时我们又希望当两个元素可以判等的时候,让 Pair 也可以判等,这时就可以用扩展来实现这个功能。

如下面的代码所示,我们使用扩展语法,约束了 T1 和 T2 在支持 equals 的情况下,Pair 也可以实现 equals 函数。

class Pair<T1, T2> {
    var first: T1
    var second: T2
    public init(a: T1, b: T2) {
        first = a
        second = b
    }
}

interface Eq<T> {
    func equals(other: T): Bool
}

extend Pair<T1, T2> where T1 <: Eq<T1>, T2 <: Eq<T2> {
    public func equals(other: Pair<T1, T2>) {
        first.equals(other.first) && second.equals(other.second)
    }
}

class Foo <: Eq<Foo> {
    public func equals(other: Foo): Bool {
        true
    }
}

main() {
    let a = Pair(Foo(), Foo())
    let b = Pair(Foo(), Foo())
    println(a.equals(b)) // true
}

编译执行上述代码,输出结果为:

true

根据扩展有没有实现新的接口,扩展可以分为 直接扩展接口扩展 两种用法,直接扩展即不包含额外接口的扩展,以上我们看到的例子都属于直接扩展;接口扩展即包含接口的扩展,接口扩展可以用来为现有的类型添加新功能并实现接口,增强抽象灵活性。

例如下面的例子,类型 Array 本身没有实现接口 PrintSizeable,但我们可以通过扩展的方式为 Array 增加额外的成员函数 printSize,并实现 PrintSizeable

interface PrintSizeable {
    func printSize(): Unit
}

extend Array<T> <: PrintSizeable {
    public func printSize() {
        println("The size is ${this.size}")
    }
}

当使用扩展为 Array 实现 PrintSizeable 之后,就相当于在 Array 定义时实现接口 PrintSizeable

因此我们可以将 Array 作为 PrintSizeable 的实现类型来使用了,如以下代码所示。

main() {
    let a: PrintSizeable = Array<Int64>()
    a.printSize() // 0
}

编译执行上述代码,输出结果为:

The size is 0

我们可以在同一个扩展内同时实现多个接口,多个接口之间使用 & 分开,接口的顺序没有先后关系。

如下面代码所示,我们可以在扩展中为 Foo 同时实现 I1I2I3

interface I1 {
    func f1(): Unit
}

interface I2 {
    func f2(): Unit
}

interface I3 {
    func f3(): Unit
}

class Foo {}

extend Foo <: I1 & I2 & I3 {
    public func f1(): Unit {}
    public func f2(): Unit {}
    public func f3(): Unit {}
}

我们也可以在接口扩展中声明额外的泛型约束,来实现一些特定约束下才能满足的接口。

例如我们可以让上面的 Pair 类型实现 Eq 接口,这样 Pair 自己也能成为一个符合 Eq 约束的类型,如下代码所示。

class Pair<T1, T2> {
    var first: T1
    var second: T2
    public init(a: T1, b: T2) {
        first = a
        second = b
    }
}

interface Eq<T> {
    func equals(other: T): Bool
}

extend Pair<T1, T2> <: Eq<Pair<T1, T2>> where T1 <: Eq<T1>, T2 <: Eq<T2> {
    public func equals(other: Pair<T1, T2>) {
        first.equals(other.first) && second.equals(other.second)
    }
}

class Foo <: Eq<Foo> {
    public func equals(other: Foo): Bool {
        true
    }
}

main() {
    let a = Pair(Foo(), Foo())
    let b = Pair(Foo(), Foo())
    println(a.equals(b)) // true
}

编译执行上述代码,输出结果为:

true

如果被扩展的类型已经包含接口要求的函数或属性,那么我们在扩展中不需要并且也不能重新实现这些函数或属性。

例如下面的例子,我们定义了一个新接口 Sizeable,目的是获得某个类型的 size,而我们已经知道 Array 中包含了这个函数,因此我们就可以通过扩展让 Array 实现 Sizeable,而不需要添加额外的函数。

interface Sizeable {
    prop size: Int64
}

extend Array<T> <: Sizeable {}

main() {
    let a: Sizeable = Array<Int64>()
    println(a.size)
}

编译执行上述代码,输出结果为:

0

扩展的修饰符

扩展本身不能使用修饰符修饰。

例如,下面的例子中对 A 的直接扩展前使用了 public 修饰,将编译报错。

public class A {}

public extend A {}  // error, expected no modifier before extend

扩展成员可使用的修饰符有:staticpublicprotected(仅限于被扩展类型是 class 类型)、privatemut

  • 使用 private 修饰的成员只能在本扩展内使用,外部不可见。
  • 使用 protected 修饰的成员除了能在本包内被访问,对包外的当前 class 子类也可以访问。
  • 没有使用 privateprotectedpublic 修饰的成员只能在本包内使用。
  • 使用 static 修饰的成员,只能通过类型名访问,不能通过实例对象访问。
  • struct 类型的扩展可以定义 mut 函数。
package p1

public open class A {}

extend A {
    public func f1() {}
    protected func f2() {}
    private func f3() {}
    static func f4() {}
}

main() {
    A.f4()
    var a = A()
    a.f1()
    a.f2()
}

扩展内的成员定义不支持使用 openoverrideredef 修饰。

class Foo {
    public open func f() {}
    static func h() {}
}

extend Foo {
    public override func f() {} // error
    public open func g() {} // error
    redef static func h() {} // error
}

扩展的孤儿规则

为一个其它 package 的类型实现另一个 package 的接口,可能造成理解上的困扰。

为了防止一个类型被意外实现不合适的接口,我们不允许定义孤儿扩展,指的是既不与接口(包含接口继承链上的所有接口)定义在同一个包中,也不与被扩展类型定义在同一个包中的接口扩展。

如下代码所示,我们不能在 package c 中,为 package a 里的 Foo 实现 package b 里的 Bar

我们只能在 package a 或者在 package b 中为 Foo 实现 Bar

// package a
public class Foo {}

// package b
public interface Bar {}

// package c
import a.Foo
import b.Bar

extend Foo <: Bar {} // Error

扩展的访问和遮盖

扩展的实例成员与类型定义处一样可以使用 thisthis 的功能保持一致。同样也可以省略 this 访问成员。扩展的实例成员不能使用 super

class A {
    var v = 0
}

extend A {
    func f() {
        print(this.v) // ok
        print(v) // ok
    }
}

扩展不能访问被扩展类型的 private 修饰的成员(意味着非 private 修饰的成员均能被访问)。

class A {
    private var v1 = 0
    protected var v2 = 0
}

extend A {
    func f() {
        print(v1) // error
        print(v2) // ok
    }
}

扩展不能遮盖被扩展类型的任何成员。

class A {
    func f() {}
}

extend A {
    func f() {} // error
}

扩展也不允许遮盖其它扩展增加的任何成员。

class A {}

extend A {
    func f() {}
}

extend A {
    func f() {} // error
}

在同一个 package 内对同一类型可以扩展多次。

在扩展中可以直接调用(不加任何前缀修饰)其它对同一类型的扩展中的非 private 修饰的函数。

class Foo {}

extend Foo { // OK
    private func f() {}
    func g() {}
}

extend Foo { // OK
    func h() {
        g() // OK
        f() // Error
    }
}

扩展泛型类型时,可以使用额外的泛型约束。泛型类型的任意两个扩展之间的可见性规则如下:

  • 如果两个扩展的约束相同,则两个扩展相互可见,即两个扩展内可以直接使用对方内的函数或属性;
  • 如果两个扩展的约束不同,且两个扩展的约束有包含关系,约束更宽松的扩展对约束更严格的扩展可见,反之,不可见;
  • 当两个扩展的约束不同时,且两个约束不存在包含关系,则两个扩展均互相不可见。

示例:假设对同一个类型 E<X> 的两个扩展分别为扩展 1 和扩展 2X 的约束在扩展 1 中比扩展 2 中更严格,那么扩展 1 中的函数和属性对扩展 2 均不可见,反之,扩展 2 中的函数和属性对扩展 1 可见。

// B <: A
class E<X> {}

interface I1 {
    func f1(): Unit
}
interface I2 {
    func f2(): Unit
}

extend E<X> <: I1 where X <: B {  // extension 1
    public func f1(): Unit {
        f2() // OK
    }
}

extend E<X> <: I2 where X <: A   { // extension 2
    public func f2(): Unit {
        f1() // Error
    }
}

扩展的导入导出

扩展也是可以被导入和导出的,但是扩展本身不能使用 public 修饰,扩展的导出有一套特殊的规则。

对于直接扩展,只有当扩展与被扩展的类型在同一个 package,并且被扩展的类型和扩展中添加的成员都使用 publicprotected 修饰时,扩展的功能才会被导出。

除此以外的直接扩展均不能被导出,只能在当前 package 使用。

如以下代码所示,Foo 是使用 public 修饰的类型,并且 fFoo 在同一个 package 内,因此 f 会跟随 Foo 一起被导出。而 gFoo 不在同一个 package,因此 g 不会被导出。

package a

public class Foo {}

extend Foo {
    public func f() {}
}

///////

package b

extend Foo {
    public func g() {}
}

///////

package c
import a.*
import b.*

main() {
    let a = Foo()
    a.f() // OK
    a.g() // Error
}

对于接口扩展则分为两种情况:

  1. 如果接口扩展和被扩展类型在同一个 package,但接口是来自导入的,只有当被扩展类型使用 public 修饰时,扩展的功能才会被导出。
  2. 如果接口扩展与接口在同一个 package,则只有当接口是使用 public 修饰时,扩展的功能才会被导出。

如下代码所示,FooI 都使用了 public 修饰,因此对 Foo 的扩展就可以被导出。

package a

public class Foo {}

public interface I {
    func g(): Unit
}

extend Foo <: I {
    public func g(): Unit {}
}

///////

package b
import a.*

main() {
    let a: I = Foo()
    a.g()
}

与扩展的导出类似,扩展的导入也不需要显式地用 import 导入,扩展的导入只需要导入被扩展的类型和接口,就可以导入可访问的所有扩展。

如下面的代码所示,在 package b 中,只需要导入 Foo 就可以使用 Foo 对应的扩展中的函数 f

而对于接口扩展,需要同时导入被扩展的类型和扩展的接口才能使用,因此在 package c 中,需要同时导入 FooI 才能使用对应扩展中的函数 g

package a

public class Foo {}
extend Foo {
    public func f() {}
}

///////

package b
import a.Foo

public interface I {
    func g(): Unit
}
extend Foo <: I {
    public func g() {
        this.f() // OK
    }
}

///////
package c
import a.Foo
import b.I

func test() {
    let a = Foo()
    a.f() // OK
    a.g() // OK
}

并发

并发编程是现代编程语言中不可或缺的特性,仓颉编程语言提供抢占式的线程模型作为并发编程机制。在谈及编程语言和线程时,线程其实可以细化为两种不同概念,语言线程native 线程

  • 前者是编程语言中并发模型的基本执行单位,语言线程的目的是屏蔽底层实现细节。例如,仓颉编程语言希望给开发者提供一个友好、高效、统一的并发编程界面,让开发者无需关心操作系统线程、用户态线程等差异,因此提供仓颉线程的概念。开发者在大多数情况下只需面向仓颉线程编写并发代码。
  • 后者指语言实现中所使用到的线程(一般是操作系统线程),他们作为语言线程的具体实现载体。不同编程语言会以不同的方式实现语言线程。例如,一些编程语言直接通过操作系统调用来创建线程,这意味着每个语言线程对应一个 native 线程,这种实现方案一般被称之为 1:1 线程模型。此外,另有一些编程语言提供特殊的线程实现,他们允许多个语言线程在多个 native 线程上切换执行,这种也被称为 M:N 线程模型,即 M 个语言线程在 N 个 native 线程上调度执行,其中 M 和 N 不一定相等。当前,仓颉语言的实现同样采用 M:N 线程模型;因此,仓颉线程本质上是一种用户态的轻量级线程,支持抢占且相比操作系统线程更轻量化。

注:本文档在没有歧义的情况下将直接以线程一词简化对仓颉线程的指代。

创建仓颉线程

当开发者希望并发执行某一段代码时,只需创建一个仓颉线程即可。要创建一个新的仓颉线程,可以使用关键字 spawn 并传递一个无形参的 lambda 表达式,该 lambda 表达式即为在新线程中执行的代码。

下方示例代码中,主线程和新线程均会尝试打印一些文本:

from std import sync.*
from std import time.*

main(): Int64 {
    spawn { =>
        println("New thread before sleeping")
        sleep(100 * Duration.millisecond) // sleep for 100ms.
        println("New thread after sleeping")
    }

    println("Main thread")

    return 0
}

注意:在上面的例子中,新线程会在主线程结束时一起停止,无论这个新线程是否已完成运行。上方示例的输出每次可能略有不同,有可能会输出类似如下的内容:

New thread before sleeping
Main thread

注意:sleep() 函数会让当前线程睡眠指定的时长,之后再恢复执行,其时间由指定的 Duration 类型决定,详细介绍请参考下方章节。

使用 Future<T> 等待线程结束并获取返回值

在上面的例子中,新创建的线程会由于主线程结束而提前结束,在缺乏顺序保证的情况下,甚至可能会出现新创建的线程还来不及得到执行就退出了。我们可以通过 spawn 表达式的返回值,来等待线程执行结束。

spawn 表达式的返回类型是 Future<T>,其中 T 是类型变元,其类型与 lambda 表达式的返回类型一致。当我们调用 Future<T>get() 成员函数时,它将等待它的线程执行完成。

Future<T> 的原型声明如下:

public class Future<T> {
    // Blocking the current thread, waiting for the result of the thread corresponding to the current Future object.
    // If an exception occurs in the corresponding thread, the method will throw the exception.
    public func get(): T

    // Blocking the current thread, waiting for the result of the thread corresponding to the current Future object.
    // If the corresponding thread has not completed execution within ns nanoseconds, the method will return a Option<T>.None.
    // If `ns` <= 0, its behavior is the same as `get()`.
    public func get(ns: Int64): Option<T>

    // Non-blocking method that immediately returns Option<T>.None if thread has not finished execution.
    // Returns the computed result otherwise.
    // If an exception occurs in the corresponding thread, the method will throw the exception.
    public func tryGet(): Option<T>
}

下方示例代码演示了如何使用 Future<T>main 中等待新创建的线程执行完成:

from std import sync.*
from std import time.*

main(): Int64 {
    let fut: Future<Unit> = spawn { =>
        println("New thread before sleeping")
        sleep(100 * Duration.millisecond) // sleep for 100ms.
        println("New thread after sleeping")
    }

    println("Main thread")

    fut.get() // wait for the thread to finish.
    return 0
}

调用 Future<T> 实例的 get() 会阻塞当前运行的线程,直到 Future<T> 实例所代表的线程运行结束。因此,上方示例有可能会输出类似如下内容:

New thread before sleeping
Main thread
New thread after sleeping

主线程在完成打印后会因为调用 get() 而等待新创建的线程执行结束。但主线程和新线程的打印顺序具有不确定性。

但是,如果我们将 fut.get() 移动到主线程的打印之前,会出现什么结果呢?就像下方这样:

from std import sync.*
from std import time.*

main(): Int64 {
    let fut: Future<Unit> = spawn { =>
        println("New thread before sleeping")
        sleep(100 * Duration.millisecond) // sleep for 100ms.
        println("New thread after sleeping")
    }

    fut.get() // wait for the thread to finish.

    println("Main thread")
    return 0
}

主线程将等待新创建的线程执行完成,然后再执行打印,因此程序的输出将变得确定,如下所示:

New thread before sleeping
New thread after sleeping
Main thread

可见,get() 的调用位置会影响线程是否能同时运行。

Future<T> 除了可以用于阻塞等待线程执行结束以外,还可以获取线程执行的结果。现在,我们来看一下它提供的具体成员函数:

  • get(): T:阻塞等待线程执行结束,并返回执行结果,如果该线程已经结束,则直接返回执行结果。

    示例代码如下:

    from std import sync.*
    from std import time.*
    
    main(): Int64 {
        let fut: Future<Int64> = spawn {
            sleep(Duration.second) // sleep for 1s.
            return 1
        }
    
        try {
            // wait for the thread to finish, and get the result.
            let res: Int64 = fut.get()
            println("result = ${res}")
        } catch (_) {
            println("oops")
        }
        return 0
    }
    

    输出结果如下:

    result = 1
    
  • get(ns: Int64): Option<T>:阻塞等待该 Future<T> 所代表的线程执行结束,并返回执行结果,当到达超时时间 ns 时,如果该线程还没有执行结束,将会返回 Option<T>.None。如果 ns <= 0,其行为与 get() 相同。

    示例代码如下:

    from std import sync.*
    from std import time.*
    
    main(): Int64 {
        let fut = spawn {
            sleep(Duration.second) // sleep for 1s.
            return 1
        }
    
        // wait for the thread to finish, but only for 1ms.
        let res: Option<Int64> = fut.get(1000 * 1000)
        match (res) {
            case Some(val) => println("result = ${val}")
            case None => println("oops")
        }
        return 0
    }
    

    输出结果如下:

    oops
    

访问线程属性

每个 Future<T> 对象都有一个对应的仓颉线程,以 Thread 对象为表示。Thread 类主要被用于访问线程的属性信息,例如线程标识等。需要注意的是,Thread 无法直接被实例化构造对象,仅能从 Future<T>thread 成员属性获取对应的 Thread 对象,或是通过 Thread 的静态成员属性 currentThread 得到当前正在执行线程对应的 Thread 对象。

Thread 类的部分方法定义如下(完整的方法描述可参考标准库用户手册)。

class Thread {
    ... ...
    // Get the currently running thread
    static prop currentThread: Thread

    // Get the unique identifier (represented as an integer) of the thread object
    prop id: Int64

    // Check whether the thread has any cancellation request
    prop hasPendingCancellation: Bool
}

下列示例代码在创建新线程后分别通过两种方式获取线程标识。由于主线程和新线程获取的是同一个 Thread 对象,所以他们能够打印出相同的线程标识。

main(): Unit {
    let fut = spawn {
        println("Current thread id: ${Thread.currentThread.id}")
    }
    println("New thread id: ${fut.thread.id}")
    fut.get()
}

终止线程

可以通过 Future<T>cancel() 方法向对应的线程发送终止请求,该方法不会停止线程执行。开发者需要使用 ThreadhasPendingCancellation 属性来检查线程是否存在终止请求。

一般而言,如果线程存在终止请求,那么开发者可以实施相应的线程终止逻辑。因此,如何终止线程都交由开发者自行处理,如果开发者忽略终止请求,那么线程继续执行直到正常结束。

示例代码如下:

from std import sync.SyncCounter

main(): Unit {
    let syncCounter = SyncCounter(1)
    let fut = spawn {
        syncCounter.waitUntilZero()
        // Check cancellation request
        if (Thread.currentThread.hasPendingCancellation) {
            println("cancelled")
            return
        }
        println("hello")
    }
    fut.cancel()    // Send cancellation request
    syncCounter.dec()
    fut.get() // Join thread
}

输出结果如下:

cancelled

线程睡眠指定时长 sleep

sleep 函数会阻塞当前运行的线程,该线程会主动睡眠一段时间,之后再恢复执行,其参数类型为 Duration 类型。函数原型为:

func sleep(dur: Duration): Unit // Sleep for at least `dur`.

注意:如果dur <= Duration.Zero, 那么当前线程只会让出执行资源,并不会进入睡眠。

以下是使用 sleep 的示例:

from std import sync.*
from std import time.*

main(): Int64 {
    println("Hello")
    sleep(Duration.second)  // sleep for 1s.
    println("World")
    return 0
}

输出结果如下:

Hello
World

同步机制

在并发编程中,如果缺少同步机制来保护多个线程共享的变量,很容易会出现数据竞争问题(data race)。

仓颉编程语言提供三种常见的同步机制来确保数据的线程安全:原子操作,互斥锁以及条件变量。

原子操作 Atomic

仓颉提供整数类型、Bool 类型和引用类型的原子操作。

其中整数类型包括: Int8Int16Int32Int64UInt8UInt16UInt32UInt64

整数类型的原子操作支持基本的读写、交换以及算术运算操作:

操作功能
load读取
store写入
swap交换,返回交换前的值
compareAndSwap比较再交换,交换成功返回 true,否则返回 false
fetchAdd加法,返回执行加操作之前的值
fetchSub减法,返回执行减操作之前的值
fetchAnd与,返回执行与操作之前的值
fetchOr或,返回执行或操作之前的值
fetchXor异或,返回执行异或操作之前的值

需要注意的是:

  1. 交换操作和算数操作的返回值是修改前的值。
  2. compareAndSwap 是判断当前原子变量的值是否等于 old 值,如果等于,则使用 new 值替换;否则不替换。

Int8 类型为例,对应的原子操作类型声明如下:

class AtomicInt8 {
    public func load(): Int8
    public func store(val: Int8): Unit
    public func swap(val: Int8): Int8
    public func compareAndSwap(old: Int8, new: Int8): Bool
    public func fetchAdd(val: Int8): Int8
    public func fetchSub(val: Int8): Int8
    public func fetchAnd(val: Int8): Int8
    public func fetchOr(val: Int8): Int8
    public func fetchXor(val: Int8): Int8
}

上述每一种原子类型的方法都有一个对应的方法可以接收内存排序参数,目前内存排序参数仅支持顺序一致性。

类似的,其他整数类型对应的原子操作类型有:

class AtomicInt16 {...}
class AtomicInt32 {...}
class AtomicInt64 {...}
class AtomicUInt8 {...}
class AtomicUInt16 {...}
class AtomicUInt32 {...}
class AtomicUInt64 {...}

下方示例演示了如何在多线程程序中,使用原子操作实现计数:

from std import sync.*
from std import time.*
from std import collection.*

let count = AtomicInt64(0)

main(): Int64 {
    let list = ArrayList<Future<Int64>>()

    // create 1000 threads.
    for (i in 0..1000) {
        let fut = spawn {
            sleep(Duration.millisecond) // sleep for 1ms.
            count.fetchAdd(1)
        }
        list.append(fut)
    }

    // Wait for all threads finished.
    for (f in list) {
        f.get()
    }

    let val = count.load()
    println("count = ${val}")
    return 0
}

输出结果应为:

count = 1000

以下是使用整数类型原子操作的一些其他正确示例:

var obj: AtomicInt32 = AtomicInt32(1)
var x = obj.load() // x: 1, the type is Int32
x = obj.swap(2) // x: 1
x = obj.load() // x: 2
var y = obj.compareAndSwap(2, 3) // y: true, the type is Bool.
y = obj.compareAndSwap(2, 3) // y: false, the value in obj is no longer 2 but 3. Therefore, the CAS operation fails.
x = obj.fetchAdd(1) // x: 3
x = obj.load() // x: 4

Bool 类型和引用类型的原子操作只提供读写和交换操作:

操作功能
load读取
store写入
swap交换,返回交换前的值
compareAndSwap比较再交换,交换成功返回 true,否则返回 false

注意,引用类型原子操作只对引用类型有效。

原子引用类型是 AtomicReference,以下是使用 Bool 类型、引用类型原子操作的一些正确示例:

from std import sync.*

class A {}

main() {
    var obj = AtomicBool(true)
    var x1 = obj.load() // x1: true, the type is Bool
    println(x1)
    var t1 = A()
    var obj2 = AtomicReference(t1)
    var x2 = obj2.load() // x2 and t1 are the same object
    var y1 = obj2.compareAndSwap(x2, t1) // x2 and t1 are the same object, y1: true
    println(y1)
    var t2 = A()
    var y2 = obj2.compareAndSwap(t2, A()) // x and t1 are not the same object, CAS fails, y2: false
    println(y2)
    y2 = obj2.compareAndSwap(t1, A()) // CAS successes, y2: true
    println(y2)
}

编译执行上述代码,输出结果为:

true
true
false
true

可重入互斥锁 ReentrantMutex

可重入互斥锁的作用是对临界区加以保护,使得任意时刻最多只有一个线程能够执行临界区的代码。当一个线程试图获取一个已被其他线程持有的锁时,该线程会被阻塞,直到锁被释放,该线程才会被唤醒,可重入是指线程获取该锁后可再次获得该锁。

注意:ReentrantMutex 是内置的互斥锁,开发者需要保证不继承它。

使用可重入互斥锁时,必须牢记两条规则:

  1. 在访问共享数据之前,必须尝试获取锁;
  2. 处理完共享数据后,必须进行解锁,以便其他线程可以获得锁。

ReentrantMutex 提供的主要成员函数如下:

public open class ReentrantMutex {
    // Create a ReentrantMutex.
    public init()

    // Locks the mutex, blocks if the mutex is not available.
    public func lock(): Unit

    // Unlocks the mutex. If there are other threads blocking on this
    // lock, then wake up one of them.
    public func unlock(): Unit

    // Tries to lock the mutex, returns false if the mutex is not
    // available, otherwise returns true.
    public func tryLock(): Bool
}

下方示例演示了如何使用 ReentrantMutex 来保护对全局共享变量 count 的访问,对 count 的操作即属于临界区:

from std import sync.*
from std import time.*
from std import collection.*

var count: Int64 = 0
let mtx = ReentrantMutex()

main(): Int64 {
    let list = ArrayList<Future<Unit>>()

    // creat 1000 threads.
    for (i in 0..1000) {
        let fut = spawn {
            sleep(Duration.millisecond) // sleep for 1ms.
            mtx.lock()
            count++
            mtx.unlock()
        }
        list.append(fut)
    }

    // Wait for all threads finished.
    for (f in list) {
        f.get()
    }

    println("count = ${count}")
    return 0
}

输出结果应为:

count = 1000

下方示例演示了如何使用 tryLock

from std import sync.*

main(): Int64 {
    let mtx: ReentrantMutex = ReentrantMutex()
    var future: Future<Unit> = spawn {
        mtx.lock()
        while (true) {}
        mtx.unlock()
    }
    let res: Option<Unit> = future.get(10*1000*1000)
    match (res) {
        case Some(v) => ()
        case None =>
            if (mtx.tryLock()) {
                return 1
            }
            return 0
    }
    return 2
}

输出结果应为空。

以下是互斥锁的一些错误示例:

错误示例 1:线程操作临界区后没有解锁,导致其他线程无法获得锁而阻塞。

var sum: Int64 = 0
let mutex = ReentrantMutex()

main() {
    for (i in 0..100) {
        spawn { =>
            mutex.lock()
            sum = sum + 1
            // Error: Because the thread is not unlocked, other threads waiting to obtain the current mutex will be blocked and cannot continue to run.
        }
    }
}

错误示例 2:在本线程没有持有锁的情况下调用 unlock 将会抛出异常。

var sum: Int64 = 0
let mutex = ReentrantMutex()

main() {
    for (i in 0..100) {
        spawn { =>
            sum = sum + 1
            mutex.unlock() // Error: Unlock without obtaining the lock and throw an exception.
        }
    }
}

错误示例 3:tryLock() 并不保证获取到锁,可能会造成不在锁的保护下操作临界区和在没有持有锁的情况下调用 unlock 抛出异常等行为。

var sum: Int64 = 0
let mutex = ReentrantMutex()

main() {
    for (i in 0..100) {
        spawn { =>
            mutex.tryLock() // Error: `tryLock()` just trying to acquire a lock, there is no guarantee that the lock will be acquired, and this can lead to abnormal behavior.
            sum = sum + 1
            mutex.unlock()
        }
    }
}

另外,ReentrantMutex 在设计上是一个可重入锁,也就是说:在某个线程已经持有一个 ReentrantMutex 锁的情况下,再次尝试获取同一个 ReentrantMutex 锁,永远可以立即获得该 ReentrantMutex 锁。注意:虽然 ReentrantMutex 是一个可重入锁,但是调用 unlock() 的次数必须和调用 lock() 的次数相同,才能成功释放该锁。

下方示例代码演示了 ReentrantMutex 可重入的特性:

from std import sync.*
from std import time.*

var count: Int64 = 0
let mtx = ReentrantMutex()

func foo() {
    mtx.lock()
    count += 10
    bar()
    mtx.unlock()
}

func bar() {
    mtx.lock()
    count += 100
    mtx.unlock()
}

main(): Int64 {
    let fut = spawn {
        sleep(Duration.millisecond) // sleep for 1ms.
        foo()
    }

    foo()

    fut.get()

    println("count = ${count}")
    return 0
}

输出结果应为:

count = 220

在上方示例中,无论是主线程还是新创建的线程,如果在 foo() 中已经获得了锁,那么继续调用 bar() 的话,在 bar() 函数中由于是对同一个 ReentrantMutex 进行加锁,因此也是能立即获得该锁的,不会出现死锁。

Monitor

Monitor 是一个内置的数据结构,它绑定了互斥锁和单个与之相关的条件变量(也就是等待队列)。Monitor 可以使线程阻塞并等待来自另一个线程的信号以恢复执行。这是一种利用共享变量进行线程同步的机制,主要提供如下方法:

public class Monitor <: ReentrantMutex {
    // Create a monitor.
    public init()

    // Wait for a signal, blocking the current thread.
    public func wait(timeout!: Duration = Duration.Max): Bool

    // Wake up one thread of those waiting on the monitor, if any.
    public func notify(): Unit

    // Wake up all threads waiting on the monitor, if any.
    public func notifyAll(): Unit
}

调用 Monitor 对象的 waitnotifynotifyAll 方法前,需要确保当前线程已经持有对应的 Monitor 锁。wait 方法包含如下动作:

  1. 添加当前线程到该 Monitor 对应的等待队列中;
  2. 阻塞当前线程,同时完全释放该 Monitor 锁,并记录锁的重入次数;
  3. 等待某个其它线程使用同一个 Monitor 实例的 notifynotifyAll 方法向该线程发出信号;
  4. 当前线程被唤醒后,会自动尝试重新获取 Monitor 锁,且持有锁的重入状态与第 2 步记录的重入次数相同;但是如果尝试获取 Monitor 锁失败,则当前线程会阻塞在该 Monitor 锁上。

注意:wait 方法接受一个可选参数 timeout。需要注意的是,业界很多常用的常规操作系统不保证调度的实时性,因此无法保证一个线程会被阻塞“精确的 N 纳秒”——可能会观察到与系统相关的不精确情况。此外,当前语言规范明确允许实现产生虚假唤醒——在这种情况下,wait 返回值是由实现决定的——可能为 truefalse。因此鼓励开发者始终将 wait 包在一个循环中:

synchronized (obj) {
  while (<condition is not true>) {
    obj.wait()
  }
}

以下是使用 Monitor 的一个正确示例:

from std import sync.*
from std import time.*

var mon = Monitor()
var flag: Bool = true

main(): Int64 {
    let fut = spawn {
        mon.lock()
        while (flag) {
            println("New thread: before wait")
            mon.wait()
            println("New thread: after wait")
        }
        mon.unlock()
    }

    // Sleep for 10ms, to make sure the new thread can be executed.
    sleep(10 * Duration.millisecond)

    mon.lock()
    println("Main thread: set flag")
    flag = false
    mon.unlock()

    mon.lock()
    println("Main thread: notify")
    mon.notifyAll()
    mon.unlock()

    // wait for the new thread finished.
    fut.get()
    return 0
}

输出结果应为:

New thread: before wait
Main thread: set flag
Main thread: notify
New thread: after wait

Monitor 对象执行 wait 时,必须在锁的保护下进行,否则 wait 中释放锁的操作会抛出异常。

以下是使用条件变量的一些错误示例:

from std import sync.*

var m1 = Monitor()
var m2 = ReentrantMutex()
var flag: Bool = true
var count: Int64 = 0

func foo1() {
    spawn {
        m2.lock()
        while (flag) {
            m1.wait() // Error:The lock used together with the condition variable must be the same lock and in the locked state. Otherwise, the unlock operation in `wait` throws an exception.
        }
        count = count + 1
        m2.unlock()
    }
    m1.lock()
    flag = false
    m1.notifyAll()
    m1.unlock()
}

func foo2() {
    spawn {
        while (flag) {
            m1.wait() // Error:The `wait` of a conditional variable must be called with a lock held.
        }
        count = count + 1
    }
    m1.lock()
    flag = false
    m1.notifyAll()
    m1.unlock()
}

main() {
    foo1()
    foo2()
    m1.wait()
    return 0
}

MultiConditionMonitor

MultiConditionMonitor 是一个内置的数据结构,它绑定了互斥锁和一组与之相关的动态创建的条件变量。该类应仅当在 Monitor 类不足以满足复杂的线程间同步的场景下使用。主要提供如下方法:

public class MultiConditionMonitor <: ReentrantMutex {
   // Constructor.
   init()

   // Returns a new ConditionID associated with this monitor. May be used to implement
   // "single mutex -- multiple wait queues" concurrent primitives.
   // Throws IllegalSynchronizationStateException("Mutex is not locked by the current thread") if the current thread does not hold this mutex.
   func newCondition(): ConditionID

   // Blocks until either a paired `notify` is invoked or `timeout` nanoseconds pass.
   // Returns `true` if the specified condition was signalled by another thread or `false` on timeout.
   // Spurious wakeups are allowed.
   // Throws IllegalSynchronizationStateException("Mutex is not locked by the current thread") if the current thread does not hold this mutex.
   // Throws IllegalSynchronizationStateException("Invalid condition") if `id` was not returned by `newCondition` of this MultiConditionMonitor instance.
   func wait(id: ConditionID, timeout!: Duration = Duration.Max): Bool

   // Wakes up a single thread waiting on the specified condition, if any (no particular admission policy implied).
   // Throws IllegalSynchronizationStateException("Mutex is not locked by the current thread") if the current thread does not hold this mutex.
   // Throws IllegalSynchronizationStateException("Invalid condition") if `id` was not returned by `newCondition` of this MultiConditionMonitor instance.
   func notify(id: ConditionID): Unit

   // Wakes up all threads waiting on the specified condition, if any (no particular admission policy implied).
   // Throws IllegalSynchronizationStateException("Mutex is not locked by the current thread") if the current thread does not hold this mutex.
   // Throws IllegalSynchronizationStateException("Invalid condition") if `id` was not returned by `newCondition` of this MultiConditionMonitor instance.
   func notifyAll(id: ConditionID): Unit
}
  1. newCondition(): ConditionID:创建一个新的条件变量并与当前对象关联,返回一个特定的 ConditionID 标识符
  2. wait(id: ConditionID, timeout!: Duration = Duration.Max): Bool:等待信号,阻塞当前线程
  3. notify(id: ConditionID): Unit:唤醒一个在 Monitor 上等待的线程(如果有)
  4. notifyAll(id: ConditionID): Unit:唤醒所有在 Monitor 上等待的线程(如果有)

初始化时,MultiConditionMonitor 没有与之相关的 ConditionID 实例。每次调用 newCondition 都会将创建一个新的条件变量并与当前对象关联,并返回如下类型作为唯一标识符:

public struct ConditionID {
   private init() { ... } // constructor is intentionally private to prevent
                          // creation of such structs outside of MultiConditionMonitor
}

请注意使用者不可以将一个 MultiConditionMonitor 实例返回的 ConditionID 传给其它实例,或者手动创建 ConditionID(例如使用 unsafe)。由于 ConditionID 所包含的数据(例如内部数组的索引,内部队列的直接地址,或任何其他类型数据等)和创建它的 MultiConditionMonitor 相关,所以将“外部” conditonID 传入 MultiConditionMonitor 中会导致 IllegalSynchronizationStateException

以下是使用 MultiConditionMonitor 去实现一个长度固定的有界 FIFO 队列,当队列为空,get() 会被阻塞;当队列满了时,put() 会被阻塞。

from std import sync.*

class BoundedQueue {
    // Create a MultiConditionMonitor, two Conditions.
    let m: MultiConditionMonitor = MultiConditionMonitor()
    var notFull: ConditionID
    var notEmpty: ConditionID

    var count: Int64 // Object count in buffer.
    var head: Int64  // Write index.
    var tail: Int64  // Read index.

    // Queue's length is 100.
    let items: Array<Object> = Array<Object>(100, {i => Object()})

    init() {
        count = 0
        head = 0
        tail = 0

        synchronized(m) {
          notFull  = m.newCondition()
          notEmpty = m.newCondition()
        }
    }

    // Insert an object, if the queue is full, block the current thread.
    public func put(x: Object) {
        // Acquire the mutex.
        synchronized(m) {
          while (count == 100) {
            // If the queue is full, wait for the "queue notFull" event.
            m.wait(notFull)
          }
          items[head] = x
          head++
          if (head == 100) {
            head = 0
          }
          count++

          // An object has been inserted and the current queue is no longer
          // empty, so wake up the thread previously blocked on get()
          // because the queue was empty.
          m.notify(notEmpty)
        } // Release the mutex.
    }

    // Pop an object, if the queue is empty, block the current thread.
    public func get(): Object {
        // Acquire the mutex.
        synchronized(m) {
          while (count == 0) {
            // If the queue is empty, wait for the "queue notEmpty" event.
            m.wait(notEmpty)
          }
          let x: Object = items[tail]
          tail++
          if (tail == 100) {
            tail = 0
          }
          count--

          // An object has been popped and the current queue is no longer
          // full, so wake up the thread previously blocked on put()
          // because the queue was full.
          m.notify(notFull)

          return x
        } // Release the mutex.
    }
}

synchronized 关键字

互斥锁 ReentrantMutex 提供了一种便利灵活的加锁的方式,同时因为它的灵活性,也可能引起忘了解锁,或者在持有互斥锁的情况下抛出异常不能自动释放持有的锁的问题。因此,仓颉编程语言提供一个 synchronized 关键字,搭配ReentrantMutex一起使用,可以在其后跟随的作用域内自动进行加锁解锁操作,用来解决类似的问题。

下方示例代码演示了如何使用 synchronized 关键字来保护共享数据:

from std import sync.*
from std import time.*
from std import collection.*

var count: Int64 = 0
let mtx = ReentrantMutex()

main(): Int64 {
    let list = ArrayList<Future<Unit>>()

    // creat 1000 threads.
    for (i in 0..1000) {
        let fut = spawn {
            sleep(Duration.millisecond) // sleep for 1ms.
            // Use synchronized(mtx), instead of mtx.lock() and mtx.unlock().
            synchronized(mtx) {
                count++
            }
        }
        list.append(fut)
    }

    // Wait for all threads finished.
    for (f in list) {
        f.get()
    }

    println("count = ${count}")
    return 0
}

输出结果应为:

count = 1000

通过在 synchronized 后面加上一个 ReentrantMutex 实例,对其后面修饰的代码块进行保护,可以使得任意时刻最多只有一个线程可以执行被保护的代码:

  1. 一个线程在进入 synchronized 修饰的代码块之前,会自动获取 ReentrantMutex 实例对应的锁,如果无法获取锁,则当前线程被阻塞;
  2. 一个线程在退出 synchronized 修饰的代码块之前,会自动释放该 ReentrantMutex 实例的锁;

对于控制转移表达式(如 breakcontinuereturnthrow),在导致程序的执行跳出 synchronized 代码块时,也符合上面第 2 条的说明,也就说也会自动释放 synchronized 表达式对应的锁。

下方示例演示了在 synchronized 代码块中出现 break 语句的情况:

from std import sync.*
from std import collection.*

var count: Int64 = 0
var mtx: ReentrantMutex = ReentrantMutex()

main(): Int64 {
    let list = ArrayList<Future<Unit>>()
    for (i in 0..10) {
        let fut = spawn {
            while (true) {
                synchronized(mtx) {
                    count = count + 1
                    break
                    println("in thread")
                }
            }
        }
        list.append(fut)
    }

    // Wait for all threads finished.
    for (f in list) {
        f.get()
    }

    synchronized(mtx) {
        println("in main, count = ${count}")
    }
    return 0
}

输出结果应为:

in main, count = 10

实际上 in thread 这行不会被打印,因为 break 语句实际上会让程序执行跳出 while 循环(当然,在跳出 while 循环之前,是先跳出 synchronized 代码块)。

线程局部变量 ThreadLocal

使用 core 包中的 ThreadLocal 可以创建并使用线程局部变量,每一个线程都有它独立的一个存储空间来保存这些线程局部变量,因此,在每个线程可以安全地访问他们各自的线程局部变量,而不受其他线程的影响。

public class ThreadLocal<T> {
    /*
     * 构造一个携带空值的仓颉线程局部变量
     */
    public init()

    /*
     * 获得仓颉线程局部变量的值,如果值不存在,则返回 Option<T>.None
     * 返回值 Option<T> - 仓颉线程局部变量的值
     */
    public func get(): Option<T>

    /*
     * 通过 value 设置仓颉线程局部变量的值
     * 如果传入 Option<T>.None,该局部变量的值将被删除,在线程后续操作中将无法获取
     * 参数 value - 需要设置的局部变量的值
     */
    public func set(value: Option<T>): Unit
}

下方示例代码演示了如何通过 ThreadLocal 类来创建并使用各自线程的局部变量

代码如下:

from std import sync.ThreadLocal

main(): Int64 {
    let tl = ThreadLocal<Int64>()
    let fut1 = spawn {
        tl.set(123)
        println("tl in spawn1 = ${tl.get().getOrThrow()}")
    }
    let fut2 = spawn {
        tl.set(456)
        println("tl in spawn2 = ${tl.get().getOrThrow()}")
    }
    fut1.get()
    fut2.get()
    0
}

可能的输出结果如下:

tl in spawn1 = 123
tl in spawn2 = 456

或者

tl in spawn2 = 456
tl in spawn1 = 123

仓颉线程和 native 线程

仓颉线程本质上是用户态的轻量级线程,每个仓颉线程都受到底层 native 线程的调度执行,并且多个仓颉线程可以由一个 native 线程执行。每个 native 线程会不断地选择一个就绪的仓颉线程完成执行,如果仓颉线程在执行过程中发生阻塞(例如等待互斥锁的释放),那么 native 线程会将当前的仓颉线程挂起,并继续选择下一个就绪的仓颉线程。发生阻塞的仓颉线程在重新就绪后会继续被 native 线程调度执行。

在大多数情况下,开发者只需要面向仓颉线程进行并发编程而不需要考虑这些细节。但在进行跨语言编程时,开发者需要谨慎调用可能发生阻塞的 foreign 函数,例如 IO 相关的操作系统调用等。例如,下列示例代码中的新线程会调用 foreign 函数 socket_read。在程序运行过程中,某一 native 线程将调度并执行该仓颉线程,在进入到 foreign 函数中后,系统调用会直接阻塞当前 native 线程直到函数执行完成。由于 native 线程被阻塞而不仅仅是仓颉线程,所以当前 native 线程在阻塞期间将无法调度其他仓颉线程来执行,这会降低程序执行的吞吐量。

foreign socket_read(sock: Int64): CPointer<Int8>

let fut = spawn {
    let sock: Int64 = ...
    let ptr = socket_read(sock)
}

元编程

元编程是一种将计算机程序(代码)当做数据的编程技术,从而修改,更新,替换已有的程序。例如可以将一些计算过程从运行时挪到编译时,并在编译期进行代码生成。仓颉语言提供的元编程能力,能支持代码复用,操作语法树,编译期求值,甚至自定义文法等功能。

下面是一个利用元编程解决具体问题的例子,利用仓颉宏为某些需要递归计算的函数进行记忆优化。

// macro_definition.cj
macro package memory

from std import ast.*

func checkBooleanAttr(attr: Tokens): Bool {
    // true or false
    if (attr.size != 1 || attr[0].kind != TokenKind.BOOL_LITERAL) {
        throw IllegalArgumentException("Attribute for memoize should be true or false")
    }
    return attr[0].value == "true"
}

public macro memoize(attr: Tokens, input: Tokens): Tokens {
    let memoized: Bool = checkBooleanAttr(attr)

    // no memorization
    if (!memoized) {
        return input
    }

    // optimizing with memory
    let fd = parseDecl(input)

    return quote(
        var memoMap: HashMap<Int64, Int64> = HashMap<Int64, Int64>()

        func $(fd.identifier)(n: Int64): Int64 {
            if (memoMap.contains(n)) {
                return memoMap.get(n).getOrThrow()
            }
            if (n == 0 || n == 1) {
                return n
            }
            let ret = Fib(n-1) + Fib(n-2)
            memoMap.put(n, ret)
            return ret
        }
    )
}

// macro_call.cj
import memory.*
from std import time.*
from std import collection.*

@memoize[true]
func Fib(n: Int64): Int64 {
    if (n == 0 || n == 1) {
        return n
    }
    return Fib(n-1) + Fib(n-2)
}

main() {
    println("Fibonacci:")

    let start1 = DateTime.now()
    let f1 = Fib(20)
    let end1 = DateTime.now()
    println("Fib(20): ${f1}")
    println("execution time: ${(end1 - start1).toMicroseconds()} us")

    let start2 = DateTime.now()
    let f2 = Fib(15)
    let end2 = DateTime.now()
    println("Fib(15): ${f2}")
    println("execution time: ${(end2 - start2).toMicroseconds()} us")

    let start3 = DateTime.now()
    let f3 = Fib(22)
    let end3 = DateTime.now()
    println("Fib(22): ${f3}")
    println("execution time: ${(end3 - start3).toMicroseconds()} us")
    0
}

上述代码中,memoize 是一个用户自定义的宏,它修饰一个函数,这个函数用来计算 Fibonacci 序列的第 n 个位置上的值。如果没有 memoize 这个宏修饰,每次调用 Fib 函数时,都会递归执行,耗时较长。使用 memoize 后,这个宏为 Fib 函数在编译期生成一些代码,记录下已经计算出的函数入参对应的函数返回值,下次可直接查表得到函数返回值,而不需要再次递归。

使用仓颉宏时,需要先编译宏定义文件,再编译宏调用文件生成可执行文件,最终运行可执行文件的输出结果如下:

// output when use @memoize[true]
Fibonacci:
Fib(20): 6765
execution time: 146 us
Fib(15): 610
execution time: 3 us
Fib(22): 17711
execution time: 16 us

当然,记忆优化的代价是额外使用了哈希表,若开发者不希望进行这样的优化,可以将 memoize 宏的属性入参设置为 false,即使用 @memoize[false] 修饰 Fib 函数,程序的运行结果如下:

// output when use @memoize[false]
Fibonacci:
Fib(20): 6765
execution time: 570 us
Fib(15): 610
execution time: 51 us
Fib(22): 17711
execution time: 1487 us

由以上运行结果可以看出,使用 @memoize[true] 时,Fib(15), Fib(22) 的计算时间显著减少,特别是 Fib(22) 的计算耗时,使用记忆优化后时长由 1487 us 减少到 16 us。

观察 memoize 宏定义,我们看到 memoize 宏使用了 Tokens 作为入参和返回值的类型,同时返回了一个 quote 表达式。为了使用仓颉宏,我们需要了解 Token, Tokens, quote, 以及宏的编译期执行(先编译宏定义文件,再编译宏调用文件)的概念,下面将分别介绍它们,最终我们能理解 memoize 宏的工作原理。

Tokens 相关类型和 quote 表达式

仓颉语言的元编程是基于语法实现的。编译器在语法分析的阶段可以完成编写或操作目标程序的工作,用于操作目标程序的程序我们称为元程序。元程序的输入输出都是词法单元(token),为此,仓颉提供了 Token 类型,Tokens 类型和 quote 表达式。其中,Token 类型是单个词法单元的类型,Tokens 类型是多个词法单元组成的结构的类型,quote 表达式是构造 Tokens 实例的一种表达式,下面依次对它们进行介绍。

Token 类型

Token 是元编程提供给用户可操作的词法单元,含义上等同编译器中词法分析器输出的 token。一个 Token 类型中包括的信息有:Token 类型(TokenKind)、构成 Token 的字符串、Token 的位置。

可以通过传入具体的 TokenKind 来构建单个 Token 对象:

Token(TokenKind.ADD) // Token representing `+`

这里的 TokenKind 是用于表示 Token 类型的 enum,即用于表示各种 Token。使用 TokenKind 的构造器可以构造出仓颉所有的 Token (TokenKind 可用值详见附录) 。

需要注意的是,由多个字符组成的 Token 与对应相同字符的多个 Token,含义不同。例如:

let a0: Unit = ()
let a1 = String()

此处第一行的 () 可以是 一个类型为 TokenKind.UNIT_LITERAL(Unit 类型的字面值常量)的 Token,或者两个 Token,其类型分别为 TokenKind.LPAREN(左括号)和 TokenKind.RPAREN(右括号),而第二行的 (),则只能是 TokenKind.LPAREN 和 TokenKind.RPAREN。

注:TokenKind、Token 类型皆由仓颉标准库 ast 包提供,使用时需要导入

from std import ast.TokenKind
from std import ast.Token

本章中为了方便描述,使用 from std import ast.* 将整个 ast 包导入。

还存在其他构造 Token 的方式,如下:

Token() // Return illegal Token.
Token(k: TokenKind)
Token(k: TokenKind, v: String)

例如,有以下方式可以构造 Token

let tk1 = Token()
let tk2 = Token(TokenKind.FUNC) // Construct a Token, which is the keyword func
let tk3 = Token(TokenKind.IDENTIFIER, "foo") // Construct an identifier Token with value foo

Tokens 类型

Tokens 类型是用仓颉编写元程序必须的输入输出类型。存在如下 3 种构造 Tokens 实例的方式:

Tokens()
Tokens(tokArr: Array<Token>)
Tokens(tokArrList: ArrayList<Token>)

除了以上构造 Tokens 的方式外,还有 quote 表达式可以构造 Tokens,详见下一小节。简单来说,Tokens 可以理解为是由词素(Token)组成的数组,同时 Tokens 类型支持如下操作:

size: 返回 Tokens 中所包含 Token 的数目。
get(index: Int64): 用于获取指定下标的 Token 元素。
[]: 返回下标索引指定的 Token。
+: 拼接两个 Tokens 或者拼接 Tokens 和 Token。
dump(): 打印包含的所有 Token,供调试使用。

下面的例子中包含了上述的所有操作:

from std import ast.*

main() {
    let ts1: Tokens = quote(1 + 2)
    let ts2: Tokens = quote(3)
    // ts includes Token: '1', '+', '2', '+' and '3'
    let ts: Tokens = ts1 + Token(TokenKind.ADD) + ts2
    println("ts.size = ${ts.size}")
    println("ts.dump():")
    ts.dump()
    let index0 = ts.get(0)
    println("ts.get(0): ${index0.value}")
    let index1 = ts[1]
    println("ts[1]: ${index1.value}")
    0
}

这个例子中,通过 quote 表达式获取了仓颉代码(如 1 + 2 + 3)的 Tokens 对象

quote 表达式

quote 是仓颉的一个关键字,quote 表达式可以将代码表示为 Tokens 对象。具体来说,对于上一节的例子:

let ts1: Tokens = quote(1 + 2)

这里 quote 的作用是:将1 + 2这行代码转换成由 '1','+','2' 这 3 个 Token 组成的 Tokens。

插值运算符

在 quote 表达式中,仓颉支持代码插值操作,使用插值运算符 $ 表示。这里的插值表达式类似于某种占位符,会被最终替换成相应的值,即 toTokens 后的结果。

说明:

插值运算符修饰的表达式必须实现 ast 包中的 toTokens 接口,否则无法正常给出插值结果并给出报错信息。关于 ast 包的介绍及其 API,请参见《仓颉库使用指南》中“ast 包”的内容。

默认情况下,插值运算符后面的表达式,需要使用小括号限定作用域,如 $(foo)。但是当后面只跟单个标识符的时候,小括号可省略,即可写为:$foo。下面是有关 quote 和插值的一个示例,这个例子中,展示了将二元表达式 (1+2) 转换为 Tokens 对象,然后调用 ast 包提供的 parseExpr 接口将其变成 AST 类型,即 BinaryExpr,通过 quote 和插值可以将这个 AST 类型变成 Tokens 对象。

from std import ast.*

main() {
    let tokens: Tokens = quote(1 + 2)
    // parseExpr is API provided by libast to parse input Tokens.
    // BinaryExpr is type provided by libast.
    var ast: BinaryExpr = match (parseExpr(tokens) as BinaryExpr) {
        case Some(v) => v
        case None => throw Exception()
    }

    let a = quote($ast)                    // without parentheses
    let b = quote($(ast))                  // with parentheses
    let c = quote($ast.leftExpr)      // without parentheses
    let d = quote($(ast.leftExpr))    // with parentheses
    println("$ast.leftExpr:")
    c.dump()
    println("===================")
    println("$(ast.leftExpr):")
    d.dump()
    return 0
}

其中 BinaryExpr 和 parseExpr 是 ast 包提供的类型和成员函数。

$ 只定界到紧跟它的一个标识符。例如,quote($ast.leftExpr) 将返回 '1','+','2','.','getLeftExpr','(' 和 ')' 这 7 个 Token,ast 之后的 .leftExpr 均被解释为 Token。

quote的入参可以为任意合法的仓颉代码且可以为空,但当前编译器不支持传入代码中有宏调用表达式。

仓颉实现元编程的主要方式是使用宏。仓颉的宏是语法宏,其定义形式上类似于函数,也和函数一样可以被调用。不同点是:

  1. 宏定义所在的 package 需使用 macro package 来声明。
  2. 宏定义需要使用关键字 macro
  3. 宏定义的输入和输出类型必须是 Tokens。
  4. 宏调用需要使用 @

从输入代码序列到输出新的代码序列的这个映射过程称为宏展开。宏在仓颉代码编译时进行展开,一直展开到目标代码中没有宏为止。宏展开的过程会实际执行宏定义体,即宏是在编译期完成求值,展开后的结果重新作用于仓颉的语法树,继续后面的编译和执行流程。

macro package

仓颉宏的定义需要放在由 macro package 声明的包中,被 macro package 限定的包仅允许宏定义对外可见(注:也允许重导出的声明对外可见),其他声明包内可见。宏定义允许返回空 Tokens 对象,不包含任何代码(Tokens)。但是如果该(返回空 Tokens 对象的)宏调用是其他表达式的一部分,编译将报错,因为空的 Tokens 对象无法构成有效的表达式。

  • 示例
// file define.cj
macro package define         // 编译 define.cjo 携带 macro 属性
from std import ast.*
public func A() {}          // error: 宏包不允许定义外部可见的非宏定义,此处需报错
public macro M(input: Tokens): Tokens { // macro M 外部可见
  return input
}

需要特殊说明的是,在 macro package 中允许 macro package 和非 macro package 被重导出,在非 macro package 中仅允许非 macro package 被重导出。

  • 示例
// A.cj -- cjc A.cj --compile-macro
macro package A
from std import ast.*

public macro M1(input: Tokens): Tokens {
    return input
}

// B.cj -- cjc B.cj --output-type=dylib -o libB.so
package B
// public import A.* // error: it is not allowed to re-export a macro package in a package.

public func F1(input: Int64): Int64 {
    return input
}

// C.cj -- cjc C.cj --compile-macro -L. -lB
macro package C
public import A.* // correct: macro package is allowed to re-exprot in a macro package.
public import B.* // correct: non-macro package is also allowed to re-exprot in a macro package.
from std import ast.*

public macro M2(input: Tokens): Tokens {
    return @M1(input) + Token(TokenKind.NL) + quote(F1(1))
}

// main.cj -- cjc main.cj -o main -L. -lB
import C.*

main() {
    @M2(let a = 1)
}

其中 main.cjM2 宏展开后得到

let a = 1
F1(1)

这里在 package C 中重导出 B 包里的符号是因为宏扩展中使用了 quote(F1(1)),方便宏的使用者仅需导入宏包,就可以正确的编译宏展开后的代码。

这里有两点需要关注

  1. 当前编译 package C 和 main 时,都需要显式的链接 libB.so;
  2. main.cj中需要使用 import C.* 导入宏和重导出符号,如果仅使用 import C.M2 依旧会报 undeclared identifier 'F1' 的错误信息。

仓颉的宏系统分为两种:非属性宏和属性宏。非属性宏只有一个入参,其输入是被宏修饰的代码。属性宏有两个入参,其增加的属性入参赋予开发者向仓颉宏传入额外信息的能力。

非属性宏

非属性宏的定义格式如下:

public macro MacroName(args: Tokens): Tokens {
    ... // Macro body
}

宏的调用格式如下:

@MacroName(...)

宏调用使用 () 括起来。括号里面可以是任意合法 tokens,也可以是空。

以下地方的宏调用可省略括号。

@MacroName func name() {}        // Before a FuncDecl
@MacroName struct name {}        // Before a StructDecl
@MacroName class name {}         // Before a ClassDecl
@MacroName var a = 1             // Before a VarDecl
@MacroName enum e {}             // Before a Enum
@MacroName interface i {}        // Before a InterfaceDecl
@MacroName extend e <: i {}      // Before a ExtendDecl
@MacroName mut prop i: Int64 {}  // Before a PropDecl
@MacroName @AnotherMacro(input)  // Before a macro call

此外,可省略括号的宏调用,只能出现在被修饰的声明允许出现的位置。

如前面提到的,宏展开过程作用于仓颉语法树,宏展开后,编译器会继续进行后续的编译过程,因此,用户需要保证宏展开后的代码依然是合法的仓颉代码,否则可能引发编译问题。

Tokens 类型定义位于 ast 包中,而宏定义的输入和输出都是 Tokens,因此宏定义必须导入 ast 包。宏定义必须比宏调用点先编译。编译器约束宏的定义与宏的调用不允许在同一包里。即存在宏调用的包中,不允许出现任意宏的定义。由于宏需在包中导出给另一个包使用,因此编译器约束宏定义必须使用 public 修饰。

以在 Linux 平台编译本章开头的示例为例(--compile-macro 的使用方式将在"宏的编译和调试"中进行说明):

# 编译宏定义
cjc macro_definition.cj --compile-macro

# 编译宏调用
cjc macro_call.cj -o main.out

若在 Windows 平台编译本章开头的示例:

# 编译宏定义
cjc macro_definition.cj --compile-macro

# 编译宏调用
cjc macro_call.cj -o main.exe

若用 CJVM 虚拟机则需要用解释器编译本章开头的示例:

# 编译宏定义
cjc macro_definition.cj --compile-macro

# 编译宏调用
cjc macro_call.cj --interp-macro -o main.cbc

下面是几个宏应用的典型示例。

  • 示例 1
// file macro_definition.cj
macro package macro_definition

from std import ast.*

public macro TestDef(input: Tokens): Tokens {
    println("I'm in macro body")
    return input
}

// file macro_call.cj
package macro_calling

import macro_definition.*

main(): Int64 {
    println("I'm in function body")
    let a: Int64 = @TestDef(1 + 2)
    println("a = ${a}")
    return 0
}

上述两段代码分别位于不同文件中,优先编译宏定义文件:macro_definition.cj。在 Linux 系统中,将生成用于包管理的 macro_definition.cjo 和实际的动态库文件。macro_call.cj 的编译需要依赖这两个文件。

我们在用例中添加了打印信息,其中宏定义中的 I'm in macro body 将在编译 macro_call.cj 的期间输出,即对宏定义求值。同时,宏调用点被展开,即在编译

let a: Int64 = @TestDef(1 + 2)

时,将宏返回的 Tokens 更新到调用点的语法树上,得到如下代码:

let a: Int64 = 1 + 2

也就是说,可执行程序中的代码实际变为了:

main(): Int64 {
    println("I'm in function body")
    let a: Int64 = 1 + 2
    println("a = ${a}")
    return 0
}

a 经过计算得到的值为 3,在打印 a 的值时插值为 3。至此,上述程序的运行结果为:

I'm in function body
a = 3

下面看一个更有意义的用宏处理函数的例子,这个宏 ModifyFunc 宏的作用是给 MyFunc 增加 Composer 参数,并在counter++前后插入一段代码。

  • 示例 2
// file macro_definition.cj
macro package macro_definition

from std import ast.*

public macro ModifyFunc(input: Tokens): Tokens {
    println("I'm in macro body")
    return quote(
    func MyFunc(composer: Composer) {
        composer.start(123)
        counter++
        composer.end()
    })
}

// file macro_call.cj
package macro_calling

import macro_definition.*

struct Composer {
    public init() { }
    public func start (id: Int32) { println("start ${id}") }
    public func end () { println("end") }
}

var counter = 0

@ModifyFunc
func MyFunc() {
    counter++
}

main(): Int64 {
    println("I'm in function body")
    let comp = Composer()
    MyFunc(comp)
    println("MyFunc called: ${counter} times")
    return 0
}

同样的,上述两段代码分别位于不同文件中,先编译宏定义文件 macro_definition.cj,再编译宏调用 macro_call.cj 生成可执行文件。

这个例子中,ModifyFunc 宏的输入是一个函数声明,因此可以省略括号:

@ModifyFunc
func MyFunc() {
    counter++
}

经过宏展开后,得到如下代码:

func MyFunc(composer: Composer) {
    composer.start(123)
    counter++
    composer.end()
}

MyFunc 会在 main 中调用,它接受的实参也是在 main 中定义的,从而形成了一段合法的仓颉程序。运行时打印如下:

I'm in function body
start 123
end
MyFunc called: 1 times

属性宏

和非属性宏相比,属性宏的定义会增加一个 Tokens 类型的输入,这个增加的入参可以让开发者输入额外的信息。比如开发者可能希望在不同的调用场景下使用不同的宏展开策略,则可以通过这个属性入参进行标记位设置。同时,这个属性入参也可以传入任意 Tokens,这些 Tokens 可以与被宏修饰的代码进行组合拼接等。下面是一个简单的例子:

// Macro definition with attribute
public macro Foo(attrTokens: Tokens, inputTokens: Tokens): Tokens {
    return attrTokens + inputTokens  // Concatenate attrTokens and inputTokens.
}

如上面的宏定义,属性宏的入参数量为 2,入参类型为 Tokens,在宏定义内,可以对 attrTokensinputTokens 进行一系列的组合,拼接等变换操作,最后返回新的 Tokens

带属性的宏与不带属性的宏的调用类似,属性宏调用时新增的入参 attrTokens 通过 [] 传入,其调用形式为:

// attribute macro with parentheses
var a: Int64 = @Foo[1+](2+3)

// attribute macro without parentheses
@Foo[public]
struct Data {
    var count: Int64 = 100
}
  • 宏 Foo 调用,当参数是 2+3 时,与 [] 内的属性 1+ 进行拼接,经过宏展开后,得到 var a: Int64 = 1+2+3
  • 宏 Foo 调用,当参数是 struct Data 时,与 [] 内的属性 public 进行拼接,经过宏展开后,得到
public struct Data {
    var count: Int64 = 100
}

关于属性宏,需要注意以下几点:

  • 带属性的宏,与不带属性的宏相比,能修饰的 AST 是相同的,可以理解为带属性的宏对可传入参数做了增强。

  • 要求属性宏调用时,[] 内中括号匹配,且可以为空。中括号内只允许对中括号的转义 \[\],该转义中括号不计入匹配规则,其他字符会被作为 Token,不能进行转义。

    @Foo[[miss one](2+3) // Illegal
    @Foo[[matched]](2+3) // Legal
    @Foo[](2+3)          // Legal, empty in []
    @Foo[\[](2+3)        // Legal, use escape for [
    @Foo[\(](2+3)        // Illegal, only [ and ] allowed in []
    
  • 宏的定义和调用的类型要保持一致:如果宏定义有两个入参,即为属性宏定义,调用时必须加上 [],且内容可以为空;如果宏定义有一个入参,即为非属性宏定义,调用时不能使用 []

宏导入时的别名

如果有两个宏定义的包 p1 和 p2,p1 p2 中都定义了一个宏,叫 Foo,我们在使用宏的文件中,同时导入了 p1 和 p2,那如何使用这个宏 Foo? 一种解决方法就是,在导入宏时使用别名。

// f1.cj
macro package p1
from std import ast.*
public macro Foo(input: Tokens) {
    return input
}

// f2.cj
macro package p2
from std import ast.*
public macro Foo(input: Tokens) {
    return input
}

// use.cj
import p1.Foo as Foo1
import p2.Foo as Foo2

@Foo1 // call Foo in p1
class A{}

@Foo2 // call Foo in p2
class B{}

main() {
    let a = A()
    let b = B()
    0
}

如果,开发者在 use.cj 中直接使用 @Foo,而不使用别名,编译器会报错,提示 "ambiguous match"。

另外,支持包名 + 宏名的方式调用宏。如下:

import p1.Foo
import p2.Foo

@p1.Foo
class A{}

@p2.Foo
class A{}

也支持对包名使用别名,如:

import p0.* as p1.* // p0 is a macro package

@p1.Foo // p1 is an alias for p0
class A{}

嵌套宏

仓颉语言不支持宏定义的嵌套;有条件地支持在宏定义和宏调用中进行宏调用。

宏调用在宏定义内

下面是一个宏定义中包含其他宏调用的例子。

// file pkg1.cj
macro package pkg1

from std import ast.*

public macro GetIdent(attr:Tokens, input:Tokens):Tokens {
    return quote(
        let decl = (parseDecl(input) as VarDecl).getOrThrow()
        let name = decl.identifier.value
        let size = name.size - 1
        let $(attr) = Token(TokenKind.IDENTIFIER, name[0..size])
    )
}

// file pkg2.cj
macro package pkg2

from std import ast.*
import pkg1.*

public macro Prop(input:Tokens):Tokens {
    let v = parseDecl(input)
    @GetIdent[ident](input)
    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}

// file pkg3.cj
package pkg3

import pkg2.*
class A {
    @Prop
    private let a_: Int64 = 1
}

main() {
    let b = A()
    println("${b.a}")
}

注意,按照宏定义必须比宏调用点先编译的约束,上述 3 个文件的编译顺序必须是:pkg1 -> pkg2 -> pkg3。如下宏定义:

public macro Prop(input:Tokens):Tokens {
    let v = parseDecl(input)
    @GetIdent[ident](input)
    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}

会先被展开成如下代码,再进行编译。

public macro Prop(input: Tokens): Tokens {
    let v = parseDecl(input)

    let decl = (parseDecl(input) as VarDecl).getOrThrow()
    let name = decl.identifier.value
    let size = name.size - 1
    let ident = Token(TokenKind.IDENTIFIER, name[0 .. size])

    return quote(
        $(input)
        public prop $(ident): $(decl.declType) {
            get() {
                this.$(v.identifier)
            }
        }
    )
}

宏调用在宏调用中

嵌套宏的常见场景,是宏修饰的代码块中,出现了宏调用。一个具体的例子如下:

// file pkg1.cj
macro package pkg1

from std import ast.*

public macro Foo(input: Tokens): Tokens {
    return input
}

public macro Bar(input: Tokens): Tokens {
    return input
}

// file pkg2.cj
macro package pkg2

from std import ast.*

public macro AddToMul(inputTokens: Tokens): Tokens {
    var expr: BinaryExpr = match (parseExpr(inputTokens) as BinaryExpr) {
        case Some(v) => v
        case None => throw Exception()
    }
    var op0: Expr = expr.leftExpr
    var op1: Expr = expr.rightExpr
    return quote(($(op0)) * ($(op1)))
}

// file pkg3.cj
package pkg3

import pkg1.*
import pkg2.*
@Foo
struct Data {
    let a = 2
    let b = @AddToMul(2+3)

    @Bar
    public func getA() {
        return a
    }

    public func getB() {
        return b
    }
}

main(): Int64 {
    let data = Data()
    var a = data.getA() // a = 2
    var b = data.getB() // b = 6
    println("a: ${a}, b: ${b}")
    return 0
}

如上代码所示,宏 Foo 修饰了 struct Data,而在 struct Data 内,出现了宏调用 AddToMulBar。这种嵌套场景下,代码变换的规则是:将嵌套内层的宏 (AddToMulBar) 展开后,再去展开外层的宏 (Foo)。允许出现多层宏嵌套,代码变换的规则总是由内向外去依次展开宏。

嵌套宏可以出现在带括号和不带括号的宏调用中,二者可以组合,但用户需要保证没有歧义,且明确宏的展开顺序:

var a = @Foo(@Foo1(2 * 3)+@Foo2(1 + 3))  // Foo1, Foo2 have to be defined.

@Foo1 // Foo2 expands first, then Foo1 expands.
@Foo2[attr: struct] // Attribute macro can be used in nested macro.
struct Data{
    @Foo3 @Foo4[123] var a = @Bar1(@Bar2(2 + 3) + 3)  // Bar2, Bar1, Foo4, Foo3 expands in order.
    public func getA() {
        return @Foo(a + 2)
    }
}

在嵌套场景下,存在多个宏时,有时不同宏间需要共享一些信息,往往通过在宏定义文件里定义某些全局变量的方式实现。不同的宏均可以访问、修改这些变量,其访问顺序与宏调用展开的顺序一致。

内层宏可以调用库函数 AssertParentContext 来保证内层宏调用一定嵌套在特定的外层宏调用中。如果内层宏调用这个函数时没有嵌套在给定的外层宏调用中,该函数将抛出一个错误。库函数 InsideParentContext 同样用于检查内层宏调用是否嵌套在特定的外层宏调用中,该函数返回一个布尔值。下面是一个简单的例子:

宏定义如下:

public macro Outer(input: Tokens): Tokens {
    return input
}

public macro Inner(input: Tokens): Tokens {
    AssertParentContext("Outer")
    return input
}

宏调用如下:

@Outer var a = 0
@Inner var b = 0 // error: The macro call 'Inner' should with the surround code contains a call 'Outer'.

如上代码所示,Inner 宏在定义时使用了 AssertParentContext 函数用于检查其在调用阶段是否位于 Outer 宏中,在代码示例的宏调用场景下,由于 OuterInner 在调用时不存在这样的嵌套关系,因此编译器将报告一个错误。

内层宏也可以通过发送键/值对的方式与外层宏通信。当内层宏执行时,通过调用标准库函数 SetItem 向外层宏发送信息;随后,当外层宏执行时,调用标准库函数 GetChildMessages 接收每一个内层宏发送的信息(一组键/值对映射)。下面是一个简单的例子:

宏定义如下:

public macro Outer(input: Tokens): Tokens {
    let messages = GetChildMessages("Inner")
    for (m in messages) {
        let value1 = m.getString("key1") // get value: "value1"
        let value2 = m.getString("key2") // get value: "value2"
    }
    return input
}

public macro Inner(input: Tokens): Tokens {
    AssertParentContext("Outer")
    SetItem("key1", "value1")
    SetItem("key2", "value2")
    return input
}

宏调用如下:

@Outer(
    @Inner var cnt = 0
)

在上面的代码中,内层宏 Inner 通过 SetItem 向外层宏发送信息;Outer 宏通过 GetChildMessages 函数接收到 Inner 发送的一组信息对象(Outer 中可以调用多次 Inner);最后通过该信息对象的 getString 函数接收对应的值。

内置宏

Attribute

仓颉语言内部提供 Attribute,开发者通过内置的 Attribute 来对某个声明设置属性值,从而达到标记声明的目的。属性值可以是 identifier 或者 string。下面是一个简单的例子:

@Attribute[State] var cnt = 0
@Attribute["Binding"] var bcnt = 0

同时,libast 提供了 getAttrs() 方法用于获取节点的属性,以及 hasAttr(attrs: String) 方法用于判断当前节点是否具有某个属性。下面是一个具体的例子:

宏定义如下:

public macro Component(input: Tokens): Tokens {
    var varDecl = parseDecl(input)
    if (varDecl.hasAttr("State")) { // 如果改节点被标记了属性且值为 “State” 返回 true, 否则返回 false
        var attrs = varDecl.getAttrs() // 返回一组 Tokens
        println(attrs[0].value)
    }
    return input
}

宏调用如下:

@Component(
    @Attribute[State] var cnt = 0
)

源码位置

仓颉语言内部提供了如下三个内置宏,用于在编译阶段获取源码的相关信息。

  • @sourcePackage() 展开后是一个 String 类型的字面量,内容为当前宏所在的源码的包名。
  • @sourceFile() 展开后是一个 String 类型的字面量,内容为当前宏所在的源码的文件名。
  • @sourceLine() 展开后是一个 Int64 类型的字面量,内容为当前宏所在的源码的代码行。

下面是相关示例:

// source.cj
package default

main() {
    var pkgName = @sourcePackage()  // pkgName 的值为 "default"
    var fileName = @sourceFile()    // fileName 的值为 "source.cj"
    var line = @sourceLine()        // line  的值为 7 (假设该语句位于文件的第 7 行)
    return 0
}

memoize 宏分析

至此,关于 Tokens,quote,以及宏定义,宏调用的概念已经介绍完毕。再回到记忆优化宏 memoize 这个例子,可以较清楚地理解其背后的工作原理。

在 macro_definition.cj 这个文件中,我们定义一个宏 memoize,其使用了 Tokens 类型,以及 parseDecl API,它们都是在标准库 ast 包中定义的,需要导入 ast 包。

我们使用了属性宏,这样可以让用户传入标记位(@memoize[true] 里的 true 即是标记位),选择是否进行记忆优化。memoize 宏定义的入参和返回值类型都是 Tokens,即表示一段代码块,对于宏来说代码即数据。在宏定义内部,若不进行优化,则将输入的代码原样返回;若进行优化,则重新构造一段代码,这段代码里有函数 Fib 前添加的记录函数入参 - 函数值的 HashMap 变量,Fib 函数也被重新设计,增加了查表优化功能。这段代码是放在 quote 表达式里的,表示要将其转为 Tokens 返回。

宏调用 @memoize[true] 修饰后的 Fib 函数最终会在编译期被展开为如下代码:

var memoMap: HashMap<Int64, Int64> = HashMap<Int64, Int64>()
func Fib(n: Int64): Int64 {
    if (memoMap.contains(n)) {
        return memoMap.get(n).getOrThrow()
    }
    if (n == 0 || n == 1) {
        return n
    }
    let ret = Fib(n-1) + Fib(n-2)
    memoMap.put(n, ret)
    return ret
}

可以看到,memoize 这个宏的主要作用是在编译期做一些代码生成的工作,让原程序执行效率更高,且优化细节不直接暴露在宏调用处,代码简洁,可读性高。

我们在使用时,先编译好宏定义文件,再编译宏调用文件。宏定义编译一次后,可被多个宏调用文件使用。若只是宏调用文件发生变动,宏定义不变,无需重新编译宏定义文件。

这里请注意,我们的宏 memoize 目前只能修饰 Fib 函数,函数名可以不同,但是不能用来修饰参数列表和返回值类型不同的函数,即 memoize 宏目前并不是通用的。开发者可以针对自己的需求,去写一个更通用的版本,比如解析函数的参数列表获取入参,解析返回值类型,根据这些信息去构造 HashMap,另外需要解析函数体获取最后的函数返回值,这些信息均可以通过 ast 包提供的 API 获取。

宏的编译和调试

宏的编译和使用流程

编译调用宏的文件,要求预先编译好宏定义文件,宏定义和宏调用要在不同的 package 中,下面是个简单的例子。

同普通仓颉工程一样,建议将源码放在 src 目录下,目录结构如下:

// Directory layout.
src
`-- macros
      |-- m.cj

`-- demo.cj

宏定义放在 macros(用户自定义) 子目录下:

// m.cj
// In this file, we define the macro Inner, Outer.
macro package define
from std import ast.*

public macro Inner(input: Tokens) {
    return input
}

public macro Outer(input: Tokens) {
    return input
}

实际调用宏的文件如下:

// demo.cj
import define.*
@Outer
class Demo {
    @Inner var state = 1
    var cnt = 42
}

@Outer
main() {
    println("test macro")
    0
}

以下为 Linux 平台的编译命令(具体编译选项会随着 cjc 更新而演进,以最新 cjc 的编译选项为准):

# 当前目录: src

# 先编译宏定义文件在当前目录产生默认的动态库文件(允许指定动态库的路径,但不能指定动态库的名字)
cjc macros/m.cj --compile-macro

# 编译使用宏的文件,宏替换完成,产生可执行文件
cjc demo.cj -o demo

# 编译时增加 --parallel-macro-expansion 选项
# 两个 @Outer 宏调用可以并行展开,从而缩短编译时间
cjc --parallel-macro-expansion demo.cj -o demo

# 运行可执行文件
./demo

宏的调试

借助宏在编译期做代码生成时,如果发生错误,处理起来十分棘手,这是开发者经常遇到但一般很难定位的问题。这是因为,开发者写的源码,经过宏的变换后变成了不同的代码片段。编译器抛出的错误信息是基于宏最终生成的代码进行提示的,但这些代码在开发者的源码中没有体现。

为了解决这个问题,仓颉宏提供 debug 模式,在这个模式下,开发者可以从编译器为宏生成的 debug 文件中看到完整的宏展开后的代码,如下所示:

// code before macro expansion
// demo.cj
@Outer
class Demo {
    @Inner var state = 1
    var cnt = 42
}

在编译使用宏的文件时,在选项中,增加 --debug-macro,即使用仓颉宏的 debug 模式。

cjc --debug-macro demo.cj

debug 模式下,会生成临时文件 demo.cj.macrocall,对应宏展开的部分如下:

// demo.cj.macrocall
// ===== Emitted by Macro Outer at line 3 =====
class Demo {
    var stateNew = 1
    var cnt = 42
    func getcnt( ) {
        return cnt
    }
}
// ===== End of the Emit =====

如果宏展开后的代码有语义错误,则编译器的错误信息会溯源到宏展开后代码的具体行列号。仓颉宏的 debug 模式有以下注意事项:

  • 宏的 debug 模式会重排源码的行列号信息,不适用于某些特殊的换行场景。比如
// before expansion
@M{} - 2 // macro M return 2

// after expansion
// ===== Emmitted my Macro M at line 1 ===
2
// ===== End of the Emit =====
- 2

这些因换行符导致语义改变的情形,不应使用 debug 模式。

  • 不支持宏调用在宏定义内的调试,会编译报错。
public macro M(input: Tokens) {
    let a = @M2(1+2) // M2 is in macro M, not suitable for debug mode.
    return input + quote($a)
}
  • 不支持带括号宏的调试。
// main.cj

main() {
    // For macro with parenthesis, newline introduced by debug will change the semantics
    // of the expression, so it is not suitable for debug mode.
    let t = @M(1+2)
    0
}

自动微分

前言

自动微分是一种对程序中的函数计算导数的技术。相比符号微分,其避免了表达式膨胀的性能问题;而相比数值微分,其解决了近似误差的正确性问题。

关于自动微分技术本身的相关背景知识,我们将不在本手册中进行详细介绍。推荐用户阅读以下参考文献进行详细了解。

在仓颉编程语言中,自动微分将作为原生语言特性被提供给用户,用户可以通过配置编译选项 --enable-ad 来开启自动微分特性。

cjc --enable-ad helloworld.cj

需要说明的是,目前仓颉自动微分特性仅支持反向模式自动微分。

可微类型

在仓颉自动微分的库中,我们提供了Differentiable interface 用于定义仓颉中数据类型的微分规则。所有参与自动微分中导数计算的数据类型均应实现该 interface 以确保被自动微分系统认为是合法的可微类型

// Defined in the AD library
public interface Differentiable<T> where T <: Differentiable<T> {
    // Initialize a zero tangent
    func TangentZero(): T
    // Sum up the tangent value `y`
    func TangentAdd(y: T): T
}

对于可微类型(例如浮点数类型、元组类型、struct类型),编译器会自动生成合理的 interface 实例,对于可微struct类型,用户也可以通过手动实现该 interface 实例。

可微数值类型

仓颉中的可微数值类型包括三种:

  • Float16
  • Float32
  • Float64

可微元组类型

当元组中所有元素均为可微类型时,则该元组是可微元组类型。

let a: (Float64, Float64) = (1.0, 1.0)   // differentiable
let b: (Float64, String) = (1.0, "foo")  // NOT differentiable

可微 struct 类型

默认情况下,struct 类型不可微,当 struct 类型不包含静态成员变量时,用户可在 struct 类型定义上方增加 @Differentiable 标注,将其定义为可微并通过 except 列表配置该 struct 类型的微分行为。

给定 except 列表后,该 struct 类型的成员变量将分为两类。

  • 不在 except 列表中的成员变量,必须为不可变变量,并且其类型为可微类型,成员变量将参与该 struct 类型对象的微分过程
  • except 列表中的成员变量将不参与该 struct 类型对象的微分过程。在微分过程中,该成员变量将被保持为未初始化状态。因此用户应确保不访问微分结果中的这些成员变量值,否则将导致未定义行为
// Differentiable
@Differentiable
struct Point {
    let x: Float64
    let y: Float64
    init(x: Float64, y: Float64) {
        this.x = x
        this.y = y
    }
}

// Differentiable, but no back-propagation will happen in the excepted field `tag`
@Differentiable [except: [tag]]
struct TaggedPoint {
    let x: Float64
    let y: Float64
    let tag: String
    init(x: Float64, y: Float64, tag: String) {
        this.x = x
        this.y = y
        this.tag = tag
    }
}

// Compilation Error: variable `tag` has non-differentiable type String,
// but it does not appear in an except list
@Differentiable
struct TaggedWrong {
    let x: Float64
    let y: Float64
    let tag: String
    init(x: Float64, y: Float64, tag: String) {
        this.x = x
        this.y = y
        this.tag = tag
    }
}

我们也提供了定义 except 列表的另外一个语法糖版本,用户可以通过以下语法定义 struct 类型的 include 列表。此时,struct 所有不在该 include 列表中的成员变量都被定义为在 except 列表中。

// Differentiable, but no back-propagation will happen in the excepted field `tag`
@Differentiable [include: [x, y]]
struct TaggedPoint {
    let x: Float64
    let y: Float64
    let tag: String
    init(x: Float64, y: Float64, tag: String) {
        this.x = x
        this.y = y
        this.tag = tag
    }
}

可微 unit 类型

Unit 类型是一种特殊的可微类型。对任何 Unit 类型对象的微分操作所得结果均仍是 Unit 类型。

不可微类型

仓颉自动微分暂不支持对 StringArrayenumClassInt16Int32Int64Interface 类型数据的微分,故这些类型均为不可微类型。

可微函数

默认情况下,函数均为不可微,但用户可以在函数定义上方增加 @Differentiable 标注,将其定义为可微并通过 except 列表配置该函数的微分行为。

给定 except 列表后,该函数的参数将分为两类。

  • 不在 except 列表中的参数,必须为可微类型。该参数将参与函数的微分过程,微分结果中将包含函数相对该参数的导数
  • except 列表中的参数将不参与函数的微分过程。在微分过程中,该类参数及依赖该参数的中间变量均将被忽略,微分结果中将不包含函数相对该参数的导数
// Differentiable function, its derivatives will be calculated with respect to `x` and `y`
@Differentiable
func f(x: Float64, y: Float64) {
    return x * y
}

// Differentiable function, its derivative will only be calculated with respect to `x`
@Differentiable [except: [y]]
func f(x: Float64, y: Float64) {
    return x * y
}

// Compilation Error: input `z` has non-differentiable type String,
// but it does not appear in an except list
@Differentiable
func f(x: Float64, y: Float64, z: String) {
    return x + y
}

相似地,我们也提供了定义 except 列表的另外一个语法糖版本,用户可以通过以下语法定义函数的 include 列表。此时,函数所有不在该 include 列表中的参数都被定义为在 except 列表中。exceptinclude 只能出现一个。

// Differentiable function, its derivative will only be calculated with respect to `x`
@Differentiable [include: [x]]
func f(x: Float64, y: Float64) {
    return x * y
}

在使用上述语法进行可微函数定义时,用户还需要确保函数满足以下条件。

  • 函数的返回类型为可微类型
  • 函数参数为可微类型,且不在 except 列表中(或在 include 列表中)
  • 函数中未使用全局变量
  • 函数中未使用静态变量
  • 函数体中所有表达式均为可微函数合法表达式,即表达式需满足以下任一条件
    • 表达式可微,即其微分规则已知。目前仓颉支持的可微表达式如下:
      • 赋值表达式
      • 算术表达式
      • 流表达式
      • 条件表达式(if
      • 循环表达式(仅 while, do-while),并且不支持使用 continuebreakreturn
      • lambda 表达式
      • 可微 Tuple 类型对象的初始化表达式
      • 可微 Tuple 类型对象的解构和下标访问表达式
      • 对可微函数的函数调用表达式,且在编译期可以确定哪一个可微函数定义被调用
    • 表达式不直接或间接地与不在 except 列表中的函数参数(包括嵌套函数的外层作用域函数参数)有数据依赖关系。若必须产生数据依赖关系,则可以使用 [stopGradient 函数接口] 来中止梯度传播
    • 可微函数(嵌套函数或匿名函数)中 return 表达式(包括自动插入的 return 表达式)只能出现一次
    • 嵌套函数或匿名函数未捕获可变变量
// Not differentiable
func f1(x: Float64, y: Float64): Float64 {
    return x + y
}

// Compilation Error: expression x is marked for back-propagation,
// but is used in a context that cannot be back-propagated
@Differentiable
func f2(x: Float64, y: Float64): Float64 {
    return f1(x, y)
}
// Compilation Error: match expression is not supported in differentiable function
@Differentiable
func f3(x: Float64) {
    var a = match (x) {
        case _ => 1.0
    }
    return a
}
@Differentiable
func f4 (x: Float64) {
   var y = 1.0
   // Compilation Error: capture mutable variable y in nested function is not supported by AD yet
   func nested (x: Float64) {
       y = y + x
   }
   nested(x)
}
@Differentiable [except: [n]]
func g(m: Float64, n: Float64) {
    return m + n
}

@Differentiable
func f5(x: Float64, y: Float64) {
    // Use `stopGradient` to stop the gradient propagation so we can
    // pass the `y` in `f5` to `n` in `g` even `n` is marked as `except`
    return g(x, stopGradient<Float64>(y))
}
func g(m: Float64) {
    return m == 1.0
}

@Differentiable
func f6(x: Float64) {
    // Use `stopGradient` to stop the gradient propagation so we can
    // pass the `x` in `f6` to non-differentiable function `g`
    let cond = g(stopGradient<Float64>(x))
    if (cond) {
        x * 2.0
    } else {
        x * 3.0
    }
}

注意:为了方便计算 lambda 表达式捕获变量的导数,我们在仓颉自动微分的库中预定义了以下两个接口,这两个接口将被仓颉的自动微分实现模块调用,不建议用户直接使用。

/* lambda 表达式捕获变量的导数组成的环境变量类型的基类 */
public abstract class EnvironmentTangent {
    /* 合并两部分环境变量导数 */
    public func TangentAdd(other: EnvironmentTangent): EnvironmentTangent
}
/* 环境变量类型对应的 0 梯度类型 */
public class EnvironmentZero <: EnvironmentTangent {
    public func TangentAdd(other: EnvironmentTangent): EnvironmentTangent {
        return other
    }
}

自定义函数微分规则

用户还可以通过手动提供伴随函数来为可微函数自定义微分规则。给定一个可微的全局函数 f (称为:源函数),用户可通过在另一个函数 g 的函数定义上方添加 @Adjoint 标注,将其指定为 f 的自定义伴随函数。

伴随函数需满足以下条件:

  • 伴随函数的输入参数的数量、类型和顺序与原可微函数完全一致
  • 伴随函数的输出是一个包含两个子元素的元组
    • 元组第一个子元素为原可微函数在当前伴随函数的参数输入下的输出结果
    • 元组第二个子元素为一个函数,作为原可微函数的梯度反向传播器
      • 梯度反向传播器的输入类型与原可微函数的输出数量、类型和顺序一致
      • 梯度反向传播器的输出类型与原可微函数的输入数量、类型和顺序一致
// Function `f` is differentiable and its custom adjoint function is `g`
@Differentiable
func f(x: Float64): Float64 {
    return x * x * x
}

@Adjoint [primal: f]
func g(x: Float64): ((Float64), ((Float64) -> Float64)) {
    let xSquare = x * x
    return (
        xSquare * x,
        { dy: Float64 =>
            return dy * xSquare * 3.0
        }
    )
}

当用户通过上述语法为源函数自定义伴随函数后,自动微分系统将直接使用该自定义伴随函数进行微分求导。需要注意的是我们当前暂不支持用户对参数或返回值包含函数类型的可微函数自定义伴随函数。

非全局可微函数

需要注意的是,上述可微函数规则默认应用于全局函数。事实上,仓颉编程语言中存在多种非全局函数。在自动微分系统中,我们支持以下非全局函数被定义为可微函数,其他类型的非全局函数均不支持定义为可微函数。

struct 构造函数和成员函数

用户可以使用相同的可微函数标注 @Differentiablestruct 中的构造函数和成员函数定义为可微。

  • struct 构造函数标注为可微。定义构造函数为可微的前提是 struct 类型本身可微。在这种情况下,调用该构造函数的 struct 初始化表达式可微。相反地,若构造函数未被标注为可微,则调用该构造函数的 struct 初始化表达式不可微
  • struct 成员函数标注为可微。根据仓颉语言定义,struct 成员函数隐藏包含了一个标识符为 this 的函数参数用于表示 struct 对象本身,故用户也可以在 except 列表中配置 this 参数来决定是否需要对 this 进行微分。
@Differentiable
struct Point {
    let a: Float64
    let b: Float64
    // Differentiable constructor
    @Differentiable
    init(x: Float64) {
        a = x
        b = x
    }
}

@Differentiable
func foo(x: Float64) {
    // The initialization will call the differentiable constructor.
    // Therefore it is differentiable here.
    return Point(x)
}
@Differentiable
struct Point {
    let a: Float64
    let b: Float64
    init(x: Float64) {
        a = x
        b = x
    }

    // Differentiable method
    @Differentiable
    public func sum(bias: Float64): Float64 {
        return a + b + bias
    }
}

Class 成员函数

用户也可以使用相同的可微函数标注 @Differentiableclass 中的非open成员函数定义为可微。同样根据仓颉语言定义,class 成员函数隐藏包含了一个标识符为 this 的函数参数用于表示 class 对象本身。由于在仓颉中由于 class 类型本身不可微,故除静态成员函数外,用户必须将 this 加入到可微成员函数的 except 列表中。另外需要注意的是,返回类型为This的成员函数因返回类型不可微,所以无法被定义成可微。

class Point {
    let a: Float64
    let b: Float64
    init(x: Float64) {
        a = x
        b = x
    }

    // Function foo can not be defined as differentiable
    func foo(): This {
        return this
    }

    // Differentiable method. But user must add `this` in the `except` list here.
    @Differentiable [except: [this]]
    func sum(bias: Float64): Float64 {
        return a + b + bias
    }
}

拓展的成员函数

用户也可以使用相同的可微函数标注语法将扩展的成员函数定义为可微,支持直接扩展和接口扩展。

interface I {
    func goo(a: Float64, b: Float64): Float64
}

@Differentiable
struct Foo {}

// direct extensions
extend Foo {
    @Differentiable
    func foo(a: Float64, b: Float64) {
        return a + b
    }
}

// interface extensions
extend Foo <: I {
    @Differentiable
    public func goo(a: Float64, b: Float64) {
        return a + b
    }
}

如果需要扩展的类型不可微,用户必须将this加入到扩展的可微成员函数的except列表中。

interface I {
    func goo(a: Float64, b: Float64): Float64
}

struct Foo {}

// direct extensions
extend Foo {
    @Differentiable[except: [this]]
    func foo(a: Float64, b: Float64) {
        return a + b
    }
}

// interface extensions
extend Foo <: I {
    @Differentiable[except: [this]]
    public func goo(a: Float64, b: Float64) {
        return a + b
    }
}

微分表达式

@Grad表达式

给定一个可微函数和相应的输入参数值,用户可使用 @Grad 表达式来获取该函数在该输入值处的梯度值。

@Differentiable [except: [negate]]
func product(x: Float64, y: Float64, negate: Bool): Float64 {
    return if (negate) { - x * y } else { x * y }
}

main(): Int64 {
    // Since `negate` is excepted, the gradient of product only has two components
    let productGrad = @Grad(product, 2.0, 3.0, true)
    print(productGrad[0].toString())        // Prints -3.000000
    print(productGrad[1].toString())        // Prints -2.000000
    return 0
}

使用@Grad表达式时有以下注意事项

  • @Grad表达式只能作为初始值用于 var 或 let 变量初始化表达式中
  • 给定的diffFunc标识符必须表示一个函数,且满足以下条件
    • 必须是全局函数
    • 必须是可微函数
    • 函数不能有命名参数和参数默认值
    • 函数的返回类型只能是一下类型之一:Float16Float32Float64
  • inputVal组成的集合必须与diffFunc表示的函数的参数匹配,匹配规则与形为diffFunc(inputVal, ...)的函数调用匹配规则相同
  • 给定上述类型为(X1, X2, ..., Xm)->Y的函数diffFunc
    • 若其except列表为空,则@Grad表达式的类型为(X1, X2, ..., Xm)。若该Tuple类型元素数量为 0,则退化为Unit类型,若该Tuple类型仅包含一个元素Xj,则退化为Xj类型
    • 若其except列表不为空,则@Grad表达式的类型中需相应地剔除在except列表中的函数参数类型。假定except列表中包含的参数为Xj,则@Grad表达式的类型为(X1, X2, ..., Xj-1, Xj+1, Xm)
@Differentiable
func foo(x: Float64, y: Float64): Float64 {
    return x * y
}

main() {
    let res = @Grad(foo, 2.0, 3.0)        // Ok
    // Compilation Error: @Grad expr must be used in var decl with an identifier
    print(@Grad(foo, 2.0, 3.0)[0].toString())
    // Compilation Error: @Grad expr must be used in var decl with an identifier
    let (gradx, grady) = @Grad(foo, 2.0, 3.0)
    return 0
}
@Differentiable
struct A {
    @Differentiable
    public func foo(x: Float64, y: Float64) {
        return x + y
    }
}

main() {
    let a = A()
    // Compilation Error: the function is not a top-level differentiable function identifier
    let temp = @Grad(a.foo, 1.0, 1.0)
    return 0
}

@ValWithGrad表达式

给定一个可微函数和相应的输入参数值,用户可使用 @ValWithGrad 表达式来获取该函数在该输入值处的结果和梯度值。

@Differentiable [except: [negate]]
func product(x: Float64, y: Float64, negate: Bool): Float64 {
    return if (negate) { - x * y } else { x * y }
}

main(): Int64 {
    let productValWithGrad = @ValWithGrad(product, 2.0, 3.0, true)
    let (productRes, productGrad) = productValWithGrad
    print(productRes.toString())        // Prints -6.000000
    print(productGrad[0].toString())    // Prints -3.000000
    print(productGrad[1].toString())    // Prints -2.000000
    return 0
}

使用@ValWithGrad表达式时有以下注意事项。

  • @ValWithGrad表达式只能作为初始值用于 var 或 let 变量初始化表达式中
  • 给定的diffFunc标识符必须表示一个函数,且满足以下条件
    • 必须是全局函数
    • 必须是可微函数
    • 函数不能有命名参数和参数默认值
    • 函数的返回类型只能是一下类型之一:Float16Float32Float64
  • inputVal组成的集合必须与diffFunc表示的函数的参数匹配,匹配规则与形为diffFunc(inputVal, ...)的函数调用匹配规则相同
  • 给定上述类型为(X1, X2, ..., Xm)->Y的函数diffFunc
    • 若其except列表为空,则@ValWithGrad表达式的类型为(Y, (X1, X2, ..., Xm))。若该Tuple类型元素数量为 0,则退化为(Y, Unit)类型,若该Tuple类型仅包含一个元素Xj,则退化为(Y, Xj)类型
    • 若其except列表不为空,则@ValWithGrad表达式的类型中需相应地剔除在except列表中的函数参数类型。假定except列表中包含的参数为Xj,则@ValWithGrad表达式的类型为(Y, (X1, X2, ..., Xj-1, Xj+1, Xm))
@Differentiable
func foo(x: Float64, y: Float64): Float64 {
    return x * y
}

main() {
    let res = @ValWithGrad(foo, 2.0, 3.0)        // Ok
    // Compilation Error: @ValWithGrad expr must be used in var decl with an identifier
    print(@ValWithGrad(foo, 2.0, 3.0)[0].toString())
    // Compilation Error: @ValWithGrad expr must be used in var decl with an identifier
    let (res, (gradx, grady)) = @ValWithGrad(foo, 2.0, 3.0)
    return 0
}
@Differentiable
struct A {
    @Differentiable
    public func foo(x: Float64, y: Float64) {
        return x + y
    }
}

main() {
    let a = A()
    // Compilation Error: the function is not a top-level differentiable function identifier
    let temp = @ValWithGrad(a.foo, 1.0, 1.0)
    return 0
}

@AdjointOf表达式

给定一个可微函数,用户还可以使用 @AdjointOf 表达式来获取对该函数微分产生的伴随函数。

  • @AdjointOf表达式只能作为初始值用于 var 或 let 变量初始化表达式中
  • 给定的diffFunc标识符必须表示一个函数,且满足以下条件
    • 必须是全局函数
    • 必须是可微函数
    • 函数不能有命名参数和参数默认值
  • 给定上述类型为(X1, X2, ..., Xm)->Y的函数diffFunc
    • 若其except列表为空,则@AdjointOf表达式的类型为(X1, X2, ..., Xm)->(Y, (Y) -> (X1, X2, ..., Xm))。若该Tuple类型元素数量为 0,则退化为(X1, X2, ..., Xm)->(Y, (Y) -> Unit)类型,若该Tuple类型仅包含一个元素Xj,则退化为(X1, X2, ..., Xm)->(Y, (Y) -> Xj)类型
    • 若其except列表不为空,则@AdjointOf表达式的类型中需相应地剔除在except列表中的函数参数类型。假定except列表中包含的参数为Xj,则@AdjointOf表达式的类型为(X1, X2, ..., Xm)->(Y, (Y) -> (X1, X2, ..., Xj-1, Xj+1, Xm))
@Differentiable
func foo(x: Float64, y: Float64): Float64 {
    return x * y
}

main() {
    // Get the adjoint function of `foo`
    let fooAdj = @AdjointOf(foo)

    // Given the value of `x` as 2.0 and `y` as 3.0, the adjoint function
    // will return:
    //     1) the result of `foo` when `x = 2.0` and `y = 3.0`
    //     2) an back-propagator which propagates the gradient from output to input for `foo`
    let res = fooAdj(2.0, 3.0)

    let fooRes = res[0]       // Prints 6.000000
    let fooBP = res[1]        // The back-propagator
    let (dx, dy) = fooBP(1.0)
    print(dx.toString())      // Prints 3.000000
    print(dy.toString())      // Prints 2.000000
}

@VJP表达式

给定一个可微函数和相应的输入参数值,用户可使用@VJP表达式来获取该函数在该输入值处的结果和反向传播函数。

使用@VJP表达式时有以下注意事项。

  • 给定的diffFunc标识符必须表示一个函数,且满足以下条件
    • 必须是全局函数
    • 必须是可微函数
    • 函数不能有命名参数和参数默认值
  • inputVal组成的集合必须与diffFunc表示的函数的参数匹配,匹配规则与形为diffFunc(inputVal, ...)的函数调用匹配规则相同
  • 给定上述类型为(X1, X2, ..., Xm)->Y的函数diffFunc
    • 若其except列表为空,则@VJP表达式的类型为(Y, (Y) -> (X1, X2, ..., Xm))。若该Tuple类型元素数量为 0,则退化为(Y, (Y) -> Unit)类型,若该Tuple类型仅包含一个元素Xj,则退化为(Y, (Y) -> Xj)类型
    • 若其except列表不为空,则@VJP表达式的类型中需相应地剔除在except列表中的函数参数类型。假定except列表中包含的参数为Xj,则@VJP表达式的类型为(Y, (Y) -> (X1, X2, ..., Xj-1, Xj+1, Xm))
@Differentiable [except: [negate]]
func product(x: Float64, y: Float64, negate: Bool): Float64 {
    return if (negate) { - x * y } else { x * y }
}

main(): Int64 {
    let productVJP = @VJP(product, 2.0, 3.0, true)
    let (productRes, productBP) = productVJP
    print(productRes.toString())        // Prints -6.000000
    let productGrad = productBP(1.0)
    print(productGrad[0].toString())    // Prints -3.000000
    print(productGrad[1].toString())    // Prints -2.000000
    return 0
}

stopGradient函数接口

在可微函数中,用户还可以使用 stopGradient 函数接口来强制中止某个变量或中间结果上的梯度传播。stopGradient 函数实现为一个泛型函数,可接受任意类型数据输入并将其直接返回。因此,该函数接口的使用不影响原可微函数的执行逻辑,但自动微分系统将识别该函数接口,并中止函数参数 x 对应变量或中间结果的梯度传播。此外,stopGradient 函数作用于函数时,可以把可微函数变成不可微函数。

public func stopGradient<T>(x: T) {
    return x
}
@Differentiable
func foo(x: Float64) {
    let t0 = x * 2.0
    let t1 = x * 3.0
    return t0 + t1                          // Both `t0` and `t1` will propagate gradient to `x`
}

@Differentiable
func goo(x: Float64) {
    let t0 = x * 2.0
    let t1 = x * 3.0
    return t0 + stopGradient<Float64>(t1)   // Only `t0` will propagate gradient to `x`
}

main() {
    let res0 = @Grad(foo, 1.0)      // `res0` will be 5.0
    let res1 = @Grad(goo, 1.0)      // `res1` will be 2.0
}

伴随函数的导入/导出

给定一个在包中定义的源函数,自动微分系统将对其微分并在该包中生成它的伴随函数。该伴随函数将具有和源函数相同的 public 属性。用户可以通过使用 import a.* 确保伴随函数与源函数拥有一致的导入/导出行为,即当用户导入源函数时其伴随函数将一并被导入,从而允许用户在当前包对该函数进行微分操作。

//================================= file A
package a

// AD system will generate `fooAdj` as the adjoint of `foo`
// `fooAdj` also has `public` attribute
@Differentiable
public func foo(x: Float64) {
    return x
}

//================================= file B
package b
// Will also import a.fooAdj implicitly
import a.*

main() {
    // The AD system will use the `a.fooAdj` as the adjoint of `a.foo` for differentiation
    let gradRes = @Grad(foo, 2.0)
    print(gradRes.toString()) // Prints 1.000000
    0
}

需要注意的是,若用户实现了自定义伴随函数,则也需要为该伴随函数手动配置 public 属性,从而确保该伴随函数也会被同步导入。除此之外,在导入/导出场景下,给定一个源函数,多个来自不同来源的伴随函数有可能同时出现。在这种情况下,我们定义了如下规则来确定不同版本伴随函数的优先级和使用规则。

  • 当前包内定义的本地伴随函数比从其他包导入的伴随函数优先级更高,后者将被前者屏蔽
  • 若出现多个从其他包导入的不同版本伴随函数且无本地伴随函数时,将触发编译器报错
//================================= file A
package a

@Differentiable
public func foo(x: Float64): Float64 {
    return x
}

@Adjoint [primal: foo]
public func dfoo(x: Float64): (Float64, (Float64)->Float64) {
    return (
        x,
        { dy: Float64 =>
            return 1.0 * dy
        }
    )
}

//================================= file B
import a.*

@Adjoint [primal: foo]
func localDFoo(x: Float64): (Float64, (Float64)->Float64) {
    return (
        x,
        { dy: Float64 =>
            return 1.0 * dy
        }
    )
}

main() {
    // The AD system will use the `localDFoo` as the
    // adjoint of `a.foo` for differentiation, since
    // it has higher priority than the `a.dfoo`
    let gradRes = @Grad(foo, 2.0)
    print(gradRes.toString())     // Prints 1.000000
    0
}
//================================= file A
package a

@Differentiable
public func foo(x: Float64): Float64 {
    return x
}

//================================= file B
package b
import a.*

@Adjoint [primal: foo]
public func dfooB(x: Float64): (Float64, (Float64)->Float64) {
    return (
        x,
        { dy: Float64 =>
            return 1.0 * dy
        }
    )
}

//================================= file C
package c
import a.*

@Adjoint [primal: foo]
public func dfooC(x: Float64): (Float64, (Float64)->Float64) {
    return (
        x,
        { dy: Float64 =>
            return 1.0 * dy
        }
    )
}

//================================= file D
import a.*, b.*, c.*

main() {
    // Compilation Error: multiple imported adjoint for function foo are found
    let grad_res = @Grad(foo, 2.0)
    0
}

高阶微分

用户可以在微分函数中使用 @Grad@ValWithGrad@AdjointOf@VJP等表达式来实现高阶微分的效果。

此时用户需要在 @Differentiable 标注中额外提供 stage 信息来标记该微分函数的最高微分阶数(若未提供则默认最高微分阶数为一阶),若阶数信息不正确则将引发编译器报错。

注:目前高阶微分最高支持 2 阶,即 stage 取值只能是 12。 阶数检查规则:

  1. stage=1 的函数中可以使用 stage=1,2 的可微函数调用表达式,可以包含 stage=2 的函数的微分表达式
  2. stage=2 的函数中可以使用 stage=2 的函数调用表达式
@Differentiable [stage: 2]
func f1(x: Float64) {
    x * x * x       // Will be differentiated as `df1/dx = 3 * x * x`
}

@Differentiable
func f2(x: Float64) {
    let dx = @Grad(f1, x)      // Will be differentiated as `df2/dx = d(df1/dx)/dx = 6 * x`
    dx
}

main() {
    let x: Float64 = 1.0
    let firstOrderGrad = @Grad(f1, x)
    let secondOrderGrad = @Grad(f2, x)
    println(firstOrderGrad.toString())     // Prints 3.000000
    println(secondOrderGrad.toString())    // Prints 6.000000
    return 0
}

需要注意的是,不允许在微分函数中对函数本身或将调用函数本身的函数使用 @Grad@ValWithGrad@AdjointOf@VJP 表达式,否则将产生循环依赖从而引发编译器报错。

包管理

随着项目的规模不断地扩大,仅在一个超大文件中管理源代码会变得十分困难。这时可以将源代码根据功能进行分组,并将不同功能的代码分开管理,每组独立管理的代码会生成一个输出文件。在使用时,通过导入对应的输出文件使用相应的功能,或者通过不同功能的交互与组合实现更加复杂的特性,使得项目管理更加高效。

在仓颉编程语言中,包是编译的最小单元,每个包可以单独输出 AST 文件、静态库文件、动态库文件等产物。每个包有自己的名字空间,在同一个包内不允许有同名的顶层定义或声明(函数重载除外)。一个包中可以包含多个源文件。

模块是若干包的集合,是第三方开发者发布的最小单元。一个模块的程序入口只能在其根目录下,它的顶层最多只能有一个作为程序入口的 main ,该 main 没有参数或参数类型为 Array<String>,返回类型为整数类型或 Unit 类型。

仓颉语言提供 CJPM 作为包管理工具,详见工具用户手册 Cangjie Tools User Guide 的对应章节。仓颉提供了包编译的方式对一个包进行编译,形如:

cjc --package path_to_package_directory --module-name moduleName --output path_to_output_directory

其中,命令各部分解析如下:

命令内容命令含义
--package表示编译方式,--package 为包编译(默认为文件编译)
path_to_package_directory包所在目录
--module-name用来指定输出模块名
moduleName输出模块名
--output用来指定输出路径
path_to_output_directory输出文件路径

对于当前包所依赖的包,编译器会先后在 --import-path 指定的路径、当前路径、CANGJIE_PATH 指定的路径和CANGJIE_HOME 指定的路径下查询依赖的相关文件。其中 CANGJIE_PATHCANGJIE_HOME 是用户设置的环境变量,可以效仿[入门指南]进行设置,它们分别是第三方库所在目录和仓颉编译器及标准库所在目录。

可以通过 --scan-dependency 指令获得指定包源码或者一个包的 cjo 文件对于其他包的直接依赖以及其他信息,标准库相关依赖不会输出,以 json 格式输出到命令行。

// this file is placed under directory pkgA
macro package pkgA
import pkgB.*
from std import io.*
from module2 import pkgB.*
cjc --scan-dependency --package pkgA --module-name module1
{"packageName":"module1/pkgA","isMacroPackage":true,"dependencies":[{"moduleName":"module1","packageName":"pkgB"},{"moduleName":"module2","packageName":"pkgB"}]}
cjc --scan-dependency pkgA.cjo
{"packageName":"module1/pkgA","isMacroPackage":true,"dependencies":[{"moduleName":"module1","packageName":"pkgB"},{"moduleName":"module2","packageName":"pkgB"}]}

仓颉命令行指令的详细解析,可以通过 --help 指令获得:

cjc --help

声明包的名字

在仓颉编程语言中,可以通过形如 package name 的方式声明名为 name 的包,其中 package 为关键字,name 须为仓颉的合法标识符。包声明必须在源文件的非空非注释的首行,且同一个包中的不同源文件的包声明必须保持一致。如

// file 1
// Comments are accepted
package test
// declarations...

// file 2
let a = 1 // Error, package declaration must appear first in a file
package test
// declarations...

仓颉的包名需反映当前源文件相对于项目源码根目录 src 的路径,并将其中的路径分隔符替换为小数点。例如包的源代码位于 src/directory_0/directory_1 下,则其源代码中的包声明应为 package directory_0.directory_1

注意:

  • 包所在的文件夹名也必须是合法标识符,不能包含.等其他符号。
  • 源码根目录默认名为 src
  • 源码根目录下的包可以没有包声明,此时编译器将默认为其指定包名 default

假设源代码目录结构如下:

// The directory structure is as follows:
src
`-- directory_0
    |-- directory_1
    |    |-- a.cj
    |    `-- b.cj
    `-- c.cj
`-- main.cj

a.cjb.cjc.cjmain.cj 中的包声明可以为

// a.cj
// in file a.cj, the declared package name must correspond to relative path directory_0/directory_1.

package directory_0.directory_1
// b.cj
// in file b.cj, the declared package name must correspond to relative path directory_0/directory_1.

package directory_0.directory_1
// c.cj
// in file c.cj, the declared package name must correspond to relative path directory_0.

package directory_0
// main.cj
// file main.cj is in the module root directory and may omit package declaration.

main() {
    return 0
}

另外,包声明不能引起命名冲突:

  • 包名与当前包的顶层声明不能重名。

  • 当前包的顶层声明不能与子目录的包名同名。

以下是一些错误示例:

// a.cj

package c    // Error, package name conflicts with top-level class name.

class c {}
// b.cj
package a
public class B {
    /* Error, top-level declaration "B" conflicts with the sub-directory of the same name */
    public func f() {}
}

// c.cj
/* Error, package "a.B" conflicts with the top-level declaration "B" in package "a".*/
package a.B
public func f {}

// main.cj
import a.*
import a.B

main() {
    /* Error, cannot distinguish a.B is the package a.B or the class B in package b. */
    a.B.f()
    return 0
}

顶层声明的可见性

顶层声明可见性为包内可见

如果希望包中某个顶层声明仅在包内被使用,顶层声明前不需要加任何修饰。所有顶层声明的可见性默认是包内可见的(internal)。

// a.cj
package a
// if top-level declaration C has no modifier,
// it's not accessible to other packages except package a.
class C {}

顶层声明可见性为包外可见

如果希望包中的某个顶层声明被导出,从而能被其他包使用,应当在顶层声明前加 public 修饰。被 public 修饰的顶层声明的可见性被认为是包外可见的(external)。

// a.cj
package a
public interface I {}

public 修饰的顶层声明不能使用包外不可见的类型

  • 函数声明中的参数与返回值

    // a.cj
    package a
    class C {}
    public func f1(a1: C) // Error, external declaration f1 cannot use internal type C.
    {
        return 0
    }
    public func f2(a1: Int8): C // Error, external declaration f2 cannot use internal type C.
    {
        return C()
    }
    public func f3 (a1: Int8) // Error, external declaration f3 cannot use internal type C.
    {
        return C()
    }
    
  • 变量声明

    // a.cj
    package a
    class C {}
    public let v1: C = C() // Error, external declaration v1 cannot use internal type C.
    public let v2 = C() // Error, external declaration v2 cannot use internal type C.
    
  • 类声明中继承的类

    // a.cj
    package a
    open class C1 {}
    public class C2 <: C1 {} // Error, external declaration C2 cannot use internal type C1.
    
  • 类型实现的接口

    // a.cj
    package a
    interface I {}
    public enum E <: I {} // Error, external declaration uses internal types.
    
  • 泛型类型的类型实参

    // a.cj
    package a
    public class C1<T> {}
    class C2 {}
    public let v1 = C1<C2>() // Error, external declaration v1 cannot use internal type C2.
    
  • where 约束中的类型上界

    // a.cj
    package a
    interface I {}
    public class B<T> where T <: I {}  // Error, external declaration B cannot use internal type I.
    

值得注意的是:

  • public 修饰的声明在其初始化表达式或者函数体里面可以使用本包可见的任意类型,包括 public 修饰的类型和没有 public 修饰的类型。

    // a.cj
    package a
    class C1 {}
    func f1(a1: C1)
    {
      return 0
    }
    public func f2(a1: Int8) // ok.
    {
      var v1 = C1()
      return 0
    }
    public let v1 = f1(C1()) // ok.
    public class C2 // ok.
    {
      var v2 = C1()
    }
    
  • public 修饰的顶层声明能使用匿名函数,或者任意顶层函数,包括 public 修饰的类型和没有 public 修饰的顶层函数。

    public var t1: () -> Unit = { => } // Ok.
    func f1(): Unit {}
    public let t2 = f1 // Ok.
    
    public func f2() // Ok.
    {
      return f1
    }
    
  • 内置类型诸如 CharInt64 等也都默认是 public 的。

    var num = 5
    public var t3 = num // Ok.
    

包的导入

使用 import 语句导入其它包中的声明或定义

在仓颉编程语言中,可以通过 from moduleName import packageName.itemName 的语法导入其他包中的一个顶层声明或定义,其中 moduleName 为模块名,packageName 为包名,itemName 为声明的名字。导入当前模块中的内容时,可以省略 from moduleName;跨模块导入时,必须使用 from moduleName 指定模块。导入语句在源文件中的位置必须在包声明之后,其他声明或定义之前。例如:

package a
from std import math.*
from module_name import package1.foo
from module_name import package1.foo, package2.bar

如果要导入的多个 itemName 同属于一个 packageName,可以使用 from moduleName import packageName.{itemName[, itemName]*} 语法,例如:

from module_name import package1.{foo, bar, fuzz}

这等价于:

from module_name import package1.foo, package1.bar, package1.fuzz

除了通过 import packagename.itemName 语法导入一个特定的顶层声明或定义外,还可以使用 import packageName.* 语法将 packageName 包中所有 public 修饰的顶层声明或定义全部导入。例如:

from module_name import package1.*
from module_name import package1.*, package2.*
from module_name import package1 // Error.

需要注意:

  • 当前源文件中导入的内容在源文件所属包的所有其他源文件中也可见。
  • 当已导出的包的模块名或者包名被篡改,使其与导出时指定的模块名或包名不一致,在导入时,即使通过修改相应的 from ... import 语句找到了对应的包,也会报错。
  • 只允许导入使用 public 修饰的顶层声明或定义,导入无 public 修饰的声明或定义将会在导入处报错。
  • 可以直接使用导入的名字访问导入的声明或定义,也可以通过形如moduleName.packageName.itemName的带路径限定的名字访问导入的声明或定义。
  • 支持使用形如 curPackageName.itemName 的带路径限定的名字访问当前包的内容,但禁止通过 import 导入当前源文件所在包的声明或定义。
  • 禁止包间的循环依赖导入,如果包之间存在循环依赖,编译器会报错。

示例如下:

// pkga/a.cj
package pkga    // Error, packages pkga pkgb are in circular dependencies.
import pkgb.*

class C {}
public struct R {}

// pkgb/b.cj
package pkgb

import pkga.*

// pkgc/c1.cj
package pkgc

import pkga.C // Error, 'C' is not accessible in package 'pkga'.
import pkga.R // OK, R is an external top-level declaration of package pkga.
import pkgc.f1 // Error, package 'pkgc' should not import itself.

public func f1() {}

// pkgc/c2.cj
package pkgc

func f2() {
    /* OK, the imported declaration is visible to all source files of the same package
     * and accessing import declaration by its name is supported.
     */
    R()

    // OK, accessing imported declaration by fully qualified name is supported.
    pkga.R()

    // OK, the declaration of current package can be accessed directly.
    f1()

    // OK, accessing declaration of current package by fully qualified name is supported.
    pkgc.f1()
}

在仓颉编程语言中,导入的声明或定义如果和当前包中的顶层声明或定义重名且不构成函数重载,则导入的声明和定义会被遮盖;导入的声明或定义如果和当前包中的顶层声明或定义重名且构成函数重载,函数调用时将会根据函数重载的规则进行函数决议。

// pkga/a.cj
package pkga

public struct R {}            // R1
public func f(a: Int32) {}    // f1
public func f(a: Bool) {} // f2

// pkgb/b.cj
package pkgb
import pkga.*

func f(a: Int32) {}         // f3
struct R {}                 // R2

func bar() {
    R()     // OK, R2 shadows R1.
    f(1)    // OK, invoke f3 in current package.
    f(true) // OK, invoke f2 in the imported package
}

隐式导入 core 包

诸如 StringRange 等类型能直接使用,并不是因为这些类型是内置类型,而是因为编译器会自动为源码隐式的导入 core 包中所有的 public 修饰的声明。

使用 import as 对导入的名字重命名

不同包的名字空间是分隔的,因此在不同的包之间可能存在同名的顶层声明。在导入不同包的同名顶层声明时,我们支持使用 import packageName.name as newName 的方式进行重命名来避免冲突。没有名字冲突的情况下仍然可以通过 import as 来重命名导入的内容。import as具有如下规则:

  • 使用import as 对导入的声明进行重命名后,当前包只能使用重命名后的新名字,原名无法使用。

  • 同一个声明只能重命名一次。

  • 使用 import as 重命名的新名字的作用域级别和当前包顶层作用域级别一致。(注意,不使用 as 时引入的名字的作用域级别比当前包顶层作用域级别低。)

  • 如果重命名后的名字与当前包顶层作用域的其它名字存在冲突,且这些名字对应的声明均为函数类型,则参与函数重载,否则报重定义的错误。

  • 支持 import pkg.* as newPkgName.* 的形式对包名进行重命名,以解决不同模块中同名包的命名冲突问题。

    // a.cj in module1/p1
    package p1
    public func f1() {}
    
    // d.cj in module2/p2
    package p2
    public func f3() {}
    
    // b.cj in module2/p1
    package p1
    public func f2() {}
    
    // c.cj in module3/pkgc
    package pkgc
    public func f1() {}
    
    // main.cj in module3
    from module1 import p1.* as A.*
    from module2 import p1.* as B.*
    from module1 import p2.f3 as f  // OK
    import pkgc.f1 as a
    import pkgc.f1 as b // Error, 'pkgc.f1' cannot be redefined here.
    
    func f(a: Int32) {}
    
    main() {
        A.f1()  // OK, package name conflict is resolved by renaming package name.
        B.f2()  // OK, package name conflict is resolved by renaming package name.
        p1.f1() // Error, the original package name cannot be used.
        a()     // OK.
        b()     // OK.
        pkgc.f1()    // Error, the original name cannot be used.
    }
    
  • 如果没有对导入的存在冲突的名字进行重命名,在 import 语句处不报错;在使用处,只能使用带路径限定的名字来访问,仅使用冲突的名字来访问将会报错。

    // a.cj in module1
    package p1
    public class C {}
    
    // b.cj in module2
    package p1
    public class C {}
    
    // c.cj in module2
    package pkgc
    public class C {}
    
    // main.cj in module2
    package pkgd
    from module1 import p1.C
    from module2 import p1.C
    import pkgc.C
    
    main() {
        C()             // Error.
        pkgc.C()        // OK.
        p1.C()          // Error.
        module1.p1.C()  // OK.
        module2.p1.C()  // OK.
    }
    

使用 public import 重导出一个导入的名字

在功能繁多的大型项目的开发过程中,这样的场景是非常常见的:包 p2 大量地使用从包 p1 中导入的声明,当包 p3 导入包 p2 并使用其中的功能时,p1 中的声明同样需要对包 p3 可见。如果要求包 p3 自行导入 p2 中使用到的 p1 中的声明,这个过程将过于繁琐。因此希望能够在 p2 被导入时一并导入 p2 使用到的 p1 中的声明。

在仓颉编程语言中,可以在导入包的时候使用 public import 指定被重导出的声明。其它包可以直接导入并使用本包中用 public import 重导出的内容,无需从原包中导入这些内容。

需要注意的是,当导入的名字出现在当前包被 public 修饰的函数的参数类型或返回类型中时,它们必须被重导出。在如下例子的 pkg2 中,f 是一个 public 修饰的函数,它的返回值类型是导入的类型 C1,另一个包 pkg3 导入了 pkg2 的函数 f。若 pkg3 未手动导入 pkg1 中的类 C1,或者在 pkg2 中没有使用 public import 指明类 C1 被重导出,则 pkg3 在使用函数 f 时将会因为找不到类 C1 而报错。强行限制 pkg2 重导出类 C1,可以使 pkg3 在使用 pkg2 中函数 f 时无需手动导入 pkg1 中的类 C1

使用 public import 导入的名字相当于是在本包内定义了一个新的名字,因此,public import 导入的名字和 import as 重命名的名字具有相同的规则。

// a.cj
package pkg1
public class C1 {}
public class C2 {}

// b.cj
package pkg2
public import pkg1.C1       // class C1 of pkg1 must be re-exported.
public func f() {
    let c = C1()
    return c
}
public class C2 {}

// main.cj
package pkg3
import pkg2.*
public import pkg1.C2       // Error, C2 is redefined.
public import pkg2.C2       // Error, C2 is redefined.

public class C2{}           // Error, C2 is redefined.

main(): Int64 {
    let obj1 = f()          // OK.
    let obj2: C1 = f()      // OK, class C1 is visible to current package.
    return 0
}

程序入口

仓颉程序入口为 main,源文件根目录下的包的顶层最多只能有一个 main

如果模块采用生成可执行文件的编译方式,编译器只在源文件根目录下的顶层查找main。如果没有找到,编译器将会报错;如果找到main,编译器会进一步对其参数和返回值类型进行检查。需要注意的是,main不可被访问修饰符修饰,当一个包被导入时,包中定义的main不会被导入。

作为程序入口的 main 可以没有参数或参数类型为 Array<String>,返回值类型为 Unit 或整数类型。

没有参数的 main

// main.cj
main(): Int64 { // OK.
    return 0
}

参数类型为 Array<String>main

// main.cj
main(args: Array<String>): Unit { // OK.
    for (arg in args) {
        println(arg)
    }
}

使用 cjc main.cj 编译完成后,通过命令行执行:./main Hello, World,将会得到如下输出:

Hello,
World

以下是一些错误示例:

// main.cj
main(): String { // Error, return type of 'main' is not 'Integer' or 'Unit'.
    return ""
}
// main.cj
main(args: Array<Int8>): Int64 { // Error, 'main' cannot be defined with parameter whose type is not Array<String>.
    return 0
}
// main.cj
// Error, multiple 'main's are found in source files.
main(args: Array<String>): Int32 {
    return 0
}

main(): Int8 {
    return 0
}

跨语言互操作

Foreign Function Interfaces (FFI) 是一种机制,通过该机制,一种编程语言写的程序可以调用另外一种编程语言编写的函数。

与 C 语言互操作

为了兼容已有的生态,仓颉支持调用 C 语言的函数,也支持 C 语言调用仓颉的函数。

仓颉调用 C 的函数

在仓颉中要调用 C 的函数,需要在仓颉语言中用 @Cforeign 关键字声明这个函数,但 @C 在修饰 foreign 声明的时候,可以省略。

举个例子,假设我们要调用 C 的 randprintf 函数,它的函数签名是这样的:

// stdlib.h
int rand();

// stdio.h
int printf (const char *fmt, ...);

那么在仓颉中调用这两个函数的方式如下:

// declare the function by `foreign` keyword, and omit `@C`
foreign func rand(): Int32
foreign func printf(fmt: CString, ...): Int32

main() {
    // call this function by `unsafe` block
    let r = unsafe { rand() }
    println("random number ${r}")
    unsafe {
        var fmt = LibC.mallocCString("Hello, No.%d\n")
        printf(fmt, 1)
        LibC.free(fmt)
    }
}

需要注意的是:

  1. foreign 修饰函数声明,代表该函数为外部函数。被 foreign 修饰的函数只能有函数声明,不能有函数实现。
  2. foreign 声明的函数,参数和返回类型必须符合 C 和仓颉数据类型之间的映射关系(详见下节:类型映射)。
  3. 由于 C 侧函数很可能产生不安全操作,所以调用 foreign 修饰的函数需要被 unsafe 块包裹,否则会发生编译错误。
  4. @C 修饰的 foreign 关键字只能用来修饰函数声明,不可用来修饰其他声明,否则会发生编译错误。
  5. @C 只支持修饰 foreign 函数、top-level 作用域中的非泛型函数和 struct 类型。
  6. foreign 函数不支持命名参数和参数默认值。foreign 函数允许变长参数,使用 ... 表达,只能用于参数列表的最后。变长参数均需要满足 CType 约束,但不必是同一类型。
  7. 仓颉虽然提供了栈扩容能力,但是由于 C 侧函数实际使用栈大小仓颉无法感知,所以 ffi 调用进入 C 函数后,仍然存在栈溢出的风险,需要开发者根据实际情况,修改 cjStackSize 的配置。

一些不合法的 foreign 声明的示例代码如下:

foreign func rand(): Int32 { // compiler error
    return 0
}
@C
foreign var a: Int32 = 0 // compiler error
@C
foreign class A{} // compiler error
@C
foreign interface B{} // compiler error

CFunc

仓颉中的 CFunc 指可以被 C 语言代码调用的函数,共有以下三种形式:

  1. @C 修饰的 foreign 函数
  2. @C 修饰的仓颉函数
  3. 类型为 CFunclambda 表达式
    • 与普通的 lambda 表达式不同,CFunc lambda 不能捕获变量。
// Case 1
foreign func free(ptr: CPointer<Int8>): Unit

// Case 2
@C
func callableInC(ptr: CPointer<Int8>) {
    print("This function is defined in Cangjie.")
}

// Case 3
let f1: CFunc<(CPointer<Int8>) -> Unit> = { ptr =>
    print("This function is defined with CFunc lambda.")
}

以上三种形式声明/定义的函数的类型均为 CFunc<(CPointer<Int8>) -> Unit>CFunc 对应 C 语言的函数指针类型。这个类型为泛型类型,其泛型参数表示该 CFunc 入参和返回值类型,使用方式如下:

foreign func atexit(cb: CFunc<() -> Unit>)

foreign 函数一样,其他形式的 CFunc 的参数和返回类型必须满足 CType 约束,且不支持命名参数和参数默认值。

CFunc 在仓颉代码中被调用时,需要处在 unsafe 上下文中。

仓颉语言支持将一个 CPointer<T> 类型的变量类型转换为一个具体的 CFunc,其中 CPointer 的泛型参数 T 可以是满足 CType 约束的任意类型,使用方式如下:

main() {
    var ptr = CPointer<Int8>()
    var f = CFunc<() -> Unit>(ptr)
    unsafe { f() } // core dumped when running, because the pointer is nullptr.
}

注意:将一个指针强制类型转换为 CFunc 并进行函数调用是危险行为,需要用户保证指针指向的是一个切实可用的函数地址,否则将发生运行时错误。

inout 参数

在仓颉中调用 CFunc 时,其实参可以使用 inout 关键字修饰,组成引用传值表达式,此时,该参数按引用传递。引用传值表达式的类型为 CPointer<T>,其中 Tinout 修饰的表达式的类型。

引用传值表达式具有以下约束:

  • 仅可用于对 CFunc 的调用处;
  • 其修饰对象的类型必须满足 CType 约束,但不可以是 CString
  • 其修饰对象不可以是用 let 定义的,不可以是字面量、入参、其他表达式的值等临时变量;
  • 通过仓颉侧引用传值表达式传递到 C 侧的指针,仅保证在函数调用期间有效,即此种场景下 C 侧不应该保存指针以留作后用。

inout 修饰的变量,可以是定义在 top-level 作用域中的变量、局部变量、struct 中的成员变量,但不能直接或间接来源于 class 的实例成员变量。

下面是一个例子:

foreign func foo1(ptr: CPointer<Int32>): Unit

@C
func foo2(ptr: CPointer<Int32>): Unit {
    let n = unsafe { ptr.read() }
    println("*ptr = ${n}")
}

let foo3: CFunc<(CPointer<Int32>) -> Unit> = { ptr =>
    let n = unsafe { ptr.read() }
    println("*ptr = ${n}")
}

struct Data {
    var n: Int32 = 0
}

class A {
    var data = Data()
}

main() {
    var n: Int32 = 0
    unsafe {
        foo1(inout n)  // OK
        foo2(inout n)  // OK
        foo3(inout n)  // OK
    }
    var data = Data()
    var a = A()
    unsafe {
        foo1(inout data.n)   // OK
        foo1(inout a.data.n) // Error, n is derived indirectly from instance member variables of class A
    }
}

**注意:**使用宏扩展特性时,在宏的定义中,暂时不能使用 inout 参数特性。

unsafe

在引入与 C 语言的互操作过程中,同时也引入了 C 的许多不安全因素,因此在仓颉中使用 unsafe 关键字,用于对跨 C 调用的不安全行为进行标识。

关于 unsafe 关键字,有以下几点说明:

  • unsafe 可以修饰函数、表达式,也可以修饰一段作用域。
  • @C 修饰的函数,被调用处需要在 unsafe 上下文中。
  • 在调用 CFunc 时,使用处需要在 unsafe 上下文中。
  • foreign 函数在仓颉中进行调用,被调用处需要在 unsafe 上下文中。
  • 当被调用函数被 unsafe 修饰时,被调用处需要在 unsafe 上下文中。

使用方式如下:

foreign func rand(): Int32

@C
func foo(): Unit {
    println("foo")
}

var foo1: CFunc<() -> Unit> = { =>
    println("foo1")
}

main(): Int64 {
    unsafe {
        rand()           // Call foreign func.
        foo()            // Call @C func.
        foo1()           // Call CFunc var.
    }
    0
}

注意: 普通 lambda 无法传递 unsafe 属性,当 unsafelambda 逃逸后,可以不在 unsafe 上下文中直接调用而未产生任何编译错误。当需要在 lambda 中调用 unsafe 函数时,建议在 unsafe 块中进行调用,参考如下用例:

unsafe func A(){}
unsafe func B(){
    var f = { =>
        unsafe { A() } // Avoid calling A() directly without unsafe in a normal lambda.
    }  
    return f  
}
main() {
    var f = unsafe{ B() }
    f()
    println("Hello World")
}

调用约定

函数调用约定描述调用者和被调用者双方如何进行函数调用(如参数如何传递、栈由谁清理等),函数调用和被调用双方必须使用相同的调用约定才能正常运行。仓颉编程语言通过 @CallingConv 来表示各种调用约定,支持的调用约定如下:

  • CDECL, CDECL 表示 clang 的 C 编译器在不同平台上默认使用的调用约定。
  • STDCALL, STDCALL 表示 Win32 API 使用的调用约定。

通过 C 语言互操作机制调用的 C 函数,未指定调用约定时将采用默认的 CDECL 调用约定。如下调用 C 标准库函数 rand 示例:

@CallingConv[CDECL]   // Can be omitted in default.
foreign func rand(): Int32

main() {
    println(rand())
}

@CallingConv 只能用于修饰 foreign 块、单个 foreign 函数和 top-level 作用域中的 CFunc 函数。当 @CallingConv 修饰 foreign 块时,会为 foreign 块中的每个函数分别加上相同的 @CallingConv 修饰。

类型映射

基础类型

仓颉与 C 语言支持基本数据类型的映射,总体原则是:

  1. 仓颉的类型不包含指向托管内存的引用类型;
  2. 仓颉的类型和 C 的类型具有同样的内存布局。

比如说,一些基本的类型映射关系如下:

Cangjie TypeC TypeSize (byte)
Unitvoid0
Boolbool1
UInt8char1
Int8int8_t1
UInt8uint8_t1
Int16int16_t2
UInt16uint16_t2
Int32int32_t4
UInt32uint32_t4
Int64int64_t8
UInt64uint64_t8
IntNativessize_tplatform dependent
UIntNativesize_tplatform dependent
Float32float4
Float64double8

注:int 类型、long 类型等由于其在不同平台上的不确定性,需要程序员自行指定对应仓颉编程语言类型。在 C 互操作场景中,与 C 语言类似,Unit 类型仅可作为 CFunc 中的返回类型和 CPointer 的泛型参数。

仓颉也支持与 C 语言的结构体和指针类型的映射。

结构体

对于结构体类型,仓颉用 @C 修饰的 struct 来对应。比如说 C 语言里面有这样的一个结构体:

typedef struct {
    long long x;
    long long y;
    long long z;
} Point3D;

那么它对应的仓颉类型可以这么定义:

@C
struct Point3D {
    var x: Int64 = 0
    var y: Int64 = 0
    var z: Int64 = 0
}

如果 C 语言里有这样的一个函数:

Point3D addPoint(Point3D p1, Point3D p2);

那么对应的,在仓颉里面可以这样声明这个函数:

foreign func addPoint(p1: Point3D, p2: Point3D): Point3D

@C 修饰的 struct 必须满足以下限制:

  • 成员变量的类型必须满足 CType 约束
  • 不能实现或者扩展 interfaces
  • 不能作为 enum 的关联值类型
  • 不允许被闭包捕获
  • 不能具有泛型参数

@C 修饰的 struct 自动满足 CType 约束。

指针

对于指针类型,仓颉提供 CPointer<T> 类型来对应 C 侧的指针类型,其泛型参数 T 需要满足 CType 约束。 比如对于 malloc 函数,在 C 里面的签名为:

void* malloc(size_t size);

那么在仓颉中,它可以声明为:

foreign func malloc(size: UIntNative): CPointer<Unit>

CPointer 可以进行读写、偏移计算、判空以及转为指针的整型形式等,详细 API 可以参考 core 包标准库文档。其中读写和偏移计算为不安全行为,当不合法的指针调用这些函数时,可能发生未定义行为,这些 unsafe 函数需要在 unsafe 块中调用。

CPointer 的使用示例如下:

foreign func malloc(size: UIntNative): CPointer<Unit>
foreign func free(ptr: CPointer<Unit>): Unit

@C
struct Point3D {
    var x: Int64
    var y: Int64
    var z: Int64

    init(x: Int64, y: Int64, z: Int64) {
        this.x = x
        this.y = y
        this.z = z
    }
}

main() {
    let p1 = CPointer<Point3D>() // create a CPointer with null value
    if (p1.isNull()) {  // check if the pointer is null
        print("p1 is a null pointer")
    }

    let sizeofPoint3D: UIntNative = 24
    var p2 = unsafe { malloc(sizeofPoint3D) }    // malloc a Point3D in heap
    var p3 = unsafe { CPointer<Point3D>(p2) }    // pointer type cast

    unsafe { p3.write(Point3D(1, 2, 3)) } // write data through pointer

    let p4: Point3D = unsafe { p3.read() } // read data through pointer

    let p5: CPointer<Point3D> = unsafe { p3 + 1 } // offset of pointer

    unsafe { free(p2) }
}

仓颉语言支持 CPointer 之间的强制类型转换,转换前后的 CPointer 的泛型参数 T 均需要满足 CType 的约束,使用方式如下:

main() {
    var pInt8 = CPointer<Int8>()
    var pUInt8 = CPointer<UInt8>(pInt8) // CPointer<Int8> convert to CPointer<UInt8>
    0
}

仓颉语言支持将一个 CFunc 类型的变量类型转换为一个具体的 CPointer,其中 CPointer 的泛型参数 T 可以是满足 CType 约束的任意类型,使用方式如下:

foreign func rand(): Int32
main() {
    var ptr = CPointer<Int8>(rand)
    0
}

注意:将一个 CFunc 强制类型转换为指针通常是安全的,但是不应该对转换后的指针执行任何的 readwrite 操作,可能会导致运行时错误。

数组

仓颉使用 VArray 类型与 C 的数组类型映射,VArray 可以用户作为函数参数和 @C struct 成员。当 VArray<T, $N> 中的元素类型 T 满足 CType 约束时, VArray<T, $N> 类型也满足 CType 约束。

作为函数参数类型:

VArray 作为 CFunc 的参数时, CFunc 的函数签名仅可以是 CPointer<T> 类型或 VArray<T, $N> 类型。当函数签名中的参数类型为 VArray<T, $N> 时,传递的参数仍以 CPointer<T> 形式传递。

VArray 作为参数的使用示例如下:

foreign func cfoo1(a: CPointer<Int32>):Unit
foreign func cfoo2(a: VArray<Int32, $3): Unit

对应的 C 侧函数定义可以是:

void cfoo1(int *a) { ... }
void cfoo2(int a[3]) { ... }

调用 CFunc 时,需要通过 inout 修饰 VArray 类型变量:

var a: VArray<Int32, $3> = [1, 2, 3]
unsafe {
    cfoo1(inout a)
    cfoo2(inout a)
}

VArray 不允许作为 CFunc 的返回值类型。

作为 @C struct 成员:

VArray 作为 @C struct 成员时,它的内存布局与 C 侧的结构体排布一致,需要保证仓颉侧声明长度与类型也与 C 完全一致:

struct S {
    int a[2];
    int b[0];
}

在仓颉中,可以声明为如下结构体与 C 代码对应:

@C
struct S {
    var a = VArray<Int32, $2>(item: 0)
    var b = VArray<Int32, $0>(item: 0)
}

注意:C 语言中允许结构体的最后一个字段为未指明长度的数组类型,该数组被称为柔性数组(flexible array),仓颉不支持包含柔性数组的结构体的映射。

字符串

特别地,对于 C 语言中的字符串类型,仓颉中设计了一个 CString 类型来对应。为简化为 C 语言字符串的操作,CString 提供了以下成员函数:

  • init(p: CPointer<UInt8>) 通过 CPointer 构造一个 CString
  • func getChars() 获取字符串的地址,类型为 CPointer<UInt8>
  • func size(): Int64 计算该字符串的长度
  • func isEmpty(): Bool 判断该字符串的长度是否为 0,如果字符串的指针为空返回 true
  • func isNotEmpty(): Bool 判断该字符串的长度是否不为 0,如果字符串的指针为空返回 false
  • func isNull(): Bool 判断该字符串的指针是否为 null
  • func startsWith(str: CString): Bool 判断该字符串是否以 str 开头
  • func endsWith(str: CString): Bool 判断该字符串是否以 str 结尾
  • func equals(rhs: CString): Bool 判断该字符串是否与 rhs 相等
  • func equalsLower(rhs: CString): Bool 判断该字符串是否与 rhs 相等,忽略大小写
  • func subCString(start: UInt64): CString 从 start 开始截取子串,返回的子串存储在新分配的空间中
  • func subCString(start: UInt64, len: UInt64): CString 从 start 开始截取长度为 len 的子串,返回的子串存储在新分配的空间中
  • func compare(str: CString): Int32 该字符串与 str 比较,返回结果与 C 语言的 strcmp(this, str) 一样
  • func toString(): String 用该字符串构造一个新的 String 对象
  • func asResource(): CStringResource 获取 CString 的 Resource 类型

另外,将 String 类型转换为 CString 类型,可以通过调用 LibC 中的 mallocCString 接口,使用完成后需要对 CString 进行释放。

CString 的使用示例如下:

foreign func strlen(s: CString): UIntNative

main() {
    var s1 = unsafe { LibC.mallocCString("hello") }
    var s2 = unsafe { LibC.mallocCString("world") }

    let t1: Int64 = s1.size()
    let t2: Bool = s2.isEmpty()
    let t3: Bool = s1.equals(s2)
    let t4: Bool = s1.startsWith(s2)
    let t5: Int32 = s1.compare(s2)

    let length = unsafe { strlen(s1) }

    unsafe {
        LibC.free(s1)
        LibC.free(s2)
    }
}

sizeOf/alignOf

在仓颉标准库中,提供了 sizeOfalignOf 两个函数,用于获取上述 C 互操作类型的内存占用和内存对齐数值(单位:字节),函数声明如下:

public func sizeOf<T>(): UIntNative where T <: CType
public func alignOf<T>(): UIntNative where T <: CType

使用示例:

@C
struct Data {
    var a: Int64 = 0
    var b: Float32 = 0.0
}

main() {
    println(sizeOf<Data>())
    println(alignOf<Data>())
}

在 64 位机器上运行,将输出:

16
8

CType

除类型映射一节提供的与 C 侧类型进行映射的类型外,仓颉还提供了一个 CType 接口,接口本身不包含任何方法,它可以作为所有 C 互操作支持的类型的父类型,便于在泛型约束中使用。

需要注意的是:

  1. CType 接口是仓颉中的一个接口类型,它本身不满足 CType 约束;
  2. CType 接口不允许被继承、扩展;
  3. CType 接口不会突破子类型的使用限制。

CType 的使用示例如下:

func foo<T>(x: T): Unit where T <: CType {
    match (x) {
        case i32: Int32 => println(i32)
        case ptr: CPointer<Int8> => println(ptr.isNull())
        case f: CFunc<() -> Unit> => unsafe { f() }
        case _ => println("match failed")
    }
}

main() {
    var i32: Int32 = 1
    var ptr = CPointer<Int8>()
    var f: CFunc<() -> Unit> = { => println("Hello") }
    var f64 = 1.0
    foo(i32)
    foo(ptr)
    foo(f)
    foo(f64)
}

执行结果如下:

1
true
Hello
match failed

C 调用仓颉的函数

仓颉提供 CFunc 类型来对应 C 侧的函数指针类型。C 侧的函数指针可以传递到仓颉,仓颉也可以构造出对应 C 的函数指针的变量传递到 C 侧。

假设一个 C 的库 API 如下:

typedef void (*callback)(int);
void set_callback(callback cb);

对应的,在仓颉里面这个函数可以声明为:

foreign func set_callback(cb: CFunc<(Int32) -> Unit>): Unit

CFunc 类型的变量可以从 C 侧传递过来,也可以在仓颉侧构造出来。在仓颉侧构造 CFunc 类型有两种办法,一个是用 @C 修饰的函数,另外一个是标记为 CFunc 类型的闭包。

@C 修饰的函数,表明它的函数签名是满足 C 的调用规则的,定义还是写在仓颉这边。foreign 修饰的函数定义是在 C 侧的。

注意:foreign 修饰的函数与 @C 修饰的函数,这两种 CFunc 的命名不建议使用 CJ_(不区分大小写)作为前缀,否则可能与标准库及运行时等编译器内部符号出现冲突,导致未定义行为。

示例如下:

@C
func myCallback(s: Int32): Unit {
    println("handle ${s} in callback")
}

main() {
    // the argument is a function qualified by `@C`
    unsafe { set_callback(myCallback) }

    // the argument is a lambda with `CFunc` type
    let f: CFunc<(Int32) -> Unit> = { i => "handle ${i} in callback" }
    unsafe { set_callback(f) }
}

假设 C 函数编译出来的库是 "libmyfunc.so",那么需要使用 cjc -L. -lmyfunc test.cj -o test.out 编译命令,使仓颉编译器去链接这个库。最终就能生成想要的可执行程序。

另外,在编译 C 代码时,请打开 -fstack-protector-all/-fstack-protector-strong 栈保护选项,仓颉侧代码默认拥有溢出检查与栈保护功能。在引入 C 代码后,需要同步保证 unsafe 块中的溢出的安全性。

编译选项

使用 C 互操作通常需要手动链接 C 的库,仓颉编译器提供了相应的编译选项。

  • --library-path <value>, -L <value>, -L<value>:指定要链接的库文件所在的目录。

    --library-path <value> 指定的路径会被加入链接器的库文件搜索路径。另外环境变量 LIBRARY_PATH 中指定的路径也会被加入链接器的库文件搜索路径中,通过 --library-path 指定的路径会比 LIBRARY_PATH 中的路径拥有更高的优先级。

  • --library <value>, -l <value>, -l<value>:指定要链接的库文件。

    给定的库文件会被直接传给链接器,库文件名的格式应为 lib[arg].[extension]

关于仓颉编译器支持的所有编译选项,详见仓颉编译器手册一章。

示例

假设我们有一个 C 库 libpaint.so,其头文件如下:

#include <stdint.h>

typedef struct {
    int64_t x;
    int64_t y;
} Point;

typedef struct {
    int64_t x;
    int64_t y;
    int64_t r;
} Circle;

int32_t DrawPoint(const Point* point);
int32_t DrawCircle(const Circle* circle);

在仓颉代码中使用该 C 库的示例代码如下:

// main.cj
foreign {
    func DrawPoint(point: CPointer<Point>): Int32
    func DrawCircle(circle: CPointer<Circle>): Int32

    func malloc(size: UIntNative): CPointer<Int8>
    func free(ptr: CPointer<Int8>): Unit
}

@C
struct Point {
    var x: Int64 = 0
    var y: Int64 = 0
}

@C
struct Circle {
    var x: Int64 = 0
    var y: Int64 = 0
    var r: Int64 = 0
}

main() {
    let SIZE_OF_POINT: UIntNative = 16
    let SIZE_OF_CIRCLE: UIntNative = 24
    let ptr1 = unsafe { malloc(SIZE_OF_POINT) }
    let ptr2 = unsafe { malloc(SIZE_OF_CIRCLE) }

    let pPoint = CPointer<Point>(ptr1)
    let pCircle = CPointer<Circle>(ptr2)

    var point = Point()
    point.x = 10
    point.y = 20
    unsafe { pPoint.write(point) }

    var circle = Circle()
    circle.r = 1
    unsafe { pCircle.write(circle) }

    unsafe {
        DrawPoint(pPoint)
        DrawCircle(pCircle)

        free(ptr1)
        free(ptr2)
    }
}

编译仓颉代码的命令如下:

cjc -L . -l paint ./main.cj

其中编译命令中 -L . 表示链接库时从当前目录查找(假设 libpaint.so 存在于当前目录),-l paint 表示链接的库的名字,编译成功后默认生成二进制文件 main,执行二进制文件的命令如下:

LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main

与 Python 语言互操作

为了兼容强大的计算和 AI 生态,仓颉支持与 Python 语言的互操作调用。Python 的互操作通过 std 模块中的 ffi.python 库为用户提供能力。

目前 Python 互操作仅支持在 Linux 平台使用,并且仅支持仓颉编译器的 llvmgc 后端。

Python 的全局资源及使用

提供内建函数类以及全局资源

代码原型:

public class PythonBuiltins {
    ...
}
public let Python = PythonBuiltins()

Python 库提供的接口不能保证并发安全,当对 Python 进行异步调用时(系统线程 ID 不一致)会抛出 PythonException 异常。

在 Python 初始化时,GIL 全局解释器锁基于当前所在 OS 线程被锁定,如果执行的代码所在的 Cangjie 线程(包括 main 所在 Cangjie 线程)在 OS 线程上发生调度(OS 线程 ID 发生变化),Python 内部再次尝试检查 GIL 时会对线程状态进行校验,发现 GIL 状态中保存的 OS 线程 ID 与当前执行的 OS 线程 ID 不一致,此时会触发内部错误,导致程序崩溃。

**注意:**由于 Python 互操作使用到大量 Python 库的 native 代码,这部分代码在仓颉侧无法对其进行相应的栈保护。仓颉栈保护默认大小为 64KB,在对 Python C API 进行调用过程中,容易造成 native 代码超出默认栈大小,发生溢出,会触发不可预期的结果。建议用户在执行 Python 互操作相关代码前,配置仓颉默认栈大小至少为 1MB:export cjStackSize=1MB

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()
    Python.unload()
    return 0
}

提供 Python 库日志类 PythonLogger

代码原型:

public class PythonLogger <: Logger {
    mut prop level: LogLevel {...}
    public func setOutput(output: io.File): Unit {} // do nothing
    public func trace(msg: String): Unit {...}
    public func debug(msg: String): Unit {...}
    public func info(msg: String): Unit {...}
    public func warn(msg: String): Unit {...}
    public func error(msg: String): Unit {...}
    public func log(level: LogLevel, msg: String): Unit {...}
}
public let PYLOG = PythonLogger()

Logger 类的几点声明:

  • PythonLogger 实现 Logger 接口仅做打印输出以及打印等级控制,不做日志转储到 log 文件;
  • setOutput 为空实现,不支持 log 转储文件;
  • info/warn/error 等接口输出打印以对应前缀开头,其他不做区分;
  • PythonLogger 默认打印等级为 LogLevel.WARN
  • PYLOG.error(msg)log(LogLevel.ERROR, msg) 接口会抛出 PythonException 异常。

使用示例:

from std import ffi.python.*
from std import log.*

main(): Int64 {
    PYLOG.level = LogLevel.WARN // Only logs of the warn level and above are printed.
    PYLOG.info("log info")
    PYLOG.warn("log warn")
    try {
        PYLOG.error("log error")
    } catch(e: PythonException) {}

    PYLOG.log(LogLevel.INFO, "loglevel info")
    PYLOG.log(LogLevel.WARN, "loglevel warn")
    try {
        PYLOG.log(LogLevel.ERROR, "loglevel error")
    } catch(e: PythonException) {}
    return 0
}

执行结果:

WARN: log warn
ERROR: log error
WARN: loglevel warn
ERROR: loglevel error

提供 Python 库异常类 PythonException

代码原型:

public class PythonException <: Exception {
    public init() {...}
    public init(message: String) {...}
}

PythonException 有以下说明:

  • PythonException 与被继承的 Exception 除了异常前缀存在差异,其他使用无差异;
  • 当 Python 内部出现异常时,外部可以通过 try-catch 进行捕获,如果不进行捕获会打印异常堆栈并退出程序,返回值为 1。

使用示例:

from std import ffi.python.*
from std import log.*

main(): Int64 {
    try {
        Python.load("/usr/lib/", loglevel: LogLevel.INFO)
    } catch(e: PythonException) {
        print("${e}") // PythonException: "/usr/lib/" does not exist or the file path is invalid.
    }
    return 0
}

提供 Python 库的版本信息类 Version

代码原型:

public struct Version <: ToString {
    public init(major: Int64, minor: Int64, micro: Int64)
    public func getMajor(): Int64
    public func getMinor(): Int64
    public func getMicro(): Int64
    public func getVersion(): (Int64, Int64, Int64)
    public func toString(): String
}

关于 Version 类的几点声明:

  • Version 版本信息包含三个部分:major versionminor versionmicro version
  • Version 版本仅通过构造函数进行初始化,一旦定义,后续无法修改。
  • 提供 toString 接口,可以直接进行打印。
  • 提供 getVersion 接口,可以获取版本的 tuple 形式。

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()
    var version = Python.getVersion()
    print("${version}")
    var tuple_version = version.getVersion()
    Python.unload()
    return 0
}

PythonBuiltins 内建函数类

Python 库的导入和加载

代码原型:

public class PythonBuiltins {
    public func load(loglevel!: LogLevel = LogLevel.WARN): Unit
    public func load(path: String, loglevel!: LogLevel = LogLevel.WARN): Unit
    public func isLoad(): Bool
    public func unload(): Unit
}
public let Python = PythonBuiltins()

关于加载与卸载有以下几点声明:

  • load 函数使用重载的方式实现,同时支持无参加载和指定动态库路径加载,提供可选参数配置 PythonLogger 的打印等级,如果不配置,会将 PYLOG 重置为 warn 打印等级;
  • load() 函数进行了 Python 相关的准备工作,在进行 Python 互操作前必须调用,其中动态库查询方式请见:动态库的加载策略;
  • load(path: String) 函数需要用户配置动态库路径 pathpath 指定到动态库文件(如:/usr/lib/libpython3.9.so),不可以配置为目录或者非动态库文件;
  • load 函数失败时会抛出 PythonException 异常,如果程序仍然需要继续执行,请注意 try-catch
  • unload 函数在进行完 Python 互操作时调用,否则会造成相关资源泄露;
  • 加载和卸载操作仅需要调用一次,并且一一对应,多次调用仅第一次生效;
  • isload() 函数用于判断 Python 库是否被加载。

使用示例:

loadunload

from std import ffi.python.*

main(): Int64 {
    Python.load()
    Python.unload()
    Python.load("/usr/lib/libpython3.9.so")
    Python.unload()
    return 0
}

isLoad 函数:

from std import ffi.python.*

main(): Int64 {
    print("${Python.isLoad()}\n")       // false
    Python.load()
    print("${Python.isLoad()}\n")       // true
    Python.unload()
    return 0
}

动态库的加载策略

Python 库需要依赖 Python 的官方动态链接库: libpython3.x.so ,推荐版本:3.9.2,支持读取 Python3.0 以上版本。

从 Python 源码编译获取动态库:

# 在Python源码路径下:
$ ./configure --enable-shared --with-system-ffi --prefix=/usr
$ make
$ make install

Python 的动态库按照以下方式进行自动查找:

1、使用指定的环境变量:

$ export PYTHON_DYNLIB=".../libpython3.9.so"

2、如果环境变量未指定,从可执行文件的依赖中查找:

  • 需要保证可执行文件 python3 可正常执行(所在路径已添加值 PATH 环境变量中),通过对 python3 可执行文件的动态库依赖进行查询。
  • 非动态库依赖的 Python 可执行文件无法使用(源码编译未使用 --enable-shared 编译的 Python 可执行文件,不会对动态库依赖)。
$ ldd $(which python3)
    ...
    libpython3.9d.so.1.0 => /usr/local/lib/libpython3.9d.so.1.0 (0x00007f499102f000)
    ...

3、如果无法找到可执行文件依赖,尝试从系统默认动态库查询路径中查找:

["/lib", "/usr/lib", "/usr/local/lib"]
  • 所在路径下查询的动态库名称必须满足 libpythonX.Y.so 的命名方式,其中 X Y 分别为主版本号以及次版本号,并且支持的后缀有:d.som.sodm.so.so,支持的版本高于 python3.0,低于或等于 python3.10。如:
libpython3.9.so
libpython3.9d.so
libpython3.9m.so
libpython3.9dm.so

使用示例:

from std import ffi.python.*
from std import log.*

main(): Int64 {
    Python.load(loglevel: LogLevel.INFO)
    print("${Python.getVersion()}\n")
    Python.unload()
    return 0
}

可以开启 Python 的 INFO 级打印,查看 Python 库路径的搜索过程:

# Specifying .so by Using Environment Variables
$ export PYTHON_DYNLIB=/root/code/python_source_code/Python-3.9.2/libpython3.9d.so
$ cjc ./main.cj -o ./main && ./main
INFO: Try to get libpython path.
INFO: Found PYTHON_DYNLIB value: /root/code/python_source_code/Python-3.9.2/libpython3.9d.so
...

# Find dynamic libraries by executable file dependency.
INFO: Try to get libpython path.
INFO: Can't get path from environment PYTHON_DYNLIB, try to find it from executable file path.
INFO: Exec cmd: "ldd $(which python3)":
INFO:   ...
        libpython3.9d.so.1.0 => /usr/local/lib/libpython3.9d.so.1.0 (0x00007fbbb5014000)
        ...

INFO: Found lib: /usr/local/lib/libpython3.9d.so.1.0.
INFO: Found exec dependency: /usr/local/lib/libpython3.9d.so.1.0
...

# Search for the dynamic library in the system path.
$ unset PYTHON_DYNLIB
$ cjc ./main.cj -o ./main && ./main
INFO: Can't get path from environment PYTHON_DYNLIB, try to find it from executable file path.
INFO: Can't get path from executable file path, try to find it from system lib path.
INFO: Find in /lib.
INFO: Found lib: /lib/libpython3.9.so.
...

# Failed to find the dynamic library.
$ cjc ./main.cj -o ./main && ./main
INFO: Can't get path from environment PYTHON_DYNLIB, try to find it from executable file path.
INFO: Can't get path from executable file path, try to find it from system lib path.
INFO: Find in /lib.
INFO: Can't find lib in /lib.
INFO: Find in /usr/lib.
INFO: Can't find lib in /usr/lib.
INFO: Find in /usr/local/lib.
INFO: Can't find lib in /usr/local/lib.
An exception has occurred:
PythonException: Can't get path from system lib path, load exit.
         at std/ffi/python.std/ffi/python::(PythonException::)init(std/core::String)(stdlib/std/ffi/python/Python.cj:82)
         at std/ffi/python.std/ffi/python::(PythonBuiltins::)load(std/log::LogLevel)(stdlib/std/ffi/python/Python.cj:127)
         at default.default::main()(/root/code/debug/src/main.cj:5)

getVersion() 函数

函数原型:

public func getVersion(): Version

接口描述:

  • getVersion() 函数用于获取当前使用的 Python 版本;

入参返回值:

  • getVersion() 函数无参数,返回 Version 类对象;

异常情况:

  • getVersion() 函数需要保证 load 函数已被调用,否则返回的版本信息号为 0.0.0

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()
    var version = Python.getVersion()
    print("${version}")
    var tuple_version = version.getVersion()
    Python.unload()
    return 0
}

Import() 函数

函数原型:

public func Import(module: String): PyModule

入参返回值:

  • Import 函数接受一个 String 类型入参,即模块名,并且返回一个 PyModule 类型的对象;

异常情况:

  • Import 函数需要保证 load 函数已被调用,否则返回的 PyModule 类型对象不可用( isAvaliable()false );
  • 如果找不到对应的模块,仅会报错,且返回的 PyModule 类型对象不可用( isAvaliable()false )。

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()
    var sys = Python.Import("sys")
    if (sys.isAvailable()) {
        print("Import sys success\n")
    }
    // Import the test.py file in the current folder.
    var test = Python.Import("test")
    if (test.isAvailable()) {
        print("Import test success\n")
    }
    var xxxx = Python.Import("xxxx")
    if (!xxxx.isAvailable()) {
        print("Import test failed\n")
    }
    Python.unload()
    return 0
}

执行结果:

Import sys success
Import test success
Import test failed

Eval() 函数

函数原型:

public func Eval(cmd: String, module!: String = "__main__"): PyObj

接口描述:

  • Eval() 函数用于创建一个 Python 数据类型;

入参返回值:

  • Eval() 接受一个 String 类型的命令 cmd ,并返回该指令的结果的 PyObj 形式;
  • Eval() 接受一个 String 类型的指定域,默认域为 "__main__"

异常情况:

  • Eval() 接口需要保证 load 函数已被调用,否则返回的 PyObj 类型对象不可用( isAvaliable()false );
  • Eval() 如果接收的命令执行失败,Python 侧会进行报错,并且返回的 PyObj 类型对象不可用( isAvaliable()false )。

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()
    var a = Python.Eval("123")
    if (a.isAvailable()) {
        Python["print"]([a])
    }
    var b = Python.Eval("x = 123") // The expression in `Eval` needs have a return value.
    if (!b.isAvailable()) {
        print("b is unavailable.\n")
    }
    Python.unload()
    return 0
}

执行结果:

123
b is unavailable.

index [] 运算符重载

接口描述:

  • [] 函数提供了其他 Python 的内置函数调用能力;

入参返回值:

  • [] 函数入参接受 String 类型的内建函数名,返回类型为 PyObj

异常处理:

  • [] 函数需要保证 load 函数已被调用,否则返回的 PyObj 类型对象不可用( isAvaliable()false );
  • 如果指定的函数名未找到,则会报错,且返回的 PyObj 类型对象不可用( isAvaliable()false )。

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()
    if (Python["type"].isAvailable()) {
        print("find type\n")
    }
    if (!Python["type1"].isAvailable()) {
        print("cant find type1\n")
    }
    Python.unload()
    return 0
}

执行结果:

find type
WARN: Dict key "type1" not found!
cant find type1

类型映射

由于 Python 与仓颉互操作基于 C API 开发,Python 与 C 的数据类型映射统一通过 PyObject 结构体指针完成,并且具有针对不同数据类型的一系列接口。对比 C 语言,仓颉具有面向对象的编程优势,因此将 PyObject 结构体指针统一封装为父类,并且被不同的数据类型进行继承。

类型映射表

仓颉类型到 Python 类型映射

Cangjie TypePython Type
BoolPyBool
UInt8/Int8/Int16/UInt16/Int32/UInt32/Int64/UInt64PyLong
Float32/Float64PyFloat
Char/StringPyString
Array< PyObj >PyTuple
ArrayPyList
HashMapPyDict
HashSetPySet

Python 类型到仓颉类型映射

Python TypeCangjie Type
PyBoolBool
PyLongInt64/UInt64
PyFloatFloat64
PyStringString
PyTuple-
PyListArray
PyDictHashMap
PySetHashSet

Python FFI 库泛型约束的接口 PyFFIType

public interface PyFFIType { }
  • 由于部分类引入了泛型,为了对用户在泛型使用过程中进行约束,引入了抽象接口 PyFFIType
  • 该接口无抽象成员函数,其仅被 PyObjCjObj 实现或继承,该接口不允许在包外进行实现,如果用户自定义类并实现改接口,可能发生未定义行为。

PyObj

与 Python 库中的结构体 PyObject 对应,对外提供细分数据类型通用的接口,如成员变量访问、函数访问、到仓颉字符串转换等。

类原型:

public open class PyObj <: ToString & PyFFIType {
    public func isAvailable(): Bool { ... }
    public open operator func [](key: String): PyObj { ... }
    public open operator func [](key: String, value!: PyObj): Unit { ... }
    public operator func ()(): PyObj { ... }
    public operator func ()(kargs: HashMap<String, PyObj>): PyObj { ... }
    public operator func ()(args: Array<PyObj>): PyObj { ... }
    public operator func ()(args: Array<PyObj>, kargs: HashMap<String, PyObj>): PyObj { ... }
    public operator func ()(args: Array<CjObj>): PyObj { ... }
    public operator func ()(args: Array<CjObj>, kargs: HashMap<String, PyObj>): PyObj { ... }
    public operator func +(b: PyObj): PyObj { ... }
    public operator func -(b: PyObj): PyObj { ... }
    public operator func *(b: PyObj): PyObj { ... }
    public operator func /(b: PyObj): PyObj { ... }
    public operator func **(b: PyObj): PyObj { ... }
    public operator func %(b: PyObj): PyObj { ... }
    public open func toString(): String { ... }
    public func hashCode(): Int64 { ... }
    public operator func ==(right: PyObj): Bool { ... }
    public operator func !=(right: PyObj): Bool { ... }
}

关于 PyObj 类的几点说明

  • PyObj 不对外提供创建的构造函数,该类不能在包外进行继承,如果用户自定义类并实现改接口,可能发生未定义行为;

  • public func isAvailable(): Bool { ... }

    • isAvailable 接口用于判断该 PyObj 是否可用(即封装的 C 指针是否为 NULL)。
  • public open operator func [](key: String): PyObj { ... }

    • [](key) 用于访问 Python 类的成员或者模块中的成员等;
    • 如果 PyObj 本身不可用( isAvaliable()false ),将抛出异常;
    • 如果 PyObj 中不存在对应的 key ,此时由 Python 侧打印对应的错误,并返回不可用的 PyObj 类对象( isAvaliable()false )。
  • public open operator func [](key: String, value!: PyObj): Unit { ... }

    • [](key, value) 设置 Python 类、模块的成员变量值为 value
    • 如果 PyObj 本身不可用( isAvaliable()false ),将抛出异常;
    • 如果 PyObj 中不存在对应的 key ,此时由 Python 侧打印对应的错误;
    • 如果 value 值为一个不可用的对象( isAvaliable()false ),此时会将对应的 key 从模块或类中删除。
  • () 括号运算符重载,可调用对象的函数调用:

    • 如果 PyObj 本身不可用( isAvaliable()false ),将抛出异常;
    • 如果 PyObj 本身为不可调用对象,将由 Python 侧报错,且返回不可用的 PyObj 类对象( isAvaliable()false );
    • () 接受无参的函数调用;
    • ([...]) 接受大于等于 1 个参数传递,参数类型支持仓颉类型 CjObj 和 Python 数据类型 PyObj ,需要注意的是,多个参数传递时,CjObjPyObj 不可混用;
    • 如果参数中包含不可用对象( isAvaliable()false ),此时将会抛出异常,避免发生在 Python 侧出现不可预测的程序崩溃;
    • () 运算符支持 kargs ,即对应 Python 的可变命名参数设计,其通过一个 HashMap 进行传递,其 key 类型 String 配置为变量名, value 类型为 PyObj 配置为参数值。
  • 二元运算符重载:

    • + 两变量相加:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat 不支持相加,其他类型均可相互相加;
      • 高级数据类型:PyDict/PySet 与所有类型均不支持相加,PyTuple/PyList 仅能与自身相加。
    • - 两变量相减:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat/PyString 不支持相减,其他类型均可相互相减;
      • 高级数据类型:PyDict/PySet/PyTuple/PyList 与所有类型均不支持相减。
    • * 两变量相乘:

      • 基础数据类型:PyStringPyFloat/PyString 不支持相乘,其他类型均可相乘;
      • 高级数据类型:PyDict/PySet 与所有类型均不支持相乘,PyTuple/PyList 仅能与 PyLong/PyBool 相乘。
    • / 两变量相除:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat/PyString 不支持相除,其他类型均可相互相除;如果除数为 0(False 在 Python 侧解释为 0,不可作为除数),会在 Python 侧进行错误打印;
      • 高级数据类型:PyDict/PySet/PyTuple/PyList 与所有类型均不支持相除。
    • ** 指数运算:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat/PyString 不支持指数运算,其他类型均可进行指数运算;
      • 高级数据类型:PyDict/PySet/PyTuple/PyList 与所有类型均不支持指数运算。
    • % 取余:

      • 基础数据类型:PyStringPyBool/PyLong/PyFloat/PyString 不支持取余运算,其他类型均可进行取余运算;如果除数为 0(False 在 Python 侧解释为 0,不可作为除数),会在 Python 侧进行错误打印;
      • 高级数据类型:PyDict/PySet/PyTuple/PyList 与所有类型均不支持取余运算。
    • 以上所有错误情况均会进行 warn 级别打印,并且返回的 PyObj 不可用(isAvaliable()false)。

  • public open func toString(): String { ... }

    • toString 函数可以将 Python 数据类型以字符串形式返回,基础数据类型将以 Python 风格返回;
    • 如果 PyObj 本身不可用( isAvaliable()false ),将抛出异常。
  • hashCode 函数为封装的 Python hash 算法,其返回一个 Int64 的哈希值;

  • == 操作符用于判定两个 PyObj 对象是否相同,!= 与之相反,如果接口比较失败,== 返回为 false 并捕获 Python 侧报错,如果被比较的两个对象存在不可用,会抛出异常。

使用示例:

test01.py 文件:

a = 10
def function():
    print("a is", a)
def function02(b, c = 1):
    print("function02 call.")
    print("b is", b)
    print("c is", c)

同级目录下的仓颉文件 main.cj:

from std import ffi.python.*
from std import collection.*

main(): Int64 {
    Python.load()

    // Create an unavailable value.
    var a = Python.Eval("a = 10")   // SyntaxError: invalid syntax
    print("${a.isAvailable()}\n")   // false

    // Uncallable value `b` be invoked
    var b = Python.Eval("10")
    b()                           // TypeError: 'int' object is not callable

    // Import .py file.
    var test = Python.Import("test01")

    // `get []` get value of `a`.
    var p_a = test["a"]
    print("${p_a}\n")               // 10

    // `set []` set the value of a to 20.
    test["a"] = Python.Eval("20")
    test["function"]()            // a is 20

    // Call function02 with a named argument.
    test["function02"]([1], HashMap<String, PyObj>([("c", 2.toPyObj())]))

    // Set `a` in test01 to an unavailable value, and `a` will be deleted.
    test["a"] = a
    test["function"]()            // NameError: name 'a' is not defined

    Python.unload()
    0
}

CjObj 接口

接口原型及类型扩展:

public interface CjObj <: PyFFIType {
    func toPyObj(): PyObj
}
extend Bool <: CjObj {
    public func toPyObj(): PyBool { ... }
}
extend Char <: CjObj {
    public func toPyObj(): PyString { ... }
}
extend Int8 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend UInt8 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend Int16 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend UInt16 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend Int32 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend UInt32 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend Int64 <: CjObj {
    public func toPyObj(): PyLong { ... }
}
extend UInt64 <: CjObj  {
    public func toPyObj(): PyLong { ... }
}
extend Float32 <: CjObj  {
    public func toPyObj(): PyFloat { ... }
}
extend Float64 <: CjObj  {
    public func toPyObj(): PyFloat { ... }
}
extend String <: CjObj  {
    public func toPyObj(): PyString { ... }
}
extend Array<T> <: CjObj where T <: PyFFIType {
    public func toPyObj(): PyList<T> { ... }
}
extend HashMap<K, V> <: CjObj where K <: Hashable & Equatable<K> & PyFFIType {
    public func toPyObj(): PyDict<K, V> { ... }
}
extend HashSet<T> <: CjObj where T <: Hashable, T <: Equatable<T> & PyFFIType {
    public func toPyObj(): PySet<T> { ... }
}

关于 CjObj 类的几点说明

  • CjObj 接口被所有基础数据类型实现并完成 toPyObj 扩展,分别支持转换为与之对应的 Python 数据类型。

PyBoolBool 的映射

类原型:

public class PyBool <: PyObj {
    public init(bool: Bool) { ... }
    public func toCjObj(): Bool { ... }
}

关于 PyBool 类的几点说明

  • PyBool 类继承自 PyObj 类, PyBool 具有所有父类拥有的接口;
  • PyBool 仅允许用户使用仓颉的 Bool 类型进行构造;
  • toCjObj 接口将 PyBool 转换为仓颉数据类型 Bool

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyBool`.
    var a = PyBool(true)        // The type of `a` is `PyBool`.
    var b = Python.Eval("True") // The type of `b` is `PyObj` and needs to be matched to `PyBool`.
    var c = true.toPyObj()      // The type of `c` is `PyBool`, which is the same as `a`.

    print("${a}\n")
    if (a.toCjObj()) {
        print("success\n")
    }

    if (b is PyBool) {
        print("b is PyBool\n")
    }
    Python.unload()
    0
}

执行效果:

True
success
b is PyBool

PyLong 与整型的映射

类原型:

public class PyLong <: PyObj {
    public init(value: Int64) { ... }
    public init(value: UInt64) { ... }
    public init(value: Int32) { ... }
    public init(value: UInt32) { ... }
    public init(value: Int16) { ... }
    public init(value: UInt16) { ... }
    public init(value: Int8) { ... }
    public init(value: UInt8) { ... }
    public func toCjObj(): Int64 { ... }
    public func toInt64(): Int64 { ... }
    public func toUInt64(): UInt64 { ... }
}

关于 PyLong 类的几点说明

  • PyLong 类继承自 PyObj 类, PyLong 具有所有父类拥有的接口;

  • PyLong 支持来自所有仓颉整数类型的入参构造;

  • toCjObjtoInt64 接口将 PyLong 转换为 Int64 类型;

  • toUInt64 接口将 PyLong 转换为 UInt64 类型;

  • PyLong 类型向仓颉类型转换统一转换为 8 字节类型,不支持转换为更低字节类型;

  • 溢出问题:

    • toInt64 原数值(以 UInt64 赋值,赋值不报错)超出 Int64 范围判定为溢出;
    • toUInt64 原数值(以 Int64 赋值,赋值不报错)超出 UInt64 范围判定为溢出;
  • PyLong 暂不支持大数处理。

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyLong`.
    var a = PyLong(10)          // The type of `a` is `PyLong`.
    var b = Python.Eval("10")   // The type of `b` is `PyObj` and needs to be matched to `PyLong`.
    var c = 10.toPyObj()        // The type of `c` is `PyLong`, which is the same as `a`.

    print("${a}\n")
    if (a.toCjObj() == 10 && a.toUInt64() == 10) {
        print("success\n")
    }

    if (b is PyLong) {
        print("b is PyLong\n")
    }
    Python.unload()
    0
}

执行效果:

10
success
b is PyLong

PyFloat 与浮点的映射

类原型:

public class PyFloat <: PyObj {
    public init(value: Float32) { ... }
    public init(value: Float64) { ... }
    public func toCjObj(): Float64 { ... }
}

关于 PyFloat 类的几点说明

  • PyFloat 类继承自 PyObj 类, PyFloat 具有所有父类拥有的接口;
  • PyBool 支持使用仓颉 Float32/Float64 类型的数据进行构造;
  • toCjObj 接口为了保证精度,将 PyFloat 转换为仓颉数据类型 Float64

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyLong`.
    var a = PyFloat(3.14)       // The type of `a` is `PyFloat`.
    var b = Python.Eval("3.14") // The type of `b` is `PyObj` and needs to be matched to `PyFloat`.
    var c = 3.14.toPyObj()      // The type of `c` is `PyFloat`, which is the same as `a`.

    print("${a}\n")
    if (a.toCjObj() == 3.14) {
        print("success\n")
    }

    if (b is PyFloat) {
        print("b is PyFloat\n")
    }
    Python.unload()
    0
}

执行效果:

3.14
success
b is PyFloat

PyString 与字符、字符串的映射

类原型:

public class PyString <: PyObj {
    public init(value: String) { ... }
    public init(value: Char) { ... }
    public func toCjObj(): String { ... }
    public override func toString(): String { ... }
}

关于 PyString 类的几点说明

  • PyString 类继承自 PyObj 类, PyString 具有所有父类拥有的接口;
  • PyString 支持使用仓颉 Char/String 类型的数据进行构造;
  • toCjObj/toString 接口为将 PyString 转换为仓颉数据类型 String

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyString`.
    var a = PyString("hello python")        // The type of `a` is `PyString`.
    var b = Python.Eval("\"hello python\"") // The type of `b` is `PyObj` and needs to be matched to `PyString`.
    var c = "hello python".toPyObj()        // The type of `c` is `PyString`, which is the same as `a`.

    print("${a}\n")
    if (a.toCjObj() == "hello python") {
        print("success\n")
    }

    if (b is PyString) {
        print("b is PyString\n")
    }
    Python.unload()
    0
}

执行结果:

hello python
success
b is PyString

PyTuple 类型

类原型:

public class PyTuple <: PyObj {
    public init(args: Array<PyObj>) { ... }
    public operator func [](key: Int64): PyObj { ... }
    public func size(): Int64 { ... }
    public func slice(begin: Int64, end: Int64): PyTuple { ... }
}

关于 PyTuple 类的几点说明

  • PyTuple 与 Python 中的元组类型一致,即 Python 代码中使用 (...) 的变量;

  • PyTuple 类继承自 PyObj 类, PyTuple 具有所有父类拥有的接口;

  • PyTuple 支持使用仓颉 Array 来进行构造, Array 的元素类型必须为 PyObj (Python 不同数据类型均可以使用 PyObj 传递,即兼容 Tuple 中不同元素的不同数据类型),当成员中包含不可用对象时,会抛出异常;

  • [] 操作符重载:

    • 父类 PyObj[] 入参类型为 String 类型,该类对象调用时能够访问或设置 Python 元组类型内部成员变量或者函数;
    • 子类 PyTuple 支持使用 [] 对元素进行访问,如果角标 key 超出 [0, size()) 区间,会进行报错,并且返回不可用的 PyObj 对象;
    • 由于 Python 的元组为不可变对象,未进行 set [] 操作符重载。
  • size 函数用于获取 PyTuple 的长度;

  • slice 函数用于对源 PyTuple 进行剪裁,并返回一个新的 PyTuple , 如果 slice 的入参 beginend 不在 [0, size()) 区间内,仍会正常裁切。

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyTuple`.
    var a = PyTuple(["Array".toPyObj(), 'a'.toPyObj(), 1.toPyObj(), 1.1.toPyObj()])
    var b = match (Python.Eval("('Array', 'a', 1, 1.1)")) {
        case val: PyTuple => val
        case _ => throw PythonException()
    }

    // Usage of size
    println(a.size())           // 4

    // Usage of slice
    println(a.slice(1, 2))      // ('a',). This print is same as Python code `a[1: 2]`.
    println(a.slice(-1, 20))    // ('Array', 'a', 'set index 3 to String', 1.1)

    Python.unload()
    return 0
}

执行结果:

4
('a',)
('Array', 'a', 1, 1.1)

PyListArray 的映射

类原型:

public class PyList<T> <: PyObj where T <: PyFFIType {
    public init(args: Array<T>) { ... }
    public operator func [](key: Int64): PyObj { ... }
    public operator func [](key: Int64, value!: T): Unit { ... }
    public func toCjObj(): Array<PyObj> { ... }
    public func size(): Int64 { ... }
    public func insert(index: Int64, value: T): Unit { ... }
    public func append(item: T): Unit { ... }
    public func slice(begin: Int64, end: Int64): PyList<T> { ... }
}

关于 PyList 类的几点说明

  • PyList 类与 Python 中的列表类型一致,即 Python 代码中使用 [...] 的变量;

  • PyList 类继承自 PyObj 类, PyList 具有所有父类拥有的接口,该类由于对仓颉的 Array 进行映射,因此该类引入了泛型 TT 类型约束为 PyFFIType 接口的子类;

  • PyList 类可以通过仓颉的 Array 类型进行构造, Array 的成员类型同样约束为 PyFFIType 接口的子类;

  • [] 操作符重载:

    • 父类 PyObj[] 入参类型为 String 类型,该类对象调用时仅能访问或设置 Python 内部成员变量或者函数;
    • 该类中的 [] 入参类型为 Int64 ,即对应 Array 的角标值,其范围为 [0, size()),如果入参不在范围内,将进行报错,并且返回的对象为不可用;
    • [] 同样支持 get 以及 set ,并且 set 时, value 类型为 T ,如果 value 其中包含不可用的 Python 对象时,会抛出异常。
  • toCjObj 函数支持将 PyList 转换为仓颉的 Array<PyObj>,请注意,此时并不会转换为 Array<T>

  • size 函数返回 PyList 的长度;

  • insert 函数将在 index 位置插入 value ,其后元素往后移,index 不在 [0, size()) 可以正常插入,如果 value 为不可用对象,将会抛出异常;

  • append 函数将 item 追加在 PyList 最后,如果 value 为不可用对象,将会抛出异常;

  • slice 函数用于截取 [begin, end) 区间内的数据并且返回一个新的 PyList , beginend 不在 [0, size()) 也可以正常截取。

使用示例:

from std import ffi.python.*

main(): Int64 {
    Python.load()

    // Creation of `PyList`.
    var a = PyList<Int64>([1, 2, 3])
    var b = match (Python.Eval("[1, 2, 3]")) {
        case val: PyList<PyObj> => val
        case _ => throw PythonException()
    }
    var c = [1, 2, 3].toPyObj()

    // Usage of `[]`
    println(a["__add__"]([b]))   // [1, 2, 3, 1, 2, 3]
    a[1]
    b[1]
    a[2] = 13
    b[2] = 15.toPyObj()

    // Usage of `toCjObj`
    var cjArr = a.toCjObj()
    for (v in cjArr) {
        print("${v} ")          // 1 2 13
    }
    print("\n")

    // Usage of `size`
    println(a.size())           // 3

    // Usage of `insert`
    a.insert(1, 4)              // [1, 4, 2, 13]
    a.insert(-100, 5)           // [5, 1, 4, 2, 13]
    a.insert(100, 6)            // [5, 1, 4, 2, 13, 6]
    b.insert(1, 4.toPyObj())    // [1, 4, 2, 15]

    // Usage of `append`
    a.append(7)                 // [5, 1, 4, 2, 13, 6, 7]
    b.append(5.toPyObj())       // [1, 4, 2, 15, 5]

    // Usage of `slice`
    a.slice(1, 2)               // [1]
    a.slice(-100, 100)          // [5, 1, 4, 2, 13, 6, 7]
    b.slice(-100, 100)          // [1, 4, 2, 15, 5]

    return 0
}

执行结果:

[1, 2, 3, 1, 2, 3]
1 2 13
3

PyDictHashMap 的映射

类原型:

public class PyDict<K, V> <: PyObj where K <: Hashable & Equatable<K> & PyFFIType {
    public init(args: HashMap<K, V>) { ... }
    public func getItem(key: K): PyObj { ... }
    public func setItem(key: K, value: V): Unit { ... }
    public func toCjObj(): HashMap<PyObj, PyObj> { ... }
    public func contains(key: K): Bool { ... }
    public func copy(): PyDict<K, V> { ... }
    public func del(key: K): Unit { ... }
    public func size(): Int64 { ... }
    public func empty(): Unit { ... }
    public func items(): PyList<PyObj> { ... }
    public func values(): PyList<PyObj> { ... }
    public func keys(): PyList<PyObj> { ... }
}

关于 PyDict 类的几点说明

  • PyDict 与 Python 的字典类型一致,即 Python 代码中使用 { a: b } 的变量;

  • PyDict 类继承自 PyObj 类, PyDict 具有所有父类拥有的接口,该类由于对仓颉的 HashMap 进行映射,因此该类引入了泛型 <K, V> ,其中 K 类型约束为 PyFFIType 接口的子类,且可被 Hash 计算以及重载了 ==!= 运算符;

  • PyDict 接受来自仓颉类型 HashMap 的数据进行构造:

    • K 仅接受 CjObjPyObj 类型或其子类;
    • 相同的 Python 数据其值也相同,例如 Python.Eval("1")1.toPyObj()== 关系。
  • getItem 函数用于获取 PyDict 对应键值的 value ,如果键值无法找到,会进行报错并返回不可用的 PyObj ,如果配置的值 key 或为 valuePyObj 类型且不可用,此时抛出异常;;

  • setItem 函数用于配置 PyDict 对应键值的 value ,如果对应键值无法找到,会进行插入,如果配置的值 key 或为 valuePyObj 类型且不可用,此时抛出异常;

  • toCjObj 函数用于将 PyDict 转换为 HashMap<PyObj, PyObj> 类型;

  • contains 函数用于判断 key 值是否包含在当前字典中,返回类型为 Bool 型,如果接口失败,进行报错,并且返回 false;

  • copy 函数用于拷贝当前字典,并返回一个新的 PyDict<T> 类型,如果拷贝失败,返回的 PyDict 不可用;

  • del 函数用于删除对应 key 的值,如果 key 值为 PyObj 类型且不可用,会抛出异常;

  • size 函数用于返回当前字典的长度;

  • empty 函数用于清空当前字典内容;

  • items 函数用于获取一个 Python list 类型的键值对列表,可以被迭代访问;

  • values 函数用于获取一个 Python list 类型的值列表,可以被迭代访问;

  • keys 函数用于获取一个 Python list 类型的键列表,可以被迭代访问。

使用示例:

from std import ffi.python.*
from std import collection.*

main() {
    Python.load()

    // Creation of `PyDict`
    var a = PyDict(HashMap<Int64, Int64>([(1, 1), (2, 2)]))             // The key type is `CjObj`.
    var b = PyDict(HashMap<PyObj, Int64>([(Python.Eval("1"), 1), (Python.Eval("2"), 2)]))   // The key type is `PyObj`.
    var c = match (Python.Eval("{'pydict': 1, 'hashmap': 2, 3: 3, 3.1: 4}")) {
        case val: PyDict<PyObj, PyObj> => val       // Python side return `PyDict<PyObj, PyObj>`
        case _ => throw PythonException()
    }
    var d = HashMap<Int64, Int64>([(1, 1), (2, 2)]).toPyObj()

    // Usage of `getItem`
    println(a.getItem(1))               // 1
    println(b.getItem(1.toPyObj()))     // 1

    // Usage of `setItem`
    a.setItem(1, 10)
    b.setItem(1.toPyObj(), 10)
    println(a.getItem(1))               // 10
    println(b.getItem(1.toPyObj()))     // 10

    // Usage of `toCjObj`
    var hashA = a.toCjObj()
    for ((k, v) in hashA) {
        print("${k}: ${v}, ")           // 1: 10, 2: 2,
    }
    print("\n")
    var hashB = b.toCjObj()
    for ((k, v) in hashB) {
        print("${k}: ${v}, ")           // 1: 10, 2: 2,
    }
    print("\n")

    // Usage of `contains`
    println(a.contains(1))              // true
    println(a.contains(3))              // false
    println(b.contains(1.toPyObj()))    // true

    // Usage of `copy`
    println(a.copy())                   // {1: 10, 2: 2}

    // Usage of `del`
    a.del(1)                            // Delete the key-value pair (1: 1).

    // Usage of `size`
    println(a.size())                   // 1

    // Usage of `empty`
    a.empty()                           // Clear all elements in dict.

    // Usage of `items`
    for (i in b.items()) {
        print("${i} ")                  // (1, 10) (2, 2)
    }
    print("\n")

    // Usage of `values`
    for (i in b.values()) {
        print("${i} ")                  // 10 2
    }
    print("\n")

    // Usage of `keys`
    for (i in b.keys()) {
        print("${i} ")                  // 1, 2
    }
    print("\n")

    Python.unload()
}

PySetHashSet 的映射

类原型:

public class PySet<T> <: PyObj where T <: Hashable, T <: Equatable<T> & PyFFIType {
    public init(args: HashSet<T>) { ... }
    public func toCjObj(): HashSet<PyObj> { ... }
    public func contains(key: T): Bool { ... }
    public func add(key: T): Unit { ... }
    public func pop(): PyObj { ... }
    public func del(key: T): Unit { ... }
    public func size(): Int64 { ... }
    public func empty(): Unit { ... }
}

关于 PySet 类的几点说明

  • PySet 对应的是 Python 中的集合的数据类型,当元素插入时会使用 Python 内部的 hash 算法对集合元素进行排序(并不一定按照严格升序,一些方法可能因此每次运行结果不一致)。

  • PySet 类继承自 PyObj 类, PySet 具有所有父类拥有的接口,该类由于对仓颉的 HashSet 进行映射,因此该类引入了泛型 TT 类型约束为 PyFFIType 接口的子类,且可被 Hash 计算以及重载了 ==!= 运算符;

  • PySet 接受来自仓颉类型 HashMap 的数据进行构造:

    • K 仅接受 CjObjPyObj 类型或其子类;
    • 相同的 Python 数据其值也相同,例如 Python.Eval("1")1.toPyObj()== 关系。
  • toCjObj 函数用于将 PySet<T> 转为 HashSet<PyObj> 需要注意的是此处只能转为元素类型为 PyObj 类型;

  • contains 函数用于判断 key 是否在当前字典中存在, key 类型为 T

  • add 函数可以进行值插入,当 PySet 中已存在键值,则插入不生效,如果 keyPyObj 且不可用,则会抛出异常;

  • pop 函数将 PySet 中的第一个元素取出;

  • del 删除对应的键值,如果 key 不在 PySet 中,则会报错并正常退出,如果 keyPyObj 且不可用,则会抛出异常;

  • size 用于返回 PySet 的长度;

  • empty 用于清空当前 PySet

注意:调用 toCjObj 完后,所有元素将被 pop 出来,此时原 PySet 将会为空( size 为 0,原 PySet 仍然可用);

使用示例:

from std import ffi.python.*
from std import collection.*

main() {
    Python.load()

    // Creation of `PySet`
    var a = PySet<Int64>(HashSet<Int64>([1, 2, 3]))
    var b = match (Python.Eval("{'PySet', 'HashSet', 1, 1.1, True}")) {
        case val: PySet<PyObj> => val
        case _ => throw PythonException()
    }
    var c = HashSet<Int64>([1, 2, 3]).toPyObj()

    // Usage of `toCjObj`
    var cja = a.toCjObj()
    println(a.size())                           // 0

    // Usage of `contains`
    println(b.contains("PySet".toPyObj()))      // true

    // Usage of `add`
    a.add(2)
    println(a.size())   // 1
    a.add(2)            // Insert same value, do nothing.
    println(a.size())   // 1
    a.add(1)            // Insert `1`.

    // Usage of `pop`
    println(a.pop())    // 1. Pop the first element.
    println(a.size())   // 1

    // Usage of `del`
    c.del(2)
    println(c.contains(2))  // false

    // Usage of `empty`
    println(c.size())   // 2
    c.empty()
    println(c.size())   // 0

    Python.unload()
}

PySlice 类型

PySlice 类型与 Python 内建函数 slice() 的返回值用法一致,可以被用来标识一段区间及步长,可以用来作为可被切片的类型下标值来剪裁获取子串。为了方便从仓颉侧构造, PySlice 类可以与仓颉的 Range 区间类型进行互相转换,详细描述见以下。

类原型:

public class PySlice<T> <: PyObj where T <: Countable<T> & Comparable<T> & Equatable<T> & CjObj {
    public init(args: Range<T>) { ... }
    public func toCjObj(): Range<Int64> { ... }
}

关于 PySlice 的几点说明:

  • PySlice 可以使用仓颉的 Range 类型来进行构造,并且支持 Range 的语法糖,其中泛型 T 在原有 Range 约束的同时,加上约束在来自 CjObj 的实现,不支持 PyObj 类型;
  • toCjObj 函数支持将 PySlice 转为仓颉 Range 的接口,应注意此时 Range 的泛型类型为 Int64 类型的整型;
  • 如果希望把 PySlice 类型传递给 PyString/PyList/PyTuple 或者是其他可被 slicePyObj 类型,可以通过其成员函数 __getitem__ 进行传递,详情见示例。

使用示例:

from std import ffi.python.*

main() {
    Python.load()
    var range = 1..6:2

    // Create a PySlice.
    var slice1 = PySlice(range)
    var slice2 = match (Python["slice"]([0, 6, 2])) {
        case val: PySlice<Int64> => val
        case _ => throw PythonException()
    }
    var slice3 = range.toPyObj()

    // Use PySlice in PyString.
    var str = PyString("1234567")
    println(str["__getitem__"]([range]))    // 246
    println(str["__getitem__"]([slice1]))   // 246

    // Use PySlice in PyList.
    var list = PyList(["a", "b", "c", "d", "e", "f", "g", "h"])
    println(list["__getitem__"]([range]))   // ['b', 'd', 'f']
    println(list["__getitem__"]([slice1]))  // ['b', 'd', 'f']

    // Use PySlice in PyTuple.
    var tup = PyTuple(list.toCjObj())
    println(tup["__getitem__"]([range]))    // ('b', 'd', 'f')
    println(tup["__getitem__"]([slice1]))   // ('b', 'd', 'f')

    Python.unload()
    0
}

执行结果:

246
246
['b', 'd', 'f']
['b', 'd', 'f']
('b', 'd', 'f')
('b', 'd', 'f')

PyObj 的迭代器类型 PyObjIterator

代码原型:

PyObj 的扩展:

extend PyObj <: Iterable<PyObj> {
    public func iterator(): Iterator<PyObj> { ... }
}

PyObjIterator 类型:

public class PyObjIterator <: Iterator<PyObj> {
    public init(obj: PyObj) { ... }
    public func next(): Option<PyObj> { ... }
    public func iterator(): Iterator<PyObj> { ... }
}

关于 PyObjIterator 的几点说明:

  • 获取 PyObjIterator 可以通过 PyObj 的 iterator 方法获取;

  • PyObjIterator 允许被外部构造,如果提供的 PyObj 不可以被迭代或提供的 PyObj 不可用,则会直接抛出异常;

    • 可以被迭代的对象有:PyString/PyTuple/PyList/PySet/PyDict
    • 注意,直接对 PyDict 进行迭代时,迭代的为其键 key 的值。
  • next 函数用于对该迭代器进行迭代;

  • iterator 方法用于返回本身。

使用示例:

from std import ffi.python.*
from std import collection.*

main() {
    Python.load()

    // iter of PyString
    var S = PyString("Str")
    for (s in S) {
        print("${s} ")      // S t r
    }
    print("\n")

    // iter of PyTuple
    var T = PyTuple(["T".toPyObj(), "u".toPyObj(), "p".toPyObj()])
    for (t in T) {
        print("${t} ")      // T u p
    }
    print("\n")

    // iter of PyList
    var L = PyList(["L", "i", "s", "t"])
    for (l in L) {
        print("${l} ")      // L i s t
    }
    print("\n")

    // iter of PyDict
    var D = PyDict(HashMap<Int64, String>([(1, "D"), (2, "i"), (3, "c"), (4, "t")]))
    for (d in D) {
        print("${d} ")      // 1 2 3 4, dict print keys.
    }
    print("\n")

    // iter of PySet
    var Se = PySet(HashSet<Int64>([1, 2, 3]))
    for (s in Se) {
        print("${s} ")      // 1 2 3
    }
    print("\n")
    0
}

执行结果:

S t r
T u p
L i s t
1 2 3 4
1 2 3

仓颉与 Python 的注册回调

Python 互操作库支持简单的函数注册及 Python 对仓颉函数调用。

Python 回调仓颉代码通过需要通过 C 作为介质进行调用,并且使用到了 Python 的三方库: ctypes 以及 _ctypes

类型映射

基础数据对照如下表:

Cangjie TypeCTypePython Type
BoolPyCBoolPyBool
CharPyCWcharPyString
Int8PyCBytePyLong
UInt8PyCUbyte/PyCCharPyLong
Int16PyCShortPyLong
UInt16PyCUshortPyLong
Int32PyCIntPyLong
UInt32PyCUintPyLong
Int64PyCLonglongPyLong
UInt64PyCUlonglongPyLong
Float32PyCFloatPyFloat
Float64PyCDoublePyFloat
[unsupport CPointer as param] CPointer<T>PyCPointerctypes.pointer
[unsupport CString as param] CStringPyCCpointerctypes.c_char_p
[unsupport CString as param] CStringPyCWcpointerctypes.c_wchar_p
UnitPyCVoid-
  • Cangjie Type 是在仓颉侧修饰的变量类型,无特殊说明则支持传递该类型参数给 Python 代码,并且支持从 Python 传递给仓颉;
  • PyCType 为仓颉侧对应的 PyCFunc 接口配置类型,详细见类原型以及示例展示;
  • Python Type 是在仓颉侧的类型映射,无指针类型映射,不支持从仓颉侧调用 Python 带有指针的函数;
  • PyCCpointerPyCWcpointer 同样都是映射到 CString ,两者区别为 PyCCpointer 为 C 中的字符串, PyCWcpointer 仅为字符指针,即使传递多个字符,也只取第一个字符;
  • 类型不匹配将会导致不可预测的结果。

PyCFunc 类原型

PyCFunc 是基于 Python 互操作库和 Python 三方库 ctype/_ctype 的一个 PyObj 子类型,该类型可以直接传递给 Python 侧使用。 PyCFunc 为用户提供了注册仓颉的 CFunc 函数给 Python 侧,并且支持由 Python 回调 CFunc 函数的能力。

代码原型:

public enum PyCType {
    PyCBool |
    PyCChar |
    PyCWchar |
    PyCByte |
    PyCUbyte |
    PyCShort |
    PyCUshort |
    PyCInt |
    PyCUint |
    PyCLonglong |
    PyCUlonglong |
    PyCFloat |
    PyCDouble |
    PyCPointer |
    PyCCpointer |
    PyCWcpointer |
    PyCVoid
}

public class PyCFunc <: PyObj {
    public init(f: CPointer<Unit>, argsTy!: Array<PyCType> = [], retTy!: PyCType = PyCType.PyCVoid) { ... }
    public func setArgTypes(args: Array<PyCType>): PyCFunc { ... }
    public func setRetTypes(ret: PyCType): PyCFunc { ... }
}

关于类的几点说明:

  • PyCFunc 继承自 PyObj ,可以使用父类的部分接口(如果不支持的接口会相应报错);

  • init 允许外部用户构造,必须提供函数指针作为第一个参数(仓颉侧需要将 CFunc 类型转换为 CPointer<Unit> 类型),后面两个可选参数分别为入参类型的数组、返回值类型;

    这里特别声明,如果传入的指针并非函数指针会导致函数调用时程序崩溃(库层面无法进行拦截);

  • setArgTypes/setRetTypes 函数用于配置参数和返回值类型,支持的参数见 PyCType 枚举;

  • 父类中的 () 操作符,支持在仓颉侧调用该注册的 CFunc 函数;

  • 该类可以直接传递给 Python 侧使用,也可以在仓颉侧直接调用(如果该类构造时使用非函数指针,这里调用将会崩溃);

  • 该类支持类似 Js 的链式调用。

示例

1、准备仓颉的 CFunc 函数:

@C
func cfoo(a: Bool, b: Int32, c: Int64): CPointer<Unit> {
    print("cfoo called.\n")
    print("${a}, ${b}, ${c}\n")
    return CPointer<Unit>()
}

2、构造 PyCFunc 类对象:

from std import ffi.python.*

// Define the @C function.
@C
func cfoo(a: Bool, b: Int32, c: Int64): CPointer<Unit> {
    print("cfoo called.\n")
    print("${a}, ${b}, ${c}\n")
    return CPointer<Unit>()
}

main() {
    Python.load()
    /*
    Construct PyCFunc class.
    Set args type:  Bool -> PyCBool
                    Int32 -> PyCInt
                    Int64 -> PyCLonglong
                    CPointer<Unit> -> PyCPointer
    */
    var f1 = PyCFunc(unsafe {CPointer<Unit>(cfoo)},
                    argsTy: [PyCBool, PyCInt, PyCLonglong],
                    retTy: PyCPointer)

    // You also can use it by chain-call.
    var f2 = PyCFunc(unsafe {CPointer<Unit>(cfoo)})
            .setArgTypes([PyCBool, PyCInt, PyCLonglong])
            .setRetTypes(PyCPointer)([true, 1, 2])

    // Call f1
    f1([true, 1, 2])
    f1([PyBool(true), PyLong(1), PyLong(2)])

    Python.unload()
    0
}

编译仓颉文件并执行:

$ cjc ./main.cj -o ./main && ./main
cfoo called.
true, 1, 2
cfoo called.
true, 1, 2
cfoo called.
true, 1, 2

3、将函数注册给 Python 并且由 Python 进行调用:

Python 代码如下:

# File test.py

# `foo` get a function pointer and call it.
def foo(func):
    func(True, 10, 40)

对上面仓颉 main 进行修改:

main() {
    Python.load()

    var f1 = PyCFunc(unsafe {CPointer<Unit>(cfoo)},
                    argsTy: [PyCBool, PyCInt, PyCLonglong],
                    retTy: PyCPointer)

    // Import test.py
    var cfunc01 = Python.Import("test")

    // Call `foo` and transfer `f1`
    cfunc01["foo"]([f1])

    Python.unload()
    0
}

4、Python 侧传递指针到仓颉侧:

为 Python 文件增加函数:

# File test.py

# If you want transfer pointer type to Cangjie CFunc, you need import ctypes.
from ctypes import *

# `foo` get a function pointer and call it.
def foo(func):
    func(True, 10, 40)

# `fooptr` get a function pointer and call it with pointer type args.
def fooptr(func):
    a = c_int(10)
    # c_char_p will get whole symbols, but c_wchar_p only get first one symbol 'd'.
    func(pointer(a), c_char_p(b'abc'), c_wchar_p('def'))

修改仓颉代码:

from std import ffi.python.*

var x = Python.load()

// Modify the `foo` param type to pointer.
@C
func foo(a: CPointer<Int64>, b: CString, c: CString): Unit {
    print("${unsafe {a.read(0)}}, ${b.toString()}, ${c.toString()}\n")
}

main(): Int64 {

    var f1 = PyCFunc(unsafe {CPointer<Unit>(foo)},
                    argsTy: [PyCPointer, PyCCpointer, PyCWcpointer],
                    retTy: PyCVoid)

    // Import test.py
    var test = Python.Import("test")

    // Call `fooptr` and transfer `f1`
    test["fooptr"]([f1])
    return 0
}
  • 由于仓颉侧调用函数不能将指针类型传递给 Python 库,所以该处仅支持在 Python 侧进行调用。

对其编译并执行:

$ cjc ./main.cj -o ./main && ./main
10, abc, d

cjc 编译器手册

本章会介绍 cjc(仓颉编译器)的基本使用方法并列出 cjc 所提供的编译选项。

cjc 基本使用方法

想必你已经在学习仓颉的过程中尝试着使用 cjc 了,我们先来看一下 cjc 的基本使用方法,如果你想了解编译选项的相关内容,请查阅本章第二节。

cjc 的使用方式如下:

cjc [option] file...

假如我们有一个名为 hello.cj 的仓颉文件:

main() {
    println("Hello, World!")
}

我们可以使用以下命令来编译此文件:

$ cjc hello.cj

此时工作目录下会新增可执行文件 maincjc 默认会将给定源代码文件编译成可执行文件,并将可执行文件命名为 main

以上为不给任何编译选项时 cjc 的默认行为, 我们可以通过使用编译选项来控制 cjc 的行为,例如让 cjc 进行整包编译, 又或者是指定输出文件的名字。

cjc-frontend

cjc-frontend (仓颉前端编译器)会随 cjc 一起通过 Cangjie SDK 提供,该程序能够将仓颉源码编译至仓颉的中间表示 (LLVM IR)。 cjc-frontend 仅进行仓颉代码的前端编译,虽然该程序和 cjc 共享部分编译选项,但编译流程会在前端编译结束时中止。使用 cjc 时仓颉编译器会自动进行前端、后端的编译以及链接工作。该程序仅作为前端编译器的实体体现提供,除编译器开发者外,仓颉代码的编译应优先使用 cjc

cjc 编译选项

接下来会介绍一些常用的 cjc 编译选项。若某一选项同时适用于 cjc-frontend,则该选项会有 [frontend] 上标;若该选项在 cjc-frontend 下行为与 cjc 不同,选项会有额外说明。

两个横杠开头的选项为长选项,如 --xxxx

一个横杠开头的选项为短选项,如 -x

对于长选项,如果其后有参数,选项和参数之前既可以用空格隔开,也可以用等号连接,如 --xxxx <value>--xxxx=<value> 等价。

基本选项

--output-type=[exe|staticlib|dylib] [frontend]

指定输出文件的类型,exe 模式下会生成可执行文件,staticlib 模式下会生成静态库文件( .a 文件),dylib 模式下会生成 动态库文件(Linux 平台为 .so 文件、Windows 平台为 .dll 文件)。 cjc 默认为 exe 模式。

我们除了可以将 .cj 文件编译成一个可执行文件以外,也可以将其编译成一个静态或者是动态的链接库, 例如使用

$ cjc tool.cj --output-type=dylib

可以将 tool.cj 编译成一个动态链接库,在 Linux 平台 cjc 会生成一个名为 libtool.so 的动态链接库文件。

[frontend]cjc-frontend 中,编译流程仅进行至 LLVM IR,因此输出总是 .bc 文件,但是不同的 --output-type 类型仍会影响前端编译的策略。

--package, -p [frontend]

编译包,使用此选项时需要指定一个目录作为输入,目录中的源码文件需要属于同一个包。

假如我们有文件 log/printer.cj

package log

public func printLog(message: String) {
    println("[Log]: ${message}")
}

与文件 main.cj:

import log.*

main() {
    printLog("Everything is great")
}

我们可以使用

$ cjc -p log --output-type=staticlib

来编译 log 包,cjc 会在当前目录下生成一个 liblog.a 文件。 然后我们可以使用 liblog.a 文件来编译 main.cj ,编译命令如下:

$ cjc main.cj liblog.a

cjc 会将 main.cjliblog.a 一同编译成一个可执行文件 main

--module-name <value> [frontend]

指定要编译的模块的名字。

假如我们有文件 MyModule/src/log/printer.cj

package log

public func printLog(message: String) {
    println("[Log]: ${message}")
}

与文件 main.cj:

from MyModule import log.*

main() {
    printLog("Everything is great")
}

我们可以使用

$ cjc -p MyModule/src/log --module-name MyModule --output-type=staticlib -o MyModule/liblog.a

来编译 log 包并指定其模块名为 MyModulecjc 会在 MyModule 目录下生成一个 MyModule/liblog.a 文件。 然后我们可以使用 liblog.a 文件来编译导入了 log 包的 main.cj ,编译命令如下:

$ cjc main.cj MyModule/liblog.a

cjc 会将 main.cjliblog.a 一同编译成一个可执行文件 main

--output <value>, -o <value>, -o<value> [frontend]

指定输出文件的路径,编译器的输出将被写入指定的文件。

例如以下命令会将输出的可执行文件名字指定为 a.out

cjc main.cj -o a.out

--library <value>, -l <value>, -l<value>

指定要链接的库文件。

给定的库文件会被直接传给链接器,此编译选项一般需要和 --library-path <value> 配合使用。

文件名的格式应为 lib[arg].[extension]。 当我们需要链接库 a 时,我们可以使用选项 -l a,库文件搜索目录下的 liba.aliba.so (或链接 Windows 目标程序时会搜索 liba.dll) 等文件会被链接器搜索到并根据需要被链接至最终输出中。

--library-path <value>, -L <value>, -L<value>

指定要链接的库文件所在的目录。

使用 --library <value> 选项时,一般也需要使用此选项来指定要链接的库文件所在的目录。

--library-path <value> 指定的路径会被加入链接器的库文件搜索路径。 另外环境变量 LIBRARY_PATH 中指定的路径也会被加入链接器的库文件搜索路径中, 通过 --library-path 指定的路径会比 LIBRARY_PATH 中的路径拥有更高的优先级。

假如我们有从以下 c 语言源文件通过 c 语言编译器编译得到的动态库文件 libcProg.so

#include <stdio.h>

void printHello() {
    printf("Hello World\n");
}

与仓颉文件 main.cj

foreign func printHello(): Unit

main(): Int64 {
  unsafe {
    printHello()
  }
  return 0
}

我们可以使用

cjc main.cj -L . -l cProg

来编译 main.cj 并指定要链接的 cProg 库,这里 cjc 会输出一个可执行文件 main 。 执行 main 会有如下输出:

$ LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main
Hello World

请注意,由于使用了动态库文件,这里需要将库文件所在目录加入 $LD_LIBRARY_PATH 以保证 main 可以在执行时进行动态链接。

-g [frontend]

生成带有调试信息的可执行文件或者是库文件。

注:-g 只能配合 -O0 使用,如果使用更高的优化级别可能会导致调试功能出现异常。

--trimpath <value> [frontend]

移除调试信息中源文件路径信息的前缀。

编译仓颉代码时 cjc 会保存源文件( .cj 文件)的绝对路径信息以在运行时提供调试与异常信息。 使用此选项可以将指定的路径前缀从源文件路径信息中移除,cjc 的输出文件中的源文件路径信息不会包含用户指定的部分。 可以多次使用 --trimpath 指定多个不同的路径前缀;对于每个源文件路径,编译器会将第一个匹配到的前缀从路径中移除。

--coverage [frontend]

生成支持统计代码覆盖率的可执行程序。编译器会为每一个编译单元都生成一个后缀名为 gcno 的代码信息文件。在执行程序后,每一个编译单元都会得到一个后缀名为 gcda 的执行统计文件。根据这两个文件,配合使用 cjcov 工具可以生成本次执行下的代码覆盖率报表。

注:--coverage 只能配合 -O0 使用,如果使用更高的优化级别,编译器将告警并强制使用 -O0--coverage 用于编译生成可执行程序,如果用于生成静态库或者动态库,那么在最终使用该库时可能出现链接错误。

--int-overflow=[throwing|wrapping|saturating] [frontend]

指定固定精度整数运算的溢出策略,默认为 throwing

  • throwing 策略下整数运算溢出时会抛出异常
  • wrapping 策略下整数运算溢出时会回转至对应固定精度整数的另外一端
  • saturating 策略下整数运算溢出时会选择对应固定精度的极值作为结果

--diagnostic-format=[default|noColor|json] [frontend]

Windows 版本暂不支持输出带颜色渲染的错误信息。

指定错误信息的输出格式,默认为 default

  • default 错误信息默认格式输出(带颜色)
  • noColor 错误信息默认格式输出(无颜色)
  • json 错误信息json格式输出

--verbose, -V [frontend]

cjc 会打印出编译器版本信息,工具链依赖的相关信息以及编译过程中执行的命令。

--help, -h [frontend]

打印可用的编译选项。

使用此选项时编译器仅会打印编译选项相关信息,不会对任何输入文件进行编译。

--version, -v [frontend]

打印编译器版本信息。

使用此选项时编译器仅会打印版本信息,不会对任何输入文件进行编译。

--save-temps <value>

保留编译过程中生成的中间文件并保存至 <value> 路径下。

编译器会保留编译过程中生成的 .bc, .o 等中间文件。

--import-path <value> [frontend]

指定导入模块的 AST 文件的搜索路径。

假如我们已经有以下目录结构,libs/myModule 目录中包含 myModule 模块的库文件和 log 包的 AST 导出文件,

.
├── libs
|   └── myModule
|       ├── log.cjo
|       └── libmyModule.a
└── main.cj

且我们有代码如下的 main.cj 文件,

from myModule import log.printLog

main() {
    printLog("Everything is great")
}

我们可以通过使用 --import-path ./libs 来将 ./libs 加入导入模块的 AST 文件搜索路径,cjc 会使用 ./libs/myModule/log.cjo 文件来对 main.cj 文件进行语义检查与编译。

--import-path 提供与 CANGJIE_PATH 环境变量相同的功能,但通过 --import-path 设置的路径拥有更高的优先级。

--scan-dependency [frontend]

打印当前编译的包对于其他包的依赖信息。

使用示例请参考 包管理 章节。

--warn-off, -Woff <value> [frontend]

关闭编译期出现的全部或部分警告。

<value> 可以为 all 或者一个设定好的警告组别。当参数为 all 时,对于编译过程中生成的所有警告,编译器都不会打印; 当参数为其他设定好的组别时,编译器将不会打印编译过程中生成的该组别警告。

在打印每个警告时,会有一行 #note 提示该警告属于什么组别并如何关闭它, 我们可以通过 --help 打印所有可用的编译选项参数,来查阅具体的组别名称。

--warn-on, -Won <value> [frontend]

开启编译期出现的全部或部分警告。

--warn-on<value>--warn-off<value> 取值范围相同,--warn-on 通常与 --warn-off 组合使用; 比如,我们可以通过设定 -Woff all -Won <value> 来仅允许组别为 <value> 的警告被打印。

特别要注意的是,--warn-on--warn-off 在使用上顺序敏感;针对同一组别,后设定的选项会覆盖之前选项的设定, 比如,调换上例中两个编译选项的位置,使其变为 -Won <value> -Woff all,其效果将变为关闭所有警告。

--error-count-limit <value> [frontend]

限制编译器打印错误个数的上限。

参数 <value> 可以为 all 或一个非负整数。当参数为 all 时,编译器会打印编译过程中生成的所有错误; 当参数为非负整数 N 时,编译器最多会打印 N 个错误。此选项默认值为 8。

--output-dir <value> [frontend]

控制编译器生成的中间文件与最终文件的保存目录。

控制编译器生成的中间文件的保存目录,例如 .cjo 文件。当指定 --output-dir <path1> 时也指定了 --output <path2>,则最终输出会被保存至 <path1>/<path2>

同时指定此选项与 --output 选项时,--output 选项的参数必须是一个相对路径。

--static-std

静态链接仓颉库的 std 模块。

此选项仅在编译动态链接库或可执行文件时生效。cjc 默认静态链接仓颉库的 std 模块。

--dy-std

动态链接仓颉库的 std 模块。

此选项仅在编译动态链接库或可执行文件时生效。

--static-libs

静态链接仓颉库非 std 的其他模块。

此选项仅在编译动态链接库或可执行文件时生效。cjc 默认静态链接仓颉库的非 std 的其他模块。

--dy-libs

动态链接仓颉库非 std 的其他模块。

此选项仅在编译动态链接库或可执行文件时生效。

注:--static-std--dy-std 选项一起叠加使用,仅最后的那个选项生效;--static-libs--dy-libs 选项一起叠加使用,仅最后的那个选项生效;--static-std--dy-libs 选项不可一起使用,否则会报错;--dy-std--static-libs选项不可一起使用,否则会报错;--dy-std 单独使用时,会默认生效 --dy-libs 选项,并有相关告警信息提示;--dy-libs 单独使用时,会默认生效 --dy-std 选项,并有相关告警信息提示。

--stack-trace-format=[default|simple|all]

指定异常调用栈打印格式,用来控制异常抛出时的栈帧信息显示,默认为 default 格式。

异常调用栈的格式说明如下:

  • default 格式:省略泛型参数的函数名(文件名:行号)
  • simple 格式: 文件名:行号
  • all 格式:完整的函数名(文件名:行号)

--lto=[full|thin]

使能且指定 LTOLink Time Optimization 链接时优化)优化编译模式。

注:支持编译可执行文件和 LTO 模式下的静态库(.bc 文件),不支持编译生成动态库,即如果在 LTO 模式下指定 --output-type=dylib 则会编译报错。 注:Windows 平台不支持该功能。 注:当使能且指定 LTOLink Time Optimization 链接时优化)优化编译模式时,不允许同时使用如下优化编译选项:-Os-Oz

LTO 优化支持两种编译模式:

  • --lto=fullfull LTO 将所有编译模块合并到一起,在全局上进行优化,这种方式可以获得最大的优化潜力,同时也需要更长的编译时间。

  • --lto=thin:相比于 full LTOthin LTO 在多模块上使用并行优化,同时默认支持链接时增量编译,编译时间比 full LTO 短,因为失去了更多的全局信息,所以优化效果不如 full LTO

    • 通常情况下优化效果对比:full LTO > thin LTO > 常规静态链接编译。
    • 通常情况下编译时间对比:full LTO > thin LTO > 常规静态链接编译。

LTO 优化使用场景:

  1. 使用以下命令编译可执行文件

    $ cjc test.cj --lto=full
    or
    $ cjc test.cj --lto=thin
    
  2. 使用以下命令编译 LTO 模式下需要的静态库(.bc 文件),并且使用该库文件参与可执行文件编译

    # 生成的静态库为 .bc 文件
    $ cjc pkg.cj --lto=full --output-type=staticlib -o libpkg.bc
    # .bc 文件和源文件一起输入给仓颉编译器编译可执行文件
    $ cjc test.cj libpkg.bc --lto=full
    

    注:LTO 模式下的静态库(.bc 文件)输入的时候需要将该文件的路径输入仓颉编译器。

  3. LTO 模式下,静态链接标准库(--static-std & -static-libs)时,标准库的代码也会参与 LTO 优化,并静态链接到可执行文件;动态链接标准库(--dy-std & -dy-libs)时,在 LTO 模式下依旧使用标准库中的动态库参与链接。

    # 静态链接,标准库代码也参与 LTO 优化
    $ cjc test.cj --lto=full --static-std
    # 动态链接,依旧使用动态库参与链接,标准库代码不会参与 LTO 优化
    $ cjc test.cj --lto=full --dy-std
    

--pgo-instr-gen

使能插桩编译,生成携带插桩信息的可执行程序。

PGO (全称Profile-Guided Optimization)是一种常用编译优化技术,通过使用运行时 profiling 信息进一步提升程序性能。Instrumentation-based PGO 是使用插桩信息的一种 PGO 优化手段,它通常包含三个步骤:

  • 编译器对源码插桩编译,生成插桩后的可执行程序(instrumented program);
  • 运行插桩后的可执行程序,生成配置文件;
  • 编译器使用配置文件,再次对源码进行编译。
# 生成支持源码执行信息统计(携带插桩信息)的可执行程序 test
$ cjc test.cj --pgo-instr-gen -o test
# 运行可执行程序 test 结束后,生成 test.profraw 配置文件
$ LLVM_PROFILE_FILE="test.profraw" ./test

注意,运行程序时使用环境变量 LLVM_PROFILE_FILE="test%c.profraw" 可开启连续模式,即在程序崩溃或被信号杀死的情况下也能生成配置文件,可使用 llvm-profdata 工具对其进行查看分析。但是,目前 PGO 不支持连续模式下进行后续的优化步骤。

--pgo-instr-use=<.profdata>

使用指定 profdata 配置文件指导编译并生成优化后的可执行程序。

注意,--pgo-instr-use 编译选项仅支持格式为 profdata 的配置文件。可使用 llvm-profdata 工具可将 profraw 配置文件转换为 profdata 配置文件。

# 将 `profraw` 文件转换为 `profdata` 文件。
$ LD_LIBRARY_PATH=$CANGJIE_HOME/third_party/llvm/lib:$LD_LIBRARY_PATH $CANGJIE_HOME/third_party/llvm/bin/llvm-profdata merge test.profraw -o test.profdata
# 使用指定 `test.profdata` 配置文件指导编译并生成优化后的可执行程序 `testOptimized`
$ cjc test.cj --pgo-instr-use=test.profdata -o testOptimized

--target <value> [frontend]

指定编译的目标平台的 triple。

参数 <value> 一般为符合以下格式的字符串:<arch>(-<vendor>)-<os>(-<env>)。其中:

  • <arch> 表示目标平台的系统架构,例如 aarch64x86_64 等;
  • <vendor> 表示开发目标平台的厂商,常见的例如 pcapple 等,在没有明确平台厂商或厂商不重要的情况下也经常写作 unknown 或直接省略;
  • <os> 表示目标平台的操作系统,例如 linuxwin32 等;
  • <env> 表示目标平台的 ABI 或标准规范,用于更细粒度地区分同一操作系统的不同运行环境,例如 gnumusl 等。在操作系统不需要根据 <env> 进行更细地区分的时候,此项也可以省略。

目前,cjc 已支持交叉编译的本地平台和目标平台如下表所示:

本地平台 (host)目标平台 (target)
x86_64-linux-gnuaarch64-hm-gnu

在使用 --target 指定目标平台进行交叉编译之前,请准备好对应目标平台的交叉编译工具链,以及可以在本地平台上运行的、向该目标平台编译的对应 Cangjie SDK 版本。

--target-cpu <value>

注意:该选项为实验性功能,使用该功能生成的二进制有可能会存在潜在的运行时问题,请注意使用该选项的风险。此选项必须配合 --experimental 选项一同使用。

指定编译目标的 CPU 类型。

指定编译目标的 CPU 类型时,编译器在生成二进制时会尝试使用该 CPU 类型特有的扩展指令集,并尝试应用适用于该 CPU 类型的优化。为某个特定 CPU 类型生成的二进制通常会失去可移植性,该二进制可能无法在其他(拥有相同架构指令集的)CPU 上运行。

该选项支持以下可选值:

x86-64 架构:

  • generic

aarch64 架构:

  • generic
  • tsv110

generic 为通用 CPU 类型,指定 generic 时编译器会生成适用于该架构的通用指令,这样生成的二进制在操作系统和二进制本身的动态依赖一致的前提下,可以在基于该架构的各种 CPU 上运行,无关于具体的 CPU 类型。--target-cpu 选项的默认值为 generic

以上可选值为经过测试的 CPU 类型。该选项还支持以下可选值,但以下 CPU 类型未经过测试验证,请注意使用以下 CPU 类型生成的二进制可能会存在运行时问题。

x86-64 架构:

  • alderlake
  • amdfam10
  • athlon
  • athlon-4
  • athlon-fx
  • athlon-mp
  • athlon-tbird
  • athlon-xp
  • athlon64
  • athlon64-sse3
  • atom
  • barcelona
  • bdver1
  • bdver2
  • bdver3
  • bdver4
  • bonnell
  • broadwell
  • btver1
  • btver2
  • c3
  • c3-2
  • cannonlake
  • cascadelake
  • cooperlake
  • core-avx-i
  • core-avx2
  • core2
  • corei7
  • corei7-avx
  • geode
  • goldmont
  • goldmont-plus
  • haswell
  • i386
  • i486
  • i586
  • i686
  • icelake-client
  • icelake-server
  • ivybridge
  • k6
  • k6-2
  • k6-3
  • k8
  • k8-sse3
  • knl
  • knm
  • lakemont
  • nehalem
  • nocona
  • opteron
  • opteron-sse3
  • penryn
  • pentium
  • pentium-m
  • pentium-mmx
  • pentium2
  • pentium3
  • pentium3m
  • pentium4
  • pentium4m
  • pentiumpro
  • prescott
  • rocketlake
  • sandybridge
  • sapphirerapids
  • silvermont
  • skx
  • skylake
  • skylake-avx512
  • slm
  • tigerlake
  • tremont
  • westmere
  • winchip-c6
  • winchip2
  • x86-64
  • x86-64-v2
  • x86-64-v3
  • x86-64-v4
  • yonah
  • znver1
  • znver2
  • znver3

aarch64 架构:

  • a64fx
  • ampere1
  • apple-a10
  • apple-a11
  • apple-a12
  • apple-a13
  • apple-a14
  • apple-a7
  • apple-a8
  • apple-a9
  • apple-latest
  • apple-m1
  • apple-s4
  • apple-s5
  • carmel
  • cortex-a34
  • cortex-a35
  • cortex-a510
  • cortex-a53
  • cortex-a55
  • cortex-a57
  • cortex-a65
  • cortex-a65ae
  • cortex-a710
  • cortex-a72
  • cortex-a73
  • cortex-a75
  • cortex-a76
  • cortex-a76ae
  • cortex-a77
  • cortex-a78
  • cortex-a78c
  • cortex-r82
  • cortex-x1
  • cortex-x1c
  • cortex-x2
  • cyclone
  • exynos-m3
  • exynos-m4
  • exynos-m5
  • falkor
  • kryo
  • neoverse-512tvb
  • neoverse-e1
  • neoverse-n1
  • neoverse-n2
  • neoverse-v1
  • saphira
  • thunderx
  • thunderx2t99
  • thunderx3t110
  • thunderxt81
  • thunderxt83
  • thunderxt88

特殊 CPU 类型:

x86-64 以及 aarch64 架构:

  • native

native 为当前 CPU 类型,编译器会尝试识别当前机器的 CPU 类型并使用该 CPU 类型作为目标类型生成二进制。

--toolchain <value>, -B <value>, -B<value>

指定编译工具链中,二进制文件存放的路径。

二进制文件包括:编译器,链接器,工具链提供的 C 运行时目标文件(例如 crt0.o, crti.o)等。

我们在准备好编译工具链后,可以在将其存放在一个自定义路径,然后通过 --toolchain <value> 向编译器传入该路径,即可让编译器调用到该路径下的二进制文件进行交叉编译。

--sysroot <value>

指定编译工具链的根目录路径。

对于目录结构固定的交叉编译工具链,如果我们没有指定该目录以外的二进制和动态库、静态库文件路径的需求,可以直接使用 --sysroot <value> 向编译器传入工具链的根目录路径,编译器会根据目标平台种类分析对应的目录结构,自动搜索所需的二进制文件和动态库、静态库文件。使用该选项后,我们无需再指定 --toolchain--library-path 参数。

假如我们向 triplearch-os-env 的平台进行交叉编译,同时我们的交叉编译工具链有以下目录结构:

/usr/sdk/arch-os-env
├── bin
|   ├── arch-os-env-gcc (交叉编译器)
|   ├── arch-os-env-ld  (链接器)
|   └── ...
├── lib
|   ├── crt1.o          (C 运行时目标文件)
|   ├── crti.o
|   ├── crtn.o
|   ├── libc.so         (动态库)
|   ├── libm.so
|   └── ...
└── ...

我们有仓颉源文件 hello.cj ,那么我们可以使用以下命令,将 hello.cj 交叉编译至 arch-os-env 平台:

cjc --target=arch-os-env --toolchain /usr/sdk/arch-os-env/bin --toolchain /usr/sdk/arch-os-env/lib --library-path /usr/sdk/arch-os-env/lib hello.cj -o hello

也可以使用简写的参数:

cjc --target=arch-os-env -B/usr/sdk/arch-os-env/bin -B/usr/sdk/arch-os-env/lib -L/usr/sdk/arch-os-env/lib hello.cj -o hello

如果该工具链的目录符合惯例的目录结构,也可以无需使用 --toolchain--library-path 参数,而使用以下的命令:

cjc --target=arch-os-env --sysroot /usr/sdk/arch-os-env hello.cj -o hello

--strip-all, -s

编译可执行文件或动态库时,指定该选项以删除输出文件中的符号表。

--discard-eh-frame

编译可执行文件或动态库时,指定该选项可以删除 eh_frame 段以及 eh_frame_hdr 段中的部分信息(涉及到 crt 的相关信息不作处理),减少可执行文件或动态库的大小,但会影响调试信息。

指定链接器选项。

cjc 会将该选项的参数透传给链接器。可用的参数会因(系统或指定的)链接器的不同而不同。 可以多次使用 --link-options 指定多个链接器选项。

单元测试选项

--test [frontend]

unittest 测试框架提供的入口,由宏自动生成,当使用 cjc --test 选项编译时,程序入口不再是 main,而是 test_entry。测试使用方法详见 unittest 库使用文档。 对于 pkgc 目录下仓颉文件 a.cj:

from std import unittest.*
from std import unittest.testmacro.*

@Test
public class TestA {
    @TestCase
    public func case1(): Unit {
        print("case1\n")
    }
}

我们可以在 pkgc 目录下使用:

cjc a.cj --test

来编译 a.cj ,执行 main 会有如下输出:

注:不保证用例每次执行的用时都相同

case1
--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 29710 ns, Result:
    TCS: TestA, time elapsed: 26881 ns, RESULT:
    [ PASSED ] CASE: case1 (16747 ns)
Summary: TOTAL: 1
    PASSED: 1, SKIPPED: 0, ERROR: 0
    FAILED: 0
--------------------------------------------------------------------------------------------------

对于如下目录结构 :

application
├── src
├── pkgc
|   ├── a1.cj
|   └── a2.cj
└── a3.cj

我们可以在 application目录下使用 -p 编译选项配合编译整包:

cjc pkgc --test -p

来编译整个 pkgc 包下的测试用例 a1.cja2.cj

/*a1.cj*/
package a

from std import unittest.*
from std import unittest.testmacro.*

@Test
public class TestA {
    @TestCase
    public func caseA(): Unit {
        print("case1\n")
    }
}
/*a2.cj*/
package a

from std import unittest.*
from std import unittest.testmacro.*

@Test
public class TestB {
    @TestCase
    public func caseB(): Unit {
        throw IndexOutOfBoundsException()
    }
}

执行 main 会有如下输出(输出信息仅供参考):

case1
--------------------------------------------------------------------------------------------------
TP: a, time elapsed: 367800 ns, Result:
    TCS: TestA, time elapsed: 16802 ns, RESULT:
    [ PASSED ] CASE: caseA (14490 ns)
    TCS: TestB, time elapsed: 347754 ns, RESULT:
    [ ERROR  ] CASE: caseB (345453 ns)
    REASON: An exception has occurred:IndexOutOfBoundsException
        at std/core.Exception::init()(std/core/exception.cj:23)
        at std/core.IndexOutOfBoundsException::init()(std/core/index_out_of_bounds_exception.cj:9)
        at a.TestB::caseB()(/home/houle/cjtest/application/pkgc/a2.cj:7)
        at a.lambda.1()(/home/houle/cjtest/application/pkgc/a2.cj:7)
        at std/unittest.TestCases::execute()(std/unittest/test_case.cj:92)
        at std/unittest.UT::run(std/unittest::UTestRunner)(std/unittest/test_runner.cj:194)
        at std/unittest.UTestRunner::doRun()(std/unittest/test_runner.cj:78)
        at std/unittest.UT::run(std/unittest::UTestRunner)(std/unittest/test_runner.cj:200)
        at std/unittest.UTestRunner::doRun()(std/unittest/test_runner.cj:78)
        at std/unittest.UT::run(std/unittest::UTestRunner)(std/unittest/test_runner.cj:200)
        at std/unittest.UTestRunner::doRun()(std/unittest/test_runner.cj:75)
        at std/unittest.entryMain(std/unittest::TestPackage)(std/unittest/entry_main.cj:11)
Summary: TOTAL: 2
    PASSED: 1, SKIPPED: 0, ERROR: 1
    FAILED: 0
--------------------------------------------------------------------------------------------------

--mock <on|off|runtime-error> [frontend]

如果传递了 on ,则该包将使能 mock 编译,该选项允许在测试用例中 mock 该包中的类。off 是一种显式禁用 mock 的方法。

请注意,在测试模式下(当使能 --test )自动启用对此包的 mock 支持,不需要显式传递 --mock 选项。

runtime-error 仅在测试模式下可用(当使能 --test 时),它允许编译带有 mock 代码的包,但不在编译器中做任何 mock 相关的处理(这些处理可能会造成一些开销并影响测试的运行时性能)。这对于带有 mock 代码用例进行基准测试时可能是有用的。使用此编译选项时,避免编译带有 mock 代码的用例并运行测试,否则将抛出运行时异常。

宏选项

cjc 支持以下宏选项,关于宏的更多内容请参阅 元编程 章节。

--compile-macro [frontend]

编译宏定义文件,生成默认的宏定义动态库文件。

--debug-macro [frontend]

生成宏展开后的仓颉代码文件。该选项可用于调试宏展开功能。

--parallel-macro-expansion [frontend]

开启宏展开并行。该选项可用于缩短宏展开编译时间。

条件编译选项

cjc 支持以下条件编译选项,关于条件编译的更多内容请参阅 条件编译 章节。

--conditional-compilation-config <value> [frontend]

指定自定义编译条件。

并行编译选项

cjc 支持以下并行编译选项以获得更高的编译效率。

--jobs <value>, -j <value> [frontend]

设置并行编译时所允许的最大并行数。其中 value 必须是一个合理的正整数,当 value 大于硬件支持最大并行能力时,编译器将会按基于硬件支持并行能力计算出的默认设置执行并行编译。

如果该编译选项未设置,编译器将会按基于硬件支持并行能力计算出的默认设置执行并行编译。

注意,--jobs 1表示完全使用串行方式进行编译。

---aggressive-parallel-compile, --apc [frontend]

开启此选项后,编译器会采用更加激进的策略(可能会对优化造成影响)执行并行编译,以便获得更高的编译效率。

注意,--aggressive-parallel-compile选项在一些场景下会由编译器强制开启/关闭。

在以下场景中--aggressive-parallel-compile选项将由编译器强制开启:

  • -O0
  • -g

在以下场景中--aggressive-parallel-compile选项将由编译器强制关闭:

  • --fobf-string
  • --fobf-const
  • --fobf-layout
  • --fobf-cf-flatten
  • --fobf-cf-bogus
  • --lto
  • --coverage
  • 交叉编译

优化选项

chircjc 编译器前端的一种中间表示,基于该中间表示 cjc 提供以下优化选项:

--fchir-constant-propagation [frontend]

开启 chir 常量传播优化。

--fno-chir-constant-propagation [frontend]

关闭 chir 常量传播优化。

--fchir-function-inlining [frontend]

开启 chir 函数内联优化。

--fno-chir-function-inlining [frontend]

关闭 chir 函数内联优化。

--fchir-devirtualization [frontend]

开启 chir 去虚函数调用优化。

--fno-chir-devirtualization [frontend]

关闭 chir 去虚函数调用优化。

除了基于 chir 的优化外,cjc 还提供以下优化选项。

--fast-math [frontend]

开启此选项后,编译器会对浮点数作一些激进且有可能损失精度的假设,以便优化浮点数运算。

-O<N> [frontend]

使用参数指定的代码优化级别。

指定越高的优化级别,编译器会越多地进行代码优化以生成更高效的程序,同时也可能会需要更长的编译时间。

cjc 默认使用 O0 级别的代码优化。当前 cjc 支持如下优化级别:O0、O1、O2、Os、Oz。

当优化等级等于 2 时,cjc 除了进行对应的优化外,还会开启以下选项:

  • --fchir-constant-propagation
  • --fchir-function-inlining
  • --fchir-devirtualization

当优化等级等于 s 时, cjc除了进行 O2 级别优化外,将针对 code size 进行优化。

当优化等级等于 z 时, cjc除了进行 Os 级别优化外,还将进一步缩减 code size 大小。

注:当优化等级等于 s 或 z 时,不允许同时使用链接时优化编译选项 --lto=[full|thin]

-O [frontend]

使用 O1 级别的代码优化,等价于 -O1

代码混淆选项

Windows 版本暂不支持本节介绍的代码混淆选项。

cjc 支持代码混淆功能以提供对代码的额外安全保护,代码混淆功能默认不开启。

cjc 支持以下代码混淆选项:

--fobf-string

开启字符串混淆。

混淆代码中出现的字符串常量,攻击者无法静态直接读取二进制程序中的字符串数据。

--fno-obf-string

关闭字符串混淆。

--fobf-const

开启常量混淆。

混淆代码中使用的数值常量,将的数值运算指令替换成等效的、更复杂的数值运算指令序列。

--fno-obf-const

关闭常量混淆。

--fobf-layout

开启外形混淆。

混淆代码中的符号(包括函数名和全局变量名)以及函数排布顺序。注意,--fobf-layout不会混淆对外导出的符号,否则可能会导致链接错误。

使用该选项编译代码后 cjc 会在当前(或 --output-dir 指定的)目录生成副产物 obf_global_fileobf_func_file 文件。obf_global_fileobf_func_file 中包含混淆的符号映射信息。使用符号映射信息我们可以进行解混淆或导入被混淆的包。

--fno-obf-layout

关闭外形混淆。

--fobf-cf-flatten

开启控制流平坦化混淆。

混淆代码中既存的控制流,使其转移逻辑变得复杂。

--fno-obf-cf-flatten

关闭控制流平坦化混淆。

--fobf-cf-bogus

开启虚假控制流混淆。

在代码中插入虚假的控制流,使代码逻辑变得复杂。

--fno-obf-cf-bogus

关闭虚假控制流混淆。

--fobf-all

开启所有混淆功能。

指定该选项等同于同时指定以下选项:

  • --fobf-string
  • --fobf-const
  • --fobf-layout
  • --fobf-cf-flatten
  • --fobf-cf-bogus

--obf-config <file>

指定代码混淆配置文件路径。

在配置文件中我们可以禁止混淆工具对某些函数或者符号进行混淆。 配置文件的具体格式如下:

obf_func1 name1
obf_func2 name2
...

其中 obf_func 可以是具体的混淆功能:

  • control-flow-bogus
  • control-flow-flatten
  • obf-const
  • obf-layout

也可以用 * 来匹配所有混淆功能。 name 是符号名称,包含包名、类名、函数名等,比如 pro0/zoo::(Triangle::)init(Int64, Int64) ,其中每个字段都可以用 * 匹配所有,比如 pro0/zoo::(Triangle::)*(Int64, Int64) 会匹配 pro0/zoo 包下 Triangle 类中所有形参为 (Int64, Int64) 的成员函数。

以下是一份合法的配置文件示例:

obf-const pro0_zoo_global_init
obf-const for_keeping_some_types
* pro0/zoo::(Triangle::)init(Int64, Int64)
control-flow-flatten pro0/zoo::(Rectangle::)*(Int64)
control-flow-flatten pro0/zoo::(*)print(Int64, Int64)
* */*::g1()
obf-const test/*::g2()
obf-layout test/koo::g3()
obf-layout test/default::*()

--obf-level <value>

指定混淆功能强度级别。

可指定 1-9 强度级别。默认强度级别为 5。级别数字越大,强度则越高,该选项会影响输出文件的大小以及执行开销。

--obf-seed <value>

指定混淆算法的随机数种子。

通过指定混淆算法的随机数种子,我们可以使同一份仓颉代码在不同构建时有不同的混淆结果。默认场景下,对于同一份仓颉代码,在每次混淆后都拥有相同的混淆结果。

自动微分选项

cjc 支持以下自动微分选项,关于自动微分的更多内容请参阅第十四章。

--enable-auto-differentiation, --enable-ad [frontend]

开启自动微分特性。

安全编译选项

Windows 版本暂不支持安全编译选项。

cjc 默认生成地址无关代码,在编译可执行文件时默认生成地址无关可执行文件。

cjc 支持通过 --link-options 设置以下安全相关的链接器选项:

设置线程栈不可执行。

设置 GOT 表重定位只读。

设置立即绑定。

代码覆盖率插桩选项

Windows 版本暂不支持代码覆盖率插桩选项。

仓颉支持对代码覆盖率插桩(SanitizerCoverage,以下简称 SanCov),提供与 LLVM 的 SanitizerCoverage 一致的接口,编译器在函数级或 BasicBlock 级插入覆盖率反馈函数,用户只需要实现约定好的回调函数即可在运行过程中感知程序运行状态。

仓颉提供的 SanCov 功能以 package 为单位,即整个 package 只有全部插桩和全部不插桩两种情况。

--sanitizer-coverage-level=0/1/2

插桩等级:0 表示不插桩;1 表示函数级插桩,只在函数入口处插入回调函数;2 表示 BasicBlock 级插桩,在各个 BasicBlock 处插入回调函数。

如不指定,默认值为 2。

该编译选项只影响 --sanitizer-coverage-trace-pc-guard--sanitizer-coverage-inline-8bit-counters--sanitizer-coverage-inline-bool-flag 的插桩等级。

--sanitizer-coverage-trace-pc-guard

开启该选项,会在每个 Edge 插入函数调用 __sanitizer_cov_trace_pc_guard(uint32_t *guard_variable),受 sanitizer-coverage-level 影响。

注意,该功能存在与 gcc/llvm 实现不一致的地方:不会在 constructor 插入 void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop)而是在 package 初始化阶段插入函数调用 uint32_t *__cj_sancov_pc_guard_ctor(uint64_t edgeCount)

__cj_sancov_pc_guard_ctor 回调函数需要开发者自行实现,开启 SanCov 的 package 会尽可能早地调用该回调函数,入参是该 Package 的 Edge 个数,返回值是通常是 calloc 创建的内存区域。

如果需要调用 __sanitizer_cov_trace_pc_guard_init,建议在 __cj_sancov_pc_guard_ctor 中调用,使用动态创建的缓冲区计算该函数的入参和返回值。

一个标准的__cj_sancov_pc_guard_ctor参考实现如下:

uint32_t *__cj_sancov_pc_guard_ctor(uint64_t edgeCount) {
    uint32_t *p = (uint32_t *) calloc(edgeCount, sizeof(uint32_t));
    __sanitizer_cov_trace_pc_guard_init(p, p + edgeCount);
    return p;
}

--sanitizer-coverage-inline-8bit-counters

开启该选项后,会在每个 Edge 插入一个累加器,每经历过一次,该累加器加一,受 sanitizer-coverage-level 影响。

注意,该功能存在与 gcc/llvm 实现不一致的地方:不会在 constructor 插入 void __sanitizer_cov_8bit_counters_init(char *start, char *stop)而是在 package 初始化阶段插入函数调用 uint8_t *__cj_sancov_8bit_counters_ctor(uint64_t edgeCount)

__cj_sancov_pc_guard_ctor 回调函数需要开发者自行实现,开启 SanCov 的 package 会尽可能早地调用该回调函数,入参是该 Package 的 Edge 个数,返回值是通常是 calloc 创建的内存区域。

如果需要调用 __sanitizer_cov_8bit_counters_init,建议在 __cj_sancov_8bit_counters_ctor 中调用,使用动态创建的缓冲区计算该函数的入参和返回值。

一个标准的__cj_sancov_8bit_counters_ctor参考实现如下:

uint8_t *__cj_sancov_8bit_counters_ctor(uint64_t edgeCount) {
    uint8_t *p = (uint8_t *) calloc(edgeCount, sizeof(uint8_t));
    __sanitizer_cov_8bit_counters_init(p, p + edgeCount);
    return p;
}

--sanitizer-coverage-inline-bool-flag

开启该选项后,会在每个 Edge 插入布尔值,经历过的 Edge 对应的布尔值会被设置为 True,受 sanitizer-coverage-level 影响。

注意,该功能存在与 gcc/llvm 实现不一致的地方:不会在 constructor 插入 void __sanitizer_cov_bool_flag_init(bool *start, bool *stop)而是在 package 初始化阶段插入函数调用 bool *__cj_sancov_bool_flag_ctor(uint64_t edgeCount)

__cj_sancov_bool_flag_ctor 回调函数需要开发者自行实现,开启 SanCov 的 package 会尽可能早地调用该回调函数,入参是该 Package 的 Edge 个数,返回值是通常是 calloc 创建的内存区域。

如果需要调用 __sanitizer_cov_bool_flag_init,建议在 __cj_sancov_bool_flag_ctor 中调用,使用动态创建的缓冲区计算该函数的入参和返回值。

一个标准的__cj_sancov_8bit_counters_ctor参考实现如下:

bool *__cj_sancov_bool_flag_ctor(uint64_t edgeCount) {
    bool *p = (bool *) calloc(edgeCount, sizeof(bool));
    __sanitizer_cov_bool_flag_init(p, p + edgeCount);
    return p;
}

--sanitizer-coverage-pc-table

该编译选项用于提供插桩点和源码之间的对应关系,当前只提供精确到函数级的对应关系。需要与 --sanitizer-coverage-trace-pc-guard--sanitizer-coverage-inline-8bit-counters--sanitizer-coverage-inline-bool-flag 共用,至少需要开启其中一项,可以同时开启多项。

注意,该功能存在与 gcc/llvm 实现不一致的地方:不会在 constructor 插入 void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg, const uintptr_t *pcs_end);而是在 package 初始化阶段插入函数调用 void __cj_sancov_pcs_init(int8_t *packageName, uint64_t n, int8_t **funcNameTable, int8_t **fileNameTable, uint64_t *lineNumberTable),各入参含义如下:

  • int8_t *packageName: 字符串,表示包名(插桩用 c 风格的 int8 数组作为入参来表达字符串,下同)。
  • uint64_t n: 共有 n 个函数被插桩。
  • int8_t **funcNameTable: 长度为 n 的字符串数组,第 i 个插桩点对应的函数名为 funcNameTable[i]。
  • int8_t **fileNameTable: 长度为 n 的字符串数组,第 i 个插桩点对应的文件名为 fileNameTable[i]。
  • uint64_t *lineNumberTable: 长度为 n 的 uint64 数组,第 i 个插桩点对应的行号为 lineNumberTable[i]。

如果需要调用 __sanitizer_cov_pcs_init,需要自行完成仓颉 pc-table 到 C 语言 pc-table 的转化。

--sanitizer-coverage-stack-depth

开启该编译选项后,由于仓颉无法获取 SP 指针的值,只能在每个函数入口处插入调用 __updateSancovStackDepth,在 C 侧实现该函数即可获得 SP 指针。

一个标准的 updateSancovStackDepth 实现如下:

thread_local void* __sancov_lowest_stack;

void __updateSancovStackDepth()
{
    register void* sp = __builtin_frame_address(0);
    if (sp < __sancov_lowest_stack) {
        __sancov_lowest_stack = sp;
    }
}

--sanitizer-coverage-trace-compares

开启该选项后,会在所有的 compare 指令和 match 指令调用前插入函数回调函数,具体列表如下,与 LLVM 系的 API 功能一致。参考 Tracing data flow

void __sanitizer_cov_trace_cmp1(uint8_t Arg1, uint8_t Arg2);
void __sanitizer_cov_trace_const_cmp1(uint8_t Arg1, uint8_t Arg2);
void __sanitizer_cov_trace_cmp2(uint16_t Arg1, uint16_t Arg2);
void __sanitizer_cov_trace_const_cmp2(uint16_t Arg1, uint16_t Arg2);
void __sanitizer_cov_trace_cmp4(uint32_t Arg1, uint32_t Arg2);
void __sanitizer_cov_trace_const_cmp4(uint32_t Arg1, uint32_t Arg2);
void __sanitizer_cov_trace_cmp8(uint64_t Arg1, uint64_t Arg2);
void __sanitizer_cov_trace_const_cmp8(uint64_t Arg1, uint64_t Arg2);
void __sanitizer_cov_trace_switch(uint64_t Val, uint64_t *Cases);

--sanitizer-coverage-trace-memcmp

该编译选项用于在 String 、 Array 等比较中反馈前缀比较信息。开启该选项后,会对 String 和 Array 的比较函数前插入函数回调函数。具体对于以下对各 String 和 Array 的 API,分别插入对应桩函数:

  • String==: __sanitizer_weak_hook_memcmp
  • String.startsWith: __sanitizer_weak_hook_memcmp
  • String.endsWith: __sanitizer_weak_hook_memcmp
  • String.indexOf: __sanitizer_weak_hook_strstr
  • String.replace: __sanitizer_weak_hook_strstr
  • String.contains: __sanitizer_weak_hook_strstr
  • CString==: __sanitizer_weak_hook_strcmp
  • CString.startswith: __sanitizer_weak_hook_memcmp
  • CString.endswith: __sanitizer_weak_hook_strncmp
  • CString.compare: __sanitizer_weak_hook_strcmp
  • CString.equalsLower: __sanitizer_weak_hook_strcasecmp
  • Array==: __sanitizer_weak_hook_memcmp
  • ArrayList==: __sanitizer_weak_hook_memcmp

CFFI 内存安全检测

Windows 版本暂不支持本节介绍的 CFFI 内存安全检测。

cjc 支持 CFFI 内存安全检测功能以提供检测仓颉代码与 C 代码互操作过程中,可能出现的时间、空间内存安全问题。

例如:

  1. 仓颉 unsafe 代码导致的空间安全问题:堆( acquireRawData 接口)、栈、全局变量( inout 语义)溢出,例如:

    unsafe {
        let array = Array<UInt8>(4, item: 0)
        let cp = acquireArrayRawData(array)
        cp.pointer.read(5)  // 仓颉堆溢出
    }
    
  2. 仓颉 unsafe 代码导致的时间安全问题:释放后使用、返回后使用、双重释放,例如:

    unsafe {
        let array = Array<UInt8>(4, item: 0)
        let cp = acquireArrayRawData(array)
        releaseArrayRawData(cp)
        array = Array<UInt8>(1, item: 0)
        ...... // GC 回收第一个 array
        cp.pointer.read()  // 释放后使用
    }
    

--sanitize=address

使能内存安全检测功能。

仓颉的内存安全检测功能是利用 Address Sanitizer 实现。支持仓颉作为主程序和 C 代码作为主程序。

用户使能该功能时,C 代码编译时建议使能 ASan 检测(通过 -fsanitize=address 开启)以获得更精准的检查。

该功能依赖 ASan 运行库,该包由仓颉或者用户系统提供,优先级如下:

  1. 仓颉提供的 ASan 运行库 $CANGJIE_HOME/lib/<system>_<arch>_llvm/libclang-rt_asan.a

  2. 用户通过 -L ,环境变量 LIBRARY_PATH--toolchain/-B 指定的路径下搜索 ASan 运行库 clang-rt.asan-<arch>.a

  3. 使用 gnu 提供的独立 ASan 运行库 libasan.so

注意:

  1. 当用户使用 C 代码作为主程序时,还需要链接 $CANGJIE_HOME/lib/<system>_<arch>_llvm/cjasan_options.o 以防止内存泄露漏报的问题。

  2. 当用户使用 ASan 的动态链接库时,需要使用 LD_PRELOADverify_asan_link_order=0 保证 ASan 运行库是第一个被加载的,以保证 ASan 的检测正确地运行。

  3. 建议用户使用 ASAN_SYMBOLIZER_PATH=$CANGJIE_HOME/third_party/llvm/bin/llvm-symbolizer 指定 ASan 运行库使用仓颉提供的 symbolizer,以保证函数符号的正常解析。

局限性

  1. 不支持仓颉宏选项。
  2. 在协程 A 调用 releaseArrayRawData 接口后,若协程 B 仍持有该段数据的引用时,协程 A 再访问该段数据会产生漏报
  3. 仓颉内存释放后使用的场景中,若该片内存再次被分配且设置为可访问后,原来的野指针(产生释放后使用的指针)访问该片内存则会产生漏报
  4. 仓颉检测内存泄露中,若 C 内存在仓颉类中初始化,但没有在仓颉 class 的 ~init() 中释放的场景中,会产生漏报
  5. 从仓颉堆内存回收到程序结束退出这一段时期程序产生的内存泄露会漏报

数据竞争检测

Windows 版本暂不支持本节介绍的数据竞争检测。

cjc 支持 数据竞争检测功能,用来检测并发的仓颉程序中存在的数据竞争缺陷。

例如:

main() {
    let x: Box<Int64> = Box<Int64>(0)

    let fut = spawn { =>
        x.value = 11 // data race
    }

    x.value = 10 // data race
    fut.get()
}

上述代码中同时有两个协程修改 x 的值,而协程之间没有通过锁等同步机制进行保护,因此存在数据竞争的风险。数据竞争检测功能可以帮助用户发现和定位程序中该类数据竞争缺陷,方便用户修补该类代码缺陷。

--sanitize=thread

使能数据竞争检测功能。 开启该编译选项后,在程序运行时如果发生数据竞争,程序会打印出发生数据竞争的代码位置、调用栈和相关的协程信息。

仓颉的数据竞争检测功能是利用 Thread Sanitizer 实现,但是对原始 Thread Sanitizer 进行了修改以适配仓颉的协程并发模型,因此只能检测仓颉侧代码访问仓颉对象时产生的数据竞争,无法检测 C 侧的数据竞争以及 C 侧代码访问仓颉对象时产生的数据竞争。

注意:当用户使用 C 代码作为主程序调用仓颉库时,如果仓颉库开启了 --sanitize=thread 选项,则 C 代码在编译时需要添加以下参数来链接相关依赖:

-L $CANGJIE_HOME/lib/<system>_<arch>_llvm -lcangjie-runtime_tsan -lgcc_s -Wl,--whole-archive -lpthread -l:libclang_rt-tsan.a -Wl,--no-whole-archive -Wl,--dynamic-list=$CANGJIE_HOME/lib/<system>_<arch>_llvm/libclang_rt-tsan.a.syms

其他功能

编译器报错信息显示颜色

对于 Windows 版本的仓颉编译器,只有运行于 Windows10 version 1511(Build 10586) 或更高版本的系统,编译器报错信息才显示颜色,否则不显示颜色。

设置 build-id

通过 --link-options "--build-id=<arg>"1 可以透传链接器选项以设置 build-id。

编译 Windows 目标时不支持此功能。

设置 rpath

通过 --link-options "-rpath=<arg>"1 可以透传链接器选项以设置 rpath。

编译 Windows 目标时不支持此功能。

1 链接器透传选项可能会因为链接器的不同而不同,具体支持的选项请查阅链接器文档。

增量编译

通过 --incremental-compile[frontend]开启增量编译。开启后,cjc会在编译时根据前次编译的缓存文件加快此次编译的速度。

cjc 用到的环境变量

这里介绍一些仓颉编译器在编译代码的过程中可能使用到的环境变量。

TMPDIR 或者 TMP

仓颉编译器会将编译过程中产生的临时文件放置到临时目录中。默认情况下 Linux 操作系统会放在 /tmp 目录下;Windows 操作系统会放在 C:\Windows\Temp 目录下。仓颉编译器也支持自行设置临时文件存放目录,Linux 操作系统上通过设置环境变量 TMPDIR 来更改临时文件目录,Windows 操作系统上通过设置环境变量 TMP 来更改临时文件目录。

例如: 在 Linux shell 中

export TMPDIR=/home/xxxx

在 Windows cmd 中

set TMP=D:\\xxxx

runtime 环境变量使用手册

本章介绍 runtime(运行时)所提供的环境变量。

在 Linux shell 中,您可以使用以下方式设置仓颉运行时提供的环境变量:

$ export VARIABLE=value

在 Windows cmd 中,您可以使用以下方式设置仓颉运行时提供的环境变量:

> set VARAIBLE=value

本节后续的示例都为 Linux shell 中的设置方式,若与您的运行平台不符,请根据您的运行平台选择合适的环境变量设置方式。

runtime 初始化可选配置

注意:

  1. 所有整型参数为 Int64 类型,浮点型参数为 Float64 类型;
  2. 所有参数如果未显式规定最大值,默认隐式最大值为该类型最大值;
  3. 所有参数若超出范围则设置无效,自动使用默认值。

cjHeapSize

指定仓颉堆的最大值,支持单位为 kb(KB)、mb(MB)、gb(GB),支持设置范围为[4MB, 系统物理内存],超出范围的设置无效,仍旧使用默认值。 默认值设置:若物理内存低于 1GB,默认值为 64 MB,否则为 256 MB。

例如:

export cjHeapSize=32GB

cjRegionSize

指定 region 分配器 thread local buffer 的大小,支持设置范围为[4, 64],单位为 KB,超出范围的设置无效,仍旧使用默认值。 默认值设置:默认为 64 KB。

例如:

export cjRegionSize=64

cjExemptionThreshold

指定存活 region 的水线值,取值 (0,1],该值与 region 的大小相乘,若 region 中存活对象数量大于相乘后的值,则该 region 不会被回收 (其中死亡对象继续占用内存)。 该值指定得越大,region 被回收的概率越大,堆中的碎片空间就越少,但频繁回收 region 也会影响性能。 默认值设定:默认为 0.8,即 80%。

例如:

export cjExemptionThreshold=0.8

cjHeapUtilization

指定仓颉堆的利用率,该参数用于 GC 后更新堆水线的参考依据之一,取值 (0, 1],堆水线是指当堆中对象总大小达到水线值时则进行 GC。 该参数指定越小,则更新后的堆水线会越高,则触发 GC 的概率会相对变低。 默认值设定:默认值为 0.8,即 80%。

例如:

export cjHeapUtilization=0.8

cjHeapGrowth

指定仓颉堆的增长率,该参数用于 GC 后更新堆水线的参考依据之一,取值必须大于 0。 增长率的计算方式为 1 + cjHeapGrowth. 该参数指定越大,则更新后的堆水线会越高,则触发 GC 的概率会相对变低。 默认值设定:默认值为 0.15,表示增长率为 1.15。

例如:

export cjHeapGrowth=0.15

cjAlloctionRate

指定仓颉运行时分配对象的速率,该值必须大于 0,单位为 MB/s,表示每秒可分配对象的数量。 默认值设定:默认值为 10240,表示每秒可分配 10240 MB 对象。

例如:

export cjAlloctionRate=10240

cjAlloctionWaitTime

指定仓颉运行时分配对象时的等待时间,该值必须大于 0,支持单位为 s、ms、us、ns,推荐单位为纳秒(ns)。若本次分配对象距离上一次分配对象的时间间隔小于此值,则将等待。 默认值设定:默认值为 1000 ns。

例如:

export cjAlloctionWaitTime=1000ns

cjGCThreshold

指定仓颉堆的参考水线值,支持单位为 kb(KB)、mb(MB)、gb(GB), 取值必须为大于 0 的整数。当仓颉堆大小超过该值时,触发 GC。

默认值设定:默认值为堆大小。

例如:

export cjGCThreshold=20480KB

cjGarbageThreshold

当 GC 发生时,如果 region 中死亡对象所占比率大于此环境变量,此 region 会被放入回收候选集中,后续可被回收(如果受到其它策略影响也可能不被回收),默认值为 0.5,无量纲,支持设置的区间为[0.0, 1.0]。

例如:

export cjGarbageThreshold=0.5

cjGCInterval

指定 2 次 GC 的间隔时间值,取值必须大于 0,支持单位为 s、ms、us、ns,推荐单位为毫秒(ms)。若本次 GC 距离上次 GC 的间隔小于此值,则本次 GC 将被忽略。该参数可以控制 GC 的频率。 默认值设定:默认值为 150 ms。

例如:

export cjGCInterval=150ms

cjBackupGCInterval

指定 backup GC 的间隔值,取值必须大于 0,支持单位为 s、ms、us、ns,推荐单位为秒(s),当仓颉运行时在该参数设定时间内未触发 GC,则触发一次 backup GC。 默认值设定:默认值为 240 秒,即 4 分钟。

例如:

export cjBackupGCInterval=240s

cjGCThreads

指定影响 GC 线程数的因数,取值必须大于 0。 GC 线程数的计算方式为:(系统支持的并发线程数 / cjGCThreads) - 1。 默认值设定:默认值为 8。

例如:

export cjGCThreads=8

cjProcessorNum

指定仓颉线程的最大并发数,支持设置范围为 (0, CPU 核数 * 2],超出范围的设置无效,仍旧使用默认值。 默认值设置:调用系统 API 获取 cpu 核数,若成功设为 cpu 核数,否则设为 8。

例如:

export cjProcessorNum=2

cjStackSize

指定仓颉线程的栈大小,支持单位为 kb(KB)、mb(MB)、gb(GB),支持设置范围为 Linux 平台下[64KB, 1GB],Windows 平台下[128KB, 1GB],超出范围的设置无效,仍旧使用默认值。 默认值设置:Linux 平台下默认值为 64KB,Windows 平台下为 128KB。

例如:

export cjStackSize=100kb

运维日志可选配置

MRT_LOG_FILE_SIZE

指定 runtime 运维日志的文件大小,默认值为 10 MB,支持单位为 kb(KB)、mb(MB)、gb(GB),设置值需大于 0。

日志大小超过该值时,会重新回到日志开头进行打印。

最终生成日志大小略大于 MRT_LOG_FILE_SIZE。

例如:

export MRT_LOG_FILE_SIZE=100kb

MRT_LOG_PATH

指定 runtime 运维日志的输出路径,若该环境变量未设置或路径设置失败,则运维日志默认打印到 stdout(标准输出)或 stderr(标准错误)中。

例如:

export MRT_LOG_PATH="/home/cangjie/runtime/runtime_log.txt"

MRT_LOG_LEVEL

指定 runtime 运维日志的最小输出级别,大于等于这个级别的日志会被打印,默认值为 e,支持设置值为[v|d|i|w|e|f|s]。 v(VERBOSE)、d(DEBUGY)、i(INFO)、w(WARNING)、e(ERROR)、f(FATAL)、s(FATAL_WITHOUT_ABORT)

例如:

export MRT_LOG_LEVEL="v"

MRT_REPORT

指定 runtime GC 日志的输出路径,若该环境变量未设置或路径设置失败,该日志默认不打印。

例如:

export MRT_REPORT="/home/cangjie/runtime/gc_log.txt"

MRT_LOG_COROUTINE

指定 coroutine 日志的输出路径,若该环境变量未设置或路径设置失败,该日志默认不打印。

例如:

export MRT_LOG_COROUTINE="/home/cangjie/runtime/coroutine_log.txt"

运行环境可选配置

MRT_STACK_CHECK

开启 native stack overflow 检查,默认不开启,支持设置值为 1、true、TRUE 开启功能。

例如:

export MRT_STACK_CHECK=true

CJ_SOF_SIZE

当 StackOverflowError 发生时,将自动进行异常栈折叠方便用户阅读,折叠后栈帧层数默认值是 32。可以通过配置此环境变量控制折叠栈长度,支持设置为 int 范围内的整数。 CJ_SOF_SIZE = 0,打印所有调用栈; CJ_SOF_SIZE < 0,从栈底开始打印环境变量配置层数; CJ_SOF_SIZE > 0,从栈顶开始打印环境变量配置层数; CJ_SOF_SIZE 未配置,默认打印栈顶开始 32 层调用栈;

例如:

export CJ_SOF_SIZE=30

条件编译

开发者可以通过预定义或自定义的条件完成条件编译;仓颉目前支持顶层条件编译。

顶层条件编译

仓颉支持除 package 声明以外的 Top Level 条件编译。

使用方法

以内置 os 编译条件为例,其 Top Level 使用方法如下:

@When[os == "linux"]
class mc{}

main(): Int64 {
    var a = mc()
    return 0
}

在上面代码中,开发者在 linux 系统中可以正确编译执行;在 非 linux 系统中,则会遇到找不到 mc 类定义的编译错误。

注:仓颉不支持顶层编译条件嵌套,以下写法均不允许

// 错误示例
@When[os == "windows"]
@When[os == "linux"]    // error: illegal nested when conditional compilation macro
from std import ast.*

@When[os == "windows"]
@When[os == "linux"]    // error: illegal nested when conditional compilation macro
func A(){}

内置条件

仓颉内置了一些条件供开发者直接使用。所有条件的变量都是以字符串的形式存在的。

os

os 是仓颉内置的条件,支持 ==!= 两种操作符。支持的系统有:windowslinuxmacOShm

使用方式如下:

@When[os == "linux"]
func foo() {
    print("linux, ")
}
@When[os == "windows"]
func foo() {
    print("windows, ")
}
@When[os != "windows"]
func fee() {
    println("NOT windows")
}
@When[os != "linux"]
func fee() {
    println("NOT linux")
}
main() {
    foo()
    fee()
}

如果在 windows 环境下编译执行,会得到 windows, NOT linux 的信息;如果是在 linux 环境下,则会得到 linux, NOT windows 的信息。

backend

backend 是仓颉内置的条件。仓颉是多后端语言,支持多种后端条件编译。backend 条件支持 ==!= 两种操作符。

支持的后端有:llvmllvm-x86llvm-x86_64llvm-armllvm-aarch64cjvmcjvm-x86cjvm-x86_64cjvm-armcjvm-aarch64

当用户使用的条件为 llvm/cjvm 时,arch 信息将会按编译器执行时环境信息自动补全。

使用方式如下:

@When[backend == "llvm"]
func foo() {
    print("llvm backend, ")
}
@When[backend == "cjvm"]
func foo() {
    print("cjvm backend, ")
}
@When[backend != "llvm"]
func fee() {
    println("NOT llvm backend")
}
@When[backend != "cjvm"]
func fee() {
    println("NOT cjvm backend")
}
main() {
    foo()
    fee()
}

llvm 后端的发布包编译执行,会得到 llvm backend, NOT cjvm backend 的信息;用 cjvm 后端的发布包编译执行,则会得到 cjvm backend, NOT llvm backend 的信息。

cjc_version

cjc_version 是仓颉内置的条件,开发者可以根据当前仓颉编译器的版本选择要编译的代码。cjc_version 条件支持 ==!=><>=<= 六种操作符,格式为 xx.xx.xx 支持每个 xx 支持 1-2 位数字,计算规则为补位 (补齐 2 位) 比较,例如:0.18.8 < 0.18.110.18.8 == 0.18.08

使用方式如下:

@When[cjc_version == "0.18.6"]
func foo() {
    println("cjc_version equals 0.18.6")
}
@When[cjc_version != "0.18.6"]
func foo() {
    println("cjc_version is NOT equal to 0.18.6")
}
@When[cjc_version > "0.18.6"]
func fnn() {
    println("cjc_version is greater than 0.18.6")
}
@When[cjc_version <= "0.18.6"]
func fnn() {
    println("cjc_version is less than or equal to 0.18.6")
}
@When[cjc_version < "0.18.6"]
func fee() {
    println("cjc_version is less than 0.18.6")
}
@When[cjc_version >= "0.18.6"]
func fee() {
    println("cjc_version is greater than or equal to 0.18.6")
}
main() {
    foo()
    fnn()
    fee()
}

根据 cjc 的版本,上面代码的执行输出结果会有不同。

debug

debug 是仓颉内置的条件。debug 条件仅支持一元运算。

使用方式如下:

@When[debug]
func foo() {
    println("cjc debug")
}
@When[!debug]
func foo() {
    println("cjc NOT debug")
}
main() {
    foo()
}

根据 cjc 是否是 debug 的版本,上面代码的执行输出结果会有不同。

如果是 debug 版本的 cjc 会输出 cjc debug,如果是 release 版本的 cjc 则会输出 cjc NOT debug

自定义条件

仓颉允许开发者自定义编译的条件。

自定义的条件拥有和内置条件一样的作用,不同点在于自定义的条件需要开发者在编译程序时写在编译选项中。自定义条件支持 ==!= 两种运算符。

假如开发者自定义条件为 feature,使用方式和内置条件没有什么不同。使用如下:

//source.cj
@When[feature == "tiger"]
func foo() {
    println("feature tiger")
}
@When[feature == "lion"]
func foo() {
    println("feature lion")
}
main() {
    foo()
}

上面这段代码,开发者想要顺利编译这段代码需要使用这段编译选项:

可以选择 feature == "tiger" 分支

cjc --conditional-compilation-config="(feature=tiger)" source.cj -o runner.out

或者选择 feature == "lion" 分支

cjc --conditional-compilation-config="(feature=lion)" source.cj -o runner.out

根据 feature 的值不同,编译运行上段代码的结果也会不同。

--conditional-compilation-config

此编译选项里的内容表示当前编译流程中 cjc 传递的自定义条件,cjc 会根据此编译选项的值来选择编译哪段代码。

这个选项中的值是以 key-value 来映射的,一个 key 只可以对应一个 value;此选项也支持多个 key-value 结构,用 逗号 (,) 分割,例如:

cjc --conditional-compilation-config="(feature=lion, target=dsp)" source.cj -o runner.out

开发者编写的条件编译代码需要和 --conditional-compilation-config 里的值对应上,否则条件编译不会生效。例如:

//source.cj
@When[feature == "lion"]
func foo() {
    print("feature lion, ")
}
@When[target == "dsp"]
func fee() {
    println("target dsp")
}
main() {
    foo()
    fee()
}

使用如下编译命令编译运行上段代码,会得到输出结果:feature lion, target dsp

cjc --conditional-compilation-config="(feature=lion,target=dsp)" source.cj -o runner.out

而使用如下编译命令编译运行上段代码,会报错:error: undeclared identifier 'foo'

cjc --conditional-compilation-config="(feature=dsp,target=dsp)" source.cj -o runner.out

多条件

仓颉条件编译允许开发者自由组合多个条件编译选项。支持逻辑运算符组合多个条件,支持括号运算符明确优先级。

使用方式如下:

//source.cj
@When[feature == "tiger" || os == "windows"]
func foo() {
    print("feature tiger, ")
}
@When[(debug || os == "linux") && cjc_version >= "0.18.6"]
func fee() {
    println("feature lion")
}
main() {
    foo()
    fee()
}

使用如下编译命令编译运行上段代码,

cjc --conditional-compilation-config="(feature=tiger)" source.cj -o runner.out

会得到输出结果如下:

feature tiger, feature lion

常量求值

常量求值允许某些特定形式的表达式在编译时求值,可以减少程序运行时需要的计算。本章主要介绍常量求值的使用方法与规则。

const 变量

const 变量是一种特殊的变量,它以关键字 const 修饰,定义在编译时完成求值,并且在运行时不可改变的变量。例如,下面的例子定义了万有引力常数 G

const G = 6.674e-11

const 变量可以省略类型标注,但是不可省略初始化表达式。const 变量可以是全局变量,局部变量,静态成员变量,实例成员变量。但是 const 变量不能在扩展中定义。 const 变量可以访问对应类型的所有实例成员,也可以调用对应类型的所有非 mut 实例成员函数。

下例定义了一个 struct,记录行星的质量和半径,同时定义了一个 const 成员函数 gravity 用来计算该行星对距离为 r 质量为 m 的物体的万有引力:

struct Planet {
    const Planet(let mass: Float64, let radius: Float64) {}

    const func gravity(m: Float64, r: Float64) {
        G * mass * m / r**2
    }
}

main() {
    const myMass = 71.0
    const earth = Planet(5.972e24, 6.378e6)
    println(earth.gravity(myMass, earth.radius))
}

编译执行得到地球对地面上一个质量为 71 kg 的成年人的万有引力:

695.657257

const 变量初始化后该类型实例的所有成员都是 const 的(深度 const,包含成员的成员),因此不能被用于左值。

main() {
    const myMass = 71.0
    myMass = 70.0 // error: cannot assign to immutable value
}

const 上下文与 const 表达式

const 上下文是指 const 变量初始化表达式,这些表达式始终在编译时求值。因此需要对 const 上下文中允许的表达式加以限制,避免修改全局状态、I/O 等副作用,确保其可以在编译时求值。

const 表达式具备了可以在编译时求值的能力。满足如下规则的表达式是 const 表达式:

  1. 数值类型、BoolUnitCharString 类型的字面量(不包含插值字符串)。
  2. 所有元素都是 const 表达式的 Array 字面量(不能是 Array 类型,可以使用 VArray 类型),tuple 字面量。
  3. const 变量,const 函数形参,const 函数中的局部变量。
  4. const 函数,包含使用 const 声明的函数名、符合 const 函数要求的 lambda、以及这些函数返回的函数表达式。
  5. const 函数调用(包含 const 构造函数),该函数的表达式必须是 const 表达式,所有实参必须都是 const 表达式。
  6. 所有参数都是 const 表达式的 enum 构造器调用,和无参数的 enum 构造器。
  7. 数值类型、BoolUnitCharString 类型的算数表达式、关系表达式、位运算表达式,所有操作数都必须是 const 表达式。
  8. ifmatchtry、控制转移表达式(包含 returnbreakcontinuethrow)、isas。这些表达式内的表达式必须都是 const 表达式。
  9. const 表达式的成员访问(不包含属性的访问),tuple 的索引访问。
  10. const initconst 函数中的 thissuper 表达式。
  11. const 表达式的 const 实例成员函数调用,且所有实参必须都是 const 表达式。

const 函数

const 函数是一类特殊的函数,这些函数具备了可以在编译时求值的能力。在 const 上下文中调用这种函数时,这些函数会在编译时执行计算。而在其它非 const 上下文,const 函数会和普通函数一样在运行时执行。

下例是一个计算平面上两点距离的 const 函数,distance 中使用 let 定义了两个局部变量 dxdy

struct Point {
    const Point(let x: Float64, let y: Float64) {}
}

const func distance(a: Point, b: Point) {
    let dx = a.x - b.x
    let dy = a.y - b.y
    (dx**2 + dy**2)**0.5
}

main() {
    const a = Point(3.0, 0.0)
    const b = Point(0.0, 4.0)
    const d = distance(a, b)
    println(d)
}

编译运行输出:

5.000000

需要注意:

  1. const 函数声明必须使用 const 修饰。
  2. 全局 const 函数和 static const 函数中只能访问 const 声明的外部变量,包含 const 全局变量、const 静态成员变量,其它外部变量都不可访问。const init 函数和 const 实例成员函数除了能访问 const 声明的外部变量,还可以访问当前类型的实例成员变量。
  3. const 函数中的表达式都必须是 const 表达式,const init 函数除外。
  4. const 函数中可以使用 letconst 声明新的局部变量。但不支持 var
  5. const 函数中的参数类型和返回类型没有特殊规定。如果该函数调用的实参不符合 const 表达式要求,那这个函数调用不能作为 const 表达式使用,但仍然可以作为普通表达式使用。
  6. const 函数不一定都会在编译时执行,例如可以在非 const 函数中运行时调用。
  7. const 函数与非 const 函数重载规则一致。
  8. 数值类型、BoolUnitCharString 类型 和 enum 支持定义 const 实例成员函数。
  9. 对于 structclass,只有定义了 const init 才能定义 const 实例成员函数。class 中的 const 实例成员函数不能是 open 的。struct 中的 const 实例成员函数不能是 mut 的。

另外,接口中也可以定义 const 函数,但会受到以下规则限制:

  1. 接口中的 const 函数,实现类型必须也用 const 函数才算实现接口。
  2. 接口中的非 const 函数,实现类型使用 const 或非 const 函数都算实现接口。
  3. 接口中的 const 函数与接口的 static 函数一样,只有在该接口作为泛型约束的时候,受约束的泛型变元或变量才能使用这些 const 函数。

在下面的例子中,在接口 I 里定义了两个 const 函数,类 A 实现了接口 I,泛型函数 g 的形参类型上界是 I

interface I {
    const func f(): Int64
    const static func f2(): Int64
}

class A <: I {
    public const func f() { 0 }
    public const static func f2() { 1 }
    const init() {}
}

const func g<T>(i: T) where T <: I {
    return i.f() + T.f2()
}

main() {
    println(g(A()))
}

编译执行上述代码,输出结果为:

1

const init

如果一个 structclass 定义了 const 构造器,那么这个 struct/class 实例可以用在 const 表达式中。

  1. 如果当前类型是 class,则不能具有 var 声明的实例成员变量,否则不允许定义 const init 。如果当前类型具有父类,当前的 const init 必须调用父类的 const init(可以显式调用或者隐式调用无参const init),如果父类没有 const init 则报错。
  2. 当前类型的实例成员变量如果有初始值,初始值必须要是 const 表达式,否则不允许定义 const init
  3. const init 内可以使用赋值表达式对实例成员变量赋值,除此以外不能有其它赋值表达式。

const initconst 函数的区别是 const init 内允许对实例成员变量进行赋值(需要使用赋值表达式)

注解

仓颉中提供了一些属性宏用来支持一些特殊情况的处理。

确保正确使用整数运算溢出策略的注解

仓颉中提供三种属性宏来控制整数溢出的处理策略,即 @OverflowThrowing@OverflowWrapping@OverflowSaturating ,这些属性宏当前只能标记于函数声明之上,作用于函数内的整数运算和整型转换。它们分别对应以下三种溢出处理策略:

(1) 抛出异常(throwing):当整数运算溢出时,抛出异常。

@OverflowThrowing
main() {
    let res: Int8 = Int8(100) + Int8(29)
    /* 100 + 29 在数学上等于 129,
     * 在 Int8 的表示范围上发生了上溢出,
     * 程序抛出异常
     */
    let con: UInt8 = UInt8(-132)
    /* -132 在 UInt8 的表示范围上发生了下溢出,
     * 程序抛出异常
     */
    0
}

(2) 高位截断(wrapping):当整数运算的结果超出用于接收它的内存空间所能表示的数据范围时,则截断超出该内存空间的部分。

@OverflowWrapping
main() {
    let res: Int8 = Int8(105) * Int8(4)
    /* 105 * 4 在数学上等于 420,
     * 对应的二进制为 1 1010 0100,
     * 超过了用于接收该结果的 8 位内存空间,
     * 截断后的结果在二进制上表示为 1010 0100,
     * 对应为有符号整数 -92
     */
    let temp: Int16 = Int16(-132)
    let con: UInt8 = UInt8(temp)
    /* -132 对应的二进制为 1111 1111 0111 1100,
     * 超过了用于接收该结果的 8 位内存空间,
     * 截断后的结果在二进制上表示为 0111 1100
     * 对应为有符号整数 124
     */
    0
}

(3) 饱和(saturating):当整数运算溢出时,选择对应固定精度的极值作为结果。

@OverflowSaturating
main() {
    let res: Int8 = Int8(-100) - Int8(45)
    /* -100 - 45 在数学上等于 -145,
     * 在 Int8 的表示范围上发生了下溢出,
     * 选择 Int8 的最小值 -128 作为结果
     */
    let con: Int8 = Int8(1024)
    /* 1024 在 Int8 的表示范围上发生了上溢出,
     * 选择 Int8 的最大值 127 作为结果
     */
    0
}

默认情况下(即未标注该类属性宏时),采取抛出异常(@OverflowThrowing)的处理策略。

实际情况下需要根据业务场景的需求正确选择溢出策略。例如要在 Int32 上实现某种安全运算,使得计算结果和计算过程在数学上相等,就需要使用抛出异常的策略。

【反例】

// 计算结果被高位截断
@OverflowWrapping
func operation(a: Int32, b: Int32): Int32 {
    a + b // No exception will be thrown when overflow occurs
}

该错误例子使用了高位截断的溢出策略,比如当传入的参数 ab 较大导致结果溢出时,会产生高位截断的情况,导致函数返回结果和计算表达式 a + b 在数学上不是相等关系。

【正例】

// 安全
@OverflowThrowing
func operation(a: Int32, b: Int32): Int32 {
    a + b
}

main() {
    try {
        operation(a, b)
    } catch (e: ArithmeticException) {
        //Handle error
    }
    0
}

该正确例子使用了抛出异常的溢出策略,当传入的参数 ab 较大导致整数溢出时,operation 函数会抛出异常。

下面总结了可能造成整数溢出的数学操作符。

操作符溢出操作符溢出操作符溢出操作符溢出
+Y-=Y<<N<N
-Y*=Y>>N>N
*Y/=Y&N>=N
/Y%=N|N<=N
%N<<=N^N==N
++Y>>=N**=Y
--Y&=N!N
=N|=N!=N
+=Y^=N**Y

性能优化注解

为了提升与 C 语言互操作的性能,仓颉提供属性宏 @FastNative 控制 cjnative 后端优化对于 C 函数的调用。值得注意的是,属性宏 @FastNative 只能用于 foreign 声明的函数。

@FastNative 使用限制

开发者在使用 @FastNative 修饰 foreign 函数时,应确保对应的 C 函数满足以下两点要求。

  • 首先,函数的整体执行时间不宜太长。例如:
    • 不允许函数内部存在很大的循环;
    • 不允许函数内部产生阻塞行为,如,调用 sleepwait 等函数。
  • 其次,函数内部不能调用仓颉方法。

自定义注解

自定义注解机制用来让反射(详见反射章节)获取标注内容,目的是在类型元数据之外提供更多的有用信息,以支持更复杂的逻辑。

开发者可以通过自定义类型标注 @Annotation 方式创建自己的自定义注解。@Annotation 只能修饰 class,并且不能是 abstractopensealed 修饰的 class。当一个 class 声明它标注了 @Annotation,那么它必须要提供至少一个 const init 函数,否则编译器会报错。

下面的例子定义了一个自定义注解 @Version,并用其修饰 A, BC。在 main 中,我们通过反射获取到类上的 @Version 注解信息,并将其打印出来。

package pkg

from std import reflect.TypeInfo

@Annotation
public class Version {
    let code: String
    const init(code: String) {
        this.code = code
    }
}

@Version["1.0"]
class A {}

@Version["1.1"]
class B {}

main() {
    let objects = [A(), B()]
    for (obj in objects) {
        let annOpt = TypeInfo.of(obj).findAnnotation("pkg.Version")
        if (let Some(ann) <- annOpt) {
            if (let Some(version) <- ann as Version) {
                println(version.code)
            }
        }
    }
}

编译并执行上述代码,输出结果为:

1.0
1.1

注解信息需要在编译时生成信息并绑定到类型上,自定义注解在使用时必须使用 const init 构建出合法的实例。注解声明语法与声明宏语法一致,后面的 [] 括号中需要按顺序或命名参数规则传入参数,且参数必须是 const 表达式(详见常量求值章节)。对于拥有无参构造函数的注解类型,声明时允许省略括号。

下面的例子中定义了一个拥有无参 const init 的自定义注解 @Deprecated,使用时 @Deprecated@Deprecated[] 这两种写法均可。

package pkg

from std import reflect.TypeInfo

@Annotation
public class Deprecated {
    const init() {}
}

@Deprecated
class A {}

@Deprecated[]
class B {}

main() {
    if (TypeInfo.of(A()).findAnnotation("pkg.Deprecated").isSome()) {
        println("A is deprecated")
    }
    if (TypeInfo.of(B()).findAnnotation("pkg.Deprecated").isSome()) {
        println("B is deprecated")
    }
}

编译并执行上述代码,输出结果为:

A is deprecated
B is deprecated

对于同一个注解目标,同一个注解类不允许声明多次,即不可重复。

@Deprecated
@Deprecated // error
class A {}

Annotation 不会被继承,因此一个类型的注解元数据只会来自它定义时声明的注解。如果需要父类型的注解元数据信息,需要开发者自己用反射接口查询。

下面的例子中,A@Deprecated 注解修饰,B 继承 A,但是 B 没有 A 的注解。

package pkg

from std import reflect.TypeInfo

@Annotation
public class Deprecated {
    const init() {}
}

@Deprecated
open class A {}

class B <: A {}

main() {
    if (TypeInfo.of(A()).findAnnotation("pkg.Deprecated").isSome()) {
        println("A is deprecated")
    }
    if (TypeInfo.of(B()).findAnnotation("pkg.Deprecated").isSome()) {
        println("B is deprecated")
    }
}

编译并执行上述代码,输出结果为:

A is deprecated

自定义注解可以用在类型声明(classstructenuminterface)、成员函数/构造函数中的参数、构造函数声明、成员函数声明、成员变量声明、成员属性声明。也可以限制自己可以使用的位置,这样可以减少开发者的误用,这类注解需要在声明 @Annotation 时标注 target 参数,参数类型为 Array<AnnotationKind>。其中,AnnotationKind 是标准库中定义的 enum。当没有限定 target 的时候,该自定义注解可以用在以上全部位置。当限定 target 时,只能用在声明的列表中。

public enum AnnotaitionKind {
    | Type
    | Parameter
    | Init
    | MemberProperty
    | MemberFunction
    | MemberVariable
}

下面的例子中,自定义注解通过 target 限定只能用在成员函数上,用在其他位置会编译报错。

@Annotation[target: [MemberFunction]]
public class Deprecated {
    const init() {}
}

class A {
    @Deprecated // ok, member funciton
    func deprecated() {}
}

@Deprecated // error, type
class B {}

main() {}

动态特性

本章我们来介绍 Cangjie 的动态特性,应用动态特性开发者能够更为优雅的实现一些功能。仓颉的动态特性主要包含反射、动态加载。

仓颉反射基本介绍

反射指程序可以访问、检测和修改它本身状态或行为的一种机制。

反射这一动态特性有以下的优点:

  • 提高了程序的灵活性和扩展性。

  • 程序能够在运行时获悉各种对象的类型,对其成员进行枚举、调用等操作。

  • 允许在运行时创建新类型,无需提前硬编码。

但使用反射调用,其性能通常低于直接调用,因此反射机制主要应用于对灵活性和拓展性要求很高的系统框架上。

如何获得 TypeInfo

对于仓颉的反射特性,我们需要知道 TypeInfo 这一类型,这个核心类型中记录任意类型的类型信息,并且定义了方法用于获取类型信息、设置值等。当然为了便于用户操作我们还提供了 ClassTypeInfo、PrimitiveTypeInfo、ParameterInfo 等一系列的信息类型。

我们可以使用三种静态的 of 方法来生成 TypeInfo 信息类。

public class TypeInfo {
    public static func of(a: Any): TypeInfo
    public static func of(a: Object): ClassTypeInfo
    public static func of<T>(): TypeInfo
}

当采用入参为 AnyObject 类型的 of 函数,输出为该实例的运行时类型信息,采用泛型参数的 of 函数则会返回传入参数的静态类型信息。两种方法产生的信息完全相同,但不保证一定对应同一对象。

例如我们可以用反射来获取一个自定义类型的类型信息。

from std import reflect.*

class Foo {}

main() {
    let a: Foo = Foo()
    let info: TypeInfo = TypeInfo.of(a)
    let info2: TypeInfo = TypeInfo.of<Foo>()
    println(info)
    println(info2)
}

编译并执行上面的代码,会输出:

default.Foo
default.Foo

此外,为配合动态加载使用,TypeInfo 还提供了静态函数 get,该接口可通过传入的类型名称获取 TypeInfo。

public class TypeInfo {
    public static func get(qualifiedName: String): TypeInfo
}

请注意,传入参数需要符合 module/package.type 的完全限定模式规则。对于编译器预导入的类型,包含 core 包中的类型和编译器内置类型,例如 primitive typeOptionIterable 等,查找的字符串需要直接使用其类型名,不能带包名和模块名前缀。当运行时无法查询到对应类型的实例,则会抛出 InfoNotFoundException

let t1: TypeInfo = TypeInfo.get("Int64")
let t1: TypeInfo = TypeInfo.get("default.Foo")
let t2: TypeInfo = TypeInfo.get("std/socket.TcpSocket")
let t3: TypeInfo = TypeInfo.get("net/http.ServerBuilder")

采用这种方式时无法获取一个未实例化的泛型类型。

from std import collection.*
from std import reflect.*

class A<T> {
    A(public let t: T) {}
}

class B<T> {
    B(public let t: T) {}
}

main() {
    let aInfo: TypeInfo = TypeInfo.get("default.A<Int64>")// error `default.A<Int64>` is not instantiated,will throw InfoNotFoundException
    let b: B<Int64> = B<Int64>(1)
    let bInfo: TypeInfo = TypeInfo.get("default.B<Int64>")// ok `default.B<Int64>` has been performed.
}

如何使用反射访问成员

在获取到对应的类型信息类即 TypeInfo,我们便可以通过其相应接口访问对应类的实例成员以及静态成员。此外 TypeInfo 的子类 ClassTypeInfo 还提供了接口用于访问类公开的构造函数以及它的成员变量、属性、函数。仓颉的反射被设计为只能访问到类型内 public 的成员,意味着 private、protected 和 default 修饰的成员在反射中是不可见的。

例如当我们想要在运行时对类的某一实例成员变量进行获取与修改。

from std import reflect.*

public class Foo {
    public static var param1 = 20
    public var param2 = 10
}

main(): Unit{
    let obj = Foo()
    let info = TypeInfo.of(obj)
    let staticVarInfo = info.getStaticVariable("param1")
    let instanceVarInfo = info.getInstanceVariable("param2")
    println("成员变量初始值")
    print("Foo 的静态成员变量 ${staticVarInfo} = ")
    println((staticVarInfo.getValue() as Int64).getOrThrow())
    print("obj 的实例成员变量 ${instanceVarInfo} = ")
    println((instanceVarInfo.getValue(obj) as Int64).getOrThrow())
    println("更改成员变量")
    staticVarInfo.setValue(8)
    instanceVarInfo.setValue(obj, 25)
    print("Foo 的静态成员变量 ${staticVarInfo} = ")
    println((staticVarInfo.getValue() as Int64).getOrThrow())
    print("obj 的实例成员变量 ${instanceVarInfo} = ")
    println((instanceVarInfo.getValue(obj) as Int64).getOrThrow())
    return
}

编译并执行上面的代码,会输出:

成员变量初始值
Foo 的静态成员变量 static param1: Int64 = 20
obj 的实例成员变量 param2: Int64 = 10
更改成员变量
Foo 的静态成员变量 static param1: Int64 = 8
obj 的实例成员变量 param2: Int64 = 25

同时我们也可以通过反射对属性进行检查以及修改。

from std import reflect.*

public class Foo {
    public let _p1: Int64 = 1
    public prop p1: Int64 {
        get() { _p1 }
    }
    public var _p2: Int64 = 2
    public mut prop p2: Int64 {
        get() { _p2 }
        set(v) { _p2 = v }
    }
}

main(): Unit{
    let obj = Foo()
    let info = TypeInfo.of(obj)
    let instanceProps = info.instanceProperties.toArray()
    println("obj的实例成员属性包含${instanceProps}")
    let PropInfo1 = info.getInstanceProperty("p1")
    let PropInfo2 = info.getInstanceProperty("p2")

    println((PropInfo1.getValue(obj) as Int64).getOrThrow())
    println((PropInfo2.getValue(obj) as Int64).getOrThrow())
    if (PropInfo1.isMutable()) {
        PropInfo1.setValue(obj, 10)
    }
    if (PropInfo2.isMutable()) {
        PropInfo2.setValue(obj, 20)
    }
    println((PropInfo1.getValue(obj) as Int64).getOrThrow())
    println((PropInfo2.getValue(obj) as Int64).getOrThrow())
    return
}

编译并执行上面的代码,会输出:

obj 的实例成员属性包含[prop p1: Int64, mut prop p2: Int64]
1
2
1
20

我们还可以通过反射机制进行函数调用。

from std import reflect.*

public class Foo {
    public static func f1(v0: Int64, v1: Int64): Int64 {
        return v0 + v1
    }
}

main(): Unit {
    var num = 0
    let intInfo = TypeInfo.of<Int64>()
    let funcInfo = TypeInfo.of<default.Foo>().getStaticFunction("f1", intInfo, intInfo)
    num = (funcInfo.apply([1, 1]) as Int64).getOrThrow()
    println(num)
}

编译并执行上面的代码,会输出:

2

动态加载

编译时刻加载称之为静态加载,而动态加载指的是仓颉程序可以在运行过程中通过特定函数来访问仓颉动态模块,以此读写全局变量、调用全局函数、获取类型信息的能力。仓颉中主要通过 ModuleInfoPackageInfo 这两个类型来提供动态加载的能力。

例如我们存在一个 module0 模块下的 package0 包含一个公开的类型 Foo,其对应的仓颉动态模块路径为 "./module_package.so" 。应用动态加载我们便可以在运行时得到这个 Foo 的类型信息。

let m = ModuleInfo.load(./module_package)
let p = m.getPackageInfo("package0").getOrThrow()
let at = TypeInfo.get("module0/package0.Foo")

附录

Linux 版本工具链的支持与安装

仓颉工具链当前基于以下 Linux 发行版进行了完整功能测试: | Linux 发行版 | | -- | | SLES 12-SP5 | | Ubuntu 18.04 | | Ubuntu 20.04 | | EulerOS R11 |

适用于各 Linux 发行版的仓颉工具链依赖安装命令

注:当前仓颉工具链依赖的部分工具在一些 Linux 发行版上可能无法通过系统默认软件源直接安装,你可以参考下一节[编译安装依赖工具]进行手动安装。

SLES 12-SP5

$ zypper install \
         binutils \
         glibc-devel \
         gcc-c++

此外,还需要安装以下工具,安装方法请参考下一节。

  • OpenSSL 3

Ubuntu 18.04

$ apt-get install \
          binutils \
          libc-dev \
          libc++-dev \
          libgcc-7-dev \

此外,还需要安装以下工具,安装方法请参考下一节。

  • OpenSSL 3

Ubuntu 20.04

$ apt-get install \
          binutils \
          libc-dev \
          libc++-dev \
          libgcc-9-dev \

此外,还需要安装以下工具,安装方法请参考下一节。

  • OpenSSL 3

EulerOS R11

$ yum install binutils \
              glibc-devel \
              gcc

其他 Linux 发行版

根据使用的 Linux 发行版的不同,你可能需要参考以上系统的依赖安装命令,使用你的系统包管理工具安装对应依赖。若你使用的系统没有提供相关软件包,你可能需要自行安装链接工具、C 语言开发工具、C++ 开发工具、GCC 编译器、以及 OpenSSL 3 以正常使用仓颉工具链。

编译安装依赖工具

当前仓颉工具链中的部分标准库(以及部分工具)使用了 OpenSSL 3 开源软件。对于系统包管理工具未提供 OpenSSL 3 的场景,用户可能需要源码编译安装 OpenSSL 3,本节提供了 OpenSSL 3 源码编译的方法和步骤。

OpenSSL 3

从以下链接可以下载到 OpenSSL 3 的源码:

  • https://www.openssl.org/source/
  • https://www.openssl.org/source/old/

建议使用 openssl-3.0.7 或更高版本。

注意:请在执行以下编译和安装命令前仔细阅读注意事项,并根据实际情况调整命令。不正确的配置和安装可能会导致系统其他软件不可用。 如果在编译安装过程中遇到问题或希望进行额外的安装配置,请参考 OpenSSL 源码中的 INSTALL 文件或 OpenSSL 的 FAQ:https://www.openssl.org/docs/faq.html。

此处以 openssl-3.0.7 为例,下载后使用以下命令解压压缩包:

$ tar xf openssl-3.0.7.tar.gz

解压完成后进入目录:

$ cd openssl-3.0.7

编译 OpenSSL:

注意:如果你的系统已经安装了 OpenSSL,建议使用 --prefix=<path> 选项指定一个自定义安装路径,例如 --prefix=/usr/local/openssl-3.0.7 或你的个人目录。在系统目录已经存在 OpenSSL 的场景下直接使用以下命令编译安装可能会使系统 OpenSSL 被覆盖,并导致依赖系统 OpenSSL 的应用不可用。

$ ./Configure --libdir=lib
$ make

测试 OpenSSL:

$ make test

将 OpenSSL 安装至系统目录(或你先前指定的 --prefix 目录),你可能需要提供 root 权限以成功执行以下命令:

$ make install

$ sudo make install

如果先前编译 OpenSSL 时没有通过 --prefix 设置自定义安装路径,你的 OpenSSL 安装已经完成了。如果先前通过 --prefix 指定了自定义的安装路径,你还需要设置以下变量,以使仓颉工具链可以找到 OpenSSL 3。

注意:如果你的系统中原先存在其他版本的 OpenSSL,通过以下方式配置后,除了仓颉工具链外,你的其他编译开发工具默认使用的 OpenSSL 版本也可能受到影响。如果使用其他编译开发工具时出现 OpenSSL 不兼容的情况,请仅为仓颉开发环境配置以下变量。

请将 <prefix> 替换为你指定的自定义安装路径。

$ export LIBRARY_PATH=<prefix>/lib:$LIBRARY_PATH
$ export LD_LIBRARY_PATH=<prefix>/lib:$LD_LIBRARY_PATH

通过以上方式所配置的环境变量仅在当前执行命令的 shell 会话窗口有效。若希望 shell 每次启动时都自动配置,你可以在 $HOME/.bashrc$HOME/.zshrc 或其他 shell 配置文件(依你的 shell 种类而定)加入以上命令。

若希望配置可以默认对所有用户生效,你可以执行以下命令:

请将 <prefix> 替换为你指定的自定义安装路径。

$ echo "export LIBRARY_PATH=<prefix>/lib:$LIBRARY_PATH" >> /etc/profile
$ echo "<prefix>/lib" >> /etc/ld.so.conf
$ ldconfig

执行完毕后重新打开 shell 会话窗口即可生效。

至此,OpenSSL 3 已经成功安装,你可以回到原来的章节继续阅读或尝试运行仓颉编译器了。

关键字

关键字是不能作为标识符使用的特殊字符串,仓颉语言的关键字如下表所示:

asabstractbreak
Boolcasecatch
classconstcontinue
Chardoelse
enumextendfor
fromfuncfalse
finallyforeignFloat16
Float32Float64if
inisinit
importinterfaceInt8
Int16Int32Int64
IntNativeletmut
mainmacromatch
Nothingopenoperator
overrideproppublic
packageprivateprotected
quoteredefreturn
spawnsuperstatic
structsynchronizedtry
thistruetype
throwThisunsafe
UnitUInt8UInt16
UInt32UInt64UIntNative
varVArraywhere
while

操作符

下表列出了仓颉支持的所有操作符的优先级及结合性,其中优先级一栏数值越小,对应操作符的优先级越高。

操作符优先级含义示例结合方向
@0宏调用@id右结合
.1成员访问expr.id左结合
[]1索引expr[expr]左结合
()1函数调用expr(expr)左结合
++2自增var++
--2自减var--
?2问号expr?.id, expr?[expr], expr?(expr), expr?{expr}
!3按位求反、逻辑非!expr右结合
-3一元负号-expr右结合
**4幂运算expr ** expr右结合
*, /5乘法,除法expr * expr, expr / expr左结合
%5取模expr % expr左结合
+, -6加法,减法expr + expr, expr - expr左结合
<<7按位左移expr << expr左结合
>>7按位右移expr >> expr左结合
..8区间操作符expr..expr
..=8expr..=expr
<9小于expr < expr
<=9小于等于expr <= expr
>9大于expr > expr
>=9大于等于expr >= expr
is9类型检查expr is Type
as9类型转换expr as Type
==10判等expr == expr
!=10判不等expr != expr
&11按位与expr & expr左结合
^12按位异或expr ^ expr左结合
|13按位或expr | expr左结合
&&14逻辑与expr && expr左结合
||15逻辑或expr || expr左结合
??16coalescing 操作符expr ?? expr右结合
|>17pipeline 操作符id |> expr左结合
~>17composition 操作符expr ~> expr左结合
=18赋值id = expr
**=18复合赋值id **= expr
*=18id *= expr
/=18id /= expr
%=18id %= expr
+=18id += expr
-=18id -= expr
<<=18id <<= expr
>>=18id >>= expr
&=18id &= expr
^=18id ^= expr
|=18id |= expr
&&=18id &&= expr
||=18id ||= expr

操作符函数

下表列出了仓颉支持的所有操作符函数。

操作符函数函数签名示例
[] (索引取值)operator func [](index1: T1, index2: T2, ...): Rthis[index1, index2, ...]
[] (索引赋值)operator func [](index1: T1, index2: T2, ..., value!: TN): Rthis[index1, index2, ...] = value
()operator func ()(param1: T1, param2: T2, ...): Rthis(param1, param2, ...)
!operator func !(): R!this
**operator func **(other: T): Rthis ** other
*operator func *(other: T): Rthis * other
/operator func /(other: T): Rthis / other
%operator func %(other: T): Rthis % other
+operator func +(other: T): Rthis + other
-operator func -(other: T): Rthis - other
<<operator func <<(other: T): Rthis << other
>>operator func >>(other: T): Rthis >> other
<operator func <(other: T): Rthis < other
<=operator func <=(other: T): Rthis <= other
>operator func >(other: T): Rthis > other
>=operator func >=(other: T): Rthis >= other
==operator func ==(other: T): Rthis == other
!=operator func !=(other: T): Rthis != other
&operator func &(other: T): Rthis & other
^operator func ^(other: T): Rthis ^ other
|operator func |(other: T): Rthis | other

TokenKind 类型

public enum TokenKind <: ToString {
    DOT|                      /*  "."           */
    COMMA|                    /*  ","           */
    LPAREN|                   /*  "("           */
    RPAREN|                   /*  ")"           */
    LSQUARE|                  /*  "["           */
    RSQUARE|                  /*  "]"           */
    LCURL|                    /*  "{"           */
    RCURL|                    /*  "}"           */
    EXP|                      /*  "**"          */
    MUL|                      /*  "*"           */
    MOD|                      /*  "%"           */
    DIV|                      /*  "/"           */
    ADD|                      /*  "+"           */
    SUB|                      /*  "-"           */
    INCR|                     /*  "++"          */
    DECR|                     /*  "--"          */
    AND|                      /*  "&&"          */
    OR|                       /*  "||"          */
    COALESCING|               /*  "??"          */
    PIPELINE|                 /*  "|>"          */
    COMPOSITION|              /*  "~>"          */
    NOT|                      /*  "!"           */
    BITAND|                   /*  "&"           */
    BITOR|                    /*  "|"           */
    BITXOR|                   /*  "^"           */
    BITNOT|                   /*  "~"           */
    LSHIFT|                   /*  "<<"          */
    RSHIFT|                   /*  ">>"          */
    COLON|                    /*  ":"           */
    SEMI|                     /*  ";"           */
    ASSIGN|                   /*  "="           */
    ADD_ASSIGN|               /*  "+="          */
    SUB_ASSIGN|               /*  "-="          */
    MUL_ASSIGN|               /*  "*="          */
    EXP_ASSIGN|               /*  "**="         */
    DIV_ASSIGN|               /*  "/="          */
    MOD_ASSIGN|               /*  "%="          */
    AND_ASSIGN|               /*  "&&="         */
    OR_ASSIGN|                /*  "||="         */
    BITAND_ASSIGN|            /*  "&="          */
    BITOR_ASSIGN|             /*  "|="          */
    BITXOR_ASSIGN|            /*  "^="          */
    LSHIFT_ASSIGN|            /*  "<<="         */
    RSHIFT_ASSIGN|            /*  ">>="         */
    ARROW|                    /*  "->"          */
    BACKARROW|                /*  "<-"          */
    DOUBLE_ARROW|             /*  "=>"          */
    RANGEOP|                  /*  ".."          */
    CLOSEDRANGEOP|            /*  "..="         */
    ELLIPSIS|                 /*  "..."         */
    HASH|                     /*  "#"           */
    AT|                       /*  "@"           */
    QUEST|                    /*  "?"           */
    LT|                       /*  "<"           */
    GT|                       /*  ">"           */
    LE|                       /*  "<="          */
    GE|                       /*  ">="          */
    IS|                       /*  "is"          */
    AS|                       /*  "as"          */
    NOTEQ|                    /*  "!="          */
    EQUAL|                    /*  "=="          */
    WILDCARD|                 /*  "_"           */
    INT8|                     /*  "Int8"        */
    INT16|                    /*  "Int16"       */
    INT32|                    /*  "Int32"       */
    INT64|                    /*  "Int64"       */
    INTNATIVE|                /*  "IntNative"   */
    UINT8|                    /*  "UInt8"       */
    UINT16|                   /*  "UInt16"      */
    UINT32|                   /*  "UInt32"      */
    UINT64|                   /*  "UInt64"      */
    UINTNATIVE|               /*  "UIntNative"  */
    FLOAT16|                  /*  "Float16"     */
    FLOAT32|                  /*  "Float32"     */
    FLOAT64|                  /*  "Float64"     */
    CHAR|                     /*  "Char"        */
    BOOLEAN|                  /*  "Bool"        */
    NOTHING|                  /*  "Nothing"     */
    UNIT|                     /*  "Unit"        */
    STRUCT|                   /*  "struct"      */
    ENUM|                     /*  "enum"        */
    CFUNC|                    /*  "CFunc"       */
    VARRAY|                   /*  "VArray"      */
    THISTYPE|                 /*  "This"        */
    PACKAGE|                  /*  "package"     */
    IMPORT|                   /*  "import"      */
    CLASS|                    /*  "class"       */
    INTERFACE|                /*  "interface"   */
    FUNC|                     /*  "func"        */
    MACRO|                    /*  "macro"       */
    QUOTE|                    /*  "quote"       */
    DOLLAR|                   /*  "$"           */
    LET|                      /*  "let"         */
    VAR|                      /*  "var"         */
    CONST|                    /*  "const"       */
    TYPE|                     /*  "type"        */
    INIT|                     /*  "init"        */
    THIS|                     /*  "this"        */
    SUPER|                    /*  "super"       */
    IF|                       /*  "if"          */
    ELSE|                     /*  "else"        */
    CASE|                     /*  "case"        */
    TRY|                      /*  "try"         */
    CATCH|                    /*  "catch"       */
    FINALLY|                  /*  "finally"     */
    FOR|                      /*  "for"         */
    DO|                       /*  "do"          */
    WHILE|                    /*  "while"       */
    THROW|                    /*  "throw"       */
    RETURN|                   /*  "return"      */
    CONTINUE|                 /*  "continue"    */
    BREAK|                    /*  "break"       */
    IN|                       /*  "in"          */
    NOT_IN|                   /*  "!in"         */
    MATCH|                    /*  "match"       */
    FROM|                     /*  "from"        */
    WHERE|                    /*  "where"       */
    EXTEND|                   /*  "extend"      */
    WITH|                     /*  "with"        */
    PROP|                     /*  "prop"        */
    STATIC|                   /*  "static"      */
    PUBLIC|                   /*  "public"      */
    PRIVATE|                  /*  "private"     */
    PROTECTED|                /*  "protected"   */
    OVERRIDE|                 /*  "override"    */
    REDEF|                    /*  "redef"       */
    ABSTRACT|                 /*  "abstract"    */
    SEALED|                   /*  "sealed"      */
    OPEN|                     /*  "open"        */
    FOREIGN|                  /*  "foreign"     */
    INOUT|                    /*  "inout"       */
    MUT|                      /*  "mut"         */
    UNSAFE|                   /*  "unsafe"      */
    OPERATOR|                 /*  "operator"    */
    SPAWN|                    /*  "spawn"       */
    SYNCHRONIZED|             /*  "synchronized */
    UPPERBOUND|               /*  "<:"          */
    MAIN|                     /*  "main"        */
    IDENTIFIER|               /*  "x"           */
    INTEGER_LITERAL|          /*  e.g. "1"      */
    CHAR_BYTE_LITERAL|        /*  e.g. "b'x'"   */
    FLOAT_LITERAL|            /*  e.g. "'1.0'"  */
    COMMENT|                  /*  e.g. "/*xx*/" */
    NL|                       /*  newline         */
    END|                      /*  end of file     */
    SENTINEL|                 /*  ";"             */
    CHAR_LITERAL|             /*  e.g. "'x'"      */
    STRING_LITERAL|           /*  e.g. ""xx""     */
    JSTRING_LITERAL|          /*  e.g. "J"xx""     */
    BYTE_STRING_ARRAY_LITERAL /*  e.g. "b"xx""       */
    MULTILINE_STRING|         /*  e.g. """"aaa""""   */
    MULTILINE_RAW_STRING|     /*  e.g. "#"aaa"#"     */
    BOOL_LITERAL|             /*  "true" or "false"  */
    UNIT_LITERAL|             /*  "()"               */
    DOLLAR_IDENTIFIER|        /*  e.g. "$x"          */
    ANNOTATION|               /*  e.g. "@When"       */
    ILLEGAL
}
仓颉库使用指南
版本号:0.51.4
发布时间:2024-05-06
华为技术有限公司

core 包

介绍

此包包括一些常用接口 ToString、 Hashable、 Equatable 等,以及 String、 Range、 Array、 Option 等常用数据结构,包括预定义的异常、错误类型等。

主要接口

interface Any

public interface Any

Any 是所有类型的父类型,所有 interface 都默认继承它,所有非 interface 类型都默认实现它。

class Object

public open class Object <: Any {
    public const init()
}

Object 是所有 class 的父类,所有 class 都默认继承它。

init

public const init()

功能:构造一个 object 实例。

interface CharExtension

public interface CharExtension

CharExtensionChar 相关的辅助接口。

interface Hasher

public interface Hasher {
    func finish(): Int64
    mut func reset(): Unit
    mut func write(value: Bool): Unit
    mut func write(value: Char): Unit
    mut func write(value: Int8): Unit
    mut func write(value: Int16): Unit
    mut func write(value: Int32): Unit
    mut func write(value: Int64): Unit
    mut func write(value: UInt8): Unit
    mut func write(value: UInt16): Unit
    mut func write(value: UInt32): Unit
    mut func write(value: UInt64): Unit
    mut func write(value: Float16): Unit
    mut func write(value: Float32): Unit
    mut func write(value: Float64): Unit
    mut func write(value: String): Unit
}

Hasher 是用来处理哈希组合运算的接口。

func finish

func finish(): Int64

功能:返回哈希运算的结果。

返回值:经过计算后的哈希值

func reset

mut func reset(): Unit

功能:重置哈希值。

func write

mut func write(value: Bool): Unit

功能:通过该函数把想要哈希运算的 Bool 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Char): Unit

功能:通过该函数把想要哈希运算的 Char 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Int8): Unit

功能:通过该函数把想要哈希运算的 Int8 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Int16): Unit

功能:通过该函数把想要哈希运算的 Int16 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Int32): Unit

功能:通过该函数把想要哈希运算的 Int32 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Int64): Unit

功能:通过该函数把想要哈希运算的 Int64 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: UInt8): Unit

功能:通过该函数把想要哈希运算的 UInt8 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: UInt16): Unit

功能:通过该函数把想要哈希运算的 UInt16 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: UInt32): Unit

功能:通过该函数把想要哈希运算的 UInt32 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: UInt64): Unit

功能:通过该函数把想要哈希运算的 UInt64 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Float16): Unit

功能:通过该函数把想要哈希运算的 Float16 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Float32): Unit

功能:通过该函数把想要哈希运算的 Float32 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Float64): Unit

功能:通过该函数把想要哈希运算的 Float64 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: String): Unit

功能:通过该函数把想要哈希运算的 String 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

class ArithmeticException

public open class ArithmeticException <: Exception {
    public init()
    public init(message: String)
}

ArithmeticException 为算术异常类,用于在发生算术异常时使用。

init

public init()

功能:构造一个默认的 ArithmeticException 实例,默认异常信息为空。

init

public init(message: String)

功能:根据异常信息构造一个 ArithmeticException 实例。

参数:

  • message:异常信息

func getClassName

protected open override func getClassName(): String

功能:获得类名。

返回值:类名字符串

class ArrayIterator

public class ArrayIterator<T> <: Iterator<T> {
    public init(data: Array<T>)
}

数组迭代器。

init

public init(data: Array<T>)

功能:创建一个数组迭代器。

参数:

  • data:数组

func next

public func next(): Option<T>

功能:返回迭代器中的下一个值。

返回值:Option,可以使用 match 解构

func iterator

public func iterator(): Iterator<T>

功能:返回迭代器。

返回值:Iterator 类型,可以使用迭代器进行遍历。

class Box

public class Box<T> {
    public var value: T
    public init(v: T)
}

Box 泛型提供了将所有的类型包装成引用类型的能力。

value

public var value: T

功能:获取或修改被包装的值。

init

public init(v: T)

功能:构造函数。

参数:

  • v:任意仓颉支持的类型参数

extend Box <: Hashable

extend Box<T> <: Hashable where T <: Hashable

func hashCode

public func hashCode(): Int64

功能:获取 Box 对象的 hashCode 值。

返回值:hashCode 值

extend Box <: Comparable

extend Box<T> <: Comparable<Box<T>> where T <: Comparable<T>

Box<T> 类扩展 Comparable<Box<T>> 接口,其中 T 必须是 Comparable<T> 类型。Box<T> 实例的大小关系与其封装的 T 实例大小关系相同。

operator func >

public operator func >(that: Box<T>): Bool

功能:比较 Box 对象的大小。

参数:

  • that:比较的另外一个 Box 对象

返回值:原 Box 对象大于 that Box 对象返回 true,否则返回 false

operator func <

public operator func <(that: Box<T>): Bool

功能:比较 Box 对象的大小。

参数:

  • that:比较的另外一个 Box 对象

返回值:原 Box 对象小于 that Box 对象返回 true,否则返回 false

operator func >=

public operator func >=(that: Box<T>): Bool

功能:比较 Box 对象的大小。

参数:

  • that:比较的另外一个 Box 对象

返回值:原 Box 对象大于等于 that Box 对象返回 true,否则返回 false

operator func <=

public operator func <=(that: Box<T>): Bool

功能:比较 Box 对象的大小。

参数:

  • that:比较的另外一个 Box 对象

返回值:原 Box 对象小于等于 that Box 对象返回 true,否则返回 false

operator func ==

public operator func ==(that: Box<T>): Bool

功能:比较 Box 对象是否相等。

参数:

  • that:比较的另外一个 Box 对象

返回值:相等返回 true,不相等返回 false

operator func !=

public operator func !=(that: Box<T>): Bool

功能:比较 Box 对象是否不相等。

参数:

  • that:比较的另外一个 Box 对象

返回值:不相等返回 true,相等返回 false

func compare

public func compare(that: Box<T>): Ordering

功能:判断当前 Box 实例与另一个 Box 实例的大小关系。

参数:

  • that:比较的另外一个 Box 对象

返回值:如果当前 Box 实例大于 that,返回 Ordering.GT,等于返回 Ordering.EQ,小于返回 Ordering.LT

extend Box <: ToString

extend Box<T> <: ToString where T <: ToString

func toString

public func toString(): String

功能:获取 Box 对象的字符串表示。

返回值:转换后的字符串

class SpawnException

public class SpawnException <: Exception {
    public init()
    public init(message: String)
}

SpawnException 为线程异常类,用于在线程处理过程中发生异常时使用。

init

public init()

功能:构造一个默认的 SpawnException 实例,默认错误信息为空。

init

public init(message: String)

功能:根据异常信息构造一个 SpawnException 实例。

参数:

  • message:预定义消息

class Error

public open class Error <: ToString

Error 是所有错误类的基类。该类不可被继承,不可初始化,但是可以被捕获到。

prop message

public open prop message: String

功能:获取错误信息。

func toString

public open func toString(): String

功能:获取当前 Error 实例的字符串值,包括类名和错误信息。

返回值:错误信息字符串

func printStackTrace

public open func printStackTrace(): Unit

功能:向控制台打印堆栈信息。

func getStackTrace

public func getStackTrace(): Array<StackTraceElement>

功能:获取堆栈信息,每一条堆栈信息用一个 StackTraceElement 实例表示,最终返回一个 StackTraceElement 的数组。

返回值:堆栈信息数组

class Exception

public open class Exception <: ToString {
    public init()
    public init(message: String)
}

Exception 是所有异常类的基类。

init

public init()

功能:构造一个默认的 Exception 实例,默认异常信息为空。

init

public init(message: String)

功能:根据异常信息构造一个 Exception 实例。

参数:

  • message:异常信息

prop message

public open prop message: String

功能:获取异常信息。

func toString

public open func toString(): String

功能:获取当前 Exception 实例的字符串值,包括类名和异常信息。

返回值:异常信息字符串

func printStackTrace

public func printStackTrace(): Unit

功能:向控制台打印堆栈信息。

func getStackTrace

public func getStackTrace(): Array<StackTraceElement>

功能:获取堆栈信息,每一条堆栈信息用一个 StackTraceElement 实例表示,最终返回一个 StackTraceElement 的数组。

返回值:堆栈信息数组

func getClassName

protected open func getClassName(): String

功能:获得类名,用字符串表示。

返回值:类名

class IllegalArgumentException

public class IllegalArgumentException <: Exception {
    public init()
    public init(message: String)
}

IllegalArgumentException 为参数非法异常。

init

public init()

功能:构造一个默认的 IllegalArgumentException 实例,默认异常信息为空。

init

public init(message: String)

功能:根据异常信息构造一个 IllegalArgumentException 实例。

参数:

  • message:异常信息

class IllegalStateException

public class IllegalStateException <: Exception {
    public init()
    public init(message: String)
}

IllegalStateException 为非法状态异常。

init

public init()

功能:构造一个默认的 IllegalStateException 实例,默认异常信息为空。

init

public init(message: String)

参数:

功能:根据异常信息构造一个 IllegalStateException 实例。

  • message:异常信息

class IndexOutOfBoundsException

public class IndexOutOfBoundsException <: Exception {
    public init()
    public init(message: String)
}

IndexOutOfBoundsException 为索引越界异常。

init

public init()

功能:构造一个默认的 IndexOutOfBoundsException 实例,默认异常信息为空。

init

public init(message: String)

功能:根据异常信息构造一个 IndexOutOfBoundsException 实例。

参数:

  • message:异常信息

class NegativeArraySizeException

public class NegativeArraySizeException <: Exception {
    public init()
    public init(message: String)
}

NegativeArraySizeException 为数组索引值为负数异常。

init

public init()

功能:构造一个默认的 NegativeArraySizeException 实例,默认异常信息为空。

init

public init(message: String)

功能:根据异常信息构造一个 NegativeArraySizeException 实例。

参数:

  • message:异常信息

class NoneValueException

public class NoneValueException <: Exception {
    public init()
    public init(message: String)
}

表示 Option 实例的值为 None 的异常类。

init

public init()

功能:构造一个默认的 NoneValueException 实例,默认异常信息为空。。

init

public init(message: String)

功能:根据异常信息构造一个 NoneValueException 实例。

参数:

  • message:异常信息

class UnsupportedException

public class UnsupportedException <: Exception {
    public init()
    public init(message: String)
}

表示功能未支持的异常类。

init

public init()

功能:构造一个默认的 UnsupportedException 实例,默认异常信息为空。

init

public init(message: String)

功能:根据指定异常信息构造 UnsupportedException 实例。

参数:

  • message:异常消息

class InternalError

public class InternalError <: Error

InternalError 继承了 Error,表示内部错误,该类不可初始化,但是可以被捕获到

class OutOfMemoryError

public class OutOfMemoryError <: Error

OutOfMemoryError 继承了 Error,表示内存不足错误,该类不可被继承,不可初始化,但是可以被捕获到

class OverflowException

public class OverflowException <: ArithmeticException {
    public init()
    public init(message: String)
}

OverflowException 为算术运算溢出异常。

init

public init()

功能:构造一个默认的 OverflowException 实例,默认异常信息为空。

init

public init(message: String)

功能:根据指定异常信息构造 OverflowException 实例。

参数:

  • message:异常信息

class IllegalMemoryException

public class IllegalMemoryException <: Exception {
    public init()
    public init(message: String)
}

IllegalMemoryException 为内存操作错误异常。

init

public init()

功能:构造一个默认的 IllegalMemoryException 实例,默认异常信息为空。

init

public init(message: String)

功能:根据指定异常信息构造 IllegalMemoryException 实例。

参数:

  • message:异常信息

class Future

public class Future<T>

spawn 表达式的返回类型是 Future<T> ,其中 T 是类型变元,其类型与闭包表达式的返回类型一致。当我们调用 Future<T> 的 get() 函数时,阻塞当前运行的线程,直到 Future<T> 实例所代表的线程运行结束。

prop thread

public prop thread: Thread

功能:获得当前线程的 Thread 实例

func get

public func get(): T

功能:阻塞当前线程,等待当前 Future 对象对应的线程的结果。

返回值:当前 Future<T> 实例代表的线程运行结束后的返回值

func get

public func get(ns: Int64): Option<T>

功能:阻塞当前线程,等待当前 Future<T> 对象对应的线程的返回值。如果相应的线程在指定时间内未完成执行,则该函数将返回 None。

参数:

  • ns:传入等待时间,单位为纳秒

返回值:如果改线程未完成,返回 None;如果 ns <= 0,等同于 get();如果线程抛出异常退出执行,在 get 调用处将继续抛出该异常

func tryGet

public func tryGet(): Option<T>

功能:尝试获取结果,不会阻塞当前线程。如果相应的线程未执行完成,则该函数返回 None,否则返回 Some。

返回值:如果未完成返回 None,否则返回 Some

func cancel

public func cancel(): Unit

功能:给当前 Future 对象对应的线程发送取消请求。该方法不会立即停止线程执行,仅发送请求,相应地,函数 hasPendingCancellation 可用于检查线程是否存在取消请求。

interface ThreadContext

public interface ThreadContext {
    func end(): Unit
    func hasEnded(): Bool
}

用户创建 thread 时,除了缺省 spawn 表达式入参,也可以通过传入不同 ThreadContext 类型的实例,选择不同的线程上下文,然后一定程度上控制并发行为。

目前不允许用户自行实现 ThreadContext 接口,仓颉语言根据使用场景,提供了MainThreadContext, 具体定义可在终端框架库中查阅。

func end

func end(): Unit

功能:结束方法,用于向当前 context 发送结束请求。

func hasEnded

func hasEnded(): Bool

功能:检查方法,用于检查当前 context 是否已结束。

返回值:如果结束返回 true,否则返回 false

class Thread

public class Thread

Thread 类为用户提供获取线程 ID 及名字、查询线程是否存在取消请求、注册线程未处理异常的处理函数等功能。

该类型实例无法通过构造得到,仅能通过 Future 对象的 thread 属性或是 Thread 类的 currentThread 静态属性获取。

prop currentThread

public static prop currentThread: Thread

功能:获取当前执行线程的 Thread 对象。

prop id

public prop id: Int64

功能:获取当前执行线程的标识,以 Int64 表示,所有存活的线程都有不同标识,但不保证当线程执行结束后复用它的标识。

prop name

public mut prop name: String

功能:获取或是设置线程的名称,获取设置都具有原子性。

prop hasPendingCancellation

public prop hasPendingCancellation: Bool

功能:线程是否存在取消请求,即是否通过 future.cancel() 发送过取消请求。

常见使用方法:Thread.currentThread.hasPendingCancellation。

func handleUncaughtExceptionBy

public static func handleUncaughtExceptionBy(exHandler: (Thread, Exception) -> Unit): Unit

功能:注册线程未处理异常的处理函数当某一线程因异常而提前终止后,如果全局的未处理异常函数被注册,那么将调用该函数并结束线程,在该函数内抛出异常时,将向终端打印提示信息并结束线程,但不会打印异常调用栈信息;如果没有注册全局异常处理函数,那么默认会向终端打印异常调用栈信息。多次注册处理函数时,后续的注册函数将覆盖之前的处理函数。当有多个线程同时因异常而终止时,处理函数将被并发执行,因而开发者需要在处理函数中确保并发正确性。处理函数的参数第一个参数类型为 Thread,是发生异常的线程第二个参数类型为 Exception,是线程未处理的异常。

class ThreadLocal

public class ThreadLocal<T>

使用 ThreadLocal 可以在每个仓颉线程安全地访问他们各自的 “全局变量”。

func get

public func get(): ?T

功能:获得仓颉线程局部变量的值。

返回值:如果当前线程局部变量不为空值,返回该值,如果为空值,返回 Option<T>.None

func set

public func set(value: ?T): Unit

功能:通过 value 设置仓颉线程局部变量的值,如果传入 Option.None,该局部变量的值将被删除,在线程后续操作中将无法获取。

参数:

  • value:需要设置的局部变量的值

struct Range

public struct Range<T> <: Iterable<T> where T <: Countable<T> & Comparable<T> & Equatable<T> {
    public let start: T
    public let end: T
    public let step: Int64
    public let hasStart: Bool
    public let hasEnd: Bool
    public let isClosed: Bool
    public const init(start: T, end: T, step: Int64, hasStart: Bool, hasEnd: Bool, isClosed: Bool)
}

Range 类用于表示拥有固定步长的序列,区间类型是一个泛型,使用 Range<T> 表示。

start

public let start: T

功能:表示开始值。

end

public let end: T

功能:表示结束值。

step

public let step: Int64

功能:表示步长。

hasStart

public let hasStart: Bool

功能:表示是否包含开始值。

hasEnd

public let hasEnd: Bool

功能:表示是否包含结束值。

isClosed

public let isClosed: Bool

功能:表示区间开闭情况,为 true 表示左闭右闭,为 false 表示左闭右开。

init

public const init(start: T, end: T, step: Int64, hasStart: Bool, hasEnd: Bool, isClosed: Bool)

功能:使用该构造函数创建 Range 序列。

参数:

  • start:开始值
  • end:结束值
  • step:步长
  • hasStart:是否有开始值
  • hasEnd:是否有结束值
  • isClosed:true 代表左闭右闭,false 代表左闭右开

异常:

  • IllegalArgumentException:当 step 等于 0 时, 抛出异常

func iterator

public func iterator(): Iterator<T>

功能:获取当前区间的迭代器。

返回值:当前区间的迭代器

func isEmpty

public const func isEmpty(): Bool

功能:判断该区间是否为空。

返回值:如果为空,返回 true,否则返回 false

class RangeIterator

public class RangeIterator<T> <: Iterator<T> where T <: Countable<T> & Comparable<T> & Equatable<T>

RangeIterator 类为 Range 类型的迭代器。

func next

public func next(): Option<T>

功能:获取迭代器中的下一个值。

返回值:迭代器中的下一个值。

func iterator

public func iterator(): Iterator<T>

功能:获取当前迭代器实例。

返回值:当前迭代器实例

class StackOverflowError

public class StackOverflowError <: Error {}

StackOverflowError 类继承了 Error,表示堆栈溢出错误,该类不可被继承,不可初始化,但是可以被捕获到。

func printStackTrace

public override func printStackTrace(): Unit

功能:向控制台打印堆栈信息。

class StackTraceElement

public open class StackTraceElement {
    public let declaringClass: String
    public let methodName: String
    public let fileName: String
    public let lineNumber: Int64
    public init(declaringClass: String, methodName: String, fileName: String, lineNumber: Int64)
}

StackTraceElement 类用于提供具体的堆栈信息。

declaringClass

public let declaringClass: String

类名

methodName

public let methodName: String

函数名

fileName

public let fileName: String

文件名

lineNumber

public let lineNumber: Int64

行号

init

public init(declaringClass: String, methodName: String, fileName: String, lineNumber: Int64)

参数:

  • declaringClass:类名
  • methodName:函数名
  • fileName:文件名
  • lineNumber:行号

struct Array

public struct Array<T> {
    public const init()
    public init(size: Int64, item!: T)
    public init(elements: Collection<T>)
    public init(size: Int64, initElement: (Int64) -> T)
}

Array 类为仓颉数组类型。

init

public const init()

功能:无参构造函数,创建一个空数组。

init

public init(size: Int64, item!: T)

功能:构造一个指定长度的数组,其中元素都用指定初始值进行初始化。

注意:该构造函数不会拷贝 item, 如果 item 是一个引用类型,构造后数组的每一个元素都将指向相同的引用。

参数:

  • size:数组大小
  • item:数组元素初始值

异常:

  • NegativeArraySizeException:当 size 小于 0,抛出异常

init

public init(elements: Collection<T>)

功能:根据 Collection 实例创建数组,把 Collection 实例中所有元素存入数组。

参数:

  • elements:根据该 Collection 实例创建数组

init

public init(size: Int64, initElement: (Int64) -> T)

功能:创建指定长度的数组,其中元素根据初始化函数计算获取。即:将 [0, size) 范围内的值分别传入初始化函数 initElement,执行得到数组对应下标的元素。

参数:

  • size:数组大小
  • initElement:初始化函数

异常:

  • NegativeArraySizeException:当 size 小于 0,抛出异常

func slice

public func slice(start: Int64, len: Int64): Array<T>

功能:返回切片后数组。

参数:

  • start:切片的起始位置
  • len:切片的长度

返回值:返回切片后的数组

异常:

  • IndexOutOfBoundsException:如果 start 小于 0 ,或者 len 小于 0 ,或者 (start + len)的值大于 Array 的长度,抛出异常

func get

public func get(index: Int64): Option<T>

功能:返回数组下标 index 对应的值。

参数:

  • index:要获取值的下标

返回值:Option 类型的值,可以使用 match 解构

func set

public func set(index: Int64, element: T): Unit

功能:修改数组中下标 index 对应的值。

参数:

  • index:需要修改的值的下标
  • element:修改的目标值

异常:

  • IndexOutOfBoundsException:如果 index 小于 0 或者大于或等于 Array 的长度,抛出异常

operator func []

public operator func [](index: Int64): T

功能:获取数组下标 index 对应的值。

参数:

  • index:要获取值的下标

返回值:获取得到的值

异常:

  • IndexOutOfBoundsException:如果 index 小于 0,或大于等于数组长度,抛出异常

operator func []

public operator func [](index: Int64, value!: T): Unit

功能:修改数组中下标 index 对应的值。

参数:

  • index:需要修改值的下标
  • value:修改的目标值

异常:

  • IndexOutOfBoundsException:如果 index 小于 0,或大于等于数组长度,抛出异常

operator func []

public operator func [](range: Range<Int64>): Array<T>

功能:返回切片后的数组。

注意:如果参数 range 是使用 Range 构造函数构造的 Range 实例,有如下行为:

  • start 的值就是构造函数传入的值本身,不受构造时传入的 hashStart 的值的影响
  • hasEnd 为 false 时,该 Range 实例为开区间,不受构造时传入的 isClosed 的值的影响

参数:

  • range:切片的范围,注意这里 range 的步长只能为 1

返回值:新的数组

异常:

  • IllegalArgumentException:如果 range 的步长不等于 1,抛出异常
  • IndexOutOfBoundsException:如果 range 表示的数组范围无效,抛出异常

operator func []

public operator func [](range: Range<Int64>, value!: T): Unit

功能:用指定的值对本数组一个连续范围的元素赋值。

注意:如果参数 range 是使用 Range 构造函数构造的 Range 实例,有如下行为:

  • start 的值就是构造函数传入的值本身,不受构造时传入的 hashStart 的值的影响
  • hasEnd 为 false 时,该 Range 实例为开区间,不受构造时传入的 isClosed 的值的影响

参数:

  • range:数组范围,注意这里 range 的步长只能为 1
  • value:修改的目标值

异常:

  • IllegalArgumentException:如果 range 的步长不等于 1,抛出异常
  • IndexOutOfBoundsException:如果 range 表示的数组范围无效,抛出异常

operator func []

public operator func [](range: Range<Int64>, value!: Array<T>): Unit

功能:用指定的数组对本数组一个连续范围的元素赋值。

注意:如果参数 range 是使用 Range 构造函数构造的 Range 实例,有如下行为:

  • start 的值就是构造函数传入的值本身,不受构造时传入的 hashStart 的值的影响
  • hasEnd 为 false 时,该 Range 实例为开区间,不受构造时传入的 isClosed 的值的影响

参数:

  • range:数组范围,注意这里 range 的步长只能为 1
  • value:修改的目标值

异常:

  • IllegalArgumentException:如果 range 的步长不等于 1,或 range 长度不等于 value 长度,抛出异常
  • IndexOutOfBoundsException:如果 range 表示的数组范围无效,抛出异常

func reverse

public func reverse(): Unit

功能:反转数组,将数组中元素的顺序进行反转。

func clone

public func clone(): Array<T>

功能:克隆数组。

返回值:新的数组

func clone

public func clone(range: Range<Int64>) : Array<T>

功能:按 Range 克隆数组。

注意:如果参数 range 是使用 Range 构造函数构造的 Range 实例,有如下行为:

  • start 的值就是构造函数传入的值本身,不受构造时传入的 hashStart 的值的影响
  • hasEnd 为 false 时,该 Range 实例为开区间,不受构造时传入的 isClosed 的值的影响

参数:

  • range:切片的范围

返回值:新的数组

异常:

  • IndexOutOfBoundsException:range 超出数组范围则抛出此异常

func copyTo

public func copyTo(dst: Array<T>, srcStart: Int64, dstStart: Int64, copyLen: Int64): Unit

功能:将当前数组中的一段数据拷贝到目标数组中。

参数:

  • dst:目标数组
  • srcStart:从 this 数组的 srcStart 下标开始拷贝
  • dstStart:从目标数组的 dstStart 下标开始拷贝
  • copyLen:拷贝数组的长度

异常:

  • IllegalArgumentException:copyLen 小于 0 则抛出此异常
  • IndexOutOfBoundsException:如果 srcStart 或 dstStart 小于 0,或 srcStart 大于或等于 this 数组大小,或 dstStart 大于或等于目标数组大小,或 copyLen 超出范围则抛出此异常。

struct DefaultHasher

public struct DefaultHasher <: Hasher {
 public init(res!: Int64 = 0)
}

DefaultHasher 是 struct 类型,提供了默认的哈希算法实现。

init

public init(res!: Int64 = 0)

功能:构造函数,创建一个 DefaultHasher。

参数:

  • res:哈希结果,默认为 0

func reset

public mut func reset(): Unit

功能:重置哈希值为 0。

func finish

public func finish(): Int64

功能:获取哈希运算的结果。

返回值:哈希运算的结果

func write

public mut func write(value: Bool): Unit

功能:通过该函数把想要哈希运算的 Bool 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: Char): Unit

功能:通过该函数把想要哈希运算的 Char 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: Int8): Unit

功能:通过该函数把想要哈希运算的 Int8 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: Int16): Unit

功能:通过该函数把想要哈希运算的 Int16 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: Int32): Unit

功能:通过该函数把想要哈希运算的 Int32 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: Int64): Unit

功能:通过该函数把想要哈希运算的 Int64 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: UInt8): Unit

功能:通过该函数把想要哈希运算的 UInt8 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: UInt16): Unit

功能:通过该函数把想要哈希运算的 UInt16 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: UInt32): Unit

功能:通过该函数把想要哈希运算的 UInt32 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: UInt64): Unit

功能:通过该函数把想要哈希运算的 UInt64 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: Float16): Unit

功能:通过该函数把想要哈希运算的 Float16 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: Float32): Unit

功能:通过该函数把想要哈希运算的 Float32 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: Float64): Unit

功能:通过该函数把想要哈希运算的 Float64 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

public mut func write(value: String): Unit

功能:通过该函数把想要哈希运算的 String 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

struct LibC

public struct LibC

LibC 是一个 struct 类型,提供了仓颉中较为高频使用的 C 接口。

func malloc

public static func malloc<T>(count!: Int64 = 1): CPointer<T> where T <: CType

功能:在堆中申请对应长度的内存。泛型参数 T 需要是 CType 的子类型。

参数:

  • count:为可选参数,默认为1,表示申请 T 类型的个数

返回值:如果申请成功返回非空指针,内存长度为 sizeOf() * count

func free

public static unsafe func free<T>(p: CPointer<T>): Unit where T <: CType

功能:释放堆内存。

参数:

  • p:表示被释放的内存地址

func mallocCString

public static unsafe func mallocCString(str: String): CString

功能:通过 String 申请与之字符内容相同的 C 风格字符串。

参数:

  • str:仓颉 String 类型

返回值:返回 C 风格字符串,以 '\0' 结束

func free

public static unsafe func free(cstr: CString): Unit

功能:释放 C 风格字符串。

参数:

  • cstr:需要释放的 C 风格字符串

enum Ordering

public enum Ordering {
    | LT
    | GT
    | EQ
}

Ordering 表示一个比较结果的 enum 类型,它包含三种情况小于,大于和等于。

LT

LT

功能:构造一个 Ordering 实例,表示小于。

GT

GT

功能:构造一个 Ordering 实例,表示大于。

EQ

EQ

功能:构造一个 Ordering 实例,表示等于。

enum Option

public enum Option<T> {
    | Some<T>
    | None
}

Option 是一个泛型 enum 类型,它包含两个构造器:SomeNone。其中,Some 会携带一个参数,表示有值,None 不带参数,表示无值。当需要表示某个类型可能有值,也可能没有值的时候,可选择使用 Option 类型。

Some

Some(T)

功能:构造一个携带参数的 Option<T> 实例,表示有值。

None

None

功能:构造一个不带参数的 Option<T> 实例,表示无值。

func getOrThrow

public func getOrThrow(): T

功能:获得值或抛出异常。

返回值:如果当前实例值是 Some<T>,返回类型为 T 的实例

异常:

  • NoneValueException:如果当前实例是 None,抛出异常

func getOrThrow

public func getOrThrow(exception: ()->Exception): T

功能:获得值或抛出指定异常。

参数:

  • exception:异常函数,如果当前实例值是 None,将执行该函数并将其返回值作为异常抛出

返回值:如果当前实例值是 Some<T>,返回类型为 T 的实例

异常:

  • Exception:如果当前实例是 None,抛出异常函数返回的异常

func getOrDefault

public func getOrDefault(other: ()->T): T

功能:获得值或返回默认值。如果 Option 值是 Some,则返回类型为 T 的实例,如果 Option 值是 None,则调用入参,返回类型 T 的值。

参数:

  • other:默认函数,如果当前实例的值是 None,调用该函数得到类型为 T 的实例,并将其返回

返回值:如果当前实例的值是 Some<T>,则返回当前实例携带的类型为 T 的实例,如果 Option 值是 None,调用入参指定的函数,得到类型为 T 的实例,并将其返回

func isNone

public func isNone(): Bool

功能:判断当前实例值是否为 None

返回值:如果当前实例值是 None,则返回 true,否则返回 false

func isSome

public func isSome(): Bool

功能:判断当前实例值是否为 Some

返回值:如果当前实例值是 Some,则返回 true,否则返回 false

extend Option <: ToString

extend Option<T> <: ToString where T <: ToString

Option<T> 枚举实现 ToString 接口。

func toString

public func toString(): String

功能:将 Option 转换为可输出的 String。

返回值:转化后的 String

extend Option <: Equatable

extend Option<T> <: Equatable<Option<T>> where T <: Equatable<T>

Option<T> 枚举实现 Equatable<Option<T>> 接口。

operator func ==

public operator func ==(that: Option<T>): Bool

功能:判断当前实例与指定 Option<T> 实例是否相等。

参数:

  • that:用于与当前实例比较的 Option<T> 实例

返回值:如果相等,则返回 true,否则返回 false

operator func !=

public operator func !=(that: Option<T>): Bool

功能:判断当前实例与指定 Option<T> 实例是否不等。

参数:

  • that:用于与当前实例比较的 Option<T> 实例

返回值:如果不相等,则返回 true,否则返回 false

extend Option <: Hashable

extend Option<T> <: Hashable where T <: Hashable

Option 类型扩展 Hashable。 其中, Some(T) 的 hashCode 等于 T 的值对应的 hashCode,None 的 hashCode 等于 Int64(0)

func hashCode

public func hashCode(): Int64

功能:获取哈希值。

返回值:哈希值

extend Array <: ToString

extend Array<T> <: ToString where T <: ToString

Array<T> 类型扩展 ToString 接口实现。

func toString

public func toString(): String

功能:将数组转换为可输出的 String。

返回值:转化后的 String

extend Array <: Equatable

extend Array<T> <: Equatable<Array<T>> where T <: Equatable<T>

Array<T> 类型扩展 Equatable<Array<T>> 接口实现。

operator func ==

public const operator func ==(that: Array<T>): Bool

功能:判断当前实例与指定 Array<T> 实例是否相等。

参数:

  • that:用于与当前实例比较的 Array<T> 实例

返回值:如果相等,则返回 true,否则返回 false

operator func !=

public const operator func !=(that: Array<T>): Bool

功能:判断当前实例与指定 Array<T> 实例是否不等。

参数:

  • that:用于与当前实例比较的 Array<T> 实例

返回值:如果不相等,则返回 true;相等则返回 false

func contains

public func contains(element: T): Bool

功能:判断 Array 是否包含指定元素。

参数:

  • element:需要判断的元素

返回值:如果存在,则返回 true,否则返回 false

extend Array <: Collection

extend Array<T> <: Collection<T>

func iterator

public func iterator(): Iterator<T>

功能:获取当前数组的迭代器,用于遍历数组。

返回值:当前数组的迭代器

prop size

public prop size: Int64

功能:获取元素数量。

func isEmpty

public func isEmpty(): Bool

功能:判断数组是否为空。

返回值:如果数组为空,返回 true,否则,返回 false

func toArray

public func toArray(): Array<T>

功能:根据当前 Array 生成新 Array 实例。

返回值:与原 Array 完全相同的另一个 Array 对象

struct CPointerHandle

public struct CPointerHandle<T> where T <: CType {
 public let pointer: CPointer<T>
 public let array: Array<T>
 public init()
 public init(ptr: CPointer<T>, arr: Array<T>)
}

CPointerHandle 用来表示获取 Array 原始指针的实例,该类型中的泛型参数应该满足 CType 约束。

pointer

public let pointer: CPointer<T>

功能:获取 Array 对应的原始指针。

array

public let array: Array<T>

功能:原始指针对应的 Array。

init

public init()

功能:默认初始化函数,初始化一个空指针和空数组。

init

public init(ptr: CPointer<T>, arr: Array<T>)

功能:通过传入的 CPointer 和 Array 初始化一个 CPointerHandle。

参数:

  • ptr:CPointer 指针
  • arr:指针对应的 Array

interface ByteExtension

public interface ByteExtension

此接口用于扩展 Byte 类型

extend Byte <: ByteExtension

extend Byte <: ByteExtension

Byte 类型为内置类型,这里是对其添加的一些在 Ascii 字符集范围内的函数。

func isAscii

public func isAscii(): Bool

功能:判断 Byte 是否是在 Ascii 范围内。

返回值:如果 Byte 在 Ascii 范围内返回 true,否则返回 false

func isAsciiLetter

public func isAsciiLetter(): Bool

功能:判断 Byte 是否是在 Ascii 拉丁字母范围内。

返回值:如果 Byte 在 Ascii 拉丁字母范围内返回 true,否则返回 false

func isAsciiNumber

public func isAsciiNumber(): Bool

功能:判断 Byte 是否是在 Ascii 十进制数字范围内。

返回值:如果 Byte 在 Ascii 十进制数字范围内返回 true,否则返回 false

func isAsciiLowerCase

public func isAsciiLowerCase(): Bool

功能:判断 Byte 是否是在 Ascii 小写拉丁字母范围内。

返回值:如果 Byte 在 Ascii 小写拉丁字母范围内返回 true,否则返回 false

func isAsciiUpperCase

public func isAsciiUpperCase(): Bool

功能:判断 Byte 是否是在 Ascii 大写拉丁字母范围内。

返回值:如果 Byte 在 Ascii 大写拉丁字母范围内返回 true,否则返回 false

func isAsciiWhiteSpace

public func isAsciiWhiteSpace(): Bool

功能:判断 Byte 是否是在 Ascii 空白字符范围内。其取值范围为 $[09, 0D] \cup 20$。

返回值:如果 Byte 在 Ascii 空白字符范围内返回 true,否则返回 false

func isAsciiHex

public func isAsciiHex(): Bool

功能:判断 Byte 是否是在 Ascii 十六进制数字范围内。

返回值:如果 Byte 在 Ascii 十六进制数字范围内返回 true,否则返回 false

func isAsciiOct

public func isAsciiOct(): Bool

功能:判断 Byte 是否是在 Ascii 八进制数字范围内。

返回值:如果 Byte 在 Ascii 八进制数字范围内返回 true,否则返回 false

func isAsciiPunctuation

public func isAsciiPunctuation(): Bool

功能:判断 Byte 是否是在 Ascii 标点符号范围内。其取值范围为 $[21, 2F] \cup [3A, 40] \cup [5B, 60] \cup [7B, 7E]$。

返回值:如果 Byte 在 Ascii 标点符号范围内返回 true,否则返回 false

func isAsciiGraphic

public func isAsciiGraphic(): Bool

功能:判断 Byte 是否是在 Ascii 图形字符范围内。其取值范围为 $[21, 7E]$。

返回值:如果 Byte 在 Ascii 图形字符范围内返回 true,否则返回 false

func isAsciiControl

public func isAsciiControl(): Bool

功能:判断 Byte 是否是在 Ascii 控制字符范围内。其取值范围为 $[00, 1F] \cup 7F$。

返回值:如果 Byte 在 Ascii 控制字符范围内返回 true,否则返回 false

func isAsciiNumberOrLetter

public func isAsciiNumberOrLetter(): Bool

功能:判断 Byte 是否是在 Ascii 十进制数字和拉丁字母范围内。

返回值:如果 Byte 在 Ascii 十进制数字和拉丁字母范围内返回 true,否则返回 false

func toAsciiUpperCase

public func toAsciiUpperCase(): Byte

功能:将 Byte 换为对应的 Ascii 大写字符 Byte,如果无法转换则保持现状。

返回值:转换后的 Byte,如果无法转换则返回原来的 Byte

func toAsciiLowerCase

public func toAsciiLowerCase(): Byte

功能:将 Byte 换为对应的 Ascii 小写字符 Byte,如果无法转换则保持现状。

返回值:转换后的 Byte,如果无法转换则返回原来的 Byte

extend Char <: CharExtension

extend Char <: CharExtension

Char 类型为内置类型,这里是对其添加的一些在 Ascii 字符集范围内的函数。

func isAscii

public func isAscii(): Bool

功能:判断字符是否是 Ascii 中的字符。

返回值:如果是 Ascii 字符返回 true,否则返回 false

func isAsciiLetter

public func isAsciiLetter(): Bool

功能:判断字符是否是 Ascii 字母字符。

返回值:如果是 Ascii 字母字符返回 true,否则返回 false

func isAsciiNumber

public func isAsciiNumber(): Bool

功能:判断字符是否是 Ascii 数字字符。

返回值:如果是 Ascii 数字字符返回 true,否则返回 false

func isAsciiLowerCase

public func isAsciiLowerCase(): Bool

功能:判断字符是否是 Ascii 小写字符。

返回值:如果是 Ascii 小写字符返回 true,否则返回 false

func isAsciiUpperCase

public func isAsciiUpperCase(): Bool

功能:判断字符是否是 Ascii 大写字符。

返回值:如果是 Ascii 大写字符返回 true,否则返回 false

func isAsciiWhiteSpace

public func isAsciiWhiteSpace(): Bool

功能:判断字符是否是 Ascii 空白字符。其取值范围为 $[09, 0D] \cup 20$。

返回值:如果是 Ascii 空白字符返回 true,否则返回 false

func isAsciiHex

public func isAsciiHex(): Bool

功能:判断字符是否是 Ascii 十六进制字符。

返回值:如果是 Ascii 十六进制字符返回 true,否则返回 false

func isAsciiOct

public func isAsciiOct(): Bool

功能:判断字符是否是 Ascii 八进制字符。

返回值:如果是 Ascii 八进制字符返回 true,否则返回 false

func isAsciiPunctuation

public func isAsciiPunctuation(): Bool

功能:判断字符是否是 Ascii 标点符号字符。其取值范围为 $[21, 2F] \cup [3A, 40] \cup [5B, 60] \cup [7B, 7E]$。

返回值:如果是 Ascii 标点符号字符返回 true,否则返回 false

func isAsciiGraphic

public func isAsciiGraphic(): Bool

功能:判断字符是否是 Ascii 图形字符。其取值范围为 $[21, 7E]$。

返回值:如果是 Ascii 图形字符返回 true,否则返回 false

func isAsciiControl

public func isAsciiControl(): Bool

功能:判断字符是否是 Ascii 控制字符。其取值范围为 $[00, 1F] \cup 7F$。

返回值:如果是 Ascii 控制字符返回 true,否则返回 false

func isAsciiNumberOrLetter

public func isAsciiNumberOrLetter(): Bool

功能:判断字符是否是 Ascii 数字或拉丁字母字符。

返回值:如果是 Ascii 数字或拉丁字母字符返回 true,否则返回 false

func toAsciiUpperCase

public func toAsciiUpperCase(): Char

功能:将字符转换为 Ascii 大写字符,如果无法转换则保持现状。

返回值:转换后的字符,如果无法转换则返回原来的 Char

func toAsciiLowerCase

public func toAsciiLowerCase(): Char

功能:将字符转换为 Ascii 小写字符,如果无法转换则保持现状。

返回值:转换后的字符,如果无法转换则返回原来的 Char

func fromUtf8

public static func fromUtf8(arr: Array<UInt8>, index: Int64): (Char, Int64)

功能:将 UInt8 数组中的某个元素,通过 UTF-8 转换成字符,并告知字符占用字节长度。

参数:

  • arr:UInt8 数组
  • index:UInt8 数组的下标

返回值:数组中 index 下标元素对应于 UTF-8 所表示的字符 字符字节所占的大小

func intoUtf8Array

public static func intoUtf8Array(c: Char, arr: Array<UInt8>, index: Int64): Int64

功能:该函数会把字符转成字节码序列然后覆盖 Array 数组内指定位置的字节码。

参数:

  • c:字符
  • arr:待覆盖的 Array 数组
  • index:目标位置的起始索引

返回值:字符的字节码长度,例如中文是三个字节码长度

func utf8Size

public static func utf8Size(arr: Array<UInt8>, index: Int64): Int64

功能:该函数会返回指定索引位置字节码对应 UTF-8 字符字节码应有的长度。准确来说,除了 ASCII 码,其他长度的字节码都有首位字节码,首位字节码开头 1 的个数表明了该字符对应的字节码长度,这个函数就用来扫描这个字节码的,如果不是首位字节码,就会抛出异常。

参数:

  • arr:Array 数组
  • index:指定字符的索引

返回值:字符的字节码长度,例如中文是三个字节码长度

异常:

  • IllegalArgumentException:如果索引位置的字节码不符合首位字节码规则,会抛出异常

func utf8Size

public static func utf8Size(c: Char): Int64

功能:返回字符对应的 UTF-8 的字节码长度,例如中文字符是 3 个长度。

参数:

  • c:字符

返回值:字符的字节码长度

func getPreviousFromUtf8

public static func getPreviousFromUtf8(arr: Array<UInt8>, index: Int64): (Char, Int64)

功能:当指定了一个索引,那么函数会找到数组对应索引位置并且根据 UTF-8 规则,查看该字节码是否是字符的首位字节码,如果不是就继续向前遍历,直到该字节码是首位字节码,然后利用字节码序列找到对应的字符。

参数:

  • arr:Array 数组
  • index:指定的索引

返回值:找到的字符 该字符首位字节码在数组中的索引

异常:

  • IllegalArgumentException:如果找不到首位字节码,就会抛出异常

extend Char <: Comparable

extend Char <: Comparable<Char>

func compare

public func compare(rhs: Char): Ordering

功能:判断当前 Char 实例与指定 Char 实例的关系。

参数:

  • rhs:另一个 Char 实例,用于与当前 Char 实例进行比较

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

extend Char <: ToString

extend Char <: ToString

Char 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 Char 转换为可输出的 String。

返回值:转化后的 String

extend Char <: Hashable

extend Char <: Hashable

Char 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:获取哈希值。

返回值:哈希值

extend Char <: Countable

extend Char <: Countable<Char>

Char 类型为内置类型,这里为其扩展 Countable<Char> 接口。

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): Char

功能:与 Int64 数进行加法运算,返回值是 Char 类型。

参数:

  • right:Int64 数

返回值:算数结果

异常:

  • OverflowException:如果与 Int64 数进行加法运算后为不合法的 Unicode 值,抛出异常

extend Int64 <: ToString

extend Int64 <: ToString

Int64 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 Int64 转换为可输出的 String。

返回值:转化后的 String

extend Int64 <: Comparable

extend Int64 <: Comparable<Int64>

Int64 类型为内置类型,这里为其扩展 Comparable<Int64> 接口。

func compare

public func compare(rhs: Int64): Ordering

功能:判断 Int64 与另一个 Int64 的关系。如果等于,返回 Ordering.EQ如果小于,返回 Ordering.LT。

参数:

  • rhs:另一个 Int64 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ如果小于,返回 Ordering.LT

extend Int64 <: Hashable

extend Int64 <: Hashable

Int64 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:获取哈希值。

返回值:哈希值

extend Int64 <: Countable

extend Int64 <: Countable<Int64>

Int64 类型为内置类型,这里为其扩展 Countable<Int64>

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): Int64

功能:与 Int64 数进行加法运算,返回值是 Int64 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend Int32 <: ToString

extend Int32 <: ToString

Int32 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 Int32 转换为可输出的 String。

返回值:转化后的 String

extend Int32 <: Comparable

extend Int32 <: Comparable<Int32>

Int32 类型为内置类型,这里为其扩展 Comparable<Int32> 接口。

func compare

public func compare(rhs: Int32): Ordering

功能:判断 Int32 与另一个 Int32 的关系。

参数:

  • rhs:另一个 Int32 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

extend Int32 <: Hashable

extend Int32 <: Hashable

Int32 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend Int32 <: Countable

extend Int32 <: Countable<Int32>

Int32 类型为内置类型,这里为其扩展 Countable<Int32>

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): Int32

功能:与 Int64 数进行加法运算,返回值是 Int32 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend Int16 <: ToString

extend Int16 <: ToString

Int16 类型为内置类型,这里为其扩展向 String 类型的转换。

func toString

public func toString(): String

功能:将 Int16 转换为可输出的 String。

返回值:转化后的 String

extend Int16 <: Comparable

extend Int16 <: Comparable<Int16>

Int16 类型为内置类型,这里为其扩展 Comparable<Int16> 接口。

func compare

public func compare(rhs: Int16): Ordering

功能:判断 Int16 与另一个 Int16 的关系。

参数:

  • rhs:另一个 Int16 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

extend Int16 <: Hashable

extend Int16 <: Hashable

Int16 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend Int16 <: Countable

extend Int16 <: Countable<Int16>

Int16 类型为内置类型,这里为其扩展 Countable<Int16>

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): Int16

功能:与 Int64 数进行加法运算,返回值是 Int16 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend Int8 <: ToString

extend Int8 <: ToString

Int8 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 Int8 转换为可输出的 String。

返回值:转化后的 String

extend Int8 <: Comparable

extend Int8 <: Comparable<Int8>

Int8 类型为内置类型,这里为其扩展 Comparable<Int8> 接口。

func compare

public func compare(rhs: Int8): Ordering

功能:判断 Int8 与另一个 Int8 的关系。

参数:

  • rhs:另一个 Int8 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ如果小于,返回 Ordering.LT。

extend Int8 <: Hashable

extend Int8 <: Hashable

Int8 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend Int8 <: Countable

extend Int8 <: Countable<Int8>

Int8 类型为内置类型,这里为其扩展 Countable<Int8> 接口。

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): Int8

功能:与 Int64 数进行加法运算,返回值是 Int8 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend IntNative <: ToString

extend IntNative <: ToString

IntNative 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 IntNative 转换为可输出的 String。

返回值:转化后的 String

extend IntNative <: Comparable

extend IntNative <: Comparable<IntNative>

IntNative 类型为内置类型,这里为其扩展 Comparable<IntNative> 接口。

func compare

public func compare(rhs: IntNative): Ordering

功能:判断 IntNative 与另一个 IntNative 的关系。

参数:

  • rhs:另一个 IntNative 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ如果小于,返回 Ordering.LT

extend IntNative <: Hashable

extend IntNative <: Hashable

IntNative 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

extend IntNative <: Countable

extend IntNative <: Countable<IntNative>

IntNative 类型为内置类型,这里为其扩展 Countable<IntNative> 接口。

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): IntNative

功能:与 Int64 数进行加法运算,返回值是 IntNative 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend UInt64 <: ToString

extend UInt64 <: ToString

UInt64 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 UInt64 转换为可输出的 String。

返回值:转化后的 String

extend UInt64 <: Comparable

extend UInt64 <: Comparable<UInt64>

UInt64 类型为内置类型,这里为其扩展 Comparable<UInt64> 接口。

func compare

public func compare(rhs: UInt64): Ordering

功能:判断 UInt64 与另一个 UInt64 的关系。

参数:

  • rhs:另一个 UInt64 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT。

extend UInt64 <: Hashable

extend UInt64 <: Hashable

UInt64 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend UInt64 <: Countable

extend UInt64 <: Countable<UInt64>

UInt64 类型为内置类型,这里为其扩展 Countable<UInt64> 接口。

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): UInt64

功能:与 Int64 数进行加法运算,返回值是 UInt64 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend UInt32 <: ToString

extend UInt32 <: ToString

UInt32 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 UInt32 转换为可输出的 String。

返回值:转化后的 String

extend UInt32 <: Comparable

extend UInt32 <: Comparable<UInt32>

UInt32 类型为内置类型,这里为其扩展 Comparable<UInt32> 接口。

func compare

public func compare(rhs: UInt32): Ordering

功能:判断 UInt32 与另一个 UInt32 的关系。

参数:

  • rhs:另一个 UInt32 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT。

extend UInt32 <: Hashable

extend UInt32 <: Hashable

UInt32 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend UInt32 <: Countable

extend UInt32 <: Countable<UInt32>

UInt32 类型为内置类型,这里为其扩展 Countable<UInt32> 接口。

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): UInt32

功能:与 Int64 数进行加法运算,返回值是 UInt32 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend UInt16 <: ToString

extend UInt16 <: ToString

UInt16 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 UInt16 转换为可输出的 String。

返回值:转化后的 String

extend UInt16 <: Comparable

extend UInt16 <: Comparable<UInt16>

UInt16 类型为内置类型,这里为其扩展 Comparable<UInt16> 接口。

func compare

public func compare(rhs: UInt16): Ordering

功能:判断 UInt16 与另一个 UInt16 的关系如果等于,返回 Ordering.EQ如果小于,返回 Ordering.LT。

参数:

  • rhs:另一个 UInt16 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT。

extend UInt16 <: Hashable

extend UInt16 <: Hashable

UInt16 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend UInt16 <: Countable

extend UInt16 <: Countable<UInt16>

UInt16 类型为内置类型,这里为其扩展 Countable<UInt16> 接口。

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): UInt16

功能:与 Int64 数进行加法运算,返回值是 UInt16 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend UInt8 <: ToString

extend UInt8 <: ToString

UInt8 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 UInt8 转换为可输出的 String。

返回值:转化后的 String

extend UInt8 <: Comparable

extend UInt8 <: Comparable<UInt8>

UInt8 类型为内置类型,这里为其扩展 Comparable<UInt8> 接口。

func compare

public func compare(rhs: UInt8): Ordering

功能:判断 UInt8 与另一个 UInt8 的关系。

参数:

  • rhs:另一个 UInt8 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

extend UInt8 <: Hashable

extend UInt8 <: Hashable

UInt8 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend UInt8 <: Countable

extend UInt8 <: Countable<UInt8>

UInt8 类型为内置类型,这里为其扩展 Countable<UInt8> 接口。

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): UInt8

功能:与 Int64 数进行加法运算,返回值是 UInt8 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend UIntNative <: ToString

extend UIntNative <: ToString

UIntNative 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 UIntNative 转换为可输出的 String。

返回值:转化后的 String

extend UIntNative <: Comparable

extend UIntNative <: Comparable<UIntNative>

UIntNative 类型为内置类型,这里为其扩展 Comparable<UIntNative> 接口。

func compare

public func compare(rhs: UIntNative): Ordering

功能:判断 UIntNative 与另一个 UIntNative 的关系。

参数:

  • rhs:另一个 UIntNative 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

extend UIntNative <: Hashable

extend UIntNative <: Hashable

UIntNative 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend UIntNative <: Countable

extend UIntNative <: Countable<UIntNative>

UIntNative 类型为内置类型,这里为其扩展 Countable<UIntNative> 接口。

func position

public func position(): Int64

功能:转成 Int64 类型。

返回值:转换后的 Int64 值

func next

public func next(right: Int64): UIntNative

功能:与 Int64 数进行加法运算,返回值是 UIntNative 类型。

参数:

  • right:Int64 数

返回值:算数结果

extend Float64 <: ToString

extend Float64 <: ToString

Float64 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换,默认保留 6 位小数,如需其他精度 String 请参考 Formatter 扩展。

func toString

public func toString(): String

功能:将 Float64 转换为可输出的 String。

返回值:转化后的 String

extend Float64 <: Comparable

extend Float64 <: Comparable<Float64>

Float64 类型为内置类型,这里为其扩展 Comparable<Float64> 接口。

func compare

public func compare(rhs: Float64): Ordering

功能:判断 Float64 与另一个 Float64 的关系。

参数:

  • rhs:另一个 Float64 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

extend Float64 <: Hashable

extend Float64 <: Hashable

Float64 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend Float32 <: ToString

extend Float32 <: ToString

Float32 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换,默认保留 6 位小数,如需其他精度 String 请参考 Formatter 扩展。

func toString

public func toString(): String

功能:将 Float32 转换为可输出的 String。

返回值:转化后的 String

extend Float32 <: Comparable

extend Float32 <: Comparable<Float32>

Float32 类型为内置类型,这里为其扩展 Comparable<Float32> 接口。

func compare

public func compare(rhs: Float32): Ordering

功能:判断 Float32 与另一个 Float32 的关系。

参数:

  • rhs:另一个 Float32 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

extend Float32 <: Hashable

extend Float32 <: Hashable

Float32 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend Float16 <: ToString

extend Float16 <: ToString

Float16 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换,默认保留 6 位小数,如需其他精度 String 请参考 Formatter 扩展。

func toString

public func toString(): String

功能:将 Float16 转换为可输出的 String。

返回值:转化后的 String

extend Float16 <: Comparable

extend Float16 <: Comparable<Float16>

Float16 类型为内置类型,这里为其扩展 Comparable<Float16> 接口。

func compare

public func compare(rhs: Float16): Ordering

功能:判断 Float16 与另一个 Float16 的关系。

参数:

  • rhs:另一个 Float16 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

extend Float16 <: Hashable

extend Float16 <: Hashable

Float16 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值

extend Ordering <: ToString

extend Ordering <: ToString

Ordering 类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 Ordering 转换为可输出的 String。

返回值:转化后的 String

extend Ordering <: Comparable

extend Ordering <: Comparable<Ordering>

Ordering 类型,这里为其扩展 Comparable<Ordering> 接口。

func compare

public func compare(that: Ordering): Ordering

功能:判断 Ordering 与另一个 Ordering 的关系。

参数:

  • that:另一个 Ordering 对象

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

operator func ==

public operator func ==(that: Ordering): Bool

功能:判断两个 Ordering 是否相等。

参数:

  • that:传入 Ordering

返回值:如果相等,则返回 true,否则返回 false

operator func !=

public operator func !=(that: Ordering): Bool

功能:判断两个 Ordering 是否不相等。

参数:

  • that:传入 Ordering

返回值:如果不相等,则返回 true,否则返回 false

operator func <=

public operator func <=(that: Ordering): Bool

功能:判断两个 Ordering 是否为小于等于关系。

参数:

  • that:传入 Ordering

返回值:如果为小于等于关系,则返回 true,否则返回 false

operator func <

public operator func <(that: Ordering): Bool

功能:判断两个 Ordering 是否为小于关系。

参数:

  • that:传入 Ordering

返回值:如果为小于关系,则返回 true,否则返回 false

operator func >=

public operator func >=(that: Ordering): Bool

功能:判断两个 Ordering 是否为大于等于关系。

参数:

  • that:传入 Ordering

返回值:如果为大于等于关系,则返回 true,否则返回 false

operator func >

public operator func >(that: Ordering): Bool

功能:判断两个 Ordering 是否为大于关系。

参数:

  • that:传入 Ordering

返回值:如果为大于关系,则返回 true,否则返回 false

extend Ordering <: Hashable

extend Ordering <: Hashable

Ordering 类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:获取哈希码。

返回值:哈希值,Ordering.GT 的哈希值是 3,Ordering.EQ 的哈希值是 2,Ordering.LT 的哈希值是 1

extend Range <: Hashable

extend Range<T> <: Hashable where T <: Hashable & Countable<T> & Comparable<T> & Equatable<T>

Range 类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:获取哈希值。

返回值:哈希值

extend Range <: Equatable

extend Range<T> <: Equatable<Range<T>> where T <: Countable<T> & Comparable<T> & Equatable<T>

Range 类型,这里为其扩展 Equatable<Range<T>> 接口。

operator func ==

public operator func ==(that: Range<T>): Bool

功能:判断两个 Range 是否相等。

返回值:true 代表相等,false 代表不相等

operator func !=

public operator func !=(that: Range<T>): Bool

功能:判断两个 Range 是否不相等。

返回值:true 代表不相等,false 代表相等

extend Bool <: ToString

extend Bool <: ToString

Bool 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 Bool 转换为可输出的 String。

返回值:转化后的 String

extend Bool <: Hashable

extend Bool <: Hashable

Bool 类型为内置类型,这里为其扩展 Hashable 接口。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

extend Bool <: Equatable

extend Bool <: Equatable<Bool>

Bool 类型为内置类型,这里为其扩展 Equatable<Bool> 接口。

extend Unit <: ToString

extend Unit <: ToString

Unit 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func toString

public func toString(): String

功能:将 Unit 转换为可输出的 String,其值为 "()"。

返回值:转化后的 String

extend Unit <: Hashable

extend Unit <: Hashable

Unit 类型为内置类型,这里为其扩展 Hashable 接口,其函数返回值为 0。

func hashCode

public func hashCode(): Int64

功能:返回哈希值。

返回值:哈希值,为 0

extend CPointer

extend CPointer<T>

CPointer 类型为内置类型,在与 C 互操作中,该数据类型映射为 C 语言中的指针类型。这里扩展一些必要的指针使用相关接口,包含判空、读写数据等接口。其中泛型 T 为指针类型,其满足 CType 约束( CType 接口描述见后面 interface CType 章节)。对 CPointer 做运算需要在 unsafe 上下文中进行。

func isNull

public func isNull(): Bool

功能:判断指针是否是空。

返回值:如果是空返回 true,否则返回 false

func isNotNull

public func isNotNull(): Bool

功能:判断指针是否不是空。

返回值:如果不是空返回 true,否则返回 false

func toUIntNative

public func toUIntNative(): UIntNative

功能:获取该指针的整型形式。

返回值:返回该指针的整型形式

func read

public unsafe func read(): T

功能:读取第一个数据,该接口需要用户保证指针的合法性,否则发生未定义行为。

返回值:返回该对象类型的第一个数据

func write

public unsafe func write(value: T): Unit

功能:写入一个数据,该数据总是在第一个,该接口需要用户保证指针的合法性,否则发生未定义行为。

参数:

  • value:要写入的数据

func read

public unsafe func read(idx: Int64): T

功能:根据下标读取对应的数据,该接口需要用户保证指针的合法性,否则发生未定义行为。

参数:

  • idx:要获取数据的下标

返回值:输入下标对应的数据

func write

public unsafe func write(idx: Int64, value: T): Unit

功能:在指定下标位置写入一个数据,该接口需要用户保证指针的合法性,否则发生未定义行为。

参数:

  • idx:指定的下标位置
  • value:写入的数据

operator func +

public unsafe operator func +(offset: Int64): CPointer<T>

功能:CPointer 对象指针后移,同 C 语言的指针加法操作。

参数:

  • offset:偏移量

返回值:返回地址变动后的对象

operator func -

public unsafe operator func -(offset: Int64): CPointer<T>

功能:CPointer 对象指针前移,同 C 语言的指针减法操作。

参数:

  • offset:偏移量

返回值:返回地址变动后的对象

func asResource

public func asResource(): CPointerResource<T>

功能:获取 CPointerResource 对象。

返回值:表示 CPointerResource 对象

struct CPointerResource

public struct CPointerResource<T> <: Resource where T <: CType {
 public var value: CPointer<T>
}

CPointer 对应的资源管理类型,其实例可以通过 CPointer 的成员函数 asResource 获取。

value

public var value: CPointer<T>

功能:表示当前实例管理的 CPointer<T> 类型实例。

func isClosed

public func isClosed(): Bool

功能:判断该指针内容是否已被释放。

返回值:返回 true 为已释放。

func close

public func close(): Unit

功能:释放其管理的 CPointer<T> 实例指向的内容。

extend CString

extend CString

CString 类型是内置类型,在与 C 互操作中,该数据类型映射为 C 语言中的字符串 char* 类型。这里扩展一些必要的字符串使用相关接口,包含判空、获取长度、截取等接口。 CString 提供了一个内建构造函数,接受 CPointer<UInt8> 类型的参数。

注意:如果使用 CString 需要将 \0 当做结束符。

init

public init(p: CPointer<UInt8>)

功能:通过字符串指针进行构造 CString ,该接口需要用户保证指针的合法性。

参数:

  • p:使用已有的 UInt8 类型字符串指针进行构造

extend CString <: ToString

extend CString <: ToString

CString 类型为内置类型,这里为其扩展 ToString 接口,实现向 String 类型的转换。

func getChars

public func getChars(): CPointer<UInt8>

功能:获取字符串的指针。

返回值:该字符串的指针

func isNull

public func isNull(): Bool

功能:判断字符串指针是否为空。

返回值:如果字符串指针为空,返回 true,否则返回 false

func size

public func size(): Int64

功能:返回该字符串长度,同 C 语言中的 strlen。

返回值:字符串长度

func isEmpty

public func isEmpty(): Bool

功能:判断字符串是否为空字符串。

返回值:如果为空字符串或字符串指针为空,返回 true,否则返回 false

func isNotEmpty

public func isNotEmpty(): Bool

功能:判断字符串是否不为空字符串。

返回值:如果不为空字符串,返回 true,如果字符串指针为空,返回 false

func startsWith

public func startsWith(prefix: CString): Bool

功能:判断字符串是否包含指定前缀。

参数:

  • prefix:匹配的目标前缀字符串

返回值:如果该字符串包含 prefix 前缀,返回 true,如果该字符串不包含 prefix 前缀,返回 false,特别地,如果原字符串或者 prefix 前缀字符串指针为空,均返回 false

func endsWith

public func endsWith(suffix: CString): Bool

功能:判断字符串是否包含指定后缀。

参数:

  • suffix:匹配的目标后缀字符串

返回值:如果该字符串包含 suffix 后缀,返回 true,如果该字符串不包含 suffix 后缀,返回 false,特别地,如果原字符串或者 suffix 后缀字符串指针为空,均返回 false

func equals

public func equals(rhs: CString): Bool

功能:判断字符串两个字符串是否相等。

参数:

  • rhs:比较的目标字符串

返回值:如果两个字符串相等,返回 true,否则返回 false

func equalsLower

public func equalsLower(rhs: CString): Bool

功能:判断字符串两个字符串是否相等,且忽略大小写。

参数:

  • rhs:匹配的目标字符串

返回值:如果两个字符串忽略大小写相等,返回 true,否则返回 false

func subCString

public func subCString(beginIndex: UIntNative): CString

功能:截取指定位置开始至字符串结束的子串,需要注意,该接口返回为字符串的副本,返回的子串使用完后需要手动 free。

参数:

  • beginIndex:截取的起始位置

返回值:截取的子串,如果 beginIndex 与字符串长度相等,返回空字符串(空指针)

异常:

  • IndexOutOfBoundsException:如果 beginIndex 大于字符串长度,抛出异常
  • IllegalMemoryException:如果内存申请失败或内存拷贝失败时,抛出异常

func subCString

public func subCString(beginIndex: UIntNative, subLen: UIntNative): CString

功能:截取字符串的子串,指定起始位置和截取长度,如果截取的末尾位置超出字符串长度,截取至字符串末尾,需要注意,该接口返回为字符串的副本,返回的子串使用完后需要手动 free。

参数:

  • beginIndex:截取的起始位置
  • subLen:截取长度

返回值:截取的子串,如果 beginIndex 与字符串长度相等,返回空字符串(空指针)

异常:

  • IndexOutOfBoundsException:如果 beginIndex 大于字符串长度,抛出异常
  • IllegalMemoryException:如果内存申请失败或内存拷贝失败时,抛出异常

func compare

public func compare(str: CString): Int32

功能:按字典序比较两个字符串,同 C 语言中的 strcmp ,如果被比较的两个 CString 中存在空指针,该接口会抛出异常。

参数:

  • str:比较的目标字符串

返回值:整数结果,相等时返回0,当该字符串比 str 小时,返回 -1,否则返回 1

异常:

  • Exception:如果自身的数据指针或 str 为 null,抛出异常

func toString

public func toString(): String

功能:将 CString 类型转为仓颉的 String 类型。

返回值:转换后的字符串

func asResource

public func asResource(): CStringResource

功能:获取 CStringResource 对象。

返回值:表示 CStringResource 对象

struct CStringResource

public struct CStringResource <: Resource {
 public let value: CString
}

CString 对应的资源管理类型,其实例可以通过 CString 的成员函数 asResource 获取。

value

public let value: CString

功能:表示当前实例管理的 CString 资源。

func isClosed

public func isClosed(): Bool

功能:判断该字符串是否被释放。

返回值:返回 true 为已释放。

func close

public func close(): Unit

功能:释放当前实例管理的 CString 类型实例指向的内容。

type Byte

public type Byte = UInt8

Byte 类型是内置类型 UInt8 的别名。

type Int

public type Int = Int64

Int 类型是内置类型 Int64 的别名。

type UInt

public type UInt = UInt64

UInt 类型是内置类型 UInt64 的别名。

func print

public func print(str: String, flush!: Bool = false): Unit

功能:向控制台输出数据。

注意:下列 print、 println、 eprint、 eprintln 函数默认为 UTF-8 编码,windows 环境需手动执行命令 chcp 65001(将 cmd 更改为 UTF-8 编码)。

参数:

  • str:待输出的 String 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(str: String): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • str:待输出的 String 类型

func print

public func print<T>(arg: T, flush!: Bool = false): Unit where T <: ToString

功能:向控制台输出数据。

参数:

  • arg:待输出的数据,支持实现了 ToString 接口的类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println<T>(arg: T): Unit where T <: ToString

功能:向控制台输出数据,输出末尾换行。

参数:

  • arg:待输出的数据,支持实现了 ToString 接口的类型

func print

public func print(b: Bool, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • b:待输出的 Bool 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(b: Bool): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • b:待输出的 Bool 类型

func print

public func print(c: Char, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • c:待输出的 Char 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(c: Char): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • c:待输出的 Char 类型

func print

public func print(f: Float16, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • f:待输出的 Float16 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(f: Float16): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • f:待输出的 Float16 类型

func print

public func print(f: Float32, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • f:待输出的 Float32 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(f: Float32): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • f:待输出的 Float32 类型

func print

public func print(f: Float64, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • f:待输出的 Float64 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(f: Float64): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • f:待输出的 Float64 类型

func print

public func print(i: Int8, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • i:待输出的 Int8 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(i: Int8): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • i:待输出的 Int8 类型

func print

public func print(i: Int16, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • i:待输出的 Int16 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(i: Int16): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • i:待输出的 Int16 类型

func print

public func print(i: Int32, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • i:待输出的 Int32 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(i: Int32): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • i:待输出的 Int32 类型

func print

public func print(i: Int64, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • i:待输出的 Int64 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(i: Int64): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • i:待输出的 Int64 类型

func print

public func print(i: UInt8, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • i:待输出的 UInt8 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(i: UInt8): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • i:待输出的 UInt8 类型

func print

public func print(i: UInt16, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • i:待输出的 UInt16 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(i: UInt16): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • i:待输出的 UInt16 类型

func print

public func print(i: UInt32, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • i:待输出的 UInt32 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(i: UInt32): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • i:待输出的 UInt32 类型

func print

public func print(i: UInt64, flush!: Bool = false): Unit

功能:向控制台输出数据。

参数:

  • i:待输出的 UInt64 类型
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(i: UInt64): Unit

功能:向控制台输出数据,输出末尾换行。

参数:

  • i:待输出的 UInt64 类型

func eprintln

public func eprintln(str: String): Unit

功能:eprintln 用于打印错误消息,如抛出异常,消息将打印到标准错误文本流,而不是标准输出,输出末尾换行。

参数:

  • str:待输出的字符串

func eprint

public func eprint(str: String, flush!: Bool = true): Unit

功能:eprint 用于打印错误消息,如抛出异常,消息将打印到标准错误文本流,而不是标准输出。

参数:

  • str:待输出的字符串
  • flush:是否清空缓存,true 清空,false 不清空,默认 false

func println

public func println(): Unit

功能:向标准输出(stdout)输出换行字符。

func ifSome

public func ifSome<T>(o: Option<T>, action: (T) -> Unit): Unit

功能:如果输入是 Option.Some 类型数据,则执行 action 函数。

参数:

  • o:Option 类型输入
  • action:待执行函数

func ifNone

public func ifNone<T>(o: Option<T>, action: () -> Unit): Unit

功能:如果输入是 Option.None 类型数据,则执行 action 函数。

参数:

  • o:Option 类型输入
  • action:待执行函数

func acquireArrayRawData

public unsafe func acquireArrayRawData<T>(arr: Array<T>): CPointerHandle<T> where T <: CType

功能:获取 Array 中数据的原始指针实例,T 需要满足 CType 约束,指针使用完后需要及时用 releaseArrayRawData 函数释放该指针约束:getRaw() 与 releaseRaw() 之间仅可包含简单的 foreign C 函数调用等逻辑,不构造例如 CString 等的仓颉对象,否则可能造成不可预期现象。

返回值:返回的原始指针实例

func releaseArrayRawData

public unsafe func releaseArrayRawData<T>(handle: CPointerHandle<T>): Unit where T <: CType

功能:释放 acquireArrayRawData 获取的原始指针实例。

参数:

  • handle:待释放的指针实例

func refEq

public func refEq(a: Object, b: Object): Bool

功能:判断两个 Object 对象的内存地址是否是同一个。

参数:

  • a:一个 Object 对象
  • b:另一个 Object 对象

返回值:同一个返回 true,否则返回 false

func CJ_CORE_AddAtexitCallback

public func CJ_CORE_AddAtexitCallback(callback: () -> Unit): Unit

功能:注册退出函数,此处建议使用 os 库内 Process.atexit 函数,除非有制作进程管理框架的需要,否则不建议使用。

参数:

  • callback:回调函数

func CJ_CORE_ExecAtexitCallbacks

public func CJ_CORE_ExecAtexitCallbacks(): Unit

功能:执行注册函数, 此处建议使用 os 库内 Process.exit 函数,除非有制作进程管理框架的需要,否则不建议使用。

func zeroValue

public unsafe func zeroValue<T>(): T

功能:获取一个已全零初始化的 T 类型实例,这个实例一定要赋值为正常初始化的值,再使用。

返回值:一个已全零初始化的 T 类型实例

func sizeOf

public func sizeOf<T>(): UIntNative where T <: CType

功能:获取类型 T 所占用的内存空间大小。

返回值:类型 T 所占用内存空间的字节数

func alignOf

public func alignOf<T>(): UIntNative where T <: CType

功能:获取类型 T 的内存对齐值。

返回值:对类型 T 做内存对齐的字节数

interface Countable

public interface Countable<T> {
    func next(right: Int64): T
    func position(): Int64
}

该接口表示类型可数。

func next

func next(right: Int64): T

功能:返回两个实例的和。

参数:

  • right:另一个为 Int64 类型的实例

返回值:返回两个实例和的 T 类型

func position

func position(): Int64

功能:将对象转为 Int64 类型。

返回值:转换后的 Int64 整型

interface Collection

public interface Collection<T> <: Iterable<T> {
    prop size: Int64
    func isEmpty(): Bool
    func toArray(): Array<T>
}

该接口用来表示集合。

prop size

prop size: Int64

功能:获取当前实例的长度。

返回值:列表的容量

func isEmpty

func isEmpty(): Bool

功能:判断当前实例是否为空。

返回值:如果为空返回 true,否则返回 false

func toArray

func toArray(): Array<T>

功能:将当前实例转为数组类型。

返回值:转换后的数组

interface Less

public interface Less<T> {
    operator func <(rhs: T): Bool
}

该接口表示小于计算。

operator func <

operator func <(rhs: T): Bool

功能:判断一个实例是否小于另一个实例。

参数:

  • rhs:另一个实例

返回值:如果小于,返回 true,否则返回 false

interface Greater

public interface Greater<T> {
    operator func >(rhs: T): Bool
}

该接口表示大于计算。

operator func >

operator func >(rhs: T): Bool

功能:判断一个实例是否大于另一个实例。

参数:

  • rhs:另一个实例

返回值:如果大于,返回 true,否则返回 false

interface LessOrEqual

public interface LessOrEqual<T> {
    operator func <=(rhs: T): Bool
}

该接口表示小于等于计算。

operator func <=

operator func <=(rhs: T): Bool

功能:判断一个实例是否小于等于另一个实例。

参数:

  • rhs:另一个实例

返回值:如果小于等于,返回 true,否则返回 false

interface GreaterOrEqual

public interface GreaterOrEqual<T> {
    operator func >=(rhs: T): Bool
}

该接口表示大于等于计算。

operator func >=

operator func >=(rhs: T): Bool

功能:判断一个实例是否大于等于另一个实例。

参数:

  • rhs:另一个实例

返回值:如果大于等于,返回 true,否则返回 false

interface Comparable

public interface Comparable<T> <: Equatable<T> & Less<T> & Greater<T> & LessOrEqual<T> & GreaterOrEqual<T> {
    func compare(that: T): Ordering
}

该接口表示比较运算。

func compare

func compare(that: T): Ordering

功能:判断一个实例与另一个实例的关系。

参数:

  • that:另一个实例

返回值:如果大于,返回 Ordering.GT,如果等于,返回 Ordering.EQ,如果小于,返回 Ordering.LT

interface Equal

public interface Equal<T> {
    operator func ==(rhs: T): Bool
}

该接口用来提供 Bool 、Char、Int64、Int32、Int16、Int8、UIntNative、UInt64、UInt32、UInt16、UInt8、Float64、Float32、Float16、String、Array、Box、ArrayList、HashSet 的直接判等操作。

operator func ==

operator func ==(rhs: T): Bool

功能:判断两个实例是否相等。

参数:

  • rhs:另一个实例

返回值:如果相等,返回 true,否则返回 false

interface NotEqual

public interface NotEqual<T> {
    operator func !=(rhs: T): Bool
}

该接口表示判不等运算。

operator func !=

operator func !=(rhs: T): Bool

功能:判断两个实例是否不相等。

参数:

  • rhs:另一个实例

返回值:如果不相等,返回 true,否则返回 false

interface Equatable

public interface Equatable<T> <: Equal<T> & NotEqual<T>

该接口表示判等和判不等两种运算。

interface Hashable

public interface Hashable {
    func hashCode(): Int64
}

该接口用来提供 Bool、Char、IntNative、Int64、Int32、Int16、Int8、UIntNative、UInt64、UInt32、UInt16、UInt8、Float64、Float32、Float16、String、Box 的哈希值。

func hashCode

func hashCode(): Int64

功能:获得实例类型的哈希值。

返回值:返回实例类型的哈希值

interface Iterable

public interface Iterable<E> {
    func iterator(): Iterator<E>
}

该接口用来提供具体类型的迭代器。

func iterator

func iterator(): Iterator<E>

功能:返回实例类型的迭代器。

返回值:返回实例类型的迭代器

interface Iterator

public interface Iterator<E> <: Iterable<E> {
    func next(): Option<E>
}

该接口用来返回迭代过程中的下一个元素。

func next

func next(): Option<E>

功能:返回迭代过程中的下一个元素。

返回值:迭代过程中的下一个元素

interface Resource

public interface Resource {
    func isClosed(): Bool
    func close(): Unit
}

该接口用于资源管理,需要用 close 释放资源的类可实现该接口。

func isClosed

func isClosed(): Bool

功能:判断资源是否关闭。

返回值:如果已经关闭返回 true,否则返回 false

func close

func close(): Unit

功能:关闭资源。

interface ToString

public interface ToString {
    func toString(): String
}

该接口用来提供具体类型的字符串表示。

func toString

func toString(): String

功能:返回实例类型的字符串表示。

返回值:返回实例类型的字符串表示

interface CType

sealed interface CType

此接口不包含任何函数,它可以作为所有 C 互操作支持的类型的父类型,便于在泛型约束中使用。

需要注意的是:

  • CType 接口是仓颉中的一个接口类型,它本身不满足 CType 约束;
  • CType 接口不允许被继承、扩展;
  • CType 接口不会突破子类型的使用限制。

type Rune

public type Rune = Char

重命名 Char 类型。

struct String

public struct String <: Collection<Byte> & Equatable<String> & Comparable<String> & Hashable & ToString {
    public static let empty: String
    public const init()
    public init(value: Array<Rune>)
    public init(value: Collection<Rune>)
}

此类主要用于实现 String 数据结构及相关操作函数。

empty

public static let empty: String

功能:创建一个空的字符串并返回。

init

public const init()

功能:无参构造函数,创建一个空的字符串。

init

public init(value: Array<Rune>)

功能:通过 Rune 数组创建一个字符串。

参数:

  • value:Rune 数组

init

public init(value: Collection<Rune>)

功能:通过 Rune 列表创建一个字符串。

参数:

  • value:Rune 列表

func getRaw

public unsafe func getRaw(): CPointerHandle<UInt8>

功能:返回当前 String 的原始指针,用于和C语言交互,使用完后需要 releaseRaw 函数释放该指针。

约束:getRaw() 与 releaseRaw() 之间仅可包含简单的 foreign C 函数调用等逻辑,不构造例如 CString 等的仓颉对象,否则可能造成不可预知的错误。

返回值:返回的原始指针实例

func releaseRaw

public unsafe func releaseRaw(cp: CPointerHandle<UInt8>): Unit

功能:释放 getRaw 函数获取的指针。

注意:释放时只能释放同一个 String 获取的指针,如果释放了其他 String 获取的指针,会出现不可预知的错误。

参数:

  • cp:待释放的指针实例

func iterator

public func iterator(): Iterator<Byte>

功能:获得字符串的 UTF-8 编码字节迭代器,可用于支持 for-in 循环。

返回值:字符串的 UTF-8 编码字节迭代器

func runes

public func runes(): Iterator<Rune>

功能:获得字符串的 Rune 迭代器。

返回值:字符串的 Rune 迭代器

func tryGet

public func tryGet(index: Int64): Option<Byte>

功能:返回字符串下标 index 对应的 UTF-8 编码字节值。

参数:

  • index:要获取的字节值的下标

返回值:获取得到下标对应的 UTF-8 编码字节值,当 index 小于 0 或者大于等于字符串长度,则返回 Option<Byte>.None

func toRuneArray

public func toRuneArray(): Array<Rune>

功能:返回字符串的 Rune 数组。如果原字符串为空字符串,则返回空数组。

返回值:字符串的 Rune 数组

func toString

public func toString(): String

功能:获得字符串本身。

返回值:返回字符串本身

func clone

public func clone(): String

功能:返回原字符串的拷贝。

返回值:拷贝得到的新字符串

prop size

public prop size: Int64

功能:返回字符串 UTF-8 编码后的字节长度

返回值:字符串 UTF-8 编码后的字节长度

func isEmpty

public func isEmpty(): Bool

功能:判断原字符串是否为空字符串。

返回值:如果为空返回 true,否则返回 false

func isAsciiBlank

public func isAsciiBlank(): Bool

功能:判断字符串是否为空或者字符串中的所有 Rune 都是 ascii 码的空白字符(包括:0x09、0x10、0x11、0x12、0x13、0x20)。

返回值:如果是返回 true,否则返回 false

func hashCode

public func hashCode(): Int64

功能:获得字符串的哈希值。

返回值:返回字符串的哈希值

func indexOf

public func indexOf(b: Byte): Option<Int64>

功能:返回指定字节 b 第一次出现的在原字符串内的索引。

参数:

  • b:搜索的 UTF-8 编码字节

返回值:如果原字符串中包含指定字节,返回其第一次出现的索引,如果原字符串中没有此字节,返回 Option<Int64>.None

func indexOf

public func indexOf(b: Byte, fromIndex: Int64): Option<Int64>

功能:从原字符串指定索引开始搜索,获取指定 UTF-8 编码字节第一次出现的在原字符串内的索引。

参数:

  • b:搜索的 UTF-8 编码字节
  • fromIndex:以指定的索引 fromIndex 开始搜索

返回值:如果搜索成功,返回指定字节第一次出现的索引,否则返回 Option<Int64>.None。特别地,当 fromIndex 小于零,效果同 0,当 fromIndex 大于等于原字符串长度,返回 Option<Int64>.None

func indexOf

public func indexOf(str: String): Option<Int64>

功能:返回指定字符串 str 在原字符串中第一次出现的起始索引。

参数:

  • str:搜索的字符串

返回值:如果原字符串包含 str 字符串,返回其第一次出现的索引,如果原字符串中没有 str 字符串,返回 None

func indexOf

public func indexOf(str: String, fromIndex: Int64): Option<Int64>

功能:从原字符串 fromIndex 索引开始搜索,获取指定字符串 str 第一次出现的在原字符串的起始索引。

参数:

  • str:待搜索的字符串
  • fromIndex:以指定的索引 fromIndex 开始搜索。

返回值:如果搜索成功,返回 str 第一次出现的索引,否则返回 None。特别地,当 str 是空字符串时,如果fromIndex 大于 0,返回 None,否则返回 Some(0)。当 fromIndex 小于零,效果同 0,当 fromIndex 大于等于原字符串长度返回 None

func lastIndexOf

public func lastIndexOf(b: Byte): Option<Int64>

功能:返回指定 UTF-8 编码字节 b 最后一次出现的在原字符串内的索引。

参数:

  • b:搜索的 UTF-8 编码字节

返回值:如果原字符串中包含此字节,返回其最后一次出现的索引,否则返回 Option<Int64>.None

func lastIndexOf

public func lastIndexOf(b: Byte, fromIndex: Int64): Option<Int64>

功能:从原字符串 fromIndex 索引开始搜索,返回指定 UTF-8 编码字节 b 最后一次出现的在原字符串内的索引。

参数:

  • b:搜索的 UTF-8 编码字节
  • fromIndex:以指定的索引 fromIndex 开始搜索

返回值:如果搜索成功,返回指定字节最后一次出现的索引,否则返回 Option<Int64>.None。特别地,当 fromIndex 小于零,效果同 0,当 fromIndex 大于等于原字符串长度,返回 Option<Int64>.None

func lastIndexOf

public func lastIndexOf(str: String): Option<Int64>

功能:返回指定字符串 str 最后一次出现的在原字符串的起始索引。

参数:

  • str:搜索的字符串

返回值:如果原字符串中包含 str 字符串,返回其最后一次出现的索引,否则返回 Option<Int64>.None

func lastIndexOf

public func lastIndexOf(str: String, fromIndex: Int64): Option<Int64>

功能:从原字符串指定索引开始搜索,获取指定字符串 str 最后一次出现的在原字符串的起始索引。

参数:

  • str:待搜索的字符串
  • fromIndex:以指定的索引 fromIndex 开始搜索

返回值:如果这个字符串在位置 fromIndex 及其之后没有出现,则返回 Option<Int64>.None。特别地,当 str 是空字符串时,如果 fromIndex 大于 0,返回 Option<Int64>.None,否则返回 Option<Int64>.Some(0),当 fromIndex 小于零,效果同 0,当 fromIndex 大于等于原字符串长度返回 Option<Int64>.None

func isAscii

public func isAscii(): Bool

功能:判断字符串是否是一个 Ascii 字符串,如果字符串为空或没有 Ascii 以外的字符,则返回 true。

返回值:是则返回 true,不是则返回 false

func toArray

public func toArray(): Array<Byte>

功能:返回字符串的 UTF-8 编码的字节数组。

返回值:Byte 类型的 Array

func count

public func count(str: String): Int64

功能:返回子字符串 str 在原字符串中出现的次数。

参数:

  • str:被搜索的子字符串

返回值:出现的次数,当 str 为空字符串时,返回原字符串中 Rune 的数量加一

func split

public func split(str: String, removeEmpty!: Bool = false): Array<String>

功能:对原字符串按照字符串 str 分隔符分割。当 str 未出现在原字符串中,返回长度为 1 的字符串数组,唯一的元素为原字符串。

参数:

  • str:字符串分隔符
  • removeEmpty:移除分割结果中的空字符串,默认值为 false

返回值:分割后的字符串数组

func split

public func split(str: String, maxSplits: Int64, removeEmpty!: Bool = false): Array<String>

功能:对原字符串按照字符串 str 分隔符分割。当 maxSplit 为 0 时,返回空的字符串数组;当 maxSplit 为 1 时,返回长度为 1 的字符串数组,唯一的元素为原字符串;当 maxSplit 为负数时,返回完整分割后的字符串数组;当 maxSplit 大于完整分割出来的子字符串数量时,返回完整分割的字符串数组;当 str 未出现在原字符串中,返回长度为 1 的字符串数组,唯一的元素为原字符串;当 str 为空时,对每个字符进行分割;当原字符串和分隔符都为空时,返回空字符串数组。

参数:

  • str:字符串分隔符
  • maxSplit:最多分割为 maxSplit 个子字符串
  • removeEmpty:移除分割结果中的空字符串,默认值为 false

返回值:分割后的字符串数组

func lazySplit

public func lazySplit(str: String, removeEmpty!: Bool = false): Iterator<String>

功能:对原字符串按照字符串 str 分隔符分割。当 str 未出现在原字符串中,返回大小为 1 的字符串迭代器,唯一的元素为原字符串。

参数:

  • str:字符串分隔符
  • removeEmpty:移除分割结果中的空字符串,默认值为 false

返回值:分割后的字符串迭代器

func lazySplit

public func lazySplit(str: String, maxSplits: Int64, removeEmpty!: Bool = false): Iterator<String>

功能:对原字符串按照字符串 str 分隔符分割。当 maxSplit 为 0 时,返回空的字符串迭代器;当 maxSplit 为 1 时,返回大小为 1 的字符串迭代器,唯一的元素为原字符串;当 maxSplit 为负数时,直接返回分割后的字符串迭代器;当 maxSplit 大于完整分割出来的子字符串数量时,返回完整分割的字符串迭代器当 str 未出现在原字符串中,返回大小为 1 的字符串迭代器,唯一的元素为原字符串;当 str 为空时,对每个字符进行分割;当原字符串和分隔符都为空时,返回空字符串迭代器。

参数:

  • str:字符串分隔符
  • maxSplit:最多分割为 maxSplit 个子字符串
  • removeEmpty:移除分割结果中的空字符串,默认值为 false

返回值:分割后的字符串迭代器

func replace

public func replace(old: String, new: String): String

功能:使用新字符串替换原字符串中旧字符串。

参数:

  • old:旧字符串
  • new:新字符串

返回值:替换后的新字符串

异常:

  • OutOfMemoryError:如果此函数分配内存时产生错误,抛出异常

func toAsciiLower

public func toAsciiLower(): String

功能:将该字符串中所有 Ascii 大写字母转化为 Ascii 小写字母。

返回值:转换后的新字符串

func toAsciiUpper

public func toAsciiUpper(): String

功能:将该字符串中所有 Ascii 小写字母转化为 Ascii 大写字母。

返回值:转换后的新字符串

func toAsciiTitle

public func toAsciiTitle(): String

功能:将该字符串标题化,只转换 Ascii 字符,当该英文字符是字符串中第一个字符或者该字符的前一个字符不是英文字符,则该字符大写,其他英文字符小写,非英文字符不变。

返回值:转换后的新字符串

func trimAscii

public func trimAscii(): String

功能:去除原字符串开头结尾以 whitespace 字符组成的子字符串(Ascii whitespace)。

返回值:转换后的新字符串

func trimAsciiLeft

public func trimAsciiLeft(): String

功能:去除原字符串开头以 whitespace 字符组成的子字符串(Ascii whitespace)。

返回值:转换后的新字符串

func trimAsciiRight

public func trimAsciiRight(): String

功能:去除原字符串结尾以 whitespace 字符组成的子字符串(Ascii whitespace)。

返回值:转换后的新字符串

func trimLeft

public func trimLeft(prefix: String): String

功能:去除前缀是 prefix 的字符串。

参数:

  • prefix:如果字符串的前缀是 prefix,则去除

返回值:转换后的新字符串

func trimRight

public func trimRight(suffix: String): String

功能:去除后缀是 suffix 的字符串。

参数:

  • suffix:如果字符串的后缀是 suffix,则去除

返回值:转换后的新字符串

func contains

public func contains(str: String): Bool

功能:判断原字符串中是否包含字符串 str。

参数:

  • str:被判断的字符串,如果 str 字符串长度为 0,返回 true

返回值:如果字符串 str 在原字符串中,返回 true,否则返回 false

func startsWith

public func startsWith(prefix: String): Bool

功能:判断原字符串是否以 prefix 字符串为前缀开始。

参数:

  • str:被判断的前缀字符串,如果 str 长度为 0,返回 true

返回值:如果字符串 str 是原字符串的前缀,返回 true,否则返回 false

func endsWith

public func endsWith(suffix: String): Bool

功能:判断原字符串是否以 suffix 字符串为后缀结尾。

参数:

  • str:被判断的后缀字符串,当 str 长度为 0 时,返回 true

返回值:如果字符串 str 是原字符串的后缀,返回 true,否则返回 false

func padLeft

public func padLeft(totalWidth: Int64, padding!: String = " "): String

功能:按指定长度右对齐原字符串,如果原字符串长度小于指定长度,在其左侧添加指定字符串。

参数:

  • totalWidth:按指定长度 totalWidth 右对齐
  • padding:当长度不够时,在左侧用指定的字符串 padding 进行填充

返回值:填充后的字符串,当指定长度小于字符串长度时,返回字符串本身,不会发生截断;当指定长度大于字符串长度时,在左侧添加 padding 字符串,当 padding 长度大于 1 时,返回字符串的长度可能大于指定长度

异常:

  • IllegalArgumentException:如果 totalWidth 小于 0,抛出异常

func padRight

public func padRight(totalWidth: Int64, padding!: String = " "): String

功能:按指定长度左对齐原字符串,如果原字符串长度小于指定长度,在其右侧添加指定字符串。

参数:

  • totalWidth:按指定长度 totalWidth 左对齐
  • padding:当长度不够时,在右侧用指定的字符串 padding 进行填充

返回值:填充后的字符串,当指定长度小于字符串长度时,返回字符串本身,不会发生截断;当指定长度大于字符串长度时,在右侧添加 padding 字符串,当 padding 长度大于 1 时,返回字符串的长度可能大于指定长度

异常:

  • IllegalArgumentException:如果 totalWidth 小于 0,抛出异常

func compare

public func compare(str: String): Ordering

功能:按字典序比较原字符串和 str 字符串,比较基于每个byte,按照UTF-8编码的byte进行比较。Ordering.GT 表示原字符串字典序大于 str 字符串,Ordering.LT 表示原字符串字典序小于 str 字符串,Ordering.EQ 表示两个字符串字典序相等。

参数:

  • str:被比较的字符串

返回值:返回 enum 值 Ordering 表示结果,

异常:

  • IllegalArgumentException:如果两个字符串的原始数据中存在无效的 UTF-8 编码,抛出异常

operator func ==

public const operator func ==(right: String): Bool

功能:判断两个字符串是否相等。

返回值:相等返回 true,不相等返回 false

operator func !=

public const operator func !=(right: String): Bool

功能:判断两个字符串是否不相等。

返回值:不相等返回 true,相等返回 false

operator func >

public const operator func >(right: String): Bool

功能:判断两个字符串大小。

返回值:原字符串字典序大于 right 时,返回 true,否则返回 false

operator func >=

public const operator func >=(right: String): Bool

功能:判断两个字符串大小。

返回值:原字符串字典序大于或等于 right 时,返回 true,否则返回 false

operator func <

public const operator func <(right: String): Bool

功能:判断两个字符串大小。

返回值:原字符串字典序小于 right 时,返回 true,否则返回 false

operator func <=

public const operator func <=(right: String): Bool

功能:判断两个字符串大小。

返回值:原字符串字典序小于或等于 right 时,返回 true,否则返回 false

operator func +

public const operator func +(right: String): String

功能:两个字符串相加,将 right 字符串拼接在原字符串的末尾。

返回值:返回拼接后的字符串

operator func *

public const operator func *(count: Int64): String

功能:原字符串重复 count 次。

返回值:返回重复 count 次后的新字符串

operator func []

public const operator func [](index: Int64): Byte

功能:返回指定索引 index 处的 UTF-8 编码字节。

参数:

  • index:要获取 UTF-8 编码字节的下标

返回值:获取得到下标对应的 UTF-8编码字节

异常:

  • IndexOutOfBoundsException:如果 index 小于 0 或大于等于字符串长度,抛出异常

operator func []

public const operator func [](range: Range<Int64>): String

功能:获取切片后的字符串。

注意:如果参数 range 是使用 Range 构造函数构造的 Range 实例,有如下行为:

  • start 的值就是构造函数传入的值本身,不受构造时传入的 hashStart 的值的影响
  • hasEnd 为 false 时,该 Range 实例为开区间,不受构造时传入的 isClosed 的值的影响

参数:

  • range:切片的范围

返回值:新的字符串

异常:

  • IndexOutOfBoundsException:如果切片范围超过原字符串边界,抛出异常
  • IllegalArgumentException:如果 range.step 不等于 1,抛出异常
  • IllegalArgumentException:如果范围起止点不是字符边界,抛出异常

func join

public static func join(strArray: Array<String>, delimiter!: String = String.empty): String

功能:连接字符串列表中的所有字符串。

参数:

  • value:需要被连接的字符串数组,当数组为空时,返回空字符串
  • delimiter:用于连接的中间字符串,其默认值为 String.empty

返回值:连接后的新字符串

func rawData

public unsafe func rawData(): Array<Byte>

功能:返回字符串的 UTF-8 编码的原始字节数组,开发者如果对该数组修改会破坏字符串的并发安全

返回值:Byte 类型的 Array

func fromUtf8

public static func fromUtf8(utf8Data: Array<UInt8>): String

功能:根据 UTF-8 数组创建一个字符串。

参数:

  • utf8Data:需要构造字符串的 UTF-8 数组

返回值:创建的字符串

异常:

  • IllegalArgumentException:入参不符合 utf-8 序列规则,抛出异常

func fromUtf8Unchecked

public static unsafe func fromUtf8Unchecked(utf8Data: Array<UInt8>): String

功能:根据 UTF-8 数组创建一个字符串,相较于 fromUtf8 函数,它并没有针对于字节数组进行 UTF-8 相关规则的检查,所以它所构建的字符串并不一定保证是合法的,甚至出现非预期的异常,如果不是某些场景下的速度考虑,请优先使用安全的 fromUtf8 函数。

参数:

  • utf8Data:需要构造字符串的 UTF-8 数组

返回值:创建的字符串

class StringBuilder

public class StringBuilder <: ToString {
    public init()
    public init(str: String)
    public init(r: Rune, n: Int64)
    public init(value: Array<Char>)
    public init(capacity: Int64)
}

该类主要用于字符串的构建,其中实现了 StringBuilder 相关接口;StringBuilder 在字符串的构建上效率高于 String,当需要多种类型和多个值构建 String 时推荐使用 StringBuilder

init

public init()

功能:构造一个初始容量为 32 的空 StringBuilder 实例。

init

public init(str: String)

功能:使用参数 str 指定的字符串初始化 StringBuilder 实例。

参数:

  • str:初始化 StringBuilder 实例的字符串

init

public init(r: Rune, n: Int64)

功能:使用 nr 字符初始化 StringBuilder 实例。

参数:

  • r:初始化 StringBuilder 实例的字符
  • n:字符 r 的数量

异常:

  • IllegalArgumentException: 当参数 n 小于 0 时,抛出异常

init

public init(value: Array<Rune>)

功能:使用参数 value 指定的字符数组初始化一个 StringBuilder 实例。

参数:

  • value:初始化 StringBuilder 实例的字符数组

init

public init(capacity: Int64)

功能:使用参数 capacity 指定的容量初始化一个空 StringBuilder 实例。

参数:

  • capacity:初始化 StringBuilder 的字节容量

异常:

  • IllegalArgumentException: 当参数 capacity 的值小于等于 0 时,抛出异常

prop size

public prop size: Int64

功能:返回 StringBuilder 实例中字符串 UTF-8 编码后的字节长度。

返回值 Int64 - StringBuilder 实例中字符串 UTF-8 编码后的字节长度

prop capacity

public prop capacity: Int64

功能:返回 StringBuilder 实例能容纳字符串 UTF-8 编码后的字节长度。

返回值 Int64 - StringBuilder 实例中能容纳字符串 UTF-8 编码后的字节长度

func reserve

public func reserve(additional: Int64): Unit

功能:将 StringBuilder 扩容 additional 大小。当 additional 小于等于零,或剩余容量大于等于 additional 时,不发生扩容;当剩余容量小于 additional 时,扩容至当前容量的 1.5 倍(向下取整)与 size + additional 的最大值。

参数:

  • additional:指定 StringBuilder 的扩容大小

func reset

public func reset(capacity!: Option<Int64> = None): Unit

功能:清空当前 StringBuilder,并将容量重置为 capacity 指定的值。

参数:

  • capacity:重置后 StringBuilder 实例的容量大小,默认值 None 表示采用默认大小容量(32)。

异常:

  • IllegalArgumentException: 当参数 capacity 的值小于等于 0 时,抛出异常

func toString

public func toString(): String

功能:返回 StringBuilder 实例中的字符串。

返回值:StringBuilder 实例中的字符串

func append

public func append(r: Rune): Unit

功能:在 StringBuilder 末尾插入参数 r 指定的字符。

参数:

  • r:插入的字符

func append

public func append(str: String): Unit

功能:在 StringBuilder 末尾插入参数 str 指定的字符串。

参数:

  • str:插入的字符串

func append

public func append(sb: StringBuilder): Unit

功能:在 StringBuilder 末尾插入参数 sb 指定的 StringBuilder 中的内容。

参数:

  • sb:插入的 StringBuilder 实例

func append

public func append(runeArr: Array<Rune>): Unit

功能:在 StringBuilder 末尾插入一个 Rune 数组中所有字符。

参数:

  • runeArr:插入的 Rune 数组

func append

public func append(cstr: CString): Unit

功能:在 StringBuilder 末尾插入参数 cstr 指定 CString 中的内容。

参数:

  • cstr:插入的 CString

func append

public func append<T>(v: T): Unit where T <: ToString

功能:在 StringBuilder 末尾插入参数 v 指定 T 类型的字符串表示,类型 T 需要实现 ToString 接口。

参数:

  • v:插入的 T 类型实例

func append

public func append<T>(val: Array<T>): Unit where T <: ToString

功能:在 StringBuilder 末尾插入参数 val 指定的 Array<T> 的字符串表示,类型 T 需要实现 ToString 接口。

参数:

  • val:插入的 Array<T> 类型实例

func append

public func append(b: Bool): Unit

功能:在 StringBuilder 末尾插入参数 b 的字符串表示。

参数:

  • b:插入的 Bool 类型的值,取值 "true" 或者 "false"

func append

public func append(n: Int64): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 Int64 类型的值

func append

public func append(n: Int32): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 Int32 类型的值

func append

public func append(n: Int16): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 Int16 类型的值

func append

public func append(n: Int8): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 Int8 类型的值

func append

public func append(n: UInt64): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 UInt64 类型的值

func append

public func append(n: UInt32): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 UInt32 类型的值

func append

public func append(n: UInt16): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 UInt16 类型的值

func append

public func append(n: UInt8): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 UInt8 类型的值

func append

public func append(n: Float64): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 Float64 类型的值

func append

public func append(n: Float32): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 Float32 类型的值

func append

public func append(n: Float16): Unit

功能:在 StringBuilder 末尾插入参数 n 的字符串表示。

参数:

  • n:插入的 Float16 类型的值

func appendFromUtf8

public func appendFromUtf8(arr: Array<Byte>): Unit

功能:在 StringBuilder 末尾插入参数 arr 指定 utf8 编码的字节数组。

参数:

  • arr:插入的 utf8 字节数组

异常:

  • IllegalArgumentException:当字节数组不符合 utf8 编码规则时,抛出异常

func appendFromUtf8Unchecked

public unsafe func appendFromUtf8Unchecked(arr: Array<Byte>): Unit

功能:在 StringBuilder 末尾插入参数 arr 指定 utf8 编码的字节数组, 相较于 appendFromUtf8 函数,它并没有针对于字节数组进行 utf8 相关规则的检查,所以它所构建的字符串并不一定保证是合法的,甚至出现非预期的异常,如果不是某些场景下的速度考虑,请优先使用安全的 appendFromUtf8 函数。

参数:

  • arr:插入的 utf8 字节数组

enum AnnotationKind

public enum AnnotationKind {
    | Type
    | Parameter
    | Init
    | MemberProperty
    | MemberFunction
    | MemberVariable
}

枚举类型 AnnotationKind 表示自定义注解希望支持的位置。

Type

Type

功能:类型声明(class、struct、enum、interface)。

Parameter

Parameter

功能:成员函数/构造函数中的参数。

Init

Init

功能:构造函数声明。

MemberProperty

MemberProperty

功能:成员属性声明

MemberFunction

MemberFunction

功能:成员函数声明

MemberVariable

MemberVariable

功能:成员变量声明

示例

Future 的使用

主线程和新线程同时尝试打印一些文本。

代码如下:

from std import sync.sleep
from std import time.{Duration, DurationExtension}

main(): Int64 {
    spawn { =>
        for (i in 0..10) {
            println("New thread, number = ${i}")
            sleep(100 * Duration.millisecond) /* 睡眠 100 毫秒 */
        }
    }

    for (i in 0..5) {
        println("Main thread, number = ${i}")
        sleep(100 * Duration.millisecond) /* 睡眠 100 毫秒 */
    }
    return 0
}

运行结果如下:

Main thread, number = 0
New thread, number = 0
Main thread, number = 1
New thread, number = 1
Main thread, number = 2
New thread, number = 2
Main thread, number = 3
New thread, number = 3
Main thread, number = 4
New thread, number = 4
New thread, number = 5
  • 注意,上述打印信息仅做参考。

Future 的 get 的使用

主线程等待创建线程执行完再执行。

代码如下:

from std import sync.sleep
from std import time.{Duration, DurationExtension}

main(): Int64 {
    let fut: Future<Unit> = spawn { =>
        for (i in 0..10) {
            println("New thread, number = ${i}")
            sleep(100 * Duration.millisecond) /* 睡眠 100 毫秒 */
        }
    }

    fut.get() /* 等待线程完成 */

    for (i in 0..5) {
        println("Main thread, number = ${i}")
        sleep(100 * Duration.millisecond) /* 睡眠 100 毫秒 */
    }
    return 0
}

运行结果如下:

New thread, number = 0
New thread, number = 1
New thread, number = 2
New thread, number = 3
New thread, number = 4
New thread, number = 5
New thread, number = 6
New thread, number = 7
New thread, number = 8
New thread, number = 9
Main thread, number = 0
Main thread, number = 1
Main thread, number = 2
Main thread, number = 3
Main thread, number = 4

创建 Array 并取值

下面是创建 Array,使用下标取值的示例。

代码如下:

main() {
    var array: Array<Int64> = [1, 2, 3, 4, 5]
    println("array[1] = ${array[1]}")
    return 0
}

运行结果如下:

array[1] = 2

创建 String 并访问每个字符

下面是创建 String 并且通过 for-in 访问每个 UTF-8 编码字节的示例。

代码如下:

main() {
    var str = "仓颉"    // E4BB93 E9A289
    for (c in str) {
        println(c)
    }
}

运行结果如下:

228
187
147
233
162
137

CString 与 C 代码交互

C 代码中分别提供两个函数: getCString 函数,用于返回一个 C 侧的字符串指针; printCString 函数,用于打印来自仓颉侧 CString 。

#include <stdio.h>

char *str = "CString in C code.";

char *getCString() { return str; }

void printCString(char *s) { printf("%s\n", s); }

在仓颉代码中,创建一个 CString 对象,传递给 C 侧打印。并且获取 C 侧字符串,在仓颉侧打印:

foreign func getCString(): CString
foreign func printCString(s: CString): Unit

main() {
    // Construct by a Cangjie String.
    unsafe {
        let s: CString = LibC.mallocCString("CString in Cangjie code.")
        printCString(s)
        LibC.free(s)
    }

    unsafe {
        // Get a CString from C code and use `toString` convert to cangjie String.
        let cs = getCString()
        println(cs.toString())
    }

    // Use CStringResource by try-with-resource
    let cs = unsafe { LibC.mallocCString("CString in Cangjie code.") }
    try (csr = cs.asResource()) {
        unsafe { printCString(csr.value) }
    }

    0
}

示例输出:

CString in Cangjie code.
CString in C code.
CString in Cangjie code.

core 包

介绍

此包包括一些常用接口 ToString、 Hashable、 Equatable 等,以及 String、 Range、 Array、 Option 等常用数据结构,包括预定义的异常、错误类型等。

主要接口

interface Any

public interface Any

Any 是所有类型的父类型,所有 interface 都默认继承它,所有非 interface 类型都默认实现它。

class Object

public open class Object <: Any {
    public const init()
}

Object 是所有 class 的父类,所有 class 都默认继承它。

init

public const init()

功能:构造一个 object 实例。

interface CharExtension

public interface CharExtension

CharExtensionChar 相关的辅助接口。

interface Hasher

public interface Hasher {
    func finish(): Int64
    mut func reset(): Unit
    mut func write(value: Bool): Unit
    mut func write(value: Char): Unit
    mut func write(value: Int8): Unit
    mut func write(value: Int16): Unit
    mut func write(value: Int32): Unit
    mut func write(value: Int64): Unit
    mut func write(value: UInt8): Unit
    mut func write(value: UInt16): Unit
    mut func write(value: UInt32): Unit
    mut func write(value: UInt64): Unit
    mut func write(value: Float16): Unit
    mut func write(value: Float32): Unit
    mut func write(value: Float64): Unit
    mut func write(value: String): Unit
}

Hasher 是用来处理哈希组合运算的接口。

func finish

func finish(): Int64

功能:返回哈希运算的结果。

返回值:经过计算后的哈希值

func reset

mut func reset(): Unit

功能:重置哈希值。

func write

mut func write(value: Bool): Unit

功能:通过该函数把想要哈希运算的 Bool 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Char): Unit

功能:通过该函数把想要哈希运算的 Char 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Int8): Unit

功能:通过该函数把想要哈希运算的 Int8 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Int16): Unit

功能:通过该函数把想要哈希运算的 Int16 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Int32): Unit

功能:通过该函数把想要哈希运算的 Int32 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Int64): Unit

功能:通过该函数把想要哈希运算的 Int64 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: UInt8): Unit

功能:通过该函数把想要哈希运算的 UInt8 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: UInt16): Unit

功能:通过该函数把想要哈希运算的 UInt16 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: UInt32): Unit

功能:通过该函数把想要哈希运算的 UInt32 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: UInt64): Unit

功能:通过该函数把想要哈希运算的 UInt64 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Float16): Unit

功能:通过该函数把想要哈希运算的 Float16 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Float32): Unit

功能:通过该函数把想要哈希运算的 Float32 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: Float64): Unit

功能:通过该函数把想要哈希运算的 Float64 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

func write

mut func write(value: String): Unit

功能:通过该函数把想要哈希运算的 String 值传入,然后进行哈希组合运算。

参数:

  • value:待运算的值

class ArithmeticException

public open class ArithmeticException <: Exception {
    public init()
    public init(message: String)
}

ArithmeticException 为算术异常类,用于在发生算术异常时使用。

init

public init()

功能:构造一个默认的 ArithmeticException 实例,默认异常信息为空。

init

public init(message: String)

功能:根据异常信息构造一个 ArithmeticException 实例。

参数:

  • message:异常信息

func getClassName

protected open override func getClassName(): String

功能:获得类名。

返回值:类名字符串

class ArrayIterator

public class ArrayIterator<T> <: Iterator<T> {
    public init(data: Array<T>)
}

数组迭代器。

init

public init(data: Array<T>)

功能:创建一个数组迭代器。

参数:

  • data:数组

func next

public func next(): Option<T>

功能:返回迭代器中的下一个值。

返回值:Option,可以使用 match 解构

func iterator

public func iterator(): Iterator<T>

功能:返回迭代器。<