9.5 其他

9.5.1 foreach和迭代器(crowbar)

在9.1.3节的注解中写道:“但是,在crowbar的标准中,并不是引入 foreach函数,而是引入 foreach 语法。”如前面所述,有了闭包,即使语言本身不支持,也可以进行类似 foreach 的实现。但在crowbar 的闭包中,不能使用 break、 continue、 while等语句 [20],因此,crowbar中支持了 foreach。

crowbar的 foreach的使用方法如下所示。

a = {1, 2, 3, 4, 5, 6};

foreach(v : a) {

print("(" + v + ")");

}

foreach的语法根据语言而异,比如在C#中是这样的。

foreach (Object o in hogeCollection)

{

//处理

}

Java中虽然没有 foreach,但是对 for语句进行了扩展,使用方法如下。

for (Object o : hogeCollection){

// 使用o进行处理

}

crowbar中的 foreach结合了上面两种语言的特点。这么做的原因,首先是如果没有 foreach语句的话,那么程序员之间就没办法在交流的时候说“这里用 foreach转一下”。可话虽如此,但是像C#这样将 in之类的又短又经常会被用到的单词作为关键字,总觉得有一些不安。

在上面的例子中,使用 foreach轮询了一个数组,这是因为数组具有迭代器(iterator)的所有方法。

crowbar中的迭代器采用了GoF [21]的风格,具有以下这些方法。

first()

返回迭代器中的第一个元素。

next()

将迭代器的指向向后移动一个。

is_done()

迭代器移动到超出最后一个元素的位置时返回 true,否则返回 false。

current_item()

返回迭代器中当前的元素。

习惯了Java的人可能会觉得这样的设计和记忆中的不太一致,这是因为Java的迭代器指向数组的元素和元素之间。调用 next()的时候,迭代器移动到下一个“元素和元素之间”,此时返回的是它跨过的那个元素。

与此相对,crowbar的迭代器(GoF风格)是直接指向元素的。因此,使用 current_item()方法可以取得当前元素,只要不调用 next(),无论调用几次 current_item(),返回的都是同样的元素。

至于哪种设计方式更好,肯定会有各种不同的意见。但是我觉得Java设计对于我来说很不好用(只是“看一下”这个元素,迭代器就移动到下一个元素了),因此,这里使用了GoF风格的设计方式。

——虽然是这么说,但取得数组的迭代器的方法如果是GoF风格的话,本应叫作 CreateIterator(),可这个名字实在是太长了,因此叫作 iterator()了。只有这点可以说是使用了Java的风格,没有与GoF风格统一。

数组迭代器的实现如下所示。 __create_array_iterator()是创建迭代器用的隐藏函数,被记录在构建脚本中。数组的 iterator()方法所返回的迭代器就是调用这个函数取得的。

function __create_array_iterator(array) {

this = new_object();

index = 0;

this.first = closure() {

index = 0;

};

this.next = closure() {

index++;

};

this.is_done = closure() {

return index >= array.size();

};

this.current_item = closure() {

return array[index];

};

return this;

}

9.5.2 switch case(Diksam)

本节将为Diksam引入 switch case。

switch case语句在C、Java、C#等语言中都存在,但是C中的 switch case却很不像话,里面并不能写 break语句,也就是说, switch case每次都要从上至下一直执行到结束。Java在这点上也是一样。C#的语法结构看上去和C一样,但是如果在 case的末尾不加上 break的话就会发生编译错误。这种设计方式正好解决了这个问题 [22]。此处贯彻了让习惯了C和Java的程序员容易上手的宗旨,在这点上Diksam做了很多妥协。但是在这个问题上如果去迎合C语言的话,就不太符合我的审美观了。话说回来,Diksam中是这样进行 switchcase的。

switch(a)

case 1 {

// a等于1时执行

} case 2,3 {

// a等于2或3时执行

} case 4 {

// a等于4时执行

} default {

// a不等于上面的1、2、3、4时执行

}

并且,Diksam的 switch case在原则上只要是能通过 ==进行比较的,就都可以通过 switch表达式(上例中的 a)和 case表达式(区别在于在 ==的时候会发生类型转换, switch的时候不会发生)。因此,字符串等类型也可以使用 switch case。

