Skip to content

cossbow/feng

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

73 Commits
 
 
 
 
 
 
 
 

Repository files navigation

【Fèng】编程语言

C++最初在C基础上扩展了面向对象,可以更方便的使用类进行封装,使得对象的使用也越来越频繁。 对一个大型项目,总有大量的对象相互引用组成很复杂的有向图,C++的手动内存管理越来越繁琐。 而且C++依然允许不安全的操作,C11中的cast语义但并不是强制性的,还缺少边界检查,甚至允许将整数转成指针等。 这些内存问题虽然可以用ASAN来检查,但ASAN无法检查出所有情况,比如非法指针恰好落在正在使用的区间时ASAN逻辑上无法判断是否非法指针。 因此内存安全需要从语法语义的约束设计才能保证。

考虑改进C++语言,或者改进“C with class”,应该是在安全的自动内存基础上扩展面向对象。

特性简介

下面是按这个的思路设计来Fèng编程语言,但目前仅写了grammar

先整理一下主要的特性:

  1. 内存安全机制的设计:
    1. 自动内存管理:指针必须指向一个对象或者为空,而对象在没有被指针时将被释放,释放的对象可以立即或延迟回收空间。
    2. 指针安全使用:指针不能通过运算生成,不能将任意整数转换成指针值,这也包括对指针进行自增自减。
    3. 强制检查边界:比如数组及buffer空间的操作。
    4. 强制检查类型:类型转换时必须检查是否允许转换,即使编译时无法分析时,也要在运行时做检查。
    5. 虚引用:用虚引用代替C语言取地址运算(&),限制在实例生命周期内进行使用。
  2. 面向对象的基本元素,包括继承多态和接口抽象。
  3. 资源类是参考C++析构函数设计的,可用回收底层库分配的资源、处理循环引用等。
  4. 用一种类型来管理一段连续空间,并且可以映射为struct和union使用。
  5. 模块化代码管理,模块之间使用符号需要导出和导入。
  6. 值类型变量,变量就是实例本身,不需要额外分配内存。

以及几个次要的特性:

  1. 异常处理机制。
  2. 泛型或者模版,泛型参数可以加约束条件,类成员方法也可以带参数。
  3. 允许给类自定义运算符,从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以及使用它定义变量c1c2

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名与目录名相同,因此无需在文件里声明。

  1. module内定义的符号在内部都可使用,但是如果在外部则需要使用export导出。
  2. 如果想使用其他module里的符号,需先要import对应的module路径:

概念

下面将详细描述各个语法元素的定义及用法。

module

module是代码的基本管理单元。

  1. 在module内部所有的内容都可见,跨module只能访问导出的符号。
  2. module名称与路径一一对应,在文件中不声明module名称。要求目录名称的规则和变量名称一样。 例如在Linux下,modulecom.jjj.base.util对应的相对路径为com/jjj/base/util

导出符号

支持导出任何全局符号;比较特殊的是成员:

  1. 导出的类,其成员不跟随导出的,需要单独导出。
  2. 导出的接口的方法默认跟随导出。
  3. 导出的结构类型的字段跟随导出,
  4. 导出的枚举类型的所有值都跟随导出。

例如,下面导出全局变量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
}

整数类型显式转换,相当于从低位到高位按位复制,因此可能会出现整数溢出:

  1. 如果位宽从大的转到小的会被截断,造成整数溢出。
  2. 有符号与无符号之间转换时,符号位会复制到对应数位上,导致整数值发生变化。

语言本身不检查溢出,需要程序员自行处理。

浮点数类型

浮点数是由IEEE 754标准定义的, 包括单精度数float32和双精度数float64两种。

浮点数支持算术运算关系运算

布尔类型

类型符号为bool,且只有true/false两种取值:

  • 支持逻辑运算,关系运算(但只支持相等和不等),及位运算(与、或、异或三种)。
  • 关系运算的结果一定是布尔值。
  • 不支持和整数、浮点数的互相转换。
  • iffor中的条件表达式返回值必须是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);
}

其他运算符

索引运算符

仅数组默认支持这种运算符。索引运算符是由中括号组成,括号中是获取索引值的表达式。 其用法有两种:

  1. 索引表达式在右边是读操作,即获取索引对应元素的值作为运算结果。在语句中可以有两种方式接收取值:
    1. 左边用两个变量接收:第一个接收的是值;第二个接收的是布尔值,表示索引对应元素是否存在。
      func test(arr [16]int) {
          if (var v, exists = arr[16]; exists) {
              // 索引16越界了,也就是不存在,所以exists=false,无法进入这个分支
          }
      }
      
    2. 左边一个变量或者在表达式中使用时,仅返回元素值,如果元素不存在则终止运行并抛出异常
      func test(arr [16]int) int {
          return arr[16]; // 索引越界,终止运行并抛出异常
      }
      
  2. 放左边为写操作,即修改索引对应元素的值。
    func test(arr [16]int) {
        arr[15] = 0; // 修改索引为15的元素值为0
    }
    
    数组的容量是创建之后就不能变了,所以索引越界自然也要终止运行并抛出异常

