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



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

假设您不想添加基元操作:您没有编辑源文件的权限,或者您不想让包中包含不相关的功能。

然后,您不能在其他包中定义对类型Vehicle'Class进行隐含调度的操作。例如,您可能希望序列化车辆(定义具有To_Xml调度功能的Vehicles_XML包)或将其显示为UI元素(定义具有Get_LabelGet_Icon…调度功能的Vehicles_GTK包)等。执行动态调度的唯一方法是明确地编写代码;例如,在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规范背后是否有更根本的原因(否则,这真的意味着规范假设/强制执行动态解除补丁的特定实现吗?)

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

我能想到几个原因:

(1) 您的示例在同一个包中定义了CarBike,它们都源自Vehicles。然而,根据我的经验,这不是"正常"的用例;更常见的做法是在自己的包中定义每个派生类型。(我认为这与其他编译语言中"类"的使用方式很接近。)还要注意的是,在之后定义新的派生类型并不罕见。这是面向对象编程的全部要点之一,以便于重用;如果在设计一个新功能时,您可以找到一些可以从中派生的现有类型,并重用其功能,这是一件好事。

因此,假设您有定义VehicleCarBikeVehicles包。现在,在其他包V2中,您希望在Vehicle上定义一个新的调度操作。为了实现这一点,您必须为CarBike及其主体提供重写操作;假设你不被允许修改Vehicles,那么语言设计者必须决定新操作的主体必须在哪里。假设你必须用V2编写它们。(一个后果是,您在V2中编写的主体将无法访问Vehicles的私有部分,因此它无法访问CarBike的实现详细信息;因此,您只能根据已定义的操作编写该操作的主体。)那么问题是:V2是否需要为从Vehicle派生的所有类型提供操作?从Vehicle派生的类型没有成为最终程序的一部分(也许它们是为了在其他人的项目中使用而派生的)呢?从Vehicle派生的尚未定义的类型(见前一段)如何?理论上,我想这可以通过在链接时检查所有内容来实现。然而,这将是该语言的一个重大范式变化。这不是一件容易的事。(顺便说一句,程序员通常认为"在语言中添加功能X会很好,而且不应该太难,因为X很容易谈论",而没有意识到这样一个"简单"的功能会产生多大的影响。)

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

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

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

在没有检查Ada 95(引入标记类型的地方)的基本原理的情况下,我非常确信标记类型的冻结规则是从一个简单的要求派生出来的,即T类中的所有对象都应该具有T类的所有调度操作。

为了满足这个要求,你必须冻结类型,并说一旦你:,就不能再向类型T添加调度操作了

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

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

回答您的扩展问题:

Ada同时提供了《参考手册》(ISO标准)、《基本原理》和《注释参考手册》。这些文件背后的大部分讨论也是公开的。

Ada 2012参见http://www.adaic.org/ada-resources/standards/ada12/

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

最新更新