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

cor*_*ump 1 ada dynamic-dispatch

在Ada中,类型T的原始操作只能在定义了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;
Run Code Online (Sandbox Code Playgroud)

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

我的问题是:

是否有理由限制在T的定义包中定义在T上动态调度的操作?

这是故意的吗?这背后有一些历史原因吗?

谢谢


编辑:

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

我同意编译器会随着时间的推移逐步开发,并且并非所有功能都能很好地适应现有工具.因此,在一个独特的包中隔离调度操作符似乎是一个主要由现有实现而不是语言设计引导的决策.C++/Java系列之外的其他语言提供了没有这种要求的动态调度(例如OCaml,Lisp(CLOS);如果重要的话,那些也是编译语言,或者更准确地说,是编译器存在的语言).

当我问这个问题时,我想知道在语言规范层面是否有更多根本原因,这些部分是Ada规范背后的原因(否则,它是否真的意味着规范假定/强制实施动态解除的特定实现?)

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

ajb*_*ajb 6

我可以想到几个原因:

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

因此,假设你有你的Vehicles包定义Vehicle,CarBike.现在在其他一些包中V2,您希望在a上定义一个新的调度操作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(或我所知的任何其他编译语言)中不存在的一个原因.