new运算符

使用格式:new(类型, 参数),例如:

// 创建类Device的实例
var a *Device = new(Device);
// 后面的参数为初始化
var b *Device = new(Device, {});

引用类型的变量和实例是分离的,这种实例则都是通过new创建的。

参数是可选的,并且对不同类型的意义也不同:

  1. 如果创建mem类型,则参数是长度,且分配的空间不需要归零。
  2. 结构类型数组的创建参数为初始化值。

断言运算符

用于判断是否能类的引用是否能进行转换的语法,比如类的实例的引用传递可以给接口或父类指针,反过来则需要判断其类型。 有两种用法:

  1. 用一个变量去接收结果,运算表达式返回的是对应类型的引用,这种情况下如果不能转换则抛出异常。同时可以参与表达式计算。
    func test(o *Object) {
       var f *File = o?(*File);   // 转换成File类的引用
       var w Writer = o?(*Writer);  // 转换成Writer接口
       o?(*Writer).write("Hello!"); // 在表达式中使用
    }
    
  2. 用两个变量去接收的话,就会返回元组,不能参与表达式计算。 第一个值是类型引用参考第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 同左操作数 布尔类型
>= 同左操作数 布尔类型

类型是指实际表达式中的操作数类型,实现中不需要指定:

  1. 表中的类型相同,不仅是类相同,而是全相同,包括是否是引用类型。
  2. 如果结果的类型是引用,那会自动创建一个初始化的实例给结果
  3. 表中无宏名称的运算符,是依赖其他运算符的实现:
    1. <=依赖的<==都实现了就可以直接使用了,相当于这两个运算的结果的或||
    2. >=依赖的是>==,也是两个结果的或。
    3. !=仅依赖==,就是后者的结果取反!

举个复数的例子:

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;
}

自定义特殊运算符

自定义索引运算符

默认只有数组支持的索引运算符也可以自定义。 由于索引运算符分有读和写两种操作,因此分成indexGetindexSet两个过程宏。

比如自定义一个字典类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;
}

字段

变量的类型定义基本都适用于类的字段,除了:

  1. 字段不能定义为虚引用类型,但可以定义为弱引用类型
  2. 字段只能用constvar定义。
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. 弱引用不影响实例释放这个原则,那就是说不会导致计数+1,因此计数会归零。
  2. 归零后实例释放了,将弱引用字段置空,这是内存安全的保证。

例如,将双向链表Node向前引用的字段prev定义为弱引用,就不会发生计数无法归零的问题:

class Node {
    var key String;
    var next *Node;
    var prev ~Node;
}

方法

方法与函数基本相同,区别是:

  1. 必须通过类实例来调用。
  2. 在方法内部能使用当前实例的类成员。
  3. 方法名称作为唯一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是一个类内部的特殊关键字,用于指代当前实例本身。

在成员方法内使用当前类的成员,本地变量可能和字段同名,那么需要通过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));
}

父类与子类仅支持引用传递,值类型变量之间不能传递。且传递规则为:

  1. 引用类型相同的情况,子类可以传递给父类。
  2. 子类的常量强引用可以传递给父类的虚引用。

比如父类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;
}

抽象

多态的父类的方法也有自己的实现,但接口的方法没有具体实现,而是接口的“子类”给出实现,因此叫抽象。 抽象出接口更好的作为约定和规范:

  1. 管理者只关注接口的实现,隐藏具体的类,并提供已实现接口的实例。
  2. 使用者不必关心是什么类,只要实现了接口就可以使用。

比如定义一个接口Task,仅包含一个简单方法run

interface Task {
   run();
}

定义两个实现类MyTaskYourTask

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关键字。

接口仅仅是约定和规范,不支持实例化,因此接口类型变量只能是引用。

接口组合

接口可以进行组合:

  1. 组合成的接口包含各个组件的所有方法原型。
  2. 组合接口可以传递给组件接口,因为实现了组合接口当然也实现了组件接口。
  3. 接口的方法名称会检查冲突,不同组件中同名的方法被视为同一个方法,如果原型不一致则不能编译。

比如文件可以读和写,那可以这样设计接口:

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;
}

接口类型变量

接口类型变量是引用类型变量,并且只能引用实现类的实例。 接口的变量声明需要加上引用标识符来标识引用类型。

允许的传递:

  1. 引用类型相同的情况,实现类可以传递给接口。
  2. 类型允许的条件下,接口的常量强引用可以传递给接口的虚引用。
  3. 类型允许的条件下,实现类的常量强引用可以传递给接口的虚引用。