9.5.3 enum(Diksam)

Diksam中也同样引入了枚举类型(enumerated type)。

enum Fruits {

APPLE,

ORANGE,

BANANA

}

有了上面的定义,就可以使用 Fruits.APPLE、 Fruits.ORANGE、 Fruits. BANANA的枚举(enumerator)了。

Diksam的枚举类型内部保存的是从0开始顺序编号的 int,但是并不能作为int类型进行四则运算、赋值给 int类型以及与 int类型进行比较运算,只能够在同一枚举类之间比较(这些功能可能经常会被用到,因此这里策略性地破坏了美感,但可以比较大小)。

另外,在用 +连接左边的字符串时,枚举类型会转换为字符串类型。

Fruits f = Fruits.ORANGE;

println("f.." + f); // 输出"f..ORANGE"

如果没有这个设计的话,枚举在编译的时候就可以转换为整数类型了(现在的Diksam还不能将字节码保存为文件)。为了将枚举作为字符串输出,必须要把对应的字符串保存在 DVM_Executable中。另外,还必须要和其他源文件进行链接。为了达到这个目的,在 ExecutableEntry 中与函数和类一样保存了一份转换对应表(请参考9.3.4节)。

9.5.4 delegate(Diksam)

在Diksam的语法规则中,函数调用是下面这样的。

primary_expression LP argument_list RP

也就是说,函数调用表达式是在表达式后面加上括号并且里面括着参数。如果要把语法规则变成下面这样的话,实际上简单了不少。

IDENTIFIER LP argument_list RP

如果变成了上面这样,当然,是为了实现在类似于C的语言中所说的函数指针。例如为GUI的按钮分配处理的时候,在Java中要创建一个实现了特定接口的类的实例(事件监听器),并将它设置到按钮中。这种方法存在以下的问题:

需要为此特意去定义一个类,不仅麻烦也会使代码变得冗长。

按下按钮时,处理会被编写在别的类里面,对于这个类来说等于放宽了类的封装。虽然使用内部类可以解决这个问题,但也因此又带来了内部类的使用问题。(对于实现语言的人来说)这个方法太麻烦了,而且对于初学者来说也不太容易掌握。

在按下按钮的时候,事件也可以由承载了按钮的类(JFrame等)接收。如果使用了这种处理方式,在这个类中有两个按钮的话,就无法为它们分配单独的动作。

因为只是“想要执行按下按钮时的处理”,所以很自然地就会想到,如果能只登录描述了处理的函数就好了(可能不得不使用闭包,但是在Diksam中却不能使用)。

为了能够达到上述效果,就需要为静态类型的语言Diksam引入“函数类型”(在C语言中的话就是指向函数的指针型)。

如果想要声明一个“接收 int参数,返回 double函数”的类型,肯定不能像C语言这样编写代码。

double (*func)(int);

说到Java,从Java7开始就有了要引入闭包的说法,当初设想的是下面这样的代码。

double(int) func;

但是在Java(Diksam)中存在检查异常,如果把 throws也作为方法必要的信息,就要写成下面这样。

double(int) throws HogeException, PiyoException func;

上面的写法是因为在语法上存在不确定性,所以需要改为下面这样的写法。

double(int) throws HogeException | PiyoException func;

相反也可以试着像下面这样定义。

{ int => double } func;

从2009年5月到现在,就连是否要引入(闭包)这个问题本身都还没有得出结论 [23]。说句题外话,无论哪种写法我都觉得太长了(使用起来至少也要像C语言中的 typeof那样)。

因此,在Diksam中,引入了C#风格的关键字 delegate。

delegate double Func(int value) throws HogeException, PiyoException;

根据这个描述, Func被定义为“接受 int型参数,返回 double,可能会抛出 HogeException和 PiyoException的函数”的类型。

基于上面的定义,就可以声明 Func类型的变量,参数中也可以接收 Func类型了。

// 定义Func

delegate double Func(int value) throws HogeException, PiyoException;

// 定义函数

double func(int value) throws HogeException, PiyoException{

}

// 将func赋值给Func型的变量f

Func f = func;

// 通过f调用func

f(5);

