Ada封装动态调度操作(原语)背后的基本原理

时间:2014-02-24 14:59:30

标签: ada dynamic-dispatch

在Ada中,类型T的Primitive operations只能在定义了T的包中定义。例如,如果Vehicules包定义了CarBike标记记录,两者都继承了一个公共Vehicle抽象标记类型,那么所有操作都可以在全班调度必须在此Vehicle'Class包中定义Vehicles类型。

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

然后,您无法在其他包中定义隐含在Vehicle'Class类型上调度的操作。 例如,您可能希望序列化车辆(使用Vehicles_XML调度功能定义To_Xml包)或将其显示为UI元素(使用Vehicles_GTK定义Get_Label包, Get_Icon,...调度功能)等 执行动态调度的唯一方法是明确地编写代码;例如,在Vechicle_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规范背后的原因(否则,它是否真的意味着规范假设/强制执行动态disapatch的特定实现?)

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

3 个答案:

答案 0 :(得分:6)

我可以想到几个原因:

(1)您的示例在同一个包中定义了CarBike,两者都来自Vehicles。但是,这不是正常的"用例,根据我的经验;在自己的包中定义每个派生类型更常见。 (我认为这与其他编译语言中的"类和#34;如何接近。)并且还要注意,之后定义新的派生类型 并不罕见。这是面向对象编程的全部要点之一,便于重用;如果在设计新功能时,您可以找到一些可以派生的现有类型,并重用其功能,那将是一件好事。

假设您拥有定义VehiclesVehicleCar的{​​{1}}个包。现在在其他一些包Bike中,您希望在V2上定义新的调度操作。为了实现这一目标,您必须为VehicleCar及其身体提供最重要的操作;并且假设您不被允许修改Bike,那么语言设计者必须决定新操作的主体必须在哪里。据推测,你必须在Vehicles中写下它们。 (一个结果是您在V2中写的正文无权访问V2的私有部分,因此无法访问Vehicles或{的实现细节{1}};因此,如果已经定义的操作的条款,您只能编写该操作的主体。)那么问题是:Car是否需要为所有类型提供操作源自Bike?那些来自V2的类型不会成为最终程序的一部分(也许它们会被派生用于其他人的项目中)?那些尚未定义的Vehicle派生的类型怎么样(参见前一段)?理论上,我认为可以通过在链接时检查所有内容来使其工作。但是,这将是语言的主要范式更改。这不是一件容易的事情。 (顺便说一句,对于程序员来说,这是非常常见的#34;将特征X添加到语言中会很不错,而且它不应该太难,因为X很容易讨论& #34;,没有意识到这样一个简单的特征会带来多大的影响。)

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

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

说实话,我可以通过添加不在&#34; main&#34;中的新操作来考虑其他方法来完成这项工作。调度矢量但辅助矢量;调度可能需要搜索正确的向量(可能使用分配给定义新操作的包的ID)。此外,向Ada 2005添加Vehicle类型已经使得简单的向量实现有些复杂化。但我确实认为这(即它不适合模型)是添加像你建议的新调度操作的能力在Ada(或任何其他编译的< / em>我所知道的语言。)

答案 1 :(得分:4)

如果没有检查Ada 95的基本原理(引入了标记类型),我很确定标记类型的冻结规则来自简单的要求,即T'Class中的所有对象都应具有类型的所有调度操作吨。

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

  • 从T或
  • 派生类型
  • 在声明T的包规范的末尾。

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

答案 2 :(得分: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/找到与该版本标准相关的文档