比如接口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的)。

结构类型

结构类型定义

结构类型定义的是内存上的数据结构,按内存布局分为两种子类型:

  1. 结构体:所有字段的存储按顺序分配。
  2. 联合体:所有字段的存储是重叠的。

字段类型只能是基本类型和结构类型,及这两种的定长数组

结构体和联合体的定义格式一样,只是开头的关键字不同:

  1. 结构体的定义格式为:struct 名称 { 字段列表 }
    struct Message {
        type int;
        success bool;
        value float32;
        ext [12]int;
    }
    
  2. 联合体的定义格式为:union 名称 { 字段列表 }
    union DataType {
        type int;
        success bool;
        uv float32;
    }
    

相邻且相同类型的字段可以合并,当然不相邻的不能合并。以结构体为例:

struct Request {
    type, code int;
    data [56]uint8;
}

位域(bit-field)是字段实际使用的位宽,只能用于基本类型的字段。 位域取值为该字段类型的位宽范围,放在在字段名称后面。 例如设置code的位域为6type未设置):

struct Request {
    type, code:6 int;
    data [56]uint8;
}

结构类型实例

结构类型可以有两种实例化方式:

  1. 定义为值类型,支持变量、类或结构类型的字段。
  2. mem类型映射为结构类型的实例。

数组

数组元素

数组是用于存储一组连续重复元素的类型,元素可以为任意类型。 每个元素相当于一个变量,也分值类型引用类型

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类型

用于管理一段连续的内存空间:

  1. 变量是引用类型的。
  2. 能映射成具体类型来使用。
  3. 运行时检查边界,任何越界都抛出异常
  4. 能进行只读控制。

mem引用类型

mem类型的变量是引用,支持强引用和虚引用。

mem强引用

由于强引用类型,其引用的mem的实例需要用new创建。

在创建时需要传入长度,比如创建一个16字节的ram实例:

var a *ram = new(ram, 16);

创建时带上映射类型,这时长度是已知的:

var a *ram = new(ram`[4]int`); // 创建一个size为16字节的ram

注意:创建时new的类型参数只能是ram,创建一个只读的没有意义。

mem虚引用

虚引用在引用值类型实例的时候,限制只能引用支持映射的类型。 例如:

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的映射

支持映射的类型:结构类型基本类型和这两个类型的定长数组。 这些类型占据的是连续空间,而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;
}

mem长度

长度为字节数,可以通过函数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设计有两种子类型:

  1. ram允许读写操作,可以传递给rom,允许创建。
  2. 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),作用域在当前函数内。

下面的例子定义了类型为Queuel和类型为inta两个参数:

func send(l Queue, a int) {
   l.push(a);
}

相邻且相同类型的参数可以合并定义,比如定义两个类型为int的参数ab可以这样:

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语句

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语句

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后面的括号内是控制体,控制体可以且必须有一个控制条件表达式,也可以包括初始化和更新子语句; 之后是需要执行的语句或语句序列,称循环体。 当控制条件满足时重复执行循环体:

  1. 控制条件是一个bool类型的条件表达式,当结果为true时才会执行循环体。
  2. 循环体是一个语句,如果需要多个语句操作则需使用块语句包起来。

简单的循环语句为括号内只有条件表达式:

func main() {
    var i = 0;
    for ( i < 100 ) {
        println(i);
        i += 1;
    }
}

完整的控制体格式为:【初始化】;【表达式】;【更新】

  1. 【初始化】在循环前执行一次,然后再进入循环过程。
  2. 循环过程的每一轮:先判断【表达式】,false则结束循环,true则执行循环体,最后执行【更新】。
  3. 循环体中可以有控制循环的操作:
    1. 遇到continue语句则直接进入下一轮循环,也就是2中描述的。
    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);
}

当然continuebreak语句对迭代循环依然有效。

循环语句遍历形式默认只对数组使用,对自定义类可以实现自定义迭代器,然后就可以用迭代循环来遍历了。 实现迭代是通过名为Iterator的helper宏实现的,但考虑循环是很常用的语法,所以利用宏直接由编译器展开。 宏的字段不限制,包含4个方法initializerconditionupdaterget

方法 作用 参数
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;
}
  1. 赋值语句并不是拆成多个语句独立执行的,而是先计算操作数的表达式,再计算表达式元组,再一一赋值。
  2. 显然,不同类型的赋值可以放在一起。

变量初始化

变量声明语句右边的初始化赋值是可选的,且也是先计算右边表达式元组,再赋值给左边变量:

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;
}

复制的时候要求原类型必须相同,如果是数组则按最小长度复制。

变量声明语句

声明一个或一组变量使用关键词varconst开头,后面紧跟变量的名称,然后是变量类型。

  1. var声明一个普通变量。后面可以有初始化值。TODO:允许未初始化并置默认值?还是强制使用前必须赋值?
  2. 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;
}