与C# 的 delegate 不同,Diksam 的 delegate 类型并不是类的对象。因此,没有必要使用 new [24],也可以说不能 new。另外,不能将多个函数赋值到一个 delegate中。

另外,在给 delegate类型的变量赋值的时候,和方法重写时一样,返回值必须共变,参数必须反变。

方法也可以赋值给 delegate类型的变量,此时,方法所在对象的引用也会被保存到变量中。因此,方法在作为事件句柄被调用的时候,也可以和平常一样使用 this引用。

在实现上, delegate 类型的值作为 DVM_Object 的共用体之一保存在堆中。 delegate型变量如果保存的是方法的话,就必须要同时保存方法所在对象的引用,因此,需要保存的信息如下所示。

/ 保存delegate信息的结构体 /

typedef struct {

/如果保存的是方法,那么这个成员保存的是方法所在对象的引用。如果是函数则为null /

DVM_ObjectRef  object;

/ 函数或者方法的索引值 /

int    index;

} Delegate;

/ 在DVM_Object结构体中通过共用体保存上述结构体 /

struct DVM_Object_tag {

ObjectType type;

unsigned int  marked:1;

union {

DVM_String  string;

DVM_Array  array;

DVM_ClassObject class_object;

NativePointer native_pointer;

Delegate  delegate; ←这个

} u;

struct DVM_Object_tag *prev;

struct DVM_Object_tag *next;

};

那么,关于 delegate 对象的创建时机,从语言实现的角度讲,无论是函数还是方法,让调用总是通过 delegate 对象的话比较容易实现。无论是调用 print("hello");这样的函数,还是调用 obj.method()这样的方法,在它们对 print、 obj.method [25]进行计算的时候就会创建 delegate。但是,向堆中保存对象是一项开销很大的处理,大多数情况下, print也不会赋值给其他变量,而是立即调用。为了不因个别情况而影响整体的效率, delegate对象在以下时机被创建。

1. 在函数的情况下,当函数赋值给 delegate变量的时候。 

2. 在方法的情况下,通过非立即调用的形式进行了(表达式)计算的时候。

另外, delegate对象在指向方法的时候也引用了对应的对象,这样做会导致这个对象不能成为GC的目标。因此也需要对GC进行修改。

9.5.5 final、const(Diksam)

crowbar想要定义常量的时候,要使用 final(Java风格)。

final HOGE = 10;

在变量声明时如果加了 final的话,在赋了初始值之后,就不能再对其进行赋值了。并且,必须要在声明的同时完成赋初始值的动作。 final也可以用于局部变量。

在Diksam中,同样使用了final。

final int HOGE = 10;

在Diksam 中函数的形式参数、 catch 子句中接收异常的变量,都默认为是 final的。这样做的目的在于,不让从被调用的位置或者异常发生的源头获得的重要信息被稀里糊涂地覆盖掉。

在Diksam中允许分割编译,并且不存在跨源文件的全局变量。为了不出现乱用全局变量的情况,我认为最好的方式是通过 get_xxx()和 set_xxx()函数。但是在大规模的程序中,可能需要被所有程序引用的全局常量。

但是,我认为,与其抛弃“不存在跨源文件的全局变量”等(和其他语言相比有些特别)的设计,直接允许使用全局变量,不如声明加了 final的全局常量可能更好一些。但实际上并没有这么简单。Diksam具有顶层结构,顶层结构是由被执行的语句组成的。因此,即使在函数外声明了变量(如果是C语言的话,就是声明全局变量),在声明语句被执行前是得不到初始值的。这样一来,因为 require了的其他文件源代码的顶层结构(在编译时)是绝对不会执行的,所以即使用 final声明的常量可以被其他源文件 require。但是在编译时来看,它并没有被赋值,因此达不到预期的效果。

因此,在Diksam中配合着 final引入了 const这个关键字。 const的使用方法如下所示。

const HOGE = 10;

因为通过初始值可以判断类型,所以 const无需指定类型。

const 与函数定义、枚举、 delegate 的声明一样,不能写在函数内。另外,可以从其他源文件中被引用。

在为const指定的常量设置值的时候,需要考虑以下的情况。

1. 与C语言的预处理相同,在编译前就要置换常量。

