ICE:Slice语言(一)-编译
Introduce简介
Slice(Specification language for ice)是分离对象和对象的实现的基础的抽象机制。Slice在客户端和服务器端之间建立契约,描述应用程序使用的类型对象的接口。这样的描述是独立于实现功能的语言的,所以服务器和客户端的实现所使用的语言没有任何关系。
编译器将语言无关的定义翻译为特定语言的类型定义和API。这些翻译后的类型定义和API将被使用在应用程序的功能中,以及用来与Ice交互。这种从一种语言无关的定义到各种语言的翻译成为语言影射。目前,ice支持下面几种语言的影射:
由于Slice只定义了接口和类型,所以Slice是单纯的声明语言。因此Slice中是不能编写可执行的代码的。
Slice将定义的焦点放在对象接口,以及接口支持的操作和可能产生的异常上。除此之外,Slice还支持对象持久化的操作。
Compilation编译
Slice产生的源代码文件必须和应用程序代码一起生成客户端和服务器端执行程序。
开发的结果就是开发可执行的客户端和服务器端程序。这些可执行的程序可以被发布在任何位置,无论他们的环境是否相同,它们的实现语言是否相同。这个唯一的约束就是宿主必须提供必要的运行时环境,例如需要的动态链接库以及客户端和服务器端能够相互连接。
Develop单一语言开发
我们来回顾一下上一篇文章,也就是那篇ICE打印机的例子。
上面只是一个示例。实际中,你没有必要只有一个客户端和服务器端。例如,你能创建多个服务器,每一个实现了相同的接口,但是却有着不同的功能实现(例如不同的服务性能)。多个这样的服务器可以共存在一个系统中。这样的方案提供了一个基础性的可伸缩的机制:假如你发现一个服务器的进程开始随着对象的增加而运行缓慢,你可以在另一台机器上运行实现相同接口的服务。这样的联合服务提供了一个分布在不同机器上的多个服务进程的单一的逻辑服务。联合服务中的每一个服务器实现了相同的接口,但是充当不同的对象实例的宿主。
Ice还支持可重复服务器。可重复服务器允许多个服务器各自实现同一组对象实例。这项功能提高了性能、可伸缩性以及冗余性。因为客户负载可以被多个服务器分担,而每一个对象可以在一个以上服务器中被实现。
Develop多种语言开发
如果客户端和服务器端使用不同的语言开发,那么该怎么做呢?
很显然,Slicse是架起两者之间的桥梁。
ICE:Slice语言(二)--源文件和词法规则
Source File源文件
Slice定义如下的Slice源文件的命名和内容规则:
文件命名
Slice的源文件以.ice为扩展名。
对于大小写不区分的系统(例如DOS),文件的扩展名可以大写,也可以小写,例如Click.ICE是有效的。而对于大小写敏感的系统(如Unix),Clock.ICE是非法的(此时的扩展名应该小写)。
文件格式
Slice是无格式语言,因此你可以使用空格、横向和纵向制表符、换行符安排你的代码布局。Slice中语义和定义的布局没有关联。
预处理
Slice支持#ifndef,#define,#endif以及#include与定义指令。不过它们有如下的限制:
#ifndef CLOCKICE
#define CLOCKICE
// #include 指令
//定义
#endif CLOCKICE
#include <File1.ice>
#include “File1.ice”
Slice不支持用这些预处理指令做其他用途,也不支持使用C++预定义指令。
#include指令允许Slice使用其它文件中的类型定义。Slice编译器会解析源文件中的所有代码,其中包括了#include指令指定的文件中的代码。实际上,编译器只编译命令中指定的顶层文件并生成代码,因此你必须单独编译每一个inlcude的文件。
定义顺序
Slice的结构,例如模块,接口或类型定义可以用任何顺序出现。但是定义必须在使用之前声明。
Lexical Rule词法规则
Slice的语法规则与C++和Java很相似,除了标示符的差异。
注释
Slice允许使用C和C++的注释风格:
/*
*C 风格注释
*C 风格注释
*C 风格注释
*/
//C++风格
关键字
Slice使用小写拼写关键字。例如class和dictionary都是关键字。不过有两个例外,Object和LocalObject也都是关键字,但是必须如显示的方式拼写。
标识符
标识符以一个字母起头,后面可以跟随字符和数字。Slice的标识符被限制在ASCII字符集内并且不支持非英语的字符。
不同于C++的标识符,Slice的标识符不能有下划线。这个限制看起来似乎很苛刻,但是却是有必要的。保留下划线,就让多语言的映射获取了命名空间,从而不会与合法的Slice标识符产生冲突。
大小写敏感
标识符是大小写不敏感的,但是必须保持拼写一致。例如TimeOfDay和TIMEOFDAY在同一个命名空间中是一样的。但是,Slice强制拼写一致。一旦你定义了一个标识符之后,你必须自始至终的拼写这个标识符的大小写,否则,编译器会认为这是非法的标识符。这条规则之所以存在,是因为这样就允许Slice可以映射到大小写敏感的语言也能映射到大小写不敏感的语言。
是关键字的标识符
你可以使用其他语言的关键字来定义Slice标示符,例如,switch可以用来作为Slice标识符,但是也是Java和 C plus plus 的关键字。Slice语言映射中针对每一种语言都定义了映射规则来处理这样的标识符。例如,Slice把switch映射为 C plus plus 的_cpp_stitch和Java的_switch。
转义的标识符
你可以用过使用\符号来将Slice的关键字转换为标识符。例如
struct dictionary{ //错误的定义
}
struct \dictionary{ //正确的定义
}
\符号改变了关键字的含义。在上面的例子中,\dictionary被当作dictionary标识符处理。使用转义的标识符可以允许我们以后加入新的关键字,而不对当前存在的规范造成影响。
保留的标识符
Slice将Ice以及以Ice开始的所有标识符作为保留的标识符。例如,Icecream将会被认为是非法的标识符。
同时,Slice还将以下标识符为后缀的标识符视为保留的标识符:
保留它们,主要是为了防止在生成代码时发生冲突。
ICE:Slice语言(三)--模块
Modules模块(Modules)
对于大型系统来说,一个常见的问题就是全局命名空间的杂乱不堪:随着开发进度的发展,独立系统的不断整合,命名冲突也就不断出现。Slice提供了模块结构来缓和命名冲突。
如下的结构:
module MutableRealms
{
module WishClient { };
module WishServer { };
};
一个模块能容纳任何合法的Slice结构,包括其他的模块定义。使用模块组织相关的定义从而避免全局命名的杂乱和减少命名冲突的可能。
Slice要求所有的定义被嵌套在模块中,这样你就不在模块之外定义一个全局的定义。例如,下面的代码是不合法的:
interface I
{
//错误:全局定义中只能是module
}
全局定义是被禁止的原因是因为某些实现语言不支持全局定义。例如Python。
模块是可以被二次定义的。例如:
module MutableRealms
{
//这里是定义
};
//也许在另一个源文件中:
module MutableRealms
{
//正确的,二次模块定义
//更多的定义
};
二次定义模块对于大型项目是非常有用的:它们允许你在几个源文件中存放一个模块的内容。这样做的好处就是当一个开发者修改了模块的某一个部分时,只有与修改的内容相关的文件需要重新编译,而不是重新编译模块的相关的全部文件。
模块映射到编程语言的对应的定义结构。例如,C++,C#和VB,模块映射到命名空间。Java则是package。
Ice ModuleIce模块(Ice Module)
Ice的运行时API,除了少部分的语言特定的调用不能在Ice中表示以外,所有的都定义在Ice模块中。换句话说,大部分的Ice API是完全用Slice定义表达的。这样做的好处就是一个单独的Slice定义就可以有效地定义适用所有支持语言的Ice运行时的API。
ICE:Slice语言(四)--基本类型
Basic Slice Types基本Slice类型
Slice提供了一些内置的基本类型,如下所示,括号中的数字表示指数:
类型 |
取值范围 |
大小 |
bool |
false / true |
没指定 |
byte |
-128 to 127(0 - 255) |
>=8bits |
short |
-2(15) to 2(15) - 1 |
>=16bits |
int |
-2(31) to 2(31) - 1 |
>=32bits |
long |
-2(63) to 2(63) - 1 |
>=64bits |
float |
IEEE single标准 |
>=32bits |
double |
IEEE double标准 |
>=64bits |
string |
所有的Unicode字符, 除了所有位为0的字符 |
变长 |
其中的byte类型的最大取值范围根据实际的语言决定。
当数据类型在服务器和客户端之间传递时,除了byte类型之外的数据类型都根据实际情况在变化。例如:一个long类型的值在从一台little-endian机器传递到bit-endian机器时会发生位变换。类似的,string在从EBCDIC发往ASCII时也是发生变化的,也许string的字符的尺寸也发生了变化,因为不是所有的机器都使用8位的字符。实际上,这些变化对于程序员来说都是透明的,而且会严格按照需要来变化。
整数类形(integer types)
Slice提供了如下的整数类型:
不过,这些类型在某些机器上会映射到更宽的原生类型。需要注意的是那些无符号类型Slice并没有提供。因为无符号类型影射到那些没有原生的无符号类型的语言相当困难,例如Java。
浮点数类型(floating-point types)
浮点数类型遵循IEEE的规范。如果一个实现语言么有支持IEEE的浮点数格式,Ice运行时会将浮点数值转换为原生浮点数表示。
字符串(strings)
Slice字符串使用Unicode字符集。唯一的不能出现在字符串中的字符就是零字符。
Slice没有null字符串的概念。这是因为null字符串很难映射到不直接支持null字符串概念的语言上,例如Python。不要设计依靠一个null字符串来表示“不在那里”的语义的接口。如果你需要表示可选的串,可以通过使用类,字符串的序列,或者一个空的字符串来表示null字符串。
布尔类型(booleans)
布尔类型的值只能由false和true。如果语言映射时有原生布尔类型,语言映射就会使用该类型。
Byte类型(bytes)
Slice定义byte是一个至少8位的,在地址空间中传递时保证不会发生变化的类型。保证不发生变化就允许交换二进制数据,而这些数据不会被篆改。其他的Slice类型都会在传递过程中会被改变表达形式。
ICE:Slice语言(五)--用户定义类型
User Defined Types用户定义类型
除了Slice提供的基本类型以外,Slice还允许你自定义复合类型:枚举、结构、序列和词典。
枚举
Slice的枚举定义看起来就像C++的枚举定义一样:
enum Fruit { Apple, Pear, Orange };
上面的代码定义了一个名为Fruit的类型,Fruit是一种拥有自己权利的新的类型。Slice没有定义如何给枚举赋顺序值,但是Slice保证枚举的顺序值从左向右递增。
不同于C++,Slice不允许你控制枚举的顺序值,例如下面的代码是错误的:
enum Fruit { Apple = 0, Pear = 7, Orange = 2 };
实际上,只要你不在地址空间之间传递枚举的顺序值,那么你就不关心枚举的值是多少。例如,客户端向服务器发送了一个0值,用来表示Apple,这样就会引起问题,因为服务器可能不使用0值来表示Apple。但是,如果直接向服务器发送Apple,如果Apple在被接受的地址空间中使用一个不同的值表示,那么那个值会被Ice运行时正确的对应到Apple。
Slice不允许定义空的枚举。同时,Slice的枚举会进入封闭的命名空间,如下的代码是错误的:
enum Fruit { Apple, Pear, Orange };
enum ComputerBrands { Apple, IBM, Sun, HP };
上面的代码中Apple被二次定义了。
结构
Slice支持容纳一个或多个任意类型的有名称成员的结构,包括了用户自定义的复合类型,例如:
struct TimeOfDay {
short hour;
short minute;
short second;
};
上面的代码定义了名为TimeOfDay的新类型。结构定义形成了一个命名空间,因此结构中的成员的命名需要保持唯一性。
结构中不能出现结构定义,例如下面的代码是错误的:
struct TwoPoints
{
struct Point
{
short x;
short y;
};
Point coord1;
Point coord2;
};
这条规则在Slice中的通用描述就是:除了模块,类型不能嵌套定义。这样做可以避免不同语言的不同的定义要求造成的冲突。所以,为了能够定义上面代码中的类型,你可以把两个结构分开定义。
序列
序列是不定长度的元素向量。如下:
sequence<Fruit> FruitPlatter;
一个序列是可以为空的,也就是说,序列可以不容纳元素,或者序列可以容纳在内存容量允许的范围之内的任意数量的元素,
序列的元素也可以是序列,就是说,你可以创建下面的序列:
sequence<FruitPlatter> FruitBanquet;
序列用来构造多种集合,例如向量、列表、队列、集合、包或者树。
一个特殊的序列的用法已经变成了惯用的方法,即使用序列来提供可选的值。例如,我们可能有一个Part结构来记录装配到汽车的零件的详细信息,这个结构可以记录如零件的名称,描述,重量,价格和其他的信息等。备用零件通常有一个序列号,我们用long类型的值来表示。实际上,一些零件,例如螺丝,我们通常没有螺丝的序列号,所以我们在螺丝的序列号里存放什么内容呢?下面有一些处理的选择:
struct Part {
string name;
string description;
//
bool serialIsValid;
long serialNumber;
};
这样做可能会给我们带来另一个麻烦:迟早会有人因为忘了判断serialIsValid的值而直接使用serialNumber。
sequence<long> SerialOpt;
struct Part{
string name;
string description;
//
SerialOpt serialNumber;
};
通常,Opt后缀表明这个一个可选的元素。如果序列是空的,那么很显然没有序号,如果序列有一个值,那么那个值就是序列号。很明显,这样做有一个缺点,那就是有人会故意放多个值到序列中。这个做法可以通过加入一个特殊通途的Slice结构来解决。实际上,可选值很少使用,没有必要加入一种特定的语言特征来解决。
字典
字典是键、值类型的映射。例如:
struct Employee {
long number;
string firstName;
string lastName;
};
dictionary<long, Employee> EmployeeMap;
上面的定义创建了一个名为EmployeeMap的字典,它达到了从employee编号到一个包含有employee详细信息的结构的映射。
字典能够用来实现稀疏数组,或者任何使用非整数键值类型的用于查找的数据结构。尽管可以用设置一个包含了键值对的结构的序列也能用来构造类似的事情。但是字典更适合:
字典的键值不一定要是整形,它可以是下面的数据类型的任何一种:
常量定义和直接量
Slice允许你定义常量。常量定义必须是下面的类型中的一种:
定义常量的语法与C plus plus和Java类似(有一点小的不同):
ICE:Slice语言(六)--接口、操作和异常(一)
Interfaces Operations And Exceptions接口、操作和异常
Slice的主要焦点就是定义接口,例如:
struct TimeOfDay
{
short hour;
short minute;
short second;
};
interface Clock
{
TimeOfDay getTime();
void setTime(TimeOfDay time);
}
上面的代码定义了一个名为Clock的接口。该接口支持两个操作:getTime和setTime。客户端通过调用代理上的操作访问支持这个接口的对象:要读取当前的时间,客户端调用getTime;要设置当前时间,客户端通过调用setTime操作,并传递一个TimeOfDay类型的参数。
要调用一个代理上的操作就会让Ice运行时发送一个消息给目标对象。目标对象是在另一个地址空间或者是与调用者配置在一起,总之,目标对象的位置对于客户端来说是透明的。如果目标对象在另一个地址空间(有可能是一个远程的机器),Ice运行时就会通过一个远程调用过程调用客户端要调用的操作;如果目标对象与客户端配置在一起,那么Ice运行时使用一个常规的方法来代理远程调用从而避免列集开销。
你可以认为接口就是等同于C plus plus的类定义的公共部分,也可以认为是Java的接口,以及操作定义是一个虚成员函数。不过,只有接口内部才能有操作定义。 而且你不能在接口定义中定义类型和异常,以及数据成员。但是,这并非意味着你的对象实现时不能包含状态。包含状态是可以的,但是状态的实现方式对于客户端来说是隐藏的,因此在对象的接口定于众不需要出现。
一个Ice对象只能有一个Slice接口。当然,你可以创建多个使用同一个接口的Ice对象。用C plus plus来做对比,一个Slice接口对应于C plus plus的类定义,一个Ice对象对应于C plus plus的类实例(不过,Ice的对象可以在多个不同的地址空间中实现)。
Ice也通过名为facets的特征提供了多重接口。
一个Slice接口定义了Ice中的最小分布粒度:每一个Ice对象有一个唯一区分于其它对象的标示。当开始通讯时,你必须在对象的代理上调用操作。在Ice中没有可寻址的实体的概念。你不能创建一个结构,然后让客户端远程来调用它。为了能够调用这个结构,你必须创建一个接口允许客户端通过这个接口访问这个结构。
因此,将应用划分为不同的接口在总体架构上有着深远的影响。分布边界必须遵从接口或类边界;你可以将接口的实现分布到多个地址空间中(你也可以在一个地址空间中实现多个接口),但是,你不能在不同的地址空间中实现接口的不同的部分。
参数和返回值
一个操作的定义必须包含一个返回类型和零个或多个参数的定义。例如,前面的代码汇总的getTime操作有一个TimeOfDay的返回类型以及setTime操作有一个void的返回类型。你必须使用void来指出操作不返回任何值。
一个操作可以有一个或多个输入参数,例如,setTime方法接受一个TimeOfDay类型的输入参数。当然,你可以使用多个输入参数,例如:
interface CircadianRhythm
{
void setSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime);
//...
};
注意,参数名是必须的。
默认的参数是从客户端发送到服务器端,也就是说,他们是输入参数。如果要从服务器端传输到客户端,你可以使用输出参数,输出参数使用out关键字标示。例如,另一个获取当前的时间的方法可以如下:
void getTime(out TimeOfDay time);
与输入参数一样,你可以使用多个输出参数:
interface CircadianThythm
{
void setSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime);
void getSleepPeriod(out TimeOfDay startTime, out TimeOfDay stopTime);
//...
};
如果你既有输入参数又有输出参数,那么输出参数必须跟在输入参数的后面:
void changeSleepPeriod(TimeOfDay startTime, TimeOfDay stopTime, out TimeOfDay prevStartTime, out TimeOfDay preStopTime);
Slice不支持既做输出又作输入的参数。
操作定义的风格'
你可能会期望,语言映射可以遵循你在Slice定义中的风格:Slice的返回类型映射到编程语言的返回类型,以及Slice的参数映射到编程语言的参数。
对于只有一个返回值的操作,它一般都是由操作返回值而不是使用输出参数。这个风格自然地映射到所有编程语言。注意,如果你使用输出参数而不是返回类型,那么你就是把一个不同的API风格强加给客户:大部分的编程语言允许函数的返回值被忽略而不允许输出参数被忽略。
如果操作返回多个值,它一般使用多个输出参数并返回一个void类型。实际上,这条规则并非都适用,因为有些有多个返回值的操作中,可能有一个值比其他返回值更重要。一个典型的例子就是从一个Collection中逐步的获取其中的项:
bool next(out RecordType r);
next操作有两个返回值:一个RecordType的值,一个bool值指出是否到了集合的最后一项。这样的定义风格是非常有用的,因为这很自然的让程序员编写控制结构。例如:
while(next(record))
//处理record...
if(next(record))
//获取一个有效的record...
重载
Slice不支持任何形式的操作重载。在同一个接口中的操作必须有不同的名字,与这些操作的类型和参数的数量无关。
Nonmutating 操作
有一些操作,例如上面代码中的getTime操作,这个操作不会修改所操作的对象的值。它们在概念上等效于C plus plus的const 成员函数。你可以如下的指出这样的操作:
interface Clock
{
nonmutating TimeOfDay getTime();
void setTime(TimeOfDay time);
};
nonmutating关键字指出了getTime操作不会修改它所操作的对象的状态。这样使用有两个原因:
对于普通的操作,Ice运行时对于如何处理错误是保守的。例如,如果一个客户端发送一个操作调用到服务器,然后丢失了连接,对于客户端的Ice运行时来说,没有办法知道调用是否成功。这就意味着,运行时不能通过尝试重新连接和再次发送请求来恢复错误,因为这可能第二次引发操作以及违背了最多一次语义。运行时没有选择,只能把错误报告给应用。对于nonmutating操作,换句话说,客户端的运行时可以尝试再次连接和安全的二次送出失败的请求。如果第二次发送能够到达服务器,那么万事OK。只有第二次再次失败,错误才会报告给应用(错误重试的次数可以在Ice的配置文件中配置)。
Idempotent操作
我们可以更进一步去修改上面的Clock接口的定义,从而可以让setTime操作是idempotent的:
interface Clock
{
nonmutating TimeOfDay getTime();
idempotent void setTime(TimeOfDay time);
};
对某一个操作进行两次成功的操作,其结果都一样,就像只调用了一次一样,那么这个操作就是idempotent操作。例如,x = 1; 是一个idempotent操作因为不管执行了一次还是两次,x的值都是1。换句话说,x += 1;就不是一个idempotent操作,因为它执行了两次后,结果不同了。
idempotent关键字指出了一个操作能够安全的执行多次。同nonmutating操作一样,Ice运行时使用idempotent来达到更积极地错误恢复。
一个操作只能是nonmutating或idempotent,不能两个都是。(nonmutating隐含了idempotent)
用户异常
查看前面的setTime操作的代码,我们发现一个潜在的问题:TimeOfDay结构中的每一个成员都是short类型,如果一个客户端调用setTime操作并且传入一个毫无意义的值,例如-199作为分钟,或者42作为小时,那么会发生什么事呢?很显然,应该提供一些提示给调用者,这个值是无意义的。Slice允许你定义用户异常来给客户端指出错误的情况。例如:
exception Error {}; //空的错误是有效的
exception RangeError
{
TimeOfDay errorTime;
TimeOfDay minTime;
TimeOfDay maxTime;
};
一个用户异常很像一个结构一样包含了一些数据成员。实际上,与结构不同的是,一场能够有零数据成员,也就是说,一个空的异常。当客户端的操作的实现出现错误的条件时,异常允许你返回任意数量的错误信息。操作使用一个异常规范来说明可能会传递给客户端的异常:
interface Clock
{
nonmutating TimeOfDay getTime();
idempotent void setTime(TimeOfDay time) throws RangeError,Error;
};
上面的定义说明了setTime操作可能会抛出一个RangeError或者一个Error用户异常。如果客户端接收到了一个RangeError异常,这个异常包含有传递给setTime的TimeOfDay值以及被引起的错误。如果setTime因为非RangeError定义的错误而调用失败,操作将报出Error异常。很显然,因为错误没有数据成员,所以客户端将无法知道发生了什么错误,客户端只知道操作没有成功。
一个操作只可以抛出那些列在异常规范中的用户异常。如果在运行时操作的实现抛出的异常没有列在异常规范中,那么客户端将收到一个运行时异常来表示操作失败。为了说明一个操作没有抛出任何用户异常,只要简单的忽略异常规范就可以了。
异常不是第一类数据类型,第一类数据类型也不是异常:
异常继承
异常支持继承:
exception ErrorBase
{
string reason;
};
enum RTError
{
DivideByZerp, NegativeRoot, IllegalNull /*...*/
};
exception RuntimeError extends ErrorBase
{
RTError err;
};
enum LError { ValueOutRange, ValuesInconsistent, /*...*/ };
exception LogicError extends ErrorBase
{
LError err;
}
exception RangeError extend LogicError
{
TimeOfDay errorTime;
TimeOfDay minTime;
TimeOfDay maxTime;
}
上面的定义建造了一个层次的异常定义:
建立这样一个异常层次结构不仅仅是有助于创建一个更易读的规范,还能够在语言层次上带来好处。例如C plus plus映射会保持异常的层次结构,这样你就可以用基类俘获异常,或者建立异常句柄来处理指定的异常。
查看上面的异常层次,这还不是很清楚,在运行时,应用将会抛出继承的异常,例如RangeError,还是基类异常,例如LogicError,RuntimeError和ErrorBase。如果你指明一个积累异常,接口或类是抽象的,你可以添加注释达到效果。
注意,如果一个操作的异常规范指明了一个异常规范类型,在运行时,操作的实现可能抛出多重继承异常。例如:
exception Base
{
//...
};
exception Derived extends Base
{
//...
};
interface Exsample
{
void op() throw Base; //可能抛出基类也可能是继承类.
}
随着系统的演变,系统中可能会加入新的,继承的异常。假设我们开始的系统中是如下定义的:
exception Error
{
//..
};
interface Application
{
void doSomthing() throw Error;
};
再假设已经部署了大量的客户端,也就说,当你升级系统时,你不能轻松的升级所有的客户端。随着这个系统的演变,一个新的异常被加入到系统中,并且服务器端要重新使用新的定义部署:
exception Error
{
//...
};
exception FatalApplicationError extend Error
{
//...
};
interface Application
{
void doSomething() throws Error;
};
如果服务器端抛出了一个FatalApplicationError,那么会发生什么事情?这就需要看客户端是否是使用新的或还是使用旧的定义了:
异常只支持单继承。