C++最初在C基础上扩展了面向对象,可以更方便的使用类进行封装,使得对象的使用也越来越频繁。 对一个大型项目,总有大量的对象相互引用组成很复杂的有向图,C++的手动内存管理越来越繁琐。 而且C++依然允许不安全的操作,C11中的cast语义但并不是强制性的,还缺少边界检查,甚至允许将整数转成指针等。 这些内存问题虽然可以用ASAN来检查,但ASAN无法检查出所有情况,比如非法指针恰好落在正在使用的区间时ASAN逻辑上无法判断是否非法指针。 因此内存安全需要从语法语义的约束设计才能保证。
考虑改进C++语言,或者改进“C with class”,应该是在安全的自动内存基础上扩展面向对象。
下面是按这个的思路设计来Fèng编程语言,但目前仅写了grammar。
先整理一下主要的特性:
- 内存安全机制的设计:
- 自动内存管理:指针必须指向一个对象或者为空,而对象在没有被指针时将被释放,释放的对象可以立即或延迟回收空间。
- 指针安全使用:指针不能通过运算生成,不能将任意整数转换成指针值,这也包括对指针进行自增自减。
- 强制检查边界:比如数组及buffer空间的操作。
- 强制检查类型:类型转换时必须检查是否允许转换,即使编译时无法分析时,也要在运行时做检查。
- 虚引用:用虚引用代替C语言取地址运算(&),限制在实例生命周期内进行使用。
- 面向对象的基本元素,包括继承多态和接口抽象。
- 资源类是参考C++析构函数设计的,可用回收底层库分配的资源、处理循环引用等。
- 用一种类型来管理一段连续空间,并且可以映射为struct和union使用。
- 模块化代码管理,模块之间使用符号需要导出和导入。
- 值类型变量,变量就是实例本身,不需要额外分配内存。
以及几个次要的特性:
- 异常处理机制。
- 泛型或者模版,泛型参数可以加约束条件,类成员方法也可以带参数。
- 允许给类自定义运算符,从C++的运算符重载简化来。
语句是程序序列的基本指令单元,除了块语句和控制语句外需要以;结尾:
比如赋值语句,等号分割成左右两个部分,左边是需要被赋值的操作数,右边是计算值的表达式:
var s = a + b;。
再比如调用语句也是常用的,仅包含一个call表达式:
start();。
语句右边的表达式是计算的主体,由多个运算根据优先级组合成:a + (b + c) * d - sin(e)。
函数和方法,是一个可重用、独立执行的代码块,它接收输入(参数),执行一系列操作,并可以选择性地返回一组输出值, 用于将复杂任务分解成更小、更易管理和维护的模块,提高代码复用性。
下面举例子来说明函数格式,这些格式也适用于方法。
定义sum函数,两个int参数,返回int类型值
func sum(a, b int) int {
return a + b;
}
假设printf是modulefmt提供的打印到终端的函数:
import fmt *;
func main() {
printf("%d + %d = %d \n", 1, 3, sum(1, 3));
}
可以没有返回值的函数:
import fmt *;
func print(a, b int) {
printf("%d\n", a + b);
}
可以有一个或多个返回值:
import fmt *;
func divAndMod(a, b int) (int, int) {
return a / b, a % b;
}
func main() {
var a, b = 1, 3;
var s, d = divAndMod(a, b);
printf("%d / %d = %d\n%d % %d = %d\n", a, b, s, a, b, d);
}
提供派生类型机制,这些派生类型分为: 类与接口、结构、枚举和属性。
例如自定义派生类Complex以及使用它定义变量c1和c2:
class Complex {
var real, imag float64;
}
func sample() {
var c1 Complex = {real=1.5,imag=2.4};
var c2 *Complex = new(Complex, {real=3.3,imag=0.6});
}
作为代码组织单元,同一个目录下的文件都属于一个module,且module名与目录名相同,因此无需在文件里声明。
- module内定义的符号在内部都可使用,但是如果在外部则需要使用
export导出。 - 如果想使用其他module里的符号,需先要
import对应的module路径:
下面将详细描述各个语法元素的定义及用法。
module是代码的基本管理单元。
- 在module内部所有的内容都可见,跨module只能访问导出的符号。
- module名称与路径一一对应,在文件中不声明module名称。要求目录名称的规则和变量名称一样。
例如在Linux下,module
com.jjj.base.util对应的相对路径为com/jjj/base/util。
支持导出任何全局符号;比较特殊的是成员:
- 导出的类,其成员不跟随导出的,需要单独导出。
- 导出的接口的方法默认跟随导出。
- 导出的结构类型的字段跟随导出,
- 导出的枚举类型的所有值都跟随导出。
例如,下面导出全局变量gFoo、函数aFoo、类Foo及其字段bar和方法go:
export var gFoo Foo;
export
func aFoo() Foo {
return gFoo;
}
export
class Foo {
export
var bar int;
export
func go() {
//
}
}
声明导入com.cossbow.fmt模块:
import com.cossbow.fmt;
func main() {
fmt$println(string("Hello Fèng!"));
}
可以导入全部可见的符号,就不需要加module前缀了:
import com.cossbow.fmt *;
func main() {
println(string("Hello Fèng!"));
}
可以设置module别名:
import com.cossbow.fmt ccfmt;
func main() {
var m Sring = ccfmt$sprintf("Hello Fèng!");
ccfmt$println(m);
}
基本类型是语言内置的类型,从内存角度看都能直接放在寄存器中。基本类型包含整数、浮点数和布尔三种类型。
虽然有字符串字面量,但没有内置字符串类型:字符串不能被寄存器直接存放,且字符串只在字符处理时才有意义。
内置的全部整数类型如下:
- 有符号:
int8/int16/int32/int64/int - 无符号:
uint8/uint16/uint32/uint64/uint
后缀的数字表示其位宽,无位数字后缀的是根据编译的目标平台决定。
有符号数最高位为符号位:默认0为正数,1为负数。因此有符号数数值的位宽少了一位。
不同整数类型之间必须显式转换:
func main() {
var a uint16 = 123;
var b int32 = int32(a); // 将uint16转换为int32
}
整数类型显式转换,相当于从低位到高位按位复制,因此可能会出现整数溢出:
- 如果位宽从大的转到小的会被截断,造成整数溢出。
- 有符号与无符号之间转换时,符号位会复制到对应数位上,导致整数值发生变化。
语言本身不检查溢出,需要程序员自行处理。
浮点数是由IEEE 754标准定义的,
包括单精度数float32和双精度数float64两种。
类型符号为bool,且只有true/false两种取值:
- 支持逻辑运算,关系运算(但只支持相等和不等),及位运算(与、或、异或三种)。
- 关系运算的结果一定是布尔值。
- 不支持和整数、浮点数的互相转换。
if、for中的条件表达式返回值必须是bool类型的。
布尔类型占1个字节,只使用最低位表示,且其他位的值不能影响布尔运算结果。 最低位的值与布尔值对应:
| 布尔值 | 整数值 |
|---|---|
| false | 0 |
| true | 1 |
下表列出了主要运算符的优先级(自上而下优先级递减):
| 顺序 | 运算符集 | 备注 |
|---|---|---|
| 1 | new(),圆括号,lambda,字面量 | |
| 2 | 断言,索引,引用字段,调用函数 | |
| 3 | +,-,! | 一元运算符 |
| 4 | ^ | 幂运算 |
| 5 | *,/,% | |
| 6 | +,- | |
| 7 | <<,>> | |
| 8 | & | |
| 9 | ~ | |
| 10 | | | |
| 11 | <,<=,==,!=,>,>= | |
| 12 | && | |
| 13 | || |
显然第1、2行是特殊的操作符,且都是左结合的。 而第3行的一元运算符是右结合的,而二元运算符除了幂运算均是左结合的。 这里特殊的是幂运算,是右结合的,其优先级高于左边的一元运算,而低于右边的:
func test(a,b int) {
var x int;
x = -a^b; // 等效于:-(a^b)
x = a^-b; // 等效于:a^(-b)
}
| 运算符 | 描述 |
|---|---|
| ^ | 幂运算 |
| * | 乘法 |
| / | 除法 |
| % | 取模 |
| + | 加法 |
| - | 减法 |
| 运算符 | 描述 |
|---|---|
| ! | 按位取反 |
| << | 左位移 |
| >> | 右位移 |
| & | 位与 |
| ~ | 位异或 |
| | | 位或 |
| 运算符 | 描述(左边*右边) |
|---|---|
| < | 小于 |
| <= | 小于或等于 |
| == | 等于 |
| != | 不等于 |
| > | 大于 |
| >= | 大于或等于 |
| 运算符 | 描述 |
|---|---|
| ! | 逻辑非 |
| & | 按位与 |
| ~ | 按位异或 |
| | | 按位或 |
| && | 逻辑与 |
| || | 逻辑或 |
因为布尔类型只有最低位有效,其他位忽略,则位运算的&、~、|对布尔值运算的结构依然是有效的布尔值,
且运算结果&与&&一致、|与||一致,差别是&&和||具有“短路”效应:
即当左边的计算结果可以决定最终结果时,右边的表达式就不会被执行了。
下面举例说明&&的“短路”效应:
func contains(v int) bool {
// TODO: check v is in the collection
}
func isEmpty() bool { return true; }
func test(v int) bool {
// isEmpty返回true,显然右边就不需要再计算了,即contains不会被调用
return !isEmpty() && contains(v);
}
仅数组默认支持这种运算符。索引运算符是由中括号组成,括号中是获取索引值的表达式。 其用法有两种:
- 索引表达式在右边是读操作,即获取索引对应元素的值作为运算结果。在语句中可以有两种方式接收取值:
- 左边用两个变量接收:第一个接收的是值;第二个接收的是布尔值,表示索引对应元素是否存在。
func test(arr [16]int) { if (var v, exists = arr[16]; exists) { // 索引16越界了,也就是不存在,所以exists=false,无法进入这个分支 } } - 左边一个变量或者在表达式中使用时,仅返回元素值,如果元素不存在则终止运行并抛出异常。
func test(arr [16]int) int { return arr[16]; // 索引越界,终止运行并抛出异常 }
- 左边用两个变量接收:第一个接收的是值;第二个接收的是布尔值,表示索引对应元素是否存在。
- 放左边为写操作,即修改索引对应元素的值。
数组的容量是创建之后就不能变了,所以索引越界自然也要终止运行并抛出异常。
func test(arr [16]int) { arr[15] = 0; // 修改索引为15的元素值为0 }
使用格式:new(类型, 参数),例如:
// 创建类Device的实例
var a *Device = new(Device);
// 后面的参数为初始化
var b *Device = new(Device, {});
引用类型的变量和实例是分离的,这种实例则都是通过new创建的。
参数是可选的,并且对不同类型的意义也不同:
用于判断是否能类的引用是否能进行转换的语法,比如类的实例的引用传递可以给接口或父类指针,反过来则需要判断其类型。 有两种用法:
- 用一个变量去接收结果,运算表达式返回的是对应类型的引用,这种情况下如果不能转换则抛出异常。同时可以参与表达式计算。
func test(o *Object) { var f *File = o?(*File); // 转换成File类的引用 var w Writer = o?(*Writer); // 转换成Writer接口 o?(*Writer).write("Hello!"); // 在表达式中使用 } - 用两个变量去接收的话,就会返回元组,不能参与表达式计算。
第一个值是类型引用参考第1条;第二个为
bool值,表示是否能进行转换。这时不会抛出异常。func valid(w *Writer) { var f, ok = w?(*File); if (ok) f.close(); // 如果匹配上,转换的结构放在变量f上 }
赋值运算符相当于运算的一种简写,即左操作数自己与右操作数参与对应的运算后再赋值给左操作数。因此也要求赋值运算的左右操作数是同类型的。 也就是说,赋值运算对应的运算是操作数与结果都是类型相同的。那也可以约定,支持了自定义运算符的类型,也可以采用相应的赋值运算符。 比如:
func test() {
var i = 0;
i += 2;
}
如果实现了运算符+,作用是按左右顺序拼接字符串,那就可以使用+=运算符,其作用是:
右边字符串拼接到左边的字符串变量的右边,再将结果传递给左边变量。
这类运算符没有返回值,不能用于表达式中,只能用于特定语句。
类是不支持运算符的,但是可自定义一部分运算实现。
自定义的运算功能代码段与方法跟函数都不一样,而是由operator宏实现的。 每一种运算符都有固定名称和原型及操作数列表:
- 每种运算符有固定名称。
- 具体因不同运算符定义操作数(就是宏参数)。
- 名称和其他方法名称可以相同。
仅有一部分支持自定义:
| 运算符 | 宏名称 | 右操作数类型 | 结果类型 |
|---|---|---|---|
| * | mul | 同左操作数 | 同左操作数 |
| / | div | 同左操作数 | 同左操作数 |
| + | add | 同左操作数 | 同左操作数 |
| - | sub | 同左操作数 | 同左操作数 |
| < | less | 同左操作数 | 布尔类型 |
| <= | 无 | 同左操作数 | 布尔类型 |
| == | equal | 同左操作数 | 布尔类型 |
| != | 无 | 同左操作数 | 布尔类型 |
| > | more | 同左操作数 | 布尔类型 |
| >= | 无 | 同左操作数 | 布尔类型 |
类型是指实际表达式中的操作数类型,实现中不需要指定:
- 表中的类型相同,不仅是类相同,而是全相同,包括是否是引用类型。
- 如果结果的类型是引用,那会自动创建一个初始化的实例给结果
- 表中无宏名称的运算符,是依赖其他运算符的实现:
<=依赖的<和==都实现了就可以直接使用了,相当于这两个运算的结果的或||。>=依赖的是>和==,也是两个结果的或。!=仅依赖==,就是后者的结果取反!。
举个复数的例子:
class Complex {
var real,imag float64;
// 实现+运算
// result存放结果,并计算完成后返回
macro operator add(lhs, rhs, result) {
result.real = lhs.real + rhs.real;
result.imag = lhs.imag + rhs.imag;
}
// 实现*运算
macro operator mul(lhs, rhs, result) {
result.real = lhs.real * rhs.real + lhs.imag * rhs.imag;
result.imag = lhs.real * rhs.imag + lhs.imag * rhs.real;
}
}
func testAdd(a,b Complex) Complex {
return a + b;
}
func testMul(a,b *Complex) *Complex {
return a * b;
}
默认只有数组支持的索引运算符也可以自定义。
由于索引运算符分有读和写两种操作,因此分成indexGet和indexSet两个过程宏。
比如自定义一个字典类Map,功能是提供派生类型的Key和Value的索引。用法示例:
class Map {
// 索引读
macro operator indexGet(key, operand, exists) {
var n = getNode(key);
operand, exists = if (n != nil) n.value, true else nil, nil;
}
// 索引写
macro operator indexSet(key int, value String) {
set(key, value);
}
}
func main() {
var m Map;
m[100] = 159;
// 带检查读:运行时如果key不存在则exists为false
var v, exists = m[100];
// 直接读:运行时如果key不存在(exists为false)则终止执行并抛出异常
var v int64 = m[100];
printf("m[100] = %s\n", v);
}
在写操作时索引不存在是否终止执行取决于内部实现: 比如一般情况的Map是可以新增key的,而数组是不能自动扩容的。
类是面向对象编程的核心概念,描述了所创建的实例共同的特性和方法。 类对应在现实世界中人类知识中的分类,在程序中则是对实例(对象)的分类,并且定义了这些实例的共性(字段)和行为(方法)。
一个常规的类定义如下(包含一个字段和一个方法):
class Car {
var engine *Engine;
func start() {
engine.start();
}
}
定义了类之后需要实例化才能使用:声明一个类的值类型,或者使用new动态的创建一个类的实例。
func sample(engine *Engine) {
var c1 Car = {engine=engine};
// 或者
var c2 *Car = new(Car);
c2.engine = engine;
}
变量的类型定义基本都适用于类的字段,除了:
class Cat {
const id int;
var name *rom;
var mothr,father *Cat;
var children []*Cat;
// var who Cat; // ✖
}
类的字段的定义不要求顺序,和实际内存布局中的位置不需要一一对应。 不同于结构类型的字段。
在实例化时,const字段则必须初始化指定,var字段则可选初始化。
比如上面的Cat类,id必须指定初始化值,name则不强制:
func main() {
var c1 Cat = {id=1001};
var c2 Cat = {id=1001, name="Tom"};
// 下面是错误用法
// var c3 Cat = {name="Tom"};
// var c4 Cat;
// var c5 Cat = {};
}
同样通过new动态实例化也是一样:
func main() {
var c1 *Cat = new(Cat, {id=1001, name="Tom"});
// 下面是错误用法
// var c2 *Cat = new(Cat);
// var c3 *Cat = new(Cat, {name="Tom"});
}
显然如果类里面没有const的字段则不强制要求初始化。
class Mouse {
var id int;
var name *rom;
}
func main() {
var m1 Mouse;
var m2 *Mouse = new(Cat);
}
如果没有初始化,或者初始化中没有指定的字段,一律置为默认状态:对应内存全0,引用类型则是nil值。
副作用:一个导出类的,但它有未导出const字段,在其他模块就无法实例化该类。
比如下面的Dog类就只能在当前模块实例化:
export
class Dog {
const id int;
var name *rom;
}
export
func newDog(id int) *Dog {
return new(Dog, {id=id});
}
export
func dog(id int, name *rom) Dog {
return {id=id, name=name};
}
字段特有的一种的引用类型,表示方法为:~ 类型名称。
这种类型的引用不影响实例释放,且所引用的实例在释放后将自动置空(nil)。
在使用引用计数管理内存时,循环引用是个逻辑问题,但可以通过弱引用手动解决这个问题:
- 弱引用不影响实例释放这个原则,那就是说不会导致计数+1,因此计数会归零。
- 归零后实例释放了,将弱引用字段置空,这是内存安全的保证。
例如,将双向链表Node向前引用的字段prev定义为弱引用,就不会发生计数无法归零的问题:
class Node {
var key String;
var next *Node;
var prev ~Node;
}
方法与函数基本相同,区别是:
- 必须通过类实例来调用。
- 在方法内部能使用当前实例的类成员。
- 方法名称作为唯一ID,在当前类的方法集中是唯一的,包括继承来的方法,但子类会覆盖同名的父类方法。
比如定义Task类用于管理任务(枚举TaskState是任务的状态):
enum TaskState {WAIT, RUN, DONE,}
class Task {
var state TaskState;
func isRunning() bool {
return state == RUN;
}
func start() {
if (isRunning()) return; // 调用另一个方法
state = RUN; // 修改状态
}
}
通过值类型的变量来调用方法:
func sample1() {
var task Task;
task.start();
printf("task state '%s'\n", task.state.name); // 打印:task state: 'RUN'
}
也可以通过引用来调用:
func sample2() {
var task *Task = new(Task);
task.start();
}
this是一个类内部的特殊关键字,用于指代当前实例本身。
在成员方法内使用当前类的成员,本地变量可能和字段同名,那么需要通过this来使用字段:
class Cat {
var name String;
func setName(name String) {
log(); // 上文没有log函数,那就指向log成员方法
this.name = name; // 上文有name变量/参数,与成员字段name冲突,必须加this
}
func log() {
printf("%s: miao~~\n", name); // 上文没有name变量,可以省略this
}
}
在方法调用时this即会引用当前实例,保证实例不会被释放:
func sample() {
new(Cat).log(); // log方法退出后创建的实例才能被释放
}
this能传递给虚引用类型的变量,但如果强引用调用方法时可传递给强引用,
如果值类型调用时可赋值给值变量。例如:
class Foo {
var name String;
func aaa() {
var x &Cat = this;
}
func bbb() {
var x *Cat = this;
}
func ccc() {
var x Cat = this;
}
}
func use1() {
var f Foo;
f.aaa();
// f.bbb(); // ✖
f.ccc();
}
func use2(f *Foo) {
f.aaa();
f.bbb();
// f.ccc(); // ✖
}
func use3(f &Foo) {
f.aaa();
// f.bbb(); // ✖
// f.ccc(); // ✖
}
this可以放在返回值的地方,但也是不表示某种类型,而是当前实例本身。
当然能立即调用方法和引用字段,也可以赋值给与调用者同类型的变量:
class Car {
var speed int;
func forward() this {}
func stop() this {}
func backward() this {}
}
func sample1(c Car) {
var speed = c.forward().stop().backward().speed; // 链式调用
var c2 Car = c.forward();
}
func sample2(c *Car) {
var c2 *Car = c.forward();
}
func sample2(c &Car) {
var c2 &Car = c.forward();
}
继承也叫扩展,即扩展已有的类以便增加新的字段和方法。 子类继承了父类的字段和方法,自己新增字段和方法是可选的。
在继承时,子类的字段不能和父类重名:
class Device {
var id int;
}
class Disk : Device {
// var id int; // ✖:重名了
var diskId int;
}
方法允许重名,但必须原型一致,也就是多态。
多态(polymorphic)是指同一个行为具有多个不同表现形式或形态。 所以严格来讲抽象(详见接口)也属于多态。
下面举例说明类的多态:先定义一个父类Animal,并且有一个字段name和一个方法eat:
class Animal {
var name *rom;
func eat(food *rom) {
printf("Animal %s eating %s\n", name, food);
}
}
然后定义一个子类Cat,继承了父类字段name,下面实现一个与父类的方法eat同名同原型的方法:
class Cat : Animal {
func eat(food *rom) {
printf("Cat %s eating %s.\n", age, name, food);
}
}
允许Animal的引用指向一个子类实例,通过父类引用调用eat方法时,允许时实际会调用子类的eat方法:
func main() {
var animal *Animal = new(Cat, {name="Tom"});
animal.eat("fish-meat"); // 将打印的是:Cat Tom eating fish-meat.
}
通过这个例子可以看到,方法eat在继承之后可以允许有多个实现,父类引用的不同子类均会指向子类实现的方法。
子类在重实现父类方法时要求原型必须一致。
支持断言运算来判断子类型:
func test(animal *Animal) {
var cat, ok = animal?(*Cat);
if (ok) cat.eat("mouse");
}
func main() {
test(new(Cat));
}
父类与子类仅支持引用传递,值类型变量之间不能传递。且传递规则为:
- 引用类型相同的情况,子类可以传递给父类。
- 子类的常量强引用可以传递给父类的虚引用。
比如父类Animal和子类Cat之间传递:
func sample1(lc *Animal) {
var c1 *Cat = lc;
}
func sample2(lc &Animal) {
var c2 &Cat = lc;
}
func sample2(lc *Animal) {
var c2 &Cat = lc;
}
多态的父类的方法也有自己的实现,但接口的方法没有具体实现,而是接口的“子类”给出实现,因此叫抽象。 抽象出接口更好的作为约定和规范:
- 管理者只关注接口的实现,隐藏具体的类,并提供已实现接口的实例。
- 使用者不必关心是什么类,只要实现了接口就可以使用。
比如定义一个接口Task,仅包含一个简单方法run:
interface Task {
run();
}
定义两个实现类MyTask和YourTask:
class MyTask (Task) {
func run() {
println("Run my task!");
}
}
class YourTask {
func run() {
println("Run your task!");
}
}
用法和多态类似了:
func asyncRun(t *Task) {
t.run(); // 假装这里在异步执行
}
func main() {
asyncRun(new(MyTask)); // 打印:Run my task!
asyncRun(new(YourTask)); // 打印:Run your task!
}
当然也支持断言运算判断类型。
由于类是单继承的,因此所有类都会按继承关系形成一棵树,而这棵树的根类就是Object类。
这是内置的类,没有声明继承任何父类的类则默认直接继承Object类。
Object类没有任何成员,可以创建一个Object的对象。
func test() {
var o *Object = new(Object);
o = new(Device);
}
类的成员可以单独设置导出,并且默认不导出。
在util里有代码:
export List`T`{
var elements []T; // elements这个成员就是需要对外隐藏的
export func get(i int) { // 但是get这个成员就需要暴露出去
// TODO: 检查下标越界
return elements[i];
}
}
当一个类添加了release方法时,这个类就被标记为资源类,release方法会在这个类的实例释放时调用。
这个特性可以用于自动释放其他资源。比如C语言lib中分配的缓冲区:
class CBuffer {
const buf uint64; // 假设这个字段保存的是buf指针值
func release() {
cFree(buf); // 假设可以这样调C语言的释放函数free
}
}
资源类只能通过new创建实例。这个限制可以避免重复调用。
比如上面的CBuffer类,值类型在赋值中复制了buf的值,多个实例在释放时就会重复调用cFree(buf);。
但是外部资源的关闭往往是耗时操作,如果放在这里处理可能对性能的影响难以预料,而且还需要处理IO错误或异常, 所以应该采用异常语句来处理。
接口是从多态分离出来的特性,是去掉了具体实现的父类,而且没有字段。
这样接口看上去是由一组方法的集合,在定义时省去了方法前面的func关键字。
接口仅仅是约定和规范,不支持实例化,因此接口类型变量只能是引用。
接口可以进行组合:
- 组合成的接口包含各个组件的所有方法原型。
- 组合接口可以传递给组件接口,因为实现了组合接口当然也实现了组件接口。
- 接口的方法名称会检查冲突,不同组件中同名的方法被视为同一个方法,如果原型不一致则不能编译。
比如文件可以读和写,那可以这样设计接口:
interface Reader {
read(b *ram) (int, Error);
}
interface Writer {
write(b *rom, off, len int) (int, Error);
}
// 组合成的接口File包含read和write方法
interface File {
Reader;
Writer;
query() *FileInfo;
}
// 实现File接口的实例自然也实现了Write接口
func use(file *File) Write {
return file;
}
接口类型变量是引用类型变量,并且只能引用实现类的实例。 接口的变量声明需要加上引用标识符来标识引用类型。
允许的传递:
- 引用类型相同的情况,实现类可以传递给接口。
- 类型允许的条件下,接口的常量强引用可以传递给接口的虚引用。
- 类型允许的条件下,实现类的常量强引用可以传递给接口的虚引用。
比如接口Cache和实现类LocalCache之间传递:
func sample1(lc *LocalCache) {
var c1 *Cache = lc;
}
func sample2(lc &LocalCache) {
var c2 &Cache = lc;
}
func sample3(lc *Cache) {
var c2 &Cache = lc;
}
func sample4(lc *LocalCache) {
var c2 &Cache = lc;
}
枚举类型的值的个数是有限的,且必须在定义时把全部值都列举出来:
enum TaskState {WAIT, RUN, DONE,} // 注意结尾必须有个逗号“,”
枚举变量的值必须是枚举值中的一个,当然不能为空。
枚举类型内置了特殊属性,这些属性在编译时就已经确定:
id:自动按定义的顺序递增产生的整数值,就是说修改了顺序就会变化。name:就是定义的字面名称。比如上面定义的WAIT,其名称就是"WAIT"。value:允许自定义的属性,整数类型。在未定义情况下,第一个枚举值的value等于0,后面的等于上一个的vaue递增1。
使用枚举的值通常需要枚举类型为前缀,当然如果变量的类型明确就可以省略前缀,例如:
enum TaskState {WAIT, RUN, DONE,} // 未设置value,就等于id
enum BillState {WAIT, PAID=4, SEND, DONE,} // 这里WAIT=0,SEND=5,DONE=6,……
func main() {
var s1 = TaskState.WAIT; // s1初始化为枚举值:WAIT
s1 = RUN; // s1类型已知,因此省略前缀
var s2 TaskState = DONE; // s2已知,也可以省略前缀
var i int = TaskState.RUN.id; // i初始化为整数:1
i = s2.id; // i赋值为:3
var n *rom = s2.name; // n初始化为rom引用,内容为字符串"DONE"
var v int = BillState.SEND.value; // v初始化为整数:5
}
类型明确的情况还有switch语句中:
func sample(s BillState) {
switch(s) {
case WAIT:
// TODO
case PAID, SEND:
// TODO
default:
// TODO
}
}
支持迭代循环所有枚举值:
func main() {
for ( s : TaskState )
printf("name: %s, id: %d \n", s.name, s.id);
}
支持直接通过索引取值:
func sample() {
var s1 TaskState = TaskState[0];
var s2, ok = TaskState[4];
}
枚举的变量或数组元素的默认值为第一个枚举值(id等于0的)。
结构类型定义的是内存上的数据结构,按内存布局分为两种子类型:
- 结构体:所有字段的存储按顺序分配。
- 联合体:所有字段的存储是重叠的。
结构体和联合体的定义格式一样,只是开头的关键字不同:
- 结构体的定义格式为:
struct名称{字段列表}。struct Message { type int; success bool; value float32; ext [12]int; } - 联合体的定义格式为:
union名称{字段列表}。union DataType { type int; success bool; uv float32; }
相邻且相同类型的字段可以合并,当然不相邻的不能合并。以结构体为例:
struct Request {
type, code int;
data [56]uint8;
}
位域(bit-field)是字段实际使用的位宽,只能用于基本类型的字段。
位域取值为该字段类型的位宽范围,放在在字段名称后面。
例如设置code的位域为6(type未设置):
struct Request {
type, code:6 int;
data [56]uint8;
}
结构类型可以有两种实例化方式:
数组是用于存储一组连续重复元素的类型,元素可以为任意类型。 每个元素相当于一个变量,也分值类型和引用类型:
var a [4]int; // 基本类型数组
var b [4]Host; // 类数组
var c [16]*Bus; // 类引用数组
var d [12][4]int; // 定长数组的数组:即多维数组
var e [10][]int; // 变长数组的数组,区别于多维数组,元素其实为引用
值类型数组的元素所需空间是和数组一起分配的,可以直接使用:
func test() {
// 基本类型数组
var a [4]int = [1,2,3,4];
a[0] += a[1];
// 类数组
var b [4]Host = [{id=1}];
b[3].id = 111;
// 定长数组的数组:即多维数组
var c [4][8]int = [[1],[2]];
c[3][4] = 222;
}
引用数组的元素需要额外引用其他实例,默认值为nil(不引用任何实例)。
func test() {
var a [4]Device;
// a[2].name = "dev-2"; // 错误✖:这里会抛出空指针异常
a[0] = new(Device);
a[0].name = "dev-0"; // 只有a[0]可使用,其他元素依然是nil
}
数组长度是指能容纳的元素总数,声明变量类型时指定和不指定分别表示两种类型的变量。
声明时如果指定了大小是定长数组,也就是说数组方括号中的必须是整数字面量,或者整数常量表达式。
var a [4]int;
这种类型的数组是值类型变量。
初始化为数组字面量,初始化值数量不能超过数组长度; 如果小于则从第一个位置开始顺序初始化,后面则归零:
// var a [4]int = [1,2,3,4,5];
var b [4]int = [1,2]; // b初始化为:[1,2,0,0]
初始化为表达式时,表达式的结果必须是同类型长度相同的数组:
func foo() [4]int {
return [1,2,3,4]
}
func foobar() {
var a [4]int = foo();
// var b [2]int = foo(); // 错误✖
}
不指定长度是引用类型变量,也就是数组引用,可指向任意长度的数组实例。
数组实例是通过new分配的,并且在分配时必须指定分配的长度。格式为:new([长度]类型)
例如创建int类型数组:
func test(size uint) {
var a []int = new([4]int);
var b []int = new([size]int);
}
类的字段类型可以为数组或数组引用,这和变量用法一样:
class Foobar {
var foo [4]int32;
var bar []int64;
}
用于管理一段连续的内存空间:
- 变量是引用类型的。
- 能映射成具体类型来使用。
- 运行时检查边界,任何越界都抛出异常。
- 能进行只读控制。
mem类型的变量是引用,支持强引用和虚引用。
由于强引用类型,其引用的mem的实例需要用new创建。
在创建时需要传入长度,比如创建一个16字节的ram实例:
var a *ram = new(ram, 16);
创建时带上映射类型,这时长度是已知的:
var a *ram = new(ram`[4]int`); // 创建一个size为16字节的ram
注意:创建时new的类型参数只能是ram,创建一个只读的没有意义。
虚引用在引用值类型实例的时候,限制只能引用支持映射的类型。 例如:
struct Message {
key int64;
}
class Response {
var time uint64;
var msg Message;
}
func sample(msg Message, resp *Response) {
var a &ram = msg;
// var b &ram = resp; // ✖:不能引用class实例
var c &ram = resp.msg;
var d &ram = resp.time;
}
支持映射的类型:结构类型、基本类型和这两个类型的定长数组。 这些类型占据的是连续空间,而mem就是一段连续的空间,映射就是从起始地址开始一一对应起来。
映射结构类型后可以直接使用字段:
struct Message {
key uint32;
value [60]uint8;
}
func foo(a *ram) {
var m *ram`Message` = a;
printf("key=%u\n", m.key); // m.key对应的是ram的前4过字节
}
映射数组后可以直接使用索引:
func foo(a *ram) {
var m *ram`[]uint` = a; // 不固定长度的
printf("m[9]=%u\n", m[9]); // 和数组一样会检查边界
var n *ram`[16]uint` = a; // 固定了长度,在映射时会检查边界
printf("m[9]=%u\n", m[9]);
}
映射成基本类型后支持所有的基本运算符:
func foo(a *rom) int {
var i *rom`int` = a;
return i + 5;
}
但是不能直接赋值,因为变量是引用类型的,想修改其指向的mem类型实例,只能用复制。 例如下面例子可以修改传入参数所引用的实例内容:
func foo(a *rom) int {
var i *rom`int` = a;
i := i + 5;
}
func bar() {
var a *ram = new(ram`int`);
foo(a);
printf("a = %d\n", a); // 应该打印:a = 5
}
不同映射之间可以直接转换,当然依然会检查边界:
func foo(a *ram`Response`) {
var r *ram`Message` = a;
}
也可以反过来去映射:
func foo(a *ram`[]int`) {
var r *ram = a;
}
长度为字节数,可以通过函数sizeof获取。
func testLen(b *ram) int {
return sizeof(b);
}
在映射时计算所需空间大小,如果所需空间超过实际size则抛出越界异常:
func testLen() {
var b *ram = new(ram`[4]uint8`);
var c *ram`int64` = b; // 这一行会抛出越界错误
}
基本类型和结构类型size是确定的,而且这两个类型的定长数组的size也是确定的,因此只存在一个边界检查的问题:
struct Message {
id uint64;
val float64;
}
func testLen(a *ram) {
var b *ram`int32` = a; // 需要4字节
var c *ram`[2]int32` = a; // 需要8自己
var d *ram`Message` = a; // 需要16字节
var e *ram`[4]Message` = a; // 需要64字节
}
而映射后变长数组的size则是动态计算的,即:mem的长度除以元素大小并向下取整:
func testLen(a *ram) {
var b *ram`[]int32` = a; // b.size == sizeof(a) / 4
var c *ram`[]Request` = a; // c.size == sizeof(a) / sizeof(Request)
}
如果实例长度小于元素大小,那么数组长度就等于0了。
mem设计有两种子类型:
ram允许读写操作,可以传递给rom,允许创建。rom是只读的,不能传递给ram,不能创建。
只读是指在语法语义上的,映射之后不能进行复制、修改元素和字段等操作:
func foo(a *rom`[4]int`, b *rom`Request`) {
// 下面操作均不允许
// a := [0];
// a[0] = 0;
// b.id = 0;
}
只运行ram传递给rom:
func foo(a *ram) {
var r *rom = a;
// var s *ram = r; // 错误✖
}
不同映射之间、映射与原mem之间可以直接传递:
func foo(a *ram) {
var b *ram`Message` = a;
var c *rom`Message` = a;
var d *rom`Message` = b;
// var e *ram`Message` = c; // 错误✖
}
定义格式为:func 函数名 ( 参数表 ) 返回表 { 函数体 }
其中函数名是必须的,参数表、返回表及函数体都可以为空。下面举3个例子:
func run() {}
func start() { run(); }
func exec(a []Sting) *Error {
return nil;
}
函数名是函数的唯一ID,在模块内的函数集中是唯一的;并且需要通过函数名调用函数。
func add(a,b int) int { return a + b; }
func test() {
var s = add(1, 2);
}
参数是参数名和类型组成,且都是常量(省略了const),作用域在当前函数内。
下面的例子定义了类型为Queue的l和类型为int的a两个参数:
func send(l Queue, a int) {
l.push(a);
}
相邻且相同类型的参数可以合并定义,比如定义两个类型为int的参数a和b可以这样:
func add(a, b int) int {
reutrn a + b;
}
在参数和代码段之间声明返回值类型表,用圆括号括起来。例如函数foo返回一个int和一个float:
func foo() (int, float) {}
只有单一返回值的时候可以省略括号:
func online() bool {};
多返回值对程序设计的影响较大。比如,在不仅需要得到函数的执行结果,还需要知道函数执行的错误信息时, 就不必加个引用传参来修改外部变量了,直接返回即可:
func createDevice(host *Host) (*Device, *Error) {
if (host.inRecovery()) {
return nil, errorInRecovery;
}
// TODO
}
错误信息还可以作为异常抛出,这两种方案在这里都是可行的。
函数体由一组语句序列组成的:
func run(s int) {
var i = s+1;
do(i);
...
}
函数体内部能访问的变量组成上下文,函数内的上下文包括全局变量、参数表和本地变量。
const PI = 3.14;
func circlyArea(diameter float) float {
var radius = diameter * 0.5;
return radius * radius * PI;
}
函数原型是变量类型的一种,函数的定义去掉函数体就是原型:func 函数名 ( 参数表 ) 返回表。
这种类型的变量要么为空,要么指向与原型兼容的函数。
例如:
func add(a, b int) int { return a + b; }
func sub(a, b int) int { return a - b; }
func mul(a, b int) int { return a * b; }
func div(a, b int) int { return a / b; }
func Calc(a, b int) int;
func test(c Calc) {
printf("%d\n", c(rand(), rand()));
}
func main() {
test(add);
test(sub);
test(mul);
test(div);
}
函数原型支持匿名定义:
func test(c func(a, b int) int) {}
func supply(c int) func(a, b int) int {
switch(c) {
case 0: return add;
case 1: return sub;
case 2: return mul;
case 3: return div;
default: return nil;
}
}
func main() {
var c1 func(a, b int) int = add;
var c2 = sub; // 也可以省略类型,自动推导
}
块语句是由{与}括起来的语句序列组成的,块内上下文会嵌套,内声明的本地变量不能在外部使用:
func test() {
println("block 1");
{
println("block 2");
{
println("block 3");
// 嵌套没有限制
}
}
}
根据控制条件选择执行其中一个分支,有两种类型。
if紧跟带括号的条件表达式,然后是当匹配条件时执行的语句;之后的else开始的语句是未匹配时执行的,这个分支不是必须的。
表达式结果作为条件,必须是bool类型。
简单的条件语句:
func abs(m int) int {
if (m < 0)
return -m;
else
return m;
}
可以省略else语句:
func printIfError(err uint) {
if (err == 0) return;
printf("Error: %u\n", err);
}
可以在条件表达式前面加一个初始化语句:
func test(m Map`int,*Node`, k int) {
if (var n,ok = m[k]; ok) { // 这里的n和ok变量只属于当前块
printf("value of %d is: %s\n", k, n.value());
}
// printf("value of %d is: %s\n", k, n.value()); // 错误✖:外层不能使用
}
显然else可以嵌套if,就组成了多分支:
func compare(a, b int) int {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
} else {
return 0;
}
}
if..else可以作为元组使用,但是else分支不能省略了,且每个分支提供的元组长度必须一致:
func compare(a, b int) int {
return if (a < b) -1 else 0;
}
func minMax(x,y int) (int,int) {
return if (x < y) x,y else y,x;
}
由switch开始的带有一个条件表达式,多个匹配规则,每个规则由case开始,其下有一组语句。
匹配到的case规则其下的语句组会被执行,当执行结束后会跳出switch语句,不会继续下降,除非语句组最后一个是fallthrough语句。
func numberName(k int) {
switch(k) {
case 0:
println("zero");
case 1:
println("one");
case 2:
println("two");
case 3:
fallthrough;
default:
println("Error");
}
}
和if类似可以在条件表达式前面加一个初始化语句:
func test(n Node) {
switch(var v=n.value; v) {
case 1:
println("one");
}
}
也可以作为元组使用,规则与条件语句一样:
for后面的括号内是控制体,控制体可以且必须有一个控制条件表达式,也可以包括初始化和更新子语句;
之后是需要执行的语句或语句序列,称循环体。
当控制条件满足时重复执行循环体:
- 控制条件是一个
bool类型的条件表达式,当结果为true时才会执行循环体。 - 循环体是一个语句,如果需要多个语句操作则需使用块语句包起来。
简单的循环语句为括号内只有条件表达式:
func main() {
var i = 0;
for ( i < 100 ) {
println(i);
i += 1;
}
}
完整的控制体格式为:【初始化】;【表达式】;【更新】
- 【初始化】在循环前执行一次,然后再进入循环过程。
- 循环过程的每一轮:先判断【表达式】,
false则结束循环,true则执行循环体,最后执行【更新】。 - 循环体中可以有控制循环的操作:
- 遇到
continue语句则直接进入下一轮循环,也就是2中描述的。 - 遇到
break则直接跳出当前循环或指定循环。
- 遇到
例如循环100次,并每次打印变量i的值:
func main() {
for (var i = 0; i < 100; i += 1) {
println(i);
}
}
对于变量数组,可以用更简单的方式遍历所有元素:
func main() {
var src []int = [0,1,2,3,4,5,6,7,8,9];
for ( v : src ) // 只获取值
handle(j);
for ( i,v : src) // 同时获取索引和值
println(i, v);
}
当然continue和break语句对迭代循环依然有效。
循环语句遍历形式默认只对数组使用,对自定义类可以实现自定义迭代器,然后就可以用迭代循环来遍历了。
实现迭代是通过名为Iterator的helper宏实现的,但考虑循环是很常用的语法,所以利用宏直接由编译器展开。
宏的字段不限制,包含4个方法initializer、condition、updater、get
| 方法 | 作用 | 参数 |
|---|---|---|
| initializer | 初始化迭代器 | 无 |
| condition | 循环条件 | 无 |
| updater | 更新迭代器 | 无 |
| get | 获取值 | 不限制 |
其中get可以写多个,但参数个数不能相同。
示例:
class Node`T` {
var next *Node`T`;
var value T;
}
export
class List`T` {
var head *Node`T`;
macro helper Iterator {
cursor *Node`T`,
index int;
initializer() {
cursor = head;
index = 0;
}
condition() {
cursor != nil
}
updater() {
cursor = cursor.next;
index += 1;
}
get(v) {
v = cursor.value;
}
get(i, v) {
i = index;
v = cursor.value;
}
}
}
func test(src List`*Team`) {
for ( t : src) { // 匹配第一个get
// TODO
}
for (i, t : src) { // 匹配第二个get
// TODO
}
}
赋值运算符只能用于语句中,即:
func test() {
var i = 0;
i += 2;
}
赋值语句的左边是操作数(指将要被修改值的对象),后边是由表达式列表组成的元组:
func test(x,y int, u *User, a []int) {
x = 2;
u.id = 1;
a[0] = 8;
x, y = 2, 4;
u.id, x, y, a[0] = 1, 2, 4, 8;
}
- 赋值语句并不是拆成多个语句独立执行的,而是先计算操作数的表达式,再计算表达式元组,再一一赋值。
- 显然,不同类型的赋值可以放在一起。
变量声明语句右边的初始化赋值是可选的,且也是先计算右边表达式元组,再赋值给左边变量:
func test() {
var a,b,c = 1, "ggyy", 1.6;
}
赋值只能修改变量本身的值,对引用类型变量只能修改其指向。 而复制是针对引用类型变量的专用语法,作用就是复制其引用的实例内容。
可以从值类型复制:
func copy1(u *User, v User) {
u := v;
}
或者从另一个引用复制:
func copy2(u *User, v *User) {
u := v;
}
复制的时候要求原类型必须相同,如果是数组则按最小长度复制。
声明一个或一组变量使用关键词var或const开头,后面紧跟变量的名称,然后是变量类型。
var声明一个普通变量。后面可以有初始化值。TODO:允许未初始化并置默认值?还是强制使用前必须赋值?const用于定义不变的量,不能重新赋值,且必须在声明时初始化值。
func main() {
var r int = 5;
var g float64;
var a float64 = 0;
const pi float64 = 3.1415926;
const pi = 3.1415926; // 当设置了初始化值时,类型可以省略
g = 2 * r * pi;
a = r * r * pi;
}
由于声明的类型可以省略,因此会出现两种情况:
- 省略时,左边可以是不同类型的表达式,这样右边对应的变量类型会自动推导为不同的类型。
- 如果显式加上,显然左边的类型统一了,右边的类型自然必须兼容。
func test() {
var a,b int = 1,2;
// var a,b int = 1, "ggyy"; // 错误✖,必须拆成两个语句
}
分为抛出和处理异常两种语句。
抛出异常是为了处理返回值没有处理的错误。抛出异常后:
- 会终止当前过程的执行,不执行返回语句,而是抛出一个包含错误信息的实例。
- 如果调用的过程抛出了一个异常A,会从调用处终止当前过程的执行,继续抛出异常A。
func example1() {
throw new(Exception);
}
func example2() {
example1();
println("example1()必然抛出异常,所以这一行不会执行!");
}
func example3() {
example2();
println("example2()也会抛出异常,所以这一行也不会执行!");
}
如果发生了抛出异常,那这个会一直按调用链往外抛,直到被catch匹配到为止。
抛出的异常的类型需要自己定义,在异常中详细说明。
异常处理语句分三个部分:
try部分:必须的部分,将需要处理的代码块包裹起来。catch部分:可以有多个,分配匹配不同的异常类型。匹配到就执行对应的代码块,否则继续往后匹配。 如果没有都未匹配成功则继续往外抛出。finally部分:上面两部分无论什么情况,都必须执行这部分。 如果第1部分有return语句,先执行return后的表达式,再执行finally部分,最后再正式返回。 如果第2部分没有或者未捕获到异常,则先执行finally部分后继续抛出。
第2和3部分至少必须有一个。
完整的例子:
func calc() {
try {
step1();
step2();
} catch(e *NilPointerError) {
println("捕获到了空指针");
} catch(e *IllegalStateError | *IllegalArgumentError) {
println("捕获到了状态错误或者参数错误");
} finally {
println("最终经过这里再往下执行");
}
return getResult();
}
没有finally部分,只有catch部分:
func calc() {
try {
step1();
} catch(e *IllegalStateError) {
println("捕获到了状态错误或者参数错误");
}
return getResult();
}
没有catch部分,只有finally部分:
func calc() {
try {
step1();
step2();
return getResult();
} finally {
println("最终经过这里再往下执行");
}
}
finally可以用来释放外部资源,避免资源泄露。比如文件关闭:
func readTxt() String {
var f, er = open("tmp.txt");
if er != nil {
return string("");
}
try {
step1(f);
step2(f);
return getTxt(f);
} finally {
f.close();
}
}
注意:catch匹配括号里的参数e是常量参数。
声明方式参考变量声明语句。
变量的声明方式有两种:可变的var和不可变的const,区别是后者在首次赋值后就不能再修改了。
变量的类型分三种情况:值类型、引用类型、枚举和函数原型。
变量与实例一体,变量的值就是实例本身,赋值相当于复制实例的数据:
- 基本类型的变量本身只是一个寄存器值,修改通常只需一个机器指令:
var a int = 1; // 变量a赋值为字面量数值1,那a的值就是1 var b int = a; // 变量b赋值为变量a,则将a的值复制给b b = 2; // a和b是两个不同变量,修改其中任何一个不会影响另一个 - 派生类型通常会占用超过寄存器位宽的空间,所以实现上往往需要一组指令,将类型的字段数据全部复制:
class Vector { var x,y,z float64; } var a Vector = { x=1.0, y=0, z=-1.0 }; var b Vector = a; // 和基本类型一样,复制a的所有字段数据给b b.x += 2.0; // 同样修改b不影响a,a.x的值依然是'1.0' - 定长数组赋值等效于遍历数组的所有元素进行赋值:
派生类型的数组,如果元素是值类型的,也是一起复制的:
var a [4]int = [1,2]; // 遍历每个元素初始化,没写出来的为默认值,int默认值为0 var b [4]int; b = a; // 就是把a的数据复制给b // 等效于循环赋值 for (var i = 0; i < a.size; i++) b[i] = a[i]; b[0] += 10; // 修改了b[0]不会影响a,a[0]值依然是'1'var a [4]Vector = [{x=1.0}, {x=2.0}]; // 遍历每个元素初始化,没写出来的为默认值,Vector默认值的每个字段都是0 var b [4]Vector; b = a; // 就是把a的数据复制给b // 也等效于循环赋值 for (var i = 0; i < a.size; i++) b[i] = a[i]; // 这里的赋值参考第2点 b[0].x += 5.0; // 同样修改b[0].x不会影响a,a[0].x的值还是'1.0'
引用类型变量是与实例分离,即给赋值变量只是改变引用的指向。
变量能引用的实例有类型安全的约束:
比如下面的Device和Bus尽管结构一样,但却不能引用:
class Device {}
class Bus {}
func test() {
var a *Device = new(Device);
var b *Bus = new(Bus);
// a = b; // 错误✖:Device的引用变量不能引用Bus的实例
}
强引用表示为*带类型符号,比如:var aDev *Device;声明了强引用变量aDev。
它可以指向一个类Device的实例,或者Device的子类的实例:
func test() {
var b *Device = new(Device); // 初始化指向一个新分配的Bus实例
var a *Device = b; // 将b引用的实例传递给a
a.speed = 10; // a和b的修改都会更新同一个实例
printf("speed=%d", b.speed); // 打印:speed=10
}
const声明的常量引用,必须初始化指向一个实例(或nil),然后不能再改变指向了:
const a *Bus = new(Bus);
// a = new(Bus); // ✖
// a = nil; // ✖
变长数组也是引用类型的变量,可以引用元素类型相同但长度任意的数组实例。
强引用在自动内存管理中的作用是标识实例是否被使用:
- 被强引用变量引用的实例不能被内存管理器回收;
- 当一个实例没有被强引用变量引用时就应该被回收。
虚引用(Phantom Reference)是指不影响内存释放的引用。 可以引用动态创建的实例,也可以引用值类型变量的实例,但只在一定条件下才能使用。
虚引用变量是常量,即只能用const声明:
func test() {
var gh Host;
const h1 &Host = gh;
}
虚引用只能是本地变量或参数。仅有下面几种情况能传递给虚引用:
- 值类型变量在作用域内可以被虚引用指向。
- 常量引用变量在作用域内,可以传递实例给虚引用。
- 虚引用可以传递实例给新的虚引用。
- 本地变量是强引用类型,在传递给虚引用之后,在虚引用作用域之内不可被修改。
- 一个类实例在可以被虚引用的作用区间内:
- 它的值类型字段可以被虚引用。
- 它的常量字段引用的实例可以被虚引用。
显然全局变量能在所有代码中被引用:
var gDrv Driver;
const rDrv *Driver = new(Driver, {});
func use() {
const d1 &Driver = gDrv;
const d2 &Driver = rDrv;
}
本地变量需要在作用域内使用虚引用:
func sample1() {
var drv Driver;
const d1 &Driver = drv;
}
func sample2() {
const drv *Driver = new(Driver);
const d1 &Driver = drv;
}
func sample3() {
var drv *Driver = new(Driver);
{
const d1 &Driver = drv;
// drv = nil; // ✖
}
drv = nil;
}
允许被虚引用指向的实例的字段:
class Device {
const driver *Driver;
var disk Disk;
}
func sample1(dev Device) {
const drv &Driver = dev.driver;
const dk &Disk = dev.disk;
}
func sample2(dev *Device) {
const drv &Driver = dev.driver;
const dk &Disk = dev.disk;
}
虚引用仅限为上面的场景才能使用,例如函数返回值不能是虚引用类型的。
枚举变量的使用详见枚举。
改变量要么为空,要么指向一个函数,详见函数原型。
上面讲述了变量的值,而常量的不可变就是指变量值不可变:
- 值类型常量,其所有内容都不可变,对除了基本类型以外的类型:
- 数组常量的每个元素都分别是常量。
- 类和结构的字段值都不能修改了。
- 引用类型常量在一经声明初始化后就只能固定指向一个实例了,直到离开作用域。
class Vector { var x,y,z int; }
class Data { var ve Vector; }
func test() {
const vec Vector = {x=1.0,y=2.0,z=3.0};
// vec.x = 4.0; // 错误✖
const vecs [4]Vector = [{x=1.0,y=2.0,z=3.0}];
// vecs[1].x = 4.0; // 错误✖
const data Data = {v={x=1.0,y=2.0,z=3.0}};
// data.ve.x = 4.0; // 错误✖
}
作用域就是变量生效的范围:变量的生命周期从声明开始,直到离开作用域变量的生命周期结束。 作用域一般分本地和全局两种情况。
本地变量声明在函数或方法中:
- 作用域是声明的代码块及其中嵌套内层代码块。
- 但是同一层不能重复声明同名的变量。
- 当内层声明同名的变量时(不要求同类型),外层的同名变量则被隐藏,不能使用。
func test() {
var v = "Hello"; // 变量v的生命周期在当前函数内
{
var s = "Fèng!"; // 变量s的生命周期在当前块内
printf("%s %s\n", v, s); // 可使用外部声明的变量v
}
// printf("%s %s\n", v, s); // 错误✖:不能使用内层块内的变量s
{
// printf("%s %s\n", v, s); // 错误✖:不能使用另一个块内的变量s
}
{
var v = "Dear Fèng"; // 内层重新声明同名的变量,外层的变量v就被隐藏了
printf("%s\n", v); // 打印:Dear Fèng
}
// var v = "Fèng"; // 错误✖:不能重新声明
}
全局变量必须放在代码的最顶层,即在函数和类型定义的外面声明。 不论是变量还是常量都必须初始化。 作用域为全局,生命周期为运行时。
var count int = 0;
var qps int = 0;
var avg float64 = 0.0;
func doCount() int {
count+=1;
return count;
}
可以使用export导出给其他module使用:
export const PI float64 = 3.1415926;
export var delay int = 0;
这里定义声明周期为运行时,但如果是动态库,这里可能会有歧义,姑且定义动态库加载期间为运行时吧。
bool的字面量只能是true或false。
空值就是nil,表示变量或字段初始值,即不指向任何实例。
可适用于引用类型变量和函数原型变量。
字符串并不是基本类型,编译器对字符串字面量进行编码。
字符串字面量即字符串常量,本身不能修改,所以只能用rom类型变量引用。
字符串常量不是在函数栈上分配的,而是一律放在常量区:
func moduleName() *rom {
var r *rom = "test-module";
return r; // 离开函数moduleName还是能使用
}
func test() {
printf("module: %s\n", moduleName());
}
将数组元素列出来放在方括号中:[1,2,3]、["Hello", "Good"]等等。
数组元素类型为兼容所有元素的类型,如果没有可兼容的类型则不允许。
元组是语言内部的特殊类型,由一组元素显示组成。不支持显示定义元组类型的变量。
声明了多返回值函数或方法,如果是返回数组,那无法自动解构成多个变量,因此采用元组来处理。
func getValue(key int) (int, bool) {
var node = get(key);
if (node == nil) return 0, false;
return node.value, true;
}
运行同时赋值给多个操作对象:
func test() {
u.id, ok = 1, true;
}
在声明变量时也可以使用:
func test() {
var id, ok = 1, true;
}
调用函数或方法返回的结构是一个元组,当然可以直接当做元组来使用。
func result(e int, r *Res) (int, *Res) {
return e, r;
}
func success(r *Res) (int, *Res) {
return result(0, r);
}
多返回值的函数或方法不能参与表达式计算,但是单返回值的函数可以(编译器应该自动拆开):
func sin(x float64) float64 { // 单返回值可以作为元组返回也可以参与表达式计算
// TODO:……
}
func cos(x float64) float64 {
return sqrt(1 - sin(x)^2); // 需要自动把元组拆开成单值
}
if元组和if语句类似,不同的是直接返回的是元组而不是语句:
func getValue(key int) (int, bool) {
var node = get(key);
return if (node == nil) 0, false else node.value, true;
}
以及与switch语句对应的switch元组:
func createIf(c int) (bool, *Object) {
return switch(c) {
case 0: true, new(Res);
default: false, nil;
};
}
不同类型元组可以嵌套组合:
func createIf(c int) (bool, *Object) {
return if (c > 0) false, nil
else if (c < 0) true, new(Res)
else true, new(Res, {code=1000});
}
宏是一种有特定格式的代码片段,这种特定格式不是随意的,而是由特定用途决定的。 特定用途就是指某种语言特性,比如当宏用于实现自定义运算符时,由运算本身设定了代码格式。 目前宏仅支持在类和接口里。
宏统一由macro开头定义,主要格式有过程宏和类宏两种。
过程宏类似一般过程(函数或方法),有名称、参数表和语句序列组成:
- 名称和其他名称互不干扰,可以与其他元素重名。
- 参数表和函数参数不同,而是相当于上下文的变量。
- 语句序列就是普通的语句序列,末尾可以有过可选的表达式。
- 宏不能被调用。
下面举自定义加法运算符的例子:
class Vector {
var x float;
var y float;
macro operator add(left, right, sum) {
sum.x = left.x + right.x;
sum.y = left.y + right.y;
}
}
包含名称、字段表和过程宏组成,能保存中间状态。 比如派生类型的迭代循环的实现。
一个能被抛出的异常类需要定义#error宏tracestack,在这个宏里追踪并收集栈信息:
class Stack {
var fn uint64;
var line uint32;
}
export
class Error {
var stacks List`Stack`;
func tracestack(fn uint64, line uint32) {
var s Stack = {fn=fn,line=line};
stacks.add(s);
}
macro error tracestack(fn uint64, line uint32) {
tracestack(fn, line);
}
}