2. 在编译时置换常量。

3. 编译时与变量采取同样的实例,在开始执行时再将值代入。

1、2在编译前和编译时进行替换的方法在执行效率方面具有优势,但是对于使用者来说,会由于不能定义如下形式的常量而困扰。

const HOURS_IN_DAY = 24;// 一天24小时

const MINUTES_IN_DAY = HOURS_IN_DAY * 60;// 1天 = 24 × 60分

如果要在编译时或者编译前置换常量,那么这些表达式在编译时就必须要全部执行。如“数组大小”等被作为常量表达式处理的话,就需要(在编译时)调用数组的 size()方法。这样的做法太困难了,因此Diksam还是采用了方法3。

所有包含有代码的 const 常量的初始值都将作为字节码保存在 DVM_Executable中,在编译/加载源代码的时候再去执行它们。初始值中可以书写任意的表达式,既可以使用 new分配对象,也可以使用原生函数分配特定的系统资源(例如 stdin、 stdout、 stderr文件指针等)。

因为 const 常量会被其他文件链接,所以与函数、类、枚举一样在ExecutableEntry中保存了转换对应表。

附录

注 释

[1]. 在Perl的ver.4中还是叫作“关联数组”,但从ver.5开始就叫作“哈希”了。我认为哈希只不过是一个实现方式,因此还是“关联数组”的名字更为贴切。

[2]. 现在作为键的字符串还不能使用变量,因此还不能作为关联数组投入使用。顺便提一下,JavaScript中可以使用 []访问数组元素,因此可以作为关联数组使用。

[3]. 但 是, 在 crowbar 的标 准 中, 并 不 是 引入 foreach 函数,而是引入 foreach语法。

[4]. 环境:environment。

[5]. 说到这里,各位一定会羡慕能够使用继承的语言吧。

[6]. 在crowbar中,LocalEn-vironment被创建在堆中,但实际上链表是在栈中实现的,因此在这里认为是创建在栈上的。

[7]. 当然这两个对象要具有原型继承关系。——译者注

[8]. 实际上是可以做出相似的异常层级结构的,即使使用crowbar的功能做出了这样一个层级结构,从语言的设计层次来讲并没有特别支持这种方式,而对我本人来说也很抗拒这种上下相逆的事情。

[9]. 由于 Exception 是在Diksam中创建的类,这里果然还是做了“上下相逆”的事情。

[10]. 这个机制模仿了JVM的指令 jsr、 return,但是在现在的Java中并没有使用,而是将 finally 子句的代码完全展开。在Sun的错误数据库(Bug Database)中的4381996号错误(① 地址:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4381996。——译者注)使用原来的方式时,即使是正确的代码,验证器也会报错。

[11]. 我本来想把内存不足之类的情况也加入到异常中,但是在现在的Diksam实现中,只有在 MEM_malloc() 中进行 exit() 的 时 候才会抛出异常, MEM_malloc()本身并不会发生异常。

[12]. 不可思议的是,Java RuntimeException。

[13]. 详细来说就是,这里涵盖了在catch子句或finally子句中发生的异常(含通过throw;再次抛出的Exception)。

[14]. 在[12]中也举出了诸如“异常过度包装”之类的缺点,也算是表达了对这个功能的不满。

[15]. 实际试一下的话,在我的环境中没有发生什么特别的问题。

[16]. 这里假设node.children的值为 null。

[17]. 不管怎么说,利用返回值进行手工处理的方式是行不通的。之所以这么说是因为,我不相信人类(当然包括我自己)。

[18]. 在不同的阶段中取值也不同。——译者注

[19]. 现在最新版本是5.9.4。——译者注

[20]. 如果使用Java提出的闭包unrestricted closure,就可以实现 break 或者 continue。

[21]. GoF是“Gang of Four”的简称。《面向对象的设计模式》的四位作者组成了四人组,被业界称为“四人帮”。他们的书也被称为GoF的书。

[22]. 如果在case后面没有任何语句(只有case语句)的情况下,可以省略break。

[23]. 很明显,在Java7中并没有加入这个功能。

[24]. C#从2.0开始也不需要 new了。

[25]. 这两行代码在被 () 调用前,会被当做表达式。——译者注