由于声明的类型可以省略,因此会出现两种情况:

  1. 省略时,左边可以是不同类型的表达式,这样右边对应的变量类型会自动推导为不同的类型。
  2. 如果显式加上,显然左边的类型统一了,右边的类型自然必须兼容。
func test() {
   var a,b int = 1,2;
   // var a,b int = 1, "ggyy"; // 错误✖,必须拆成两个语句
}

异常语句

分为抛出和处理异常两种语句。

抛出异常

抛出异常是为了处理返回值没有处理的错误。抛出异常后:

  1. 会终止当前过程的执行,不执行返回语句,而是抛出一个包含错误信息的实例。
  2. 如果调用的过程抛出了一个异常A,会从调用处终止当前过程的执行,继续抛出异常A。
func example1() {
   throw new(Exception);
}
func example2() {
    example1();
    println("example1()必然抛出异常,所以这一行不会执行!");
}
func example3() {
    example2();
    println("example2()也会抛出异常,所以这一行也不会执行!");
}

如果发生了抛出异常,那这个会一直按调用链往外抛,直到被catch匹配到为止。

抛出的异常的类型需要自己定义,在异常中详细说明。

处理异常

异常处理语句分三个部分:

  1. try部分:必须的部分,将需要处理的代码块包裹起来。
  2. catch部分:可以有多个,分配匹配不同的异常类型。匹配到就执行对应的代码块,否则继续往后匹配。 如果没有都未匹配成功则继续往外抛出。
  3. 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,区别是后者在首次赋值后就不能再修改了。

变量值的类型

变量的类型分三种情况:值类型、引用类型、枚举和函数原型。

值类型变量

变量与实例一体,变量的值就是实例本身,赋值相当于复制实例的数据:

  1. 基本类型的变量本身只是一个寄存器值,修改通常只需一个机器指令:
    var a int = 1; // 变量a赋值为字面量数值1,那a的值就是1
    var b int = a; // 变量b赋值为变量a,则将a的值复制给b
    b = 2; // a和b是两个不同变量,修改其中任何一个不会影响另一个
    
  2. 派生类型通常会占用超过寄存器位宽的空间,所以实现上往往需要一组指令,将类型的字段数据全部复制:
    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'
    
  3. 定长数组赋值等效于遍历数组的所有元素进行赋值:
    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'
    

引用类型变量

引用类型变量是与实例分离,即给赋值变量只是改变引用的指向。

变量能引用的实例有类型安全的约束:

  1. 接口的引用有多态抽象的约束。
  2. 接口引用类的实例有抽象兼容的约束。

比如下面的DeviceBus尽管结构一样,但却不能引用:

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;
}

虚引用只能是本地变量或参数。仅有下面几种情况能传递给虚引用:

  1. 值类型变量在作用域内可以被虚引用指向。
  2. 常量引用变量在作用域内,可以传递实例给虚引用。
  3. 虚引用可以传递实例给新的虚引用。
  4. 本地变量是强引用类型,在传递给虚引用之后,在虚引用作用域之内不可被修改。
  5. 一个类实例在可以被虚引用的作用区间内:
    1. 它的值类型字段可以被虚引用。
    2. 它的常量字段引用的实例可以被虚引用。

显然全局变量能在所有代码中被引用:

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;
}

虚引用仅限为上面的场景才能使用,例如函数返回值不能是虚引用类型的。

枚举变量

枚举变量的使用详见枚举

函数原型变量

改变量要么为空,要么指向一个函数,详见函数原型

常量

上面讲述了变量的值,而常量的不可变就是指变量值不可变:

  1. 值类型常量,其所有内容都不可变,对除了基本类型以外的类型:
    1. 数组常量的每个元素都分别是常量。
    2. 类和结构的字段值都不能修改了。
  2. 引用类型常量在一经声明初始化后就只能固定指向一个实例了,直到离开作用域。
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; // 错误✖
}

变量作用域

作用域就是变量生效的范围:变量的生命周期从声明开始,直到离开作用域变量的生命周期结束。 作用域一般分本地和全局两种情况。

本地变量

本地变量声明在函数或方法中:

  1. 作用域是声明的代码块及其中嵌套内层代码块。
  2. 但是同一层不能重复声明同名的变量。
  3. 当内层声明同名的变量时(不要求同类型),外层的同名变量则被隐藏,不能使用。
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的字面量只能是truefalse

空值字面量

空值就是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;
    }
}

类宏

包含名称、字段表和过程宏组成,能保存中间状态。 比如派生类型的迭代循环的实现。

异常

一个能被抛出的异常类需要定义#errortracestack,在这个宏里追踪并收集栈信息:

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);
   }
}

属性

About

内存安全的C with Classes

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published