- 在线地址:https://wa-lang.org/man/
- 在线地址(英文):https://wa-lang.github.io/man/en/
- 社区共建:凹语言 MVP 文档共建邀请
前言
凹语言是针对 WebAssembly 平台设计的通用静态类型编译型语言。它是由国内的一群编程语言爱好者发起的项目,设计重点是降低使用者的心智负担。
从2018年底立项,2019年开始前期准备,2020年加入新成员,2021年项目联合发起人共同出版合著书籍 《Go语言定制指南》,2022年项目正式启动并于7月20日开源,凹语言一步一个脚印,在国内的同类项目中,首个实现了浏览器内编译、执行全链路。
开源一周年之际,凹语言在8月12日发布了 MVP版本(MVP 是 “Minimum Viable Product” 的首字母缩写,即“最小可用产品”)。我们希望 MVP 版的发布让大家进行更多的尝试和探索,为未来的发展畅想更多可能。
最后,感谢中国编程语言爱好者的支持,凹语言 MVP 版本是一个新的起点,欢迎大家参与共建!
1. 安装及入门
本章讲述了最简单的凹语言例子、如何安装凹语言程序、凹语言IDE插件、凹语言工程目录结构和命令行功能等内容。
1.1. 你好,凹语言
打印“hello world”是C语言之后的惯用案例,凹语言例子打印的是中文“你好,凹语言!”。
1.1.1 你好,凹语言!
创建 hello.wa 文件,内容如下:
// 版权 @2019 凹语言 作者。保留所有权利。
import "fmt"
import "runtime"
global year: i32 = 2023
func main {
println("你好,凹语言!", runtime.WAOS)
println(add(40, 2), year)
fmt.Println("1+1 =", 1+1)
}
func add(a: i32, b: i32) => i32 {
return a+b
}
其中//
开始的是行注释,import
关键字导入了2个包准库的包,global
关键字定义了一个全局变量,并给了2023的初始值。func
关键字定义了main
函数和add
函数。main
函数是程序的入口,其中通过内置的println
函数打印了“你好,凹语言!”,同时使用fmt
包的Println
字符串和整数表达式的结果。在main
函数还使用了全局的year
变量,此外还调用了add
函数并打印了返回值。add
函数有2个输入参数和一个返回值。
如果在本地已经安装有凹语言的wa
命令(安装方式参考1.2节),可以输入以下命令执行:
$ wa run hello.wa
你好,凹语言! wasi
42
1+1 = 2
1.1.2 在线的 Playground
凹语言是面向 WebAssembly 设计的通用编程语言,从诞生起就将浏览器作为第一支持目标。可以通过 https://wa-lang.org/playground 访问 Playground,界面如下:
点击“RUN”按钮,可以看到输出结果。
已知问题:
- 在线 Playground 仅支持单文件模式,暂不支持多文件工程模式。该问题不影响语法兼容性,后续对本问题的修正不影响已有源代码,凹程序开发者无须对此进行特别处理。
1.2. 安装凹语言
除了访问在线的凹语言 Playground,也可以在本地安装凹语言。
1.2.1 二进制安装
从 Github 下载最新的二进制文件:https://github.com/wa-lang/wa/releases 。需要确保和本地平台对应,比如 v0.8.1 对应 macOS/amd64 平台下载的是 wa_0.8.1_darwin_amd64.tar.gz。
解压后目录内容如下:
$ tree ./wa_0.8.1_darwin_amd64
./wa_0.8.1_darwin_amd64
├── LICENSE
├── README-zh.md
├── README.md
└── wa
1 directory, 5 files
将该目录路径添加到系统的 PATH
环境,然后重新打开命令行环境执行 wa -v
命令查看版本信息。
$ wa -v
Wa version v0.8.1
第一次执行 wa
命令时会在命令同一个目录下生成一个 wa.wat2wasm.exe
命令。
到此安装工作完成。
1.2.2 从源码安装
本地要求安装 Go1.17+ 版本,然后执行以下命令安装最新的 wa
命令:
go install wa-lang.org/wa
默认会安装到 $HOME/go/bin
目录,因此需要将该该目录路径添加到系统的 PATH
环境。
然后重新打开命令行环境执行一次 wa
命令会输出以上相同的帮助信息。
第一次执行 wa -v
命令查看版本信息时会在命令同一个目录下生成一个 wa.wat2wasm.exe
命令。
到此安装工作完成。
1.2.3 Homebrew (MacOS & Linux)
对于 macOS 和 Linux 系统也可以通过 Homebrew 安装:
brew install wa-lang/tap/wa
1.2.4 Scoop (Windows)
对于 Windows 系统也可以通过 Scoop 安装:
scoop bucket add wa-lang https://github.com/wa-lang/scoop-bucket.git
scoop install wa-lang/wa
1.2.5 本地 Playground
安装成功之后,在命令行输入 wa play
可以打开本地 Playground。程序会默认打开浏览器页面,也可以输入 http://localhost:2023/ 地址访问。效果如下:
点击“执行”按钮,可以看到输出结果。
1.3. 命令行功能
本节介绍 wa
命令主要功能。
1.3.1. 帮助信息
输入 wa
命令或 wa -h
可查看命令行帮助信息,如下:
$ wa
NAME:
Wa - Wa is a tool for managing Wa source code.
USAGE:
wa [global options] command [command options] [arguments...]
VERSION:
v0.8.1-mvp
COMMANDS:
play start Wa playground
init init a sketch Wa module
build compile Wa source code
run compile and run Wa program
fmt format Wa source code file
test test Wa packages
yacc generates parsers for LALR(1) grammars
logo print Wa text format logo
GLOBAL OPTIONS:
--debug, -d set debug mode (default: false)
--trace value, -t value set trace mode (*|app|compiler|loader)
--help, -h show help (default: false)
--version, -v print the version (default: false)
COPYRIGHT:
Copyright 2018 The Wa Authors. All rights reserved.
See "https://wa-lang.org" for more information.
主要有以下子命令:
- play:启动本地版本的 playground;
- init:初始化一个凹语言工程;
- build:编译凹语言程序;
- run:编译并执行凹语言程序;
- fmt:格式化凹语言源代码文件;
- test:执行凹语言工程的单元测试;
- yacc:凹语言版本的 yacc,用于生成 LALR(1) 语法规则的解析器代码;
- logo:打印凹语言文字版本的Logo。
其中 build 命令在 1.1.1 节展示过,play 命令已经在 1.2.5 节展示过,本节简要介绍之外的子命令。
1.3.2 初始化工程
在 1.1.1 节已经展示过 wa run hello.wa
执行一个独立文件的凹语言程序。但是单文件的凹语言程序有一个巨大的限制——它只有一个文件且不能引用非标准库的代码。对于更大的凹程序推荐用凹工程的方式组织。
使用 wa init
命令可以初始化一个凹工程,先看看命令行帮助:
$ wa init -h
NAME:
wa init - init a sketch Wa module
USAGE:
wa init [command options] [arguments...]
OPTIONS:
--name value, -n value set app name (default: "hello")
--pkgpath value, -p value set pkgpath file (default: "myapp")
--update, -u update example (default: false)
--help, -h show help (default: false)
该命令有 -name
和 -pkgpath
两个重要的参数,分别对应工程的名字和对应的包路径。每个参数都有默认值,可以用 wa init
生成一个 hello 工程。
$ wa init
$ tree hello
hello
├── LICENSE
├── README.md
├── src
│ ├── main.wa
│ ├── mymath
│ │ └── math.wa
│ ├── mypkg
│ │ └── pkg.wa
│ └── zz_test.wa
├── vendor
│ └── 3rdparty
│ └── pkg
│ └── pkg.wa
└── wa.mod
7 directories, 8 files
工程的结构在 1.4 节介绍。
1.3.3 编译和执行
命令行环境进入 hello 目录,输入 wa build
将在 output 目录构建出 wasm 模块:
$ wa build
$ tree output/
output/
├── hello.wasm
└── hello.wat
1 directory, 2 files
默认输出的是 WASI 规范的 output/hello.wat
和 output/hello.wasm
文件。可以用标准的工具执行输出的 wasm 模块。也可以用 wa
命令执行:
$ wa run ./output/hello.wasm
你好,凹语言!
5050
...
如果不带参赛执行 wa run
命令,表示编译并执行当前凹语言工程,会先构建出 output/hello.wasm
然后再执行。
1.3.4 格式化代码
wa fmt
命令用于格式化代码,其命令行帮助信息如下:
$ wa fmt -h
NAME:
wa fmt - format Wa source code file
USAGE:
wa fmt [command options] [<file.wa>|<path>|<path>/...]
OPTIONS:
--help, -h show help (default: false)
命令行参数是要进行格式化的路径:
wa fmt file.wa
格式化指定的凹语言文件wa fmt path
格式化指定目录下的全部凹语言文件wa fmt path/...
递归格式化指定路径的凹语言文件,含子目录
如果不指定参数,则默认格式化当前目录下全部的凹语言文件。如果当前目录属于凹语言工程中,则默认格式化全部子目录的凹语言文件。
1.3.5 单元测试
默认生成的工程会有一个 src/zz_test.wa
测试文件,内容如下:
func TestSum {
assert(sum(100) == 5050, "sum(100) failed")
}
func ExampleSum {
println(sum(100))
// Output:
// 5050
}
在 TestSum
测试函数中通过内置的 assert
函数测试 sum(100)
的结果为 5050。在 ExampleSum
示例测试函数中通过 // Output:
测试输出的内容符合期望的结果。
在工程目录的命令行环境通过 wa test
命令执行测试:
$ wa test
ok myapp 104ms
1.3.6 凹语言版本的 yacc
yacc 是用于生成语法解析器的程序,是编译器爱好者的工具。凹语言的 yacc 从 goyacc 移植而来,详细的用法可以参考凹语言官网碎碎念部分的 相关文章。
1.3.7 打印 Logo
wa logo
可以输出一些文本格式的 Logo 图案,读者可以通过 wa logo -h
命令帮助自行探索。
1.4. 工程目录结构
工程目录结构是整个外围工具工作的基础,比如 init 根据该结构生成工程、包管理工具则依次管理依赖关系。
1.4.1 工程目录结构简介
凹语言程序以包来组织代码,包可以是一个单文件,包也可以是一个目录。凹语言自带的 waroot/examples/hello
案例的是一个更为完整的工程,其目录结构如下:
examples/hello/
├── LICENSE
├── README.md
├── src
│ ├── main.wa
│ └── mymath
│ └── math.wa
├── vendor
│ └── 3rdparty
│ └── pkg
│ └── pkg.wa
└── wa.mod
除了版权文件、说明文件外,最重要的是 wa.mod
包工程文件,其定义了当前应用的包路径。此外 src 目录下的是当前包路径下的代码,是默认的 main 入口包。
wa.mod
文件内容如下:
name = "hello"
pkgpath = "myapp"
version = "0.0.1"
其中 pkgpath 表示当前包的路径,从而可以推导出 mymath 子目录对应的包路径为 "myapp/mymath"
。vendor 目录是依赖的第三方代码,其中 vendor/3rdparty/pkg
对应的包路径为 "3rdparty/pkg"
。
1.4.2 包管理工具
凹语言目前还没有包管理工具,如果依赖第三方包则需要手工同步 vendor 目录。开发组希望在 MVP 版本后启动包管理工具开放工作。
1.5. IDE插件
现代化的编程语言一般都会为各种IDE和编辑器提供扩展,以提高编程的体验。凹语言为 VS Code、Fleet 和 Vim 提供了基本插件支持。
1.5.1 VS Code 插件
在 VS Code 扩展商店检索 “wa” 即可以查到凹语言插件。安装之后会有基本的语法高亮等功能。
VS Code 效果如下:
1.5.2 Fleet 插件
Fleet 插件仓库:https://github.com/wa-lang/fleet-wa
根据仓库提示安装,效果如下:
1.5.3 Vim 插件
Vim 插件仓库:https://github.com/wa-lang/vim-wa
根据仓库提示安装,效果如下:
在编写文档过程中突然得知 Vim 的作者 Bram Moolenaar 去世,谨以此插件祝愿 Vim 永垂不朽!
1.5.4 其他编辑器
MVP 之后凹语言的语法已经基本固定,希望社区可以参与其他编辑器支持共建。
2. 程序结构
凹语言和其他编程语言一样,一个大的程序是由很多小的部分组成的。本章介绍全局变量声明、函数声明、常量声明、导入声明和类型声明。
2.1. 全局声明
一个典型的 凹语言 程序源码如下例所示:
// 版权 @2023 凹语言 作者。保留所有权利。
import "errors"
const PI = 3.1415926
global 终极问题的答案: i32
func main {
终极问题的答案 = getAnswer()
println("宇宙的答案:", 终极问题的答案)
println("π:", PI)
err := errors.New("!!!")
println("err:", err.Error())
}
func getAnswer => i32 {
return 42
}
与很多语言类似,在 凹语言 中,双斜杠
//
后至行尾的部分为注释,不产生实际作用。
凹代码由全局声明
组成,比如在上面的例子中:
import "errors"
是一个导入errors
模块的声明const PI = 3.1415926
是一个常量声明,它声明了一个名为PI
的常量,值为 3.1415926global 终极问题的答案: i32
是一个全局变量声明,它声明了一个名为终极问题的答案
的全局变量,类型为32位整数func getAnswer => i32 {...}
是一个函数声明,它声明了一个返回值为32位整数的函数
凹语言 共有5种全局声明,每种声明均由特定的关键字开始(其后跟随该声明对象的实体),声明及对应关键字的关系如下:
global
:全局变量声明func
:函数声明const
:常量声明import
:导入声明type
:类型声明
本章接下来的小节将依次简介全局变量声明、函数声明、常量声明、导入声明,类型声明将在第6章单独讲解。
2.2. 全局变量声明
全局变量声明以关键字 global
开始,一般语法如下:
global 变量名: 类型 = 初始值表达式
比如下面的例子:
// 版权 @2023 凹语言 作者。保留所有权利。
global aInt: i32 = 42 // 32位有符号整数
global _num: f32 = 952.7 // 32位浮点数
func main {
println(aInt)
println(_num)
println(名字)
println(counter)
}
global 名字: string = "张三" // 字符串
global counter: u32 // 32位无符号整数
该程序运行的输出如下:
42
952.7
张三
0
全局变量在模块内部的任何地方都可以使用——哪怕全局变量的声明与使用位于不同的源文件中,只要它们位于同一个模块内即可;在同一个源文件内,也并不要求“先声明再使用”,上面的例子中,变量 名字
、counter
就可体现该特点。
需要注意的是,上例中变量 counter
声明时没有给出初始值:
在 凹语言 中,未给定初始值的变量一概以0值初始化,这有助于消除不确定性。
2.3. 函数声明
函数声明以关键字 func
开始,一般语法如下:
func 函数名(参数列表) => (返回值列表) {函数体}
比如下面的例子:
// 版权 @2023 凹语言 作者。保留所有权利。
func swap(i, j: i32) => (i32, i32) {
return j, i
}
func main {
a, b := swap(4, 2)
println("a:", a, ", b:", b)
println(add(a, b))
}
func add(i, j: i32) => i32 {
return i + j
}
该程序运行的输出如下:
a: 2 , b: 4
6
对于没有返回值的函数,=> (返回值列表)
的部分可省略,没有输入参数的函数 (参数列表)
的部分可省略,比如上例中的:func main {...}
,即为:func main() => () {...}
的简写。
与全局变量类似,函数可在包内的任何源文件中声明且无需“先声明再使用”。
关于函数的更多信息见第4章。
2.4. 常量声明
常量声明以关键字 const
开始,一般语法如下:
const 常量名: 类型 = 常量值
比如下面的例子:
// 版权 @2023 凹语言 作者。保留所有权利。
const Answer: i32 = 42
const aConstString: string = "你好,凹语言"
func main {
println(Answer)
println(aConstString)
println(aConstInt)
}
const aConstInt = 13
该程序运行的输出如下:
42
你好,凹语言
13
声明常量时,如果不指定类型(比如上例中的 aConstInt
),那么它将是无类型常量,无类型常量有4种类型,分别为:无类型整数、无类型浮点数、无类型字符、无类型字符串,常量值写法如下:
const aUntypedInt = 11 // 无类型整数
const aUntypedFloat = 13.0 // 无类型浮点数
const aUntypedRune = 'a' // 无类型字符
const aUntypedString = "abc" // 无类型字符串
对常量值的算数逻辑运算是在编译时完成的,比如:
// 版权 @2019 凹语言 作者。保留所有权利。
const K = 4200000000000000000000000
const J = 4200000000000000000000000
func main {
println(K/J)
}
虽然K
和J
的值均超过了凹中位数最多的整数类型i64
的表达范围,但是K/J
的值仍然能被正确打印。这也侧面体现了数值常量拥有超过变量基本类型的表达范围和精度。
将常量赋值给变量时的相关规则,将在第3章详细讨论。
2.5. 导入声明
导入声明以关键字 import
开始,一般语法如下:
import 导入模块路径
比如下面的例子:
// 版权 @2023 凹语言 作者。保留所有权利。
import "errors"
func main {
err := errors.New("!!!")
println("err:", err.Error())
}
func getAnswer => i32 {
return 42
}
程序开始处的 import "errors"
声明导入了凹的内置 errors
模块,后续的函数中即可使用该模块的公开对象——如例子中的 errors.New
函数;.
在这里被称为选择操作符,它的含义是从左侧的对象(模块)中选择名称与右侧相同的那个对象来使用,该操作符除了用于选择模块公开的函数、全局变量等对象,还用于选择结构体的成员(详见第6章)。
导入声明应位于源文件内的所有非导入声明之前(既位于头部,紧随文件头注释之后)。若导入多个模块,可以使用括号成组导入,形如:
import (
"errors"
"strconv"
)
该声明与下述声明是等价的:
import "errors"
import "strconv"
在导入模块时,可以给模块起别名,一般语法如下:
import 导入模块路径 => 模块别名
这种用法可以解决同时导入两个路径不同,但名字相同的模块时名字冲突的问题,例如:
import (
"errors"
"mypackage/errors" => myerrors
)
func main {
err := errors.New("!!!") // 调用内置 errors 模块
myerr := myerrors.New("!!!") // 调用 mypackage/errors 模块
}
与其他声明不同,导入声明的作用范围是当前源文件,如果一个模块内的两个源文件使用了同一个第三方模块,那么两个文件内都需要其导入声明。
在凹语言中,每个源文件导入的其他模块都必须被使用,也就是说,如果导入了一个模块,但并未使用其任何对象,将被视为语法错误。
3. 基础数据类型
从底层而言,所有的数据都是由比特组成。对应的凹语言基础数据类型有整型数、浮点数、字符串等。本章介绍基本数据类型以及局部变量的使用。
3.1. 局部变量声明
第2章介绍了全局变量和常量声明,同样常用的还有局部变量(函数内部定义的变量)声明,它的一般语法为:
局部变量名: 数据类型 = 初始值
与全部变量和常量不同的是:局部变量的声明不以关键字开始;在声明局部变量时,如果省略 = 初始值
部分,则该变量将以0值初始化,如:
aI32: i32 = 42
aString: string = "你好"
aF32: f32 // 0.0
另一种常用的声明局部变量的语法使用快捷定义符 :=
,语法如下:
局部变量名 := 表达式
使用这种写法时变量的类型将与快捷定义符右侧表达式的类型保持一致,且表达式的值将被赋为该局部变量的初始值,如:
a := 13 // int
f := 3.14 // f64
s := genString() // string
...
func genString() => string { return "Hello" }
凹语言是静态类型语言,合法表达式的类型可以在编译时推定,因此变量的类型是确定的。该
:=
语法类似于C++的auto
类型。
3.2. 整数
凹语言目前支持以下几种整数类型:
u8
:无符号8位整数;u16
:无符号16位整数;i32
:有符号32位整数;u32
:无符号32位整数;i64
:有符号64位整数;u64
:无符号64位整数;int
:不定宽有符号整数;uint
:不定宽无符号整数;bool
:布尔型。
其中:
int
和uint
为不定宽整数,它们的宽度是由目标平台决定的。之所以有不定宽整数类型,是因为目标平台的寻址范围可能不同,内建函数len
等涉及存储范围的操作,需要统一的数据类型以保持代码在不同的目标平台上能正常编译,并充分利用平台寻址范围;bool
型实际内存布局为u8
,合法取值的字面值为true
、false
,对应内存数值为 1 和 0。
当前凹语言的主要目标平台为 wasm32,在该平台下,不定宽整数的位宽为32位,既4字节。
除布尔型外的整数支持以下单目运算:
^
:按位取反-
:取算术负值(既用0减去操作数)
例如:
i: u8 = 9
println(^i) // 246
println(-i) // 247
j: i32 = 9
println(^i) // -10
println(-i) // -9
除布尔型外的整数支持以下双目算术运算:
+
:求和,两个操作数类型必须一致,返回值类型与操作数一致;-
:求差,两个操作数类型必须一致,返回值类型与操作数一致;*
:求积,两个操作数类型必须一致,返回值类型与操作数一致;/
:求商,两个操作数类型必须一致,返回值类型与操作数一致;%
:求余,两个操作数类型必须一致,返回值类型与操作数一致。
例如:
i, j: u8 = 9, 250
println(i + j) // 3
println(i - j) // 15
println(i * j) // 202
println(j / i) // 27
println(j % i) // 7
除布尔型的整数支持以下双目位运算:
&
:按位取与,两个操作数类型必须一致,返回值类型与操作数一致;|
:按位取或,两个操作数类型必须一致,返回值类型与操作数一致;^
:按位取异或,两个操作数类型必须一致,返回值类型与操作数一致;&^
:按位清空,两个操作数类型必须一致,返回值类型与操作数一致。对z = x ^& y
,设xn
、yn
、zn
分别为x
、y
、z
的第n位,则当yn
为1时zn
为0,否则zn
等于xn
。该运算等价于z = x & (^y)
;<<
:左移,对z = x << y
,z
的类型与x
一致,y
必须为大于0的整数,移位时低位补0;>>
:右移,对z = x >> y
,z
的类型与x
一致,y
必须为大于0的整数,移位时高位补0。
例如:
i, j: u16 = 343, 47831
println(i & j) // 87
println(i | j) // 48087
println(i ^ j) // 48000
println(i &^ j) // 256
println(i << 5) // 10976
println(j >> 5) // 1494
加、减、乘、左移等运算的结果可能超过操作数的表达范围,此时将截取低位部分作为结果。
除布尔型的整数支持以下比较运算(双目):
==
:相等。操作数类型必须一致,返回值为bool
型,符合判断条件返回true
,否则返回false
,下同;!=
:不等;>
:大于;>=
:大等于;<
:小于;<=
:小等于。
如果参与比较的两个操作数中有一个为常数,则常数应位于比较运算符的右侧。
布尔型支持以下单目运算:
!
:取反,操作数为false
返回true
,否则返回false
。
实际上除了通过2.4节介绍的常量声明的具名常量外,代码中出现的很多字面值,也是常量,比如:
i := 13
代码中的 13
就是一个无类型的整数常量。使用无类型整数常量进行变量快捷声明时,变量的类型为不定宽有符号整数(既 int
),上述代码等价于:
i: int
i = 13
将整数常量赋值给整数变量时,会在编译时执行类型和范围检查,自动匹配至变量类型——向无符号整数赋予负数常量、或常量值超过被赋值变量宽度等行为将被判定为非法。
整数拥有所有的二元运算符,二元运算符的优先级按以下顺序递减(同一行内的优先级相同,从左至右执行):
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
||
3.3. 浮点数
凹语言目前支持以下两种浮点数(均为IEEE 754标准):
f32
:32位浮点数;f64
:64位浮点数.
浮点数支持以下单目运算符
-
:取算术负值(既用0减去操作数)
例如:
i: f32 = 1.25
println(-i) //-1.25
浮点数支持以下双目算术运算:
+
:求和,两个操作数类型必须一致,返回值类型与操作数一致;-
:求差,两个操作数类型必须一致,返回值类型与操作数一致;*
:求积,两个操作数类型必须一致,返回值类型与操作数一致;/
:求商,两个操作数类型必须一致,返回值类型与操作数一致;
例如:
i, j: f64 = 1, 0.5
println(i + j) // 1.5
println(i - j) // 0.5
println(i * j) // 0.5
println(j / i) // 2
浮点数支持以下比较运算(双目):
==
:相等。操作数类型必须一致,返回值为bool
型,符合判断条件返回true
,否则返回false
,下同;!=
:不等;>
:大于;>=
:大等于;<
:小于;<=
:小等于。
使用无类型浮点常量进行变量快捷声明时,变量的类型为 f64
,如下面两种写法是等价的:
f := 1.5
f: f64 = 1.5
3.4. 字符串
字符串在凹语言中被视为基础数据类型,类型名称为:string
,字符串字面常量通过双引号 ""
括起定义,采用 UTF-8 编码,例如:
s: string = "你好,凹语言"
println(s) // 你好,凹语言
println("+42") // +42
与整数、浮点数类似,字符串变量也可以使用 :=
快捷定义,例如:
s := "编号9527"
字符串支持加法(+
)双目操作,返回值为两个字符串的连接,例如:
s1 := "abc"
s2 := "123"
println(s1 + s2) // abc123
容纳字符串的底层结构是一个字节(既u8
)数组,可以使用 []
获取其中某个字节的数值,或一个子串,例如:
s := "abcdefg"
println(s[2]) // 99,既 'c' 的ASCII值
println(s[1:3]) // bc
在这种用法中,[]
内的下标单位是字节,而不是字符。如果源字符串包含非 ASCII 码字符(如中文字符),而下标未处于整字符边界处,则返回的子字串可能非法,例如:
s := "你好"
println(s[1:3]) // ��
s[m:n]
用法从第n
个字节处开始截取,返回的字串长度为 n-m
字节。若省略 m
则表示从字符串开始截取,若省略 n
则表示截取至字符串末尾,例如:
s := "abcdefg"
println(s[:3]) // abc
println(s[3:]) // defg
从底层数据的角度看,截取子字符串时没有重新申请字节数组拷贝,而是直接引用原始字符串的地址。为避免多个引用同一片内存的字符串相互修改的影响,字符串被设定为不能局部修改——既不能向 s[n]
赋值。下述写法是非法的:
s := "abcdefg"
s[0] = 99 // 非法操作
两个字符串间可使用 ==
、!=
运算符进行相等、不等判断,例如:
s := "abc"
println(s == "123") // false
println(s != "123") // true
内建函数 len
可用于获取字符串长度(以字节为单位),如:
s := "abcdefg"
println(len(s)) // 7
已知问题列表:
- 使用
[]
获取字符串变量的指定字节或子串时,未执行边界检查。该问题不影响语法兼容性,后续对本问题的修正不影响已有源代码,凹程序开发者无须对此进行特别处理。
4. 函数
函数是语句序列的打包,以便于被多次重复使用。本章介绍凹语言函数基本用法,以及函数值、匿名函数和闭包等特性。
4.1. 函数调用
我们在之前的章节中已接触过很多函数,比如常用的内置打印函数 println
。函数调用的一般语法为:
函数名(实参列表)
实参
指函数调用时实际传入的参数,与之对应的是函数声明时定义的形参
,形参只在函数体内有效。凹语言在调用函数时,参数使用值传递,在函数体内对形参值的变更不会影响实参的值,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
func Double(i: i32) => i32 {
i = i * 2
return i
}
func main {
j: i32 = 42
println(Double(j)) // 84
println(j) // 42
}
关键字 return
用于退出函数并返回值,一般语法为:
return 返回值列表
如果函数有多个返回值,应使用 ,
分隔,例如:
func MulRet() => (i32, i32) {
return 42, 13
}
类似于形参,函数声明时可定义具名返回值,例如:
func showAnswer() => (answer: i32) {
answer = 42
return
}
这种写法等价于:
func showAnswer() => i32 {
answer: i32
answer = 42
return answer
}
与其他变量类似,具名返回值以 0 值初始化。假如某个函数需要返回错误码、分支很多并且大多数分支错误码为 0 值,则使用具名返回值写法可以简化代码。
即使声明了具名返回值,return
时仍然可以指定别的值,比如:
// 版权 @2019 凹语言 作者。保留所有权利。
func showAnswer() => (answer: i32) {
answer = 13
return 42
}
func main {
println(showAnswer()) // 42
}
因此我们可以这样来理解:具名返回值实际上是在函数体内定义了一组局部变量,当该函数内的return
语句未指明返回值时,自动将这一组局部变量作为返回值填入。
4.2. 函数值
在凹语言中,函数可以被当作一种特殊的值,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
func Inc(i: i32) => i32 { return i + 1 }
func Dec(i: i32) => i32 { return i - 1 }
func main {
f := Inc
println(f(42)) // 43
f = Dec
println(f(42)) // 41
}
上例中,f
即为函数值,函数值可以被调用,调用方法与函数调用无异。
函数的类型由其参数以及返回值类型决定,通常这些信息被称为函数签名(Signature),如果两个函数 A 和 B 拥有相同签名,意味着它们:
- 参数个数相同;
- 返回值个数相同;
- 对于任意 n,函数 A 的第 n 个参数的类型与 B 的第 n 个参数类型相同;
- 对于任意 m,函数 A 的第 m 个返回值的类型与 B 的第 m 个返回值类型相同。
函数值的类型也是通过函数签名定义的,比如上例中函数值 f
的类型为 func(i32) => i32
,因此上例中 f
的快捷声明 f := Inc
等价于:
f: func(i32) => i32 // f == nil
f = Inc
与其他类型的值一样,函数值也为0值初始化,对应值为
nil
在凹语言中,类型不同的值不能相互赋值,这一点对函数值同样有效,由于函数类型由签名确定,因此将一个函数赋值给签名不同的函数值被视为非法,例如:
func Inc(i: i32) => i32 { return i + 1 }
func main {
f: func(i32)
f = Inc // 编译错误
}
既然被称为“值”,意味着函数值可以作为参数、和返回值在不同函数间传递,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
func inc(i: i32) => i32 { return i + 1 }
func dec(i: i32) => i32 { return i - 1 }
func getFunc(opCode: i32) => func(i32) => i32 {
if opCode == 0 {
return inc
} else if opCode == 1 {
return dec
} else {
return nil
}
}
func useFunc(i: i32, f: func(i32) => i32) {
if f == nil {
println("f == nil")
return
}
println(f(i))
}
func main {
useFunc(42, getFunc(0)) // 43
useFunc(42, getFunc(1)) // 41
useFunc(42, getFunc(2)) // f == nil
getFunc(2)(42) // 运行时异常
}
与其他基本类型不同,函数值只能与 nil
比较,既:函数值位于操作符 ==
、!=
左侧时,右侧只能为 nil
,对两个非常量函数值执行比较操作被视为非法。
如果被调用的函数值为 nil
,将触发不可恢复的运行时异常。
函数值与 C 系语言中的函数指针作用类似,可以更灵活的动态调整执行分支。但需要指出的时,相比于直接调用函数,调用函数值有一些额外消耗,性能敏感的场合需要格外注意。
4.3. 匿名函数及闭包
上一节介绍了函数值的基本用法,既然函数可被视为值,那么,在凹语言函数内部,是否可以像声明基本类型字面量那样,声明函数字面量?答案是肯定的,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
func useFunc(i: i32, f: func(i32) => i32) {
if f == nil {
println("f == nil")
return
}
println(f(i))
}
func main {
f := func(i: i32) => i32 { return i * i } // 声明匿名函数并赋值给 f
useFunc(3, f) // 9
}
其中快捷声明的函数值 f
,它的初始值是字面量 func(i: i32) => i32 { return i * i }
,既一个没有名字的函数。在凹语言中,这种没有名字的函数字面量被称为匿名函数。在访问者模式、自定义快速排序等应用场景中,经常需要传入一些函数值参数,而这些函数可能仅在当前上下文环境出现一次,为此额外定义模块级的全局函数有诸多不便,这时即可使用匿名函数。
在函数A内部声明的匿名函数B,可以访问A内部的局部变量,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
func useFunc(i: i32, f: func(i32) => i32) {
if f == nil {
println("f == nil")
return
}
println(f(i))
}
func main {
n: i32 = 0
f := func(i: i32) => i32 {
n = i * i
return n
}
useFunc(3, f)
println(n) // 9
}
可见函数值 f
可以读写外层的局部变量 n
。再来看一个更加复杂的例子:
// 版权 @2023 凹语言 作者。保留所有权利。
func genClosure(i: i32) => func() => i32 {
n := i
return func() => i32 {
n = n + 1
return n
}
}
func main {
c := genClosure(0)
d := genClosure(99)
println(c()) // 1
println(d()) // 100
println(c()) // 2
println(d()) // 101
}
每次调用 genFunc
都将生成一个函数值,这个函数值捕获了局部变量 n
,函数值每次执行会对捕获的 n
执行加1,多次执行 genFunc
所获得的函数值,它们捕获的 n
是不同的,每执行一次,捕获一个新的实例。
在函数内声明的匿名函数值,携带了本次运行时捕获的局部变量的状态。显然,这种函数值实质上就是闭包。
4.4. 条件语句
条件语句的一般形式为:
if 初始语句, 条件表达式 {
代码块1
} else {
代码块2
}
其中,条件表达式
必须为布尔型,条件语句先执行可选的初始语句
(初始语句,
可省略,此时表示没有初始动作),然后判断 条件表达式
是否为 true
,是则执行 代码块1
,否则执行 代码块2
。如果条件表达式
为 false
时无需执行任何操作,那么 else {...}
可省略。
需要注意的是,凹语言默认换行为语句结束,因此 else
语句需要跟 if
代码块的 }
位于同行,若 else
新起一行,将产生编译错误。
下面是一个多重条件判断的例子:
func Compare(x, y: int) => int {
if x < y {
return 1
} else if x > y {
return -1
} else {
return 0
}
}
4.5. 循环语句
循环语句有三种基本形式:
for { 代码块 }
for 条件表达式 { 代码块 }
for 初始语句; 条件表达式; 循环操作语句 { 代码块 }
其中 for { 代码块 }
将一直循环,直到代码块内的语句使用 break
关键字退出循环,使用 continue
关键字将略过后续语句,执行下一次循环,例如:
i: int
for {
i++
if i == 2 {
continue
}
println(i)
if i == 3 {
break
}
}
上述代码将输出:
1
3
for 条件表达式 { 代码块 }
循环每次执行 代码块
前会判断 条件表达式
是否为 true
,是则执行代码块,否则退出循环。在代码块内的语句也可以使用 break
、continue
退出循环或跳过后续语句执行下一次循环:
i: int
for i < 3 {
println(i)
i++
}
for 初始语句; 条件表达式; 循环操作语句 { 代码块 }
循环先执行一次 初始语句
,然后每次执行 代码块
前判断 条件表达式
是否为 true
,是则执行代码块,否则退出循环;每次代码块执行后,会执行一次 循环操作语句
。在代码块中使用 break
关键字将直接退出循环,使用 continue
关键字将跳过后续语句执行下一次循环(此时 循环操作语句
仍然会被执行),例如:
for i := 0; i < 100; i++ {
if i == 1 {
continue
}
println(i)
if i == 2 {
break
}
}
上述代码将输出:
0
2
4.6. 分支语句
分支语句常用于替代多重条件语句,一般形式为:
switch 初始语句, 条件表达式 {
case 分支表达式1:
代码块1
case 分支表达式2:
代码块2
default:
默认代码块
}
分支语句首先会执行可选的 初始语句
(初始语句,
可省略,此时表示没有初始动作),然后从上至下判断 条件表达式
的值是否与某条 分支表达式
相等,若相等,则执行对应分支的 代码块
;若所有分支条件均不满足,则执行可选的 默认代码块
(省略 default
分支表示没有默认代码块)。例如:
func f(x: int) {
switch x {
case 0:
println("x 为 0")
case 1:
println("x 为 1")
default:
println("x ==", x)
}
}
注意凹语言中分支语句默认跳出:进入某个分支,执行完对应代码块后,将直接跳出分支语句(既隐式break
),这与 C系语言的默认行为相反。
分支语句另一个特殊的用于类型断言的用法见 7.1节。
5. 复合数据类型
复合数据类型是内置的复杂类型的基础。
5.1. 引用
在凹语言中,在一个变量前添加 &
符号被称为 取引用 操作,假设该变量的类型为 T
,取引用操作返回值的类型为 *T
,被称为 T型引用,例如:
i: i32 = 42
j := &i // j的类型为 *i32 ,既:i32型引用
在引用型变量前添加 *
符号被称为 解引用 操作,解引用表达式的值为它所引用的原始变量的值,例如:
i: i32 = 42
j := &i
println(*j) // 42
在这里 *j
返回了 i
的值,类型与 i
一样为 i32
。解引用可以被赋值,其作用为向被引用的原始变量赋值,例如:
i: i32 = 13
j := &i
*j = 42
println(i) // 42
由此可见,凹语言中的引用与C系语言中的指针作用类似,但由于凹语言使用自动内存管理,这种相似性仅存在于表面,因此我们使用引用这一术语以示区别。二者最显著的不同,可通过下面这个例子窥见一斑:
// 版权 @2023 凹语言 作者。保留所有权利。
func genI32Ref() => *i32 {
i: i32 = 9527
return &i
}
func main {
p := genI32Ref()
*p = 13
q := genI32Ref()
println(*p) // 13
println(*q) // 9527
}
在凹语言中,跨函数传递引用是合法操作。返回局部变量的引用安全无害,编译器和运行时会跟踪变量使用的内存,自动执行清理回收。当然这导致了引用与指针的另一个直观的不同,既:引用不能执行算术运算。
对于引用类型 *T
,T
可以是基础类型,也可以是任何复合类型或自定义类型,**T
这样的多级引用(类似于C语言多级指针)也是合法的。
已知问题列表:
- 目前使用的RC模式无法自动回收孤环,进而导致内存泄漏。该问题不影响语法兼容性,后续对本问题的修正不影响已有源代码,凹程序开发者无须对此进行特别处理。
5.2. 数组
数组类型的基本声明如下:
[N]T
其中,N
为数组长度(大等于0常整数),T
为数组元素类型;例如:
a: [3]i32
a[0] = 42
println(a[0]) // 42
与很多语言类似,凹语言使用 x[M]
语法访问数组内的指定元素。数组变量声明时,可使用以下方式设定数组元素初始值:
a: [3]i32 =
println(a[0], a[1], a[2]) // 13 42 9527
[3]i32{13, 42, 9527}
声明了一个数组字面值,因此上例中数组变量 a
的声明可以使用快捷声明简化为:
a := [3]i32{13, 42, 9527}
声明数组字面值时,其后 {}
内所含元素的个数可以小于数组长度(但不可大于),不足的部分为 0 值,例如:
a := [3]i32{13, 42}
println(a[2]) // 0
内置函数 len
可用于获取数组长度既数组中所含元素的个数,例如:
a := [3]i32
println(len(a)) // 3
声明数组字面值时,若数组长度部分写为 ...
,则表明数组长度由其后 {}
内的元素个数决定,例如:
a := [...]i32{13, 42}
println(len(a)) // 2
在凹语言中数组是值类型,例如:
a := [...]i32{13, 42}
b: [2]i32
b = a
println(b[0], b[1]) // 13 42
b[0] = 9527
println(a[0]) // 13
由此可见,将一个数组赋值给另一个数组时,会将其中的每个对应元素都进行赋值,既执行深拷贝操作。由于数组是值,赋值后的数组间不存在相互关联。
元素类型相同,但长度不同的数组,被认为是不同的类型,因此下列程序非法:
a: [2]i32
b: [3]i32
b = a // 非法,类型不同不可赋值
已知问题:
- 通过变量下标访问数组元素时,未执行边界检查。该问题不影响语法兼容性,后续对本问题的修正不影响已有源代码,凹程序开发者无须对此进行特别处理。
- 在目前版本的实现中,数组被展开为一组线性值,因此数组赋值时,虚拟寄存器和指令数与数组长度成整倍数关系,若长度过大,目标代码的体积会急剧膨胀。该问题不影响语法兼容性,后续对本问题的修正不影响已有源代码,在现阶段,程序开发者应关注:除全局变量外,尽可能不要使用长度大于8的数组。
5.3. 切片
切片类型的基本声明如下,T
为元素类型:
[]T
切片的第一印象与数组很相似:它们都是特定类型对象的序列,但它们的实际行为存在巨大区别,切片是数组的部分引用,它时常取自于数组,例如:
arr := [...]i32{1, 2, 3, 4}
sl: []i32 = arr[0:2]
println(len(sl)) // 2
println(sl[0], sl[1]) // 1 2
表达式 arr[m:n]
返回一个切片,切片始于数组 arr
的第 m
个元素,切片长度为 n-m
,与字符串的类似语法相似,若省略 m
,则表示始于数组首个元素;若省略 n
,则终于数组最后一个元素。m
和 n
不可超过数组实际范围,否则会触发异常。
切片中并不保存实际数据,通过 []
访问到的对象位于它所引用的数组中,这意味着更改数组中的对象可能影响到切片,反之亦然,例如:
arr := [...]i32{1, 2, 3, 4}
sl := arr[0:2]
println(sl[0]) // 1
arr[0] = 13
println(sl[0]) // 13
sl[1] = 42
println(arr[1]) // 42
内置函数 cap
可用于获取切片的可用容量——既切片所引用的数组的长度减去切片开始位置,例如:
arr := [...]i32{11, 12, 13, 14}
sl1 := arr[1:2]
println(len(sl1), cap(sl1)) // 2 3
由定义可知,切片的容量恒大等于其长度。
一个数组可以被多个切片引用,如果引用的部分之间存在重叠,那么重叠部分的更改也会互相影响,例如:
arr := [...]i32{1, 2, 3, 4}
sl1 := arr[0:2]
sl2 := arr[1:3]
println(sl2[0]) // 2
sl1[1] = 42
println(sl2[0]) // 42
实际上,对切片使用[m:n]
操作符也可以获得一个新的切片,新切片始于源切片的第 m
个元素,其余规则与从数组中获取一个切片类似。
获取切片的方法除了引用数组或已有切片外,还可以通过内置函数 make
直接创建,形式签名为:
make([]T, Len: int, Cap: int) => []T
make([]T, Len: int) => []T // 等价于 make([]T, Len, Len)
返回值是一个类型为 []T
、长度为 Len
、容量为 Cap
的切片,其中 Cap
可以省略,此时切片的容量为 Len
,例如:
sl1 := make([]i32, 3, 5)
println(sl1[0], len(sl1), cap(sl1)) // 0 3 5
使用 make
函数创建切片时,隐式的创建了一个长度为 Cap
的数组,并将其引用为切片。
另一个与切片密切相关的内建函数是 append
,它用于向切片中追加元素,形式签名为:
append(sl []T, e T) => []T
该函数将元素 e
追加至切片 s
的尾部,并返回一个新的切片。由于凹语言的函数调用使用值传递,追加行为不会影响源切片 s
,因此实际常用的写法如下例:
sl: []i32
//...
sl = append(sl, 42)
既将 append
返回的新切片赋值给源切片。append
不仅向切片中追加元素,还可以追加另一个切片,例如:
sl1 := []i32{13, 42}
sl2 := []i32{9527, 1024}
sl1 = append(sl1, sl2...)
println(sl1[0], sl1[1], sl1[2], sl1[3]) // 13 42 9527 1024
当被追加对象是切片时,应在变量名后添加 ...
。
由于切片底层引用了一个长度固定的数组,如果使用 append
追加元素后,切片的长度未超过数组可用容量,那么数组对应元素的内容将被替换为追加元素,例如:
arr := [...]i32{1, 2, 3}
sl1 := arr[0:2]
sl1 = append(sl1, 5)
println(arr[2]) // 5
倘若新切片的长度超过原始数组容量,那么 append
函数会自动重新申请一个足够大的数组,将源切片的元素拷贝至新数组(既自动执行了一次深拷贝),然后再执行追加,这种情况我们称为切片扩容,例如:
arr := [...]i32{1, 2, 3}
sl1 := arr[:]
sl2 := append(sl1, 4)
sl2[0] = 42
println(sl1[0]) // 1
显然,如果发生了切片扩容,新切片与源切片的相互引用关系就切断了。
与前面章节介绍过的数据类型不同,切片类型的变量不可比较,因为切片不是纯值类型,而是与底层数组甚至其他切片存在引用关系,这种关联使得切片在运行时无法保证其中的元素值不变。
切片只能与常量 nil
进行比较,用于判断切片是否为 0 值,例如:
sl: []i32
println(sl == nil, sl != nil) // true false
实际上,如果需要判断某个切片是否为空,不应将其与 nil
比较,而应判断其长度是否为 0,因为存在长度为 0,但不为 nil
的切片,例如:
arr := [...]i32{1, 2, 3}
sl1 := arr[0:0]
println(sl1 == nil, len(sl1), cap(sl1)) // false 0 3
除特别说明外,凹语言程序应以相同的方式处理长度为 0 的切片,与 nil
值的切片。内置函数 append
既符合该要求,例如下列程序是合法的:
sl: []i32 // sl == nil
sl := append(sl, 5)
println(sl[0]) // 5
已知问题:
- 访问切片元素时,未执行边界检查。该问题不影响语法兼容性,后续对本问题的修正不影响已有源代码,凹程序开发者无须对此进行特别处理。
- 使用
[]
从数组或切片中获取新切片时,未执行边界检查。该问题不影响语法兼容性,后续对本问题的修正不影响已有源代码,凹程序开发者无须对此进行特别处理。
6. 自定义类型
自定义类型包括结构体、以及围绕结构体方法产生的接口。
6.1. 结构体
凹语言中结构体声明的一般形式为:
type 类型名 struct {
成员列表
}
其中成员列表
的部分与变量声明格式一致,比如下例:
// 版权 @2023 凹语言 作者。保留所有权利。
type Info struct {
name: string
age: i32
}
func PrintInfo(i: Info) {
println("名字:", i.name, ",年龄:", i.age)
}
func main {
i: Info
i.name = "张三"
i.age = 35
PrintInfo(i) // 名字: 张三 ,年龄: 35
}
与很多语言类似,凹语言 使用选择操作符 .
访问结构体值的成员。另外需要特别注意的是,选择操作符 .
也可以用于访问结构体引用的成员,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
type Info struct {
name: string
age: i32
}
func GetInfo() => *Info {
i: Info
i.name = "李四"
i.age = 42
return &i
}
func main {
j := GetInfo() // j 的类型是引用, *Info
println(j.name, j.age) // 李四 42
}
由此可见,无论是值还是引用,访问其成员的方式是一致的,这与 C 语言不同(C 语言使用 ->
访问结构体指针的成员)。
结构体的成员类型,不能包含结构体本身,因为这会引起无限嵌套;事实上任何会引起无限嵌套的结构体都是非法的,比如两个结构体互相包含对方。但是结构体中包含本类型的引用是合法的(因为引用的实质是指针),这种用法常用于创建链表结构,比如:
type Node struct {
data: i32
next: *Node
}
结构体字面值的例子如下:
// 版权 @2023 凹语言 作者。保留所有权利。
type Info struct {
name: string
age: i32
}
func main {
i := Info{name: "王五"}
println(j.name, j.age) // 王五 0
}
在声明结构体字面值时 {}
内为成员字面值列表,未列出的成员为 0 值。
如果结构体内的所有成员变量都可比(既该成员类型的变量间可执行 ==
操作),则该结构体的变量间也可比。在目前已介绍的数据类型中,切片是不可比类型,因此直接或间接包含切片的结构体均不可比。与其他类型的声明类似,结构体可在模块内的任意文件中声明,且无需“先声明再使用”。
6.2. 方法
自定义类型除了可以通过结构体达到对成员数据的封装外,最大的作用是它们可以拥有方法。在凹语言中,方法是一类特殊的、依附于特定类型的函数,见下例:
// 版权 @2023 凹语言 作者。保留所有权利。
type Info struct {
name: string
age: i32
}
// 方法声明:
func Info.Print {
println("名字:", this.name, ",年龄:", this.age)
}
func main {
i := Info{name: "张三", age: 35}
i.Print() // 名字: 张三 ,年龄: 35
}
方法声明一般形式如下:
func 类型名.方法名(参数列表) => (返回值列表) {方法函数体}
方法声明与普通全局函数声明的区别是函数名的部分增加了 类型名.
。在方法体内部,this
是方法所属类型的引用,通过 this.
可读写其成员。
如果仅从目前已介绍的语法来看,方法和全局函数可以完成同样的功能,比如上述例子和下面的代码几乎等价:
// 版权 @2023 凹语言 作者。保留所有权利。
type Info struct {
name: string
age: i32
}
func Print(this: *Info) {
println("名字:", this.name, ",年龄:", this.age)
}
func main {
i := Info{name: "张三", age: 35}
Print(&i) // 名字: 张三 ,年龄: 35
}
如上例所示,如果把全局函数的第一个参数设为自定义类型的引用,那么它的作用和方法几乎是一致的——事实上在凹语言中,从运行时层面看,方法就是首参数为自定义类型引用的函数。既然如此,那么为何要特意引入这一概念呢?原因有两个:
- 方法有助于聚合对象的功能;
接口
这一概念直接依赖于方法——类型的方法集合决定了它所实现的接口,第7章将对此进行介绍。
需要特别注意的是,按照凹语言语法,使用
func T.xxx()...
声明的方法并不属于类型T
,而是属于类型*T
——既T
的引用;也就是说具名类型本身不能拥有方法,拥有方法的只能是具名类型的引用。
6.3. 方法值
由于方法也是函数,因此可以仿照 4.2 节的模式使用它,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
type Vertex struct{
x, y: i32
}
func Vertex.Scale(s: i32) {
this.x *= s
this.y *= s
}
func Vertex.Sub(s: i32) {
this.x -= s
this.y -= s
}
func Vertex.Print {
println("x:", this.x, "y:", this.y)
}
func FnOp(s: i32) {
println("FnOp, s:", s)
}
func main {
v := Vertex{x: 100, y:200}
op : func(s: i32)
op = FnOp // op此时是函数值FnOp
op(13) // FnOp, s: 13
op = v.Scale // op此时是方法值v.Scale
op(2)
v.Print() // x: 200 y: 400
op = v.Sub // op此时是方法值v.Sub
op(50)
v.Print() // x: 150 y: 350
}
v.Scale
、v.Sub
是结构体变量 v
的方法,当它们被当成值来使用时,被称为方法值,比如上例中,它们先后被赋值给了 op
。方法值可以像普通函数值那样被调用,并且调用时可以影响方法上关联的引用(就如同直接调用原始方法那样),由此可见方法值捕获了原始对象引用,是带状态
的,在这一点上,方法值与闭包存在相似性。
从上例中还可以得知:函数值变量(比如例中的 op
),既可以存储函数值,也可以存储方法值,对调用方(caller)来说,二者没有区别。
6.4. 嵌入结构体
在声明结构体类型时,如果某个成员的类型是结构体,但省略该成员的名称,这种用法被称为嵌入结构体,例如下面代码中,结构体 Sc
中嵌入了 Sp
成员:
type Sp struct {
x: i32
}
type Sc struct {
Sp // 嵌入结构体
y: i32
}
嵌入结构体的成员名称就是其类型名称,我们依然可以使用选择符 .
访问它,例如下面的打印代码:
v: Sc
println(v.Sp.x)
在这个例子中,甚至可以省略 .Sp
的部分,比如上面的代码跟下述代码是等价的:
v: Sc
println(v.x)
在这种用法中,结构体 Sp
看起来似乎被嵌到结构体 Sc
中去了,这也是嵌入结构体名称的来源。但是如果结构体中包含了和被嵌结构体同样名称的成员,则访问被嵌结构体同名成员时不能进行省略,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
type Sp struct {
x: i32
}
type Sc2 strct {
Sp
x: f32
}
func main(){
v: Sc2
println(v.x) // 打印的是Sc2.x,f32类型
println(v.Sp.x) // 打印的是Sc2.Sp.x,i32类型
}
嵌入结构体除了可以复用类型的数据布局,另一个重要的功能是它可以复用类型方法,结构体会自动拥有被嵌入类型的方法,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
type Sp struct {
x: i32
}
func Sp.Show {
println(this.x)
}
type Sc struct {
Sp
y: i32
}
func main {
v := Sc{Sp:Sp{x: 42}, y: 13}
v.Show() // 42
}
在声明嵌入结构体字面量时,不能省略被嵌入结构体名,比如上例中的
Sc{Sp:Sp{x: 42}, y: 13}
,如果省略为{x: 42, y: 13}
将被视为非法。
Sc
中 嵌入 Sp
后,获得了后者的方法,使得 Sc
类型的变量 v
可以执行 Show
操作;在该例中,v.Show()
等价于 v.Sp.Show()
。如果结构体拥有和被嵌结构体同样名称的方法,处理方法与同名成员类似,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
type Sp struct {
x: i32
}
func Sp.Show {
println(this.x)
}
type Sc struct {
Sp
x: f32
}
func Sc.Show {
println(this.x)
}
func main {
v := Sc{Sp:Sp{x: 42}, x: 13.14}
v.Show() // 13.14
v.Sp.Show() // 42
}
为了实现对象复用,凹语言没有采用继承的设计(这与C++不同),而是使用了组合的设计。嵌入结构体就是组合的具体表现,嵌入结构体复用了被嵌入类型的内存布局和方法集,与接口(将在第7章介绍)一起,构成了凹语言对象抽象、复用的基础。
6.5. 匿名结构体
在本章前几节中,我们使用到的结构体都是按以下形式声明的:
type 类型名 struct {
成员列表
}
实际上该语法的内在含义是:
struct {...}
的部分定义了一个结构体;type 类型名
的部分为刚才定义的结构体赋予了一个名字。
那么在凹语言中,是否可以通过声明结构体字面量的方式直接创建一个结构体变量,而无需对该结构体命名?确实是可以的,这种用法被称为匿名结构体,例如:
// 版权 @2021 凹语言 作者。保留所有权利。
//全局匿名结构体变量:
global G: struct{
name: string
age: i32
}
func main {
G.name = "张三"
G.age = 88
println(G.name, " ", G.age) // 张三 88
//局部匿名结构体变量:
k := struct {name: string; age: i32}{name: "李四", age: 66}
println(k.name, " ", k.age) // 李四 66
G = k
println(G.name, " ", G.age) // 李四 66
}
由于匿名结构体没有类型名,因此声明匿名结构体变量时只能使用 变量名: struct{...}
或其快捷定义形式直接指定类型(结构体)。除此之外,匿名结构体以及其成员的使用,与普通的具名结构体基本一致。匿名结构体同样遵循0值初始化规则,其字面值中,未指定初始值的成员均为0值。
匿名结构体最常用的场景是全局配置变量。很多全局配置变量的类型,仅仅在声明该全局变量时会被使用一次,为仅存在一个实例的变量单独定义一个类型略显繁琐,此时即可使用匿名结构体替代(例如上例中的全局变量 G
)。
具名类型位于模块的名字空间下,但匿名结构体因为没有名字,其定义实际上位于全局空间,因此如果两个匿名结构体变量的内存布局完全一致(既成员个数、对应成员名、对应成员类型均一致),它们将被认为属于同一个类型,可以互相赋值(例如上例中的全局变量 G
和 局部变量 k
),哪怕这两个变量位于不同模块中,该特性依然成立;这引出了匿名结构体的另一个使用场景:跨模块传递参数。
由于匿名结构体没有类型名,因此按照语法规则,无法为其添加方法。
7. 接口
接口是凹语言抽象能力的灵魂。未使用接口的凹语言代码都是纯正的写实派,但是有了接口之后就可以通过鸭子类型的面向对象来达到抽象的目的。本章讲述接口的基本用法。
7.1. 空接口-万能封包器
在凹语言中,最简单的接口是空接口,既 interface {}
,声明接口类型变量的方法跟其它类型一致,例如下面的代码声明了一个名为 i
的空接口变量:
i: interface{}
习惯上我们一般将 接口类型变量 称为 接口值。空接口有一个非常独特的特性:任何类型的值都可以赋值给空接口值,例如下面的操做全是合法的:
iface: interface{}
iface = 777 // 无类型整数赋值给空接口
iface = 13.14 // 无类型浮点数赋值给空接口
iface = "你好,空接口" // 字符串赋值给空接口
i: i64 = 58372665865
iface = i // 64位整数赋值给空接口
// 匿名结构体赋值给空接口:
iface = struct{name: string; age: i32}{name: "凹语言", age: 1}
这种赋值行为执行的是传值操作,相当于在接口值内复制了一份原始数据的拷贝,这份拷贝被称为接口值的具体值,具体值的类型被称为具体类型。
那么如何判断一个已被赋值的接口值所持有的具体类型?如何读取具体值?这就需要用到类型断言语法,它的一般形式为:
v, ok = iface.(Type) // 断言iface的具体类型是否为Type
其中 v
是类型为 Type
的值, ok
是 bool
型值,该语句执行后,若 ok
为 true
,则表明接口值 iface
的具体类型确实是 Type
,并且其具体值将被赋予 v
;否则表明 iface
的具体类型不为 Type
。实际示例如下:
// 版权 @2021 凹语言 作者。保留所有权利。
type T1 struct {
a: i32
}
func main {
ival: i32 = 777
printConcrete(ival) // i32: 777
printConcrete("你好凹语言") // string: 你好凹语言
v1 := T1{a: 42}
printConcrete(v1) // T1, T1.a: 42
printConcrete(13.14) // 未知类型
}
func printConcrete(iface: interface{}) {
ok: bool
i: i32
s: string
t: T1
i, ok = iface.(i32)
if ok {
println("i32:", i)
return
}
s, ok = iface.(string)
if ok {
println("string:", s)
return
}
t, ok = iface.(T1)
if ok {
println("T1, T1.a:", t.a)
return
}
println("未知类型")
}
在函数 printConcrete
内,通过接口类型断言,可以动态的判断传入的空接口值的具体类型,并获取其具体值。由于函数内未进行浮点数断言,因此输入浮点数时会输出“未知类型”。
注意函数 printConcrete
的参数类型为空接口(interface{}
),在 main
函数中调用它时,实际上执行了隐式转换(拷贝),比如语句 printConcrete(ival)
实际上等价于:
iface: interface{} = ival
printConcrete(iface)
凹语言在绝大多数情况下不允许隐式类型转换,但接口是个例外。当函数参数类型为接口时,若调用方填入的实参是具体类型,则编译器会自动执行赋值转换的操作。
如果接口值的具体类型存在多种可能,那么使用多个类型断言加条件判断的方法无疑很累赘,在这种场景下,可以使用switch...case...
格式的分支类型断言,例如上述 printConcrete
函数可以改写为:
func printConcrete(iface: interface{}) {
//分支类型断言
switch v := iface.(type) {
case i32:
println("i32:", v) // v是iface的具体值,该分支下,其类型为 i32,下同
case string:
println("string:", v)
case T1:
println("T1, T1.a:", v.a)
default:
println("未知类型")
}
}
其中 iface.(type)
是固定写法,后续每个 case
分支表示具体类型满足该分支条件。
任何类型的值都可以赋予空接口,它在凹语言中实际起到了万能封包器的作用,经常用于在函数间传递类型会动态变化的值。
本文中“空接口”指
interface{}
,既方法集为空的接口类型,下一节中的“非空接口”指方法集不为空的接口类型;当我们要描述值为0的接口值时,将使用“0值接口”,或“nil接口”,请注意区分。
7.2. 非空接口
接口是方法的集合,接口声明的一般形式如下:
type 接口名 interface {
方法集合
}
在方法集合
中的方法,其属性包括方法名以及方法的函数签名,比如我们定义一个接口如下:
type Stringer interface {
String() => string
}
该接口名为 Stringer
,其中包含一个名为 String
的方法,该方法没有输入参数,返回值为字符串。
如果一个具体类型 T
的方法集合,是某个接口 I
的方法集合 MethodSet_i的超集,那么我们称:类型 T
满足接口 I
。换句话说,设类型 T
的方法集合为 St
,接口 I
的方法集合为 Si
,类型 T
满足接口 I
的充要条件是:任取 m ∈ Si
,存在 m' ∈ St
,使得 m
与 m'
的名字相同,且函数签名相同。
如果类型 T
满足接口 I
,那么类型为 T
的值将可以被赋值给 I
型的接口值,在执行赋值操作时,类型 T
的值将被拷贝至接口值内部;这也是上一节中,空接口是万能封包器的由来,因为按照上述定义,interface{}
的方法集为空,任何类型都满足它。
接口方法可以被调用,其调用将动态切至接口值内部所包含的具体值的同名方法(如果接口值内部所包含的具体值为nil,那么调用将触发运行时异常)。非空接口是凹语言中重要的抽象手段。不同类型的对象可以满足同一个接口,使得调用者可以通过接口,按照统一的方式使用不同类型的对象,因此接口作用的本质,是一组方法约定,该约定的检查(具体类型是否满足某个接口),是编译时完成的。下面是一个具体的例子:
// 版权 @2023 凹语言 作者。保留所有权利。
type Printer interface {
Print()
}
type T1 struct {
i: i32
}
func T1.Print {
println("This is T1, this.i:", this.i)
}
type T2 struct {
s: string
}
func T2.Print {
println("This is T2, this.s:", this.s)
}
func PrintObj(p: Printer) {
p.Print()
}
func main {
p: Printer
v1: T1
v1.i = 42
p = &v1
PrintObj(p) // This is T1, this.i: 42
v2: T2
v2.s = "你好"
p = &v2
PrintObj(p) // This is T2, this.s: 你好
}
由此可见,同一个接口值 p
中封装了不同的对象时,使用同样的方法使用它,其行为也会随着对象类型的不同发生变化。
由于具名类型本身无法拥有方法,而只有其引用才能拥有方法(参考6.2节),因此上例中,v1
不能赋值给 p
,而只有其引用 &v1
方可。如果试图将 v1
赋值给 p
,将会引发编译错误。
7.3. 类型断言总结
7.1节介绍了如何从类型为 interface{}
的接口值中通过类型断言获取它所包含的具体值,该用法对于非空接口值依然成立,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
type Printer interface {
Print()
}
type T1 struct {
i: i32
}
func T1.Print { println("This is T1, this.i:", this.i) }
type T2 struct {
s: string
}
func T2.Print { println("This is T2, this.s:", this.s) }
func doConcrete(p: Printer) {
switch v := p.(type) {
case *T1:
v.Print() // 方法直接调用,而非接口调用
case *T2:
v.Print()
}
}
func main {
v1 := T1{i: 42}
doConcrete(&v1) // This is T1, this.i: 42
v2 := T2{s: "hello"}
doConcrete(&v2) // This is T2, this.s: hello
}
注意函数 doConcrete
中 v.Print()
是直接调用,而非接口调用,因为在 case *T1
分支中,v
的类型是 *T1
。另外,非空接口值也可以通过 v, ok = iface.(Type)
形式进行具体类型断言,这与7.1节中空接口值的对应用法一致。
实际上类型断言的用法还不仅于此,在某些情况下,一个具体类型 *T
可能同时满足多个接口 I1
、I2
,那么当一个 I1
的接口值中包含的具体值类型为 *T
时,可以在该接口值上通过类型断言,直接获取一个类型为 I2
的接口值,例如:
// 版权 @2023 凹语言 作者。保留所有权利。
type I1 interface {
f1()
}
type I2 interface {
f2()
}
type T struct {
i: i32
}
func T.f1 { println("T.f1(), T.i:", this.i) }
func T.f2 { println("T.f2(), T.i:", this.i) }
func main {
v1 := T{i: 42}
i1: I1 = &v1
i1.f1() // T.f1(), T.i: 42
i2, ok := i1.(I2) // 断言为另一个接口
if ok {
i2.f2() // T.f2(), T.i: 42
}
}
这种用法一般常见于从 interface{}
接口值中获取非空接口。
除了形如 v, ok = iface.(Type)
的类型断言外,还有另一种模式的类型断言:
v = iface.(Type)
该模式取消了操作成功标志的返回值 ok
,只返回被断言类型的值。如果类型断言失败,则会触发运行时异常,建议仅在完全确认断言不会失败的情况下才使用该模式。
7.4. 接口的其它特性
在声明接口时,接口的方法集可以包含另一个接口,例如:
type I1 interface {
f1()
}
type I2 interface {
I1
f2()
}
使用该形式,编译器会将接口 I1
的方法拷贝至 I2
的方法集中,这与直接在 I2
的方法集中增加 f1()
是等价的。
如果接口 I2
的方法集是接口 I1
方法集的真超集,我们称“与I1相比,I2是小接口”,这一说法看起来有些反直觉,其内在逻辑在于:接口是方法合约,由于 I2
中的方法更多,因此满足 I2
的类型的集合,一定是满足 I1
的类型的集合的子集——或者说满足 I2
的类型少等于满足I1
的类型。从这个角度来说,空接口(interface{}
)是最大的接口,非空接口中所包含的方法越多,接口倾向于越小。
我们也可以声明匿名接口值,匿名接口各方面都与匿名结构体类似,它们都位于全局名字空间(可跨模块使用)。
接口调用比具体类型直接调用略慢,因此没有必要为只有一种类型满足的行为(或者说方法集)创建接口。
接口值是可比的,只有同时满足以下条件,两个非nil接口值 i1
、i2
才相等(既表达式 i1==i2
为 true
):
i1
的具体类型与i2
的具体类型相同,且该类型可比i1
的具体值与i2
的具体值相等
附录
附录包含语法规范简介和标准库简介。
A. 语法规范
这里是简化的凹语言语法规范,主要是作为理解参考。
A.1 文件结构
凹语言是一个精心设计的语言,语法非常利于理解和解析。一个凹语言文件中,顶级的语法元素只有5种:import、type、const、var以及fn。每个文件的语法规范定义如下:
SourceFile = { ImportDecl ";" } { TopLevelDecl ";" } .
TopLevelDecl = Declaration | FuncDecl | MethodDecl .
Declaration = ConstDecl | TypeDecl | GlobalDecl .
SourceFile表示一个凹源文件,由以下两个部分组成:ImportDec(导入声明)和TopLevelDecl(顶级声明)。其中TopLevelDecl由通用声明、函数声明和方法声明组成,通用声明再分为常量、类型和变量声明。
导入语法如下:
ImportDecl = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) .
ImportSpec = ImportPath [ "=>" PackageName ] .
ImportPath = string_lit .
PackageName = identifier .
imort 关键字用于导入包,导入的包还可以被重新命名(对应PackageName)。
以下代码是一个凹源文件的对应例子:
// 凹语言例子
import ("a", "b")
type SomeType int
const PI = 3.14
global Length = 1 // 全局变量
func main {
sum: int // 局部变量
println(sum)
}
只要通过每行开头的不同关键字就可以明确属于那种声明类型。
A.2 函数和方法
函数是所有编程语言中的核心,因为只有函数的语句才有了计算的功能。凹语言的函数也是一种值数据,可以定义包级别的函数,也可以为自定义的类型定义方法,同时还可以在局部作用域内定义闭包函数。在顶级声明中包含函数和方法的声明,从语法角度看函数是没有接收者参数的方法特例。
函数的语法规则如下:
FuncDecl = "func" MethodName [ Signature ] [ FnBody ] .
MethodDecl = "func" Receiver "." MethodName [ Signature ] [ FnBody ] .
MethodName = identifier .
Receiver = identifier .
Signature = Parameters [ "=>" Result ] .
Result = Parameters | ":" Type .
Parameters = "(" [ ParameterList [ "," ] ] ")" .
ParameterList = ParameterDecl { "," ParameterDecl } .
ParameterDecl = [ IdentifierList ] ":" [ "..." ] Type .
其中FnDecl表示函数,而MethodDecl表示方法。MethodDecl表示的方法规范比函数多了Receiver语法结构,Receiver表示方法的接收者参数。然后是MethodName表示的函数或方法名,Signature表示函数的签名(或者叫类型),最后是函数的主体。需要注意的是函数的签名只有输入参数和返回值部分,因此函数或方法的名字、以及方法的接收者类型都不是函数签名的组成部分。从以上定义还可以发现,Receiver、Parameters和Result都是ParameterList定义,因此有着相同的语法结构(在语法树中也是有着相同的结构)。
下面是函数和方法的常见形式:
# 函数声明
func()
func(x :int) => int
func(a, _ :int, z :f32) => bool
func(a, b :int, z :f32) => (bool)
func(prefix :string, values :...int)
func(a, b :int, z :f64, opt :...any) => (success bool)
func(int, int, f64) => (f64, *[]int)
func(n :int) => func(p :*T)
# 方法定义
func Person.GetName() => string { return this.name }
A.3 关键字和运算符
关键字是语法的组成元素,不能用于标识符。凹语言目前有19个关键字:
break defer if return
case else import struct
const for interface switch
continue func map type
default global range
以下是凹语言的运算符和与标点:
+ & += &= && == != ( )
- | -= |= || < <= [ ]
* ^ *= ^= <- > >= { }
/ << /= <<= ++ = := , ;
% >> %= >>= -- ! ... . :
&^ &^= =>
运算符用于组成表达式,标点用于组成或分隔语句。
A.4 数据类型
除了布尔类型、字符、整数、浮点数、字符串等基础类型,凹语言还提供了指针、数组、切片、结构体、map、函数和接口等复合类型。复合类型的语法定义如下:
TypeDecl = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec = AliasDecl | TypeDef .
AliasDecl = identifier "=" Type .
TypeDef = identifier ":" Type .
Type = TypeName | TypeLit | "(" Type ")" .
TypeName = identifier | PackageName "." identifier .
TypeLit = PointerType | ArrayType | SliceType
| StructType | MapType | FnType | InterfaceType
.
其中TypeDecl定义了类型声明的语法规范,可以是每个类型独立定义或通过小括弧包含按组定义。其中AliasDecl是定义类型的别名(名字和类型中间有个赋值符号),而TypeDef则是定义一个新的类型。而基础的Type就是由标识符或者是小括弧包含的其它类型表示。TypeName不仅仅可以从当前空间的标识符定义新类型,还支持从其它包导入的标识符定义类型。而TypeLit表示类型面值,比如基于已有类型的指针,或者是匿名的结构体都属于类型的面值。
下面以结构体为例展示复合类型的语法:
StructType = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl = (IdentifierList ":" Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName .
Tag = string_lit .
IdentifierList = identifier { "," identifier } .
TypeName = identifier | PackageName "." identifier .
结构体通过struct关键字开始定义,然后在大括弧中包含成员的定义。每一个FieldDecl表示一组有着相同类型和Tag字符串的标识符名字,或者是嵌入的匿名类型或类型指针。
以下是结构体的例子:
type MyStruct struct {
a, b : int "int value"
string
}
其中a和b成员不仅仅有着相同的int类型,同时还有着相同的Tag字符串,最后的成员是嵌入一个匿名的字符串。
A.5 语句块和语句
语句近似看作是函数体内可独立执行的代码,语句块是由大括弧定义的语句容器,语句块和语句只能在函数体内部定义。语句块和语句是在函数体部分定义,函数体就是一个语句块。语句块的语法规范如下:
FnBody = Block .
Block = "{" StatementList "}" .
StatementList = { Statement ";" } .
Statement = Declaration | ExpressionStmt
| IfStmt | Block | ReturnStmt
| ForStmt | BreakStmt | ContinueStmt
.
FnBody函数体对应一个Block语句块。每个Block语句块内部由多个语句列表StatementList组成,每个语句之间通过分号分隔。语句又可分为声明语句、标签语句、普通表达式语句和其它诸多控制流语句。需要注意的是,Block语句块也是一种合法的语句,因此函数体实际上是又Block组成的多叉树结构表示,每个Block结点又可以递归保存其他的可嵌套Block的控制流等语句。
其中声明语句和表达式语句语法如下(声明语句的细节可参考语言文档,这里不做展开):
Declaration = ConstDecl | TypeDecl | GlobalDecl .
TopLevelDecl = Declaration | FuncDecl | MethodDecl .
ExpressionStmt = Expression .
下面是声明语句和表达式语句的例子:
const Pi = 3.14
type MyInt int32
global x = 123 // 全局变量
x = x + 1
if 和 return 语句的语法定义如下:
IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
ReturnStmt = "return" [ ExpressionList ] .
ExpressionList = Expression { "," Expression } .
if 和 return 语句的例子如下:
func main {
if 1+1 == 2 { return }
}
for 循环语句的语法定义如下:
ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
Condition = Expression .
ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt = SimpleStmt .
PostStmt = SimpleStmt .
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
循环的例子如下:
for {} # 死循环
for true {} # 死循环
for ; true ; {} # 死循环
# C 语言风格循环
for i := 0; i < 10; i++ {}
# 循环迭代列表的元素
list := make([]int, 10)
for i, v := range list {}
基于声明、定义、分支、循环这些基本的语句,就可以构造出任意复杂的程序。
B. 标准库
包含 apple、builtin、errors、regexp、runtime、syscall 等包的简要说明。
B.1 apple - 苹果派
apple 包用于展示最简单的包和测试的构造。
B.1.1 函数
func Apple => string
- Apple 返回苹果的字符串
B.2 builtin - 内置包
凹语言中 bool
、int
、string
和 println
等都不是关键字,而是编译器内置的包定义。builtin
包中定义的名字都是小写字母开头,不需要 import
就可以使用。
B.2.1 类型
- bool:布尔类似
- u8:8比特无符号整数
- u16:16比特无符号整数
- u32:32比特无符号整数
- u64:64比特无符号整数
- i32:32比特有符号整数
- i64:64比特有符号整数
- int:有符号整数
- uint;无符号整数
- f32:IEEE754 单精度浮点数
- f64:IEEE754 双精度浮点数
- string:UTF8 编码的字符串
- rune:字符,底层为 i32
- byte:字节,底层为 u8
- error:错误接口
B.2.2 常量
- nil:空引用,可以用于切片、接口的比较。
B.2.3 函数
func print(args: ...Type)
func println(args: ...Type)
func new(Type) => *Type
func make(t: Type, size: ...IntegerType) => Type
func len(v: Type) => int
func cap(v: Type) => int
func append(slice: []Type, elems: ...Type) => []Type
func copy(dst, src: []Type) => int
func panic(msg: string)
- print:打印基础类型
- println:打印基础类型并换行
- new:创新一个 Type 的实体并返回引用
- make:创建切片,并返回引用
- len:获取数组、切片、字符串等类型值的元素长度
- cap:获取数组、切片的容量
- append:向切片添加元素
- copy:赋值切片
- panic:泡出异常
B.3 errors - 错误包
errors 包用于创新一个可以表示字符串信息的错误对象。
B.3.1 函数
func New(text: string) => error
- New 创建一个错误对象
B.4 regexp - 正则包
正则包是 Rob Pike 最小正则实现:https://www.cs.princeton.edu/courses/archive/spr09/cos333/beautiful.html
B.4.1 函数
func Match(regexp, text: string) => bool
B.5 runtime - 运行时包
凹语言运行时类型和函数的实现。
B.5.1 常量
const WAOS = "..."
- WAOS:凹语言目标OS名字,目前有 wasi、mvp 等
B.6 syscall - 系统调用包
凹语言不同目标平台提供的系统调用。通常是 runtime 或其他包封装 syscall 包的函数。