1

在 Ada 中,类型 T 的原始操作只能在定义了 T 的包中定义。例如,如果一个Vehicules包定义CarBike标记了记录,它们都继承了一个通用的Vehicle抽象标记类型,那么所有可以在类范围Vehicle'Class类型上调度的操作都必须在这个Vehicles包中定义。

假设您不想添加原始操作:您没有编辑源文件的权限,或者您不想用不相关的功能使包混乱。

然后,您不能在其他包中定义隐式调度 type的操作Vehicle'Class。例如,您可能想要序列化车辆(定义Vehicles_XML带有To_Xml调度功能的包)或将它们显示为 UI 元素(使用, , ... 调度功能定义Vehicles_GTK包)等。执行动态调度的唯一方法是编写代码明确;例如,里面:Get_LabelGet_IconVechicle_XML

if V in Car'Class then
   return Car_XML (Car (V));
else
   if V in Bike'Class then
      return Bike_XML (Bike (V));
   else
      raise Constraint_Error 
         with "Vehicle_XML is only defined for Car and Bike."
end if;

(当然,在其他地方定义和使用的访问者模式Vehicles也可以工作,但这仍然需要相同类型的显式调度代码。实际上编辑,不,但仍然需要编写一些样板代码)

我的问题是:

为什么在 T 上动态调度的操作仅限于在 T 的定义包中定义?

这是故意的吗?这背后有什么历史原因吗?

谢谢


编辑:

感谢当前的答案:基本上,这似乎是语言实现的问题(冻结规则/虚拟表)。

我同意编译器是随着时间的推移逐渐开发的,并且并非所有功能都很好地适合现有工具。因此,将调度运算符隔离在一个独特的包中似乎是一个主要受现有实现指导而不是语言设计的决定。C++/Java 家族之外的其他语言在没有这种要求的情况下提供动态调度(例如 OCaml、Lisp (CLOS);如果这很重要,它们也是编译语言,或者更准确地说,是存在编译器的语言)。

当我问这个问题时,我想知道在语言规范级别,这部分 Ada 规范背后是否有更根本的原因(否则,这是否真的意味着规范假设/强制执行动态调度的特定实现?)

理想情况下,我正在寻找权威来源,例如参考手册中的基本原理指南部分,或有关该语言特定部分的任何类型的存档讨论。

4

3 回答 3

6

我能想到几个原因:

(1) 您的示例具有CarBike定义在同一个包中,均源自Vehicles. 但是,根据我的经验,这不是“正常”的用例;在自己的包中定义每个派生类型更为常见。(我认为这与“类”在其他编译语言中的使用方式很接近。)还要注意,之后定义新的派生类型并不少见。这是面向对象编程的全部要点之一,以促进重用;如果在设计新功能时,您可以找到一些可以派生的现有类型并重用其功能,那将是一件好事。

因此,假设您的Vehicles包定义了VehicleCarBike. 现在在其他一些包V2中,您想在Vehicle. 为此,您必须为其主体提供Car和的覆盖操作;Bike并且假设您不允许修改Vehicles,那么语言设计者必须决定新操作的主体必须在哪里。据推测,您必须将它们写入V2. (一个后果是您写入的主体V2将无法访问 的私有部分Vehicles,因此它无法访问Car或的实现细节Bike; 所以你只能在已经定义的操作条件下编写该操作的主体。)那么问题是:是否V2需要为派生自的所有类型提供操作Vehicle?那些派生出来Vehicle的类型不会成为最终程序的一部分(也许它们派生出来是为了在其他人的项目中使用)?派生出来Vehicle的类型尚未定义(见上一段)呢?理论上,我想这可以通过在链接时检查所有内容来实现。然而,这将是一个主要的语言的范式变化。这不是一件容易的事。(顺便说一句,程序员认为“将特性 X 添加到语言中会很好,而且它不应该太难,因为 X 很容易谈论”,这是很常见的,而没有意识到有多么巨大的影响这样一个“简单”的功能就会有。)

(2) 一个实际的原因与调度是如何实现的有关。通常,它是通过过程/函数指针向量来完成的。(我不确定在所有情况下的确切实现是什么,但我认为这基本上适用于每个 Ada 编译器以及 C++ 和 Java 编译器,可能还有 C#。)这意味着当你定义一个标记类型(或一个类,在其他语言中),编译器将设置一个指针向量,并根据为该类型定义的操作数,比如 N,它将在向量中保留插槽 1..N对于子程序的地址。如果一个类型派生自该类型并定义了覆盖子程序,则派生类型将获得自己的向量,其中槽 1..N 将是指向实际覆盖子程序的指针。然后,在调用调度子程序时,程序可以在分配给该子程序的某个已知槽索引中查找地址,并且它将根据对象的实际类型跳转到正确的地址。如果派生类型定义新的原始子程序,新的插槽被分配 N+1..N 2,并且从中派生的类型可以定义获得插槽 N 2 +1..N 3的新子程序,依此类推。

添加新的调度子程序Vehicle会干扰这一点。由于新类型派生自Vehicle,因此您不能在 N 之后将新区域插入向量中,因为已经生成了假定从 N+1 开始的插槽已分配给派生类型派生的新操作的代码。而且由于我们可能不知道派生出的所有类型,Vehicle也不知道将来会派生出哪些其他类型Vehicle以及将为它们定义多少新操作,因此很难在可用于新操作的向量。同样,如果所有插槽分配都推迟到链接时间,则可以这样做,但这又是一个重大的范式变化。

老实说,我可以想到其他方法来完成这项工作,不是在“主”调度向量中而是在辅助向量中添加新操作;调度可能需要搜索正确的向量(可能使用分配给定义新操作的包的 ID)。此外,向interfaceAda 2005 添加类型已经使简单的向量实现有些复杂。但我确实认为这(即它不适合模型)是为什么在 Ada(或我知道的任何其他编译语言)中不存在像您建议的那样添加新调度操作的能力的原因之一。

于 2014-02-24T16:39:37.767 回答
4

在没有检查 Ada 95 的基本原理(引入标记类型的地方)的情况下,我很确定标记类型的冻结规则源自一个简单的要求,即 T'Class 中的所有对象都应该具有类型 T 的所有调度操作。

为了满足该要求,您必须冻结类型并说一旦您执行以下操作就不能再向类型 T 添加调度操作:

  • 从 T 派生一个类型,或
  • 位于声明 T 的包规范的末尾。

如果你不这样做,你可能有一个从类型 T 派生的类型(即在 T'Class 中),它没有继承类型 T 的所有调度操作。如果你将该类型的对象作为 T' 传递子程序的类参数,它知道类型 T 上的另一个调度操作,对该操作的调用将不得不失败。- 我们不希望这种情况发生。

于 2014-02-25T07:58:07.303 回答
2

回答您的扩展问题:

Ada 附带参考手册(ISO 标准)、基本原理和带注释的参考手册。这些文件背后的大部分讨论也是公开的。

对于 Ada 2012,请参阅http://www.adaic.org/ada-resources/standards/ada12/

标记类型(动态调度)是在 Ada 95 中引入的。与该版本标准相关的文档可以在http://www.adaic.org/ada-resources/standards/ada-95-documents/找到

于 2014-03-07T08:10:09.760 回答