我有机械工程背景,但我有兴趣学习Ada的良好软件工程实践。我有几个问题。
Q1。如果我理解正确,那么有人可以编写一个包规范(ads)文件,编译它然后编译使用该包的主程序。稍后,当人们知道要在包体中包含什么时,可以编写和编译后者。之后,现在可以运行主程序。我试过这个,我想确认这是一个好习惯。
Q2。我的第二个问题是关于存根(子单元)和SEPARATE的使用。假设我有一个主程序如下:
WITH Ada.Float_Text_IO;
WITH Ada.Text_IO;
WITH Ada.Integer_Text_IO;
PROCEDURE TEST2 IS
A,B : FLOAT;
N : INTEGER;
PROCEDURE INPUT(A,B: OUT FLOAT; N: OUT INTEGER) IS SEPARATE;
BEGIN -- main program
INPUT(A,B,N);
Ada.Float_Text_IO.Put(Item => A);
Ada.Text_IO.New_line;
Ada.Integer_Text_IO.Put(Item => N);
END TEST2;
然后我将INPUT程序放在一个单独的文件中:
separate(TEST2)
PROCEDURE INPUT(A,B: OUT FLOAT; N: OUT INTEGER) IS
BEGIN
Ada.Float_Text_IO.Get(Item => A);
Ada.Text_IO.New_line;
Ada.Float_Text_IO.Get(Item => B);
Ada.Text_IO.New_line;
Ada.Integer_Text_IO.Get(Item => N);
END INPUT;
我的问题:
a)AdaGIDE建议我将INPUT过程文件保存为input.adb。但是在编译主程序test2时,我收到警告:
warning: subunit "TEST2.INPUT" in file "test2-input.adb" not found
cannot generate code for file test2.adb (missing subunits)
对于AdaGIDE,这更像是一个错误,因为上述警告出现在消息之前:
Compiling...
Done--error detected
所以我将input.adb文件重命名为test2-input.adb,正如AdaGIDE在编译时向我建议的那样。现在编译主文件时,我没有任何警告。我现在的问题是如果可以写
PROCEDURE INPUT(A,B: OUT FLOAT; N: OUT INTEGER) IS
就像我在子单元文件test2-input.adb中所做的那样,或者写一个更具描述性的术语如
更好PROCEDURE TEST2-INPUT(A,B: OUT FLOAT; N: OUT INTEGER) IS
强调过程输入有一个父过程test2?这个想法来自AdaGIDE,如上所述,暗示了test2-input.adb。
b)我的下一个问题:
如果我理解编译顺序,那么我应该首先编译主文件test2.adb然后编译stub test2-input.adb。在编译存根时,我收到错误消息:
cannot generate code for file test2-input.adb (subunit)
Done--error detected
但是,我现在可以为test2.adb执行绑定和链接并运行程序。
我想通过尝试编译存根test2-input.adb或者是否应该编译它来确定我是否做错了?
Q3。有子单元有什么用?是否只是将一个大型程序分解成更小的部分?我知道如果没有在子单元中的BEGIN和END之间放置任何语句,则会出现错误。所以这意味着总是必须在那里发表声明。如果以后想要编写语句,可以在子单元中的BEGIN和END之间放置一个NULL语句,稍后再返回后者。这是软件工程在实践中的完成方式吗?
非常感谢...
答案 0 :(得分:7)
Q1:那是优秀的练习。
通过将包规范视为规范,您可以将其提供给其他开发人员,以便他们知道如何与您的代码进行交互。
Q2:我认为AdaGIDE实际上使用GNAT编译器进行所有编译,因此它实际上是负责可接受文件名的GNAT。 (这可以配置,但除非你有一个非常令人信服的理由这样做,简单地使用GNAT / AdaGIDE的文件命名约定要简单得多。)但是,与你的问题更相关的是,没有强有力的理由包括父母单位作为单独单位名称的一部分。但是看看Q3的答案......
第三季度:第一版Ada-Ada 83引入了子单元 - 部分是为了帮助模块化代码,并允许延迟开发和编译。但是,Ada软件开发实践几乎放弃了子单元的使用,所有的过程/函数/任务/等机构都简单地全部保存在包的主体中。它们仍然在某些领域使用,例如,如果可能需要特定于平台的子程序版本,但在大多数情况下它们很少使用。它留下较少的文件来跟踪,并将包的实现代码保持在一起。因此,我强烈建议您忽略子单元功能,并将所有实现代码放在包体中。答案 1 :(得分:4)
将问题分解为组件(包)是很正常的,每个组件都支持不同的方面。如果你已经学会了Ada,那么首先编写软件包的规格是正常的,并且(或许与你自己)争论为什么这是正确的设计,然后实现它们。我认为,这在任何支持规范和实体的语言中都是正常的 - 例如,C。
我个人会在去的时候检查编辑,只是为了确保我没有做任何愚蠢的事情。
至于分离 - 一个(不是很好)的原因是减少杂乱,停止单位过长。另一个原因(对于我编写的代码生成器)是因为代码生成器不需要担心在UML模型中保留开发人员的手写代码;所有代码主体都是分开的。第三种可能是依赖于环境的实现(例如,Windows vs Unix),在这种情况下,您可以让编译器为每个环境看到不同版本的单独主体(尽管人们通常使用库包)。
编译器有自己的关于文件名的规则,以及可以编译的顺序。当GNAT看到
时procedure Foo is
procedure Bar is separate;
它希望在名为foo.adb
的文件中找到Foo的主体,在foo-bar.adb
中找到Bar的主体(我相信,你可以告诉它不同的是gnatmake
的包{{1} } - 但它可能不值得麻烦)。最好顺其自然;
Naming
很清楚。
您可以编译separate (Foo)
procedure Bar is
,这将进行全面分析并捕获代码中的几乎所有错误;但GNAT无法自行生成代码。相反,当您编译foo-bar.adb
时,它包含一个生成的目标文件中的所有单独的主体。这样做肯定不是错误。
使用GNAT,无需担心编译顺序,您可以按照自己喜欢的顺序进行编译。但最好使用foo.adb
并让计算机承受压力!
答案 2 :(得分:1)
您确实可以按照您描述的方式工作,当然除非所有包体都有某种实现,否则您的程序不会链接。出于这个原因,我认为编写虚拟包体并将所有过程实现为:
更为正常begin
null;
end;
所有功能都实现为:
begin
return The_Return_Type'first; --'
end;
至于分居......我不喜欢他们。对我来说,我更愿意遵循规则,即包的所有代码都在其包体中。如果由于某种原因例程 huge ,则分离是可以接受的,但在这种情况下,更好的解决方案几乎总是重构代码。所以每次看到一个,都是一个大红旗。
至于文件名的东西,这是一个gnat问题,而不是Ada问题。 Gnat为编译器采取了不寻常的立场,即文件内容的名称决定了文件本身必须命名的内容。世界上可能还有其他编译器可以做到这一点,但我还没有找到30年编码的编译器。