Ada 编程/泛型
代码重用理念源于构建大型软件系统的必要性,这些系统将成熟的构建块组合在一起。代码的可重用性提高了软件的生产力和质量。泛型单元是 Ada 语言支持这种特性的方式之一。泛型单元是子程序或包,它们根据在用户实例化它们之前未定义的类型和操作来定义算法。
注意 C++ 程序员:泛型单元类似于 C++ 模板。
例如,要定义一个用于交换任何(非受限)类型的变量的过程
generic
type
Element_Tis
private
; -- Generic formal type parameterprocedure
Swap (X, Y :in
out
Element_T);
procedure
Swap (X, Y :in
out
Element_T)is
Temporary :constant
Element_T := X;begin
X := Y; Y := Temporary;end
Swap;
Swap
子程序被称为泛型。子程序规范之前是泛型形式部分,由保留字generic
后面跟着可能为空的泛型形式参数列表。声明为泛型的实体不能直接使用,必须先实例化它们。
为了能够使用 Swap
,有必要为想要的类型创建一个实例。例如
procedure
Swap_Integersis
new
Swap (Integer);
现在,Swap_Integers
过程可以用于 Integer
类型的变量。
泛型过程可以为所有需要的类型实例化。它可以用不同的名称实例化,或者,如果在实例化中使用相同的标识符,则每个声明都将重载该过程
procedure
Instance_Swapis
new
Swap (Float);procedure
Instance_Swapis
new
Swap (Day_T);procedure
Instance_Swapis
new
Swap (Element_T => Stack_T);
类似地,泛型包可用于实现任何类型的堆栈
generic
Max: Positive;type
Element_Tis
private
;package
Generic_Stackis
procedure
Push (E: Element_T);function
Pop return Element_T;end
Generic_Stack;
package
body
Generic_Stackis
Stack:array
(1 .. Max)of
Element_T; Top : Integerrange
0 .. Max := 0; -- initialise to empty -- ...end
Generic_Stack;
可以通过这种方式定义给定大小和类型的堆栈
declare
package
Float_100_Stackis
new
Generic_Stack (100, Float);use
Float_100_Stack;begin
Push (45.8); -- ...end
;
泛型单元声明了 *泛型形式参数*,它们可以是
- 对象(模式为 *in* 或 *in out*,但绝不是 *out*)
- 类型
- 子程序
- 另一个指定泛型单元的实例。
在实例化泛型时,程序员为每个形式参数传递一个 *实际参数*。形式值和子程序可以有默认值,因此传递实际值是可选的。
模式为 *in* 的形式参数接受指定类型的所有值、常量或变量。实际参数被复制到泛型实例中,并在泛型内部充当常量;这意味着指定类型不能受限。可以指定一个默认值,例如
generic
Object :in
Natural := 0;
对于模式为 *in out*,实际参数必须是变量。
泛型形式对象的一个限制是它们从不被视为静态,即使实际参数恰好是静态的。如果该对象是一个数字,则不能使用它来创建新的类型。但是,它可以用于创建新的派生类型或子类型
generic
Size :in
Natural := 0;package
Pis
type
T1is
mod
Size; -- illegal!type
T2is
range
1 .. Size; -- illegal!type
T3is
new
Integerrange
1 .. Size; -- OKsubtype
T4is
Integerrange
1 .. Size; -- OKend
P;
形式对象是非静态的原因是允许编译器只为泛型生成一次目标代码,并让所有实例共享它,并将其实际对象的地址作为参数传递给它。这种编译器技术被称为 *共享泛型*。如果形式对象是静态的,编译器将不得不为每个实例生成目标代码的一个副本,其中包含该对象,这可能会导致目标代码大小急剧膨胀(*代码膨胀*)。
(注意 C++ 程序员:在 C++ 中,由于形式对象可以是静态的,因此编译器在一般情况下不能实现共享泛型;它必须检查泛型的整个主体,才能决定是否共享其目标代码。相反,Ada 泛型被设计成这样,即编译器可以在 *不查看其主体* 的情况下实例化泛型。)
语法允许程序员指定哪些类型类别可以作为实际参数。根据经验:语法表达了泛型如何看待类型,即它假设最坏的情况,而不是实例创建者如何看待类型。
formal_type_declaration ::=type
defining_identifier[discriminant_part]is
formal_type_definition; formal_type_definition ::= formal_private_type_definition | formal_derived_type_definition | formal_discrete_type_definition | formal_signed_integer_type_definition | formal_modular_type_definition | formal_floating_point_definition | formal_ordinary_fixed_point_definition | formal_decimal_fixed_point_definition | formal_array_type_definition | formal_access_type_definition | formal_interface_type_definition
这很复杂,因此下面给出一些示例。使用语法
声明的类型表示具有 *未知辨别符* 的类型。这是 Ada 语言中不定类型的术语,即不能在没有给出初始表达式的类型,例如具有没有默认值的辨别符的类型,另一个示例是非约束数组类型。type
T (<>)
泛型形式类型 | 可接受的实际类型 |
---|---|
|
任何类型。实际类型可以是 有限的 或不是,不定或确定,但 *泛型* 将其视为有限的和不定的,即不假定该类型可以使用赋值。 |
|
任何非受限类型:泛型知道可以为这种类型的变量赋值,但不能在没有初始值的情况下声明这种类型的对象。 |
|
任何非受限确定类型:泛型知道可以为这种类型的变量赋值,并且可以在没有初始值的情况下声明对象。 |
|
任何 标记类型,抽象或具体,有限或不是。 |
|
任何具体标记类型,有限或不是。 |
|
任何非受限标记类型,抽象或具体。 |
|
任何非受限、具体标记类型。 |
|
从 Parent 派生的任何类型。泛型知道 Parent 的操作,因此可以调用它们。T 和 Parent 都不能是抽象的。 |
|
从 Parent 派生的任何类型,抽象或具体,其中 Parent 是标记类型,因此对 T 的操作的调用可以动态调度。 |
|
从标记类型 Parent 派生的任何具体类型。 |
|
任何离散类型:整数、模 或 枚举。 |
|
任何带符号整数类型 |
|
任何模类型 |
|
任何(非十进制)定点类型 |
|
任何十进制定点类型 |
|
任何 浮点类型 |
|
任何 数组类型,其索引类型为 I ,元素类型为 E (I 和 E 也可以是形式参数) |
|
任何指向 O 类型对象的 访问类型(O 也可以是形式参数) |
在主体中,我们只能使用为形式参数的类型类别预定义的操作。也就是说,泛型规范是泛型实现者与实例化泛型单元的客户端之间的契约。这与其他语言(如 C++)的参数特性不同。
可以通过以下方式进一步限制可接受的实际类型集
泛型形式类型 | 可接受的实际类型 |
---|---|
... |
确定或不定类型(简单来说:有或没有辨别符的类型,但存在其他形式的不确定性) |
... |
具有 DT 类型辨别符的类型(也可以指定多个辨别符) |
... |
确定类型(简单来说,没有辨别符或具有具有默认值的辨别符的类型) |
可以将子程序作为参数传递给泛型。泛型指定一个泛型形式子程序,包含参数列表和返回类型(如果子程序是函数)。实际参数必须与该参数配置文件匹配,但参数的名称不必匹配。
这是一个将另一个子程序作为参数的泛型子程序的规范
generic
type
Element_Tis
private
;with
function
"*" (X, Y: Element_T)return
Element_T;function
Square (X : Element_T)return
Element_T;
这是泛型子程序的主体;它像调用任何其他子程序一样调用参数。
function
Square (X: Element_T)return
Element_Tis
begin
return
X * X; -- The formal operator "*".end
Square;
例如,这个泛型函数可以与矩阵一起使用,假设已经定义了矩阵乘积。
with
Square;with
Matrices;procedure
Matrix_Exampleis
function
Square_Matrixis
new
Square (Element_T => Matrices.Matrix_T, "*" => Matrices.Product); A : Matrices.Matrix_T := Matrices.Identity;begin
A := Square_Matrix (A);end
Matrix_Example;
generic
type
Element_Tis
private
;with
function
"*" (X, Y: Element_T)return
Element_Tis
<>;
这意味着,如果在实例化时,实际类型存在一个函数“*”,并且该函数是直接可见的,那么它将被默认用作实际子程序。
主要用途之一是传递需要的运算符。以下示例显示了这一点(请点击下载链接查看完整示例)
generic
type
Element_Typeis
private
; ...with
function
"<" (Left :in
Element_Type; Right :in
Element_Type)return
Booleanis
<>;procedure
Search (Elements :in
Array_Type; Search :in
Element_Type; Found :out
Boolean; Index :out
Index_Type'Base) ...
其他泛型包的泛型实例
[edit | edit source]泛型形式可以是包;它必须是泛型包的实例,以便泛型知道包导出的接口
generic
with
package
Pis
new
Q (<>);
这意味着实际参数必须是泛型包 Q 的实例。Q 后的方框表示我们不关心用于创建 P 的实际参数。可以指定确切的参数,也可以指定必须使用默认值,如下所示
generic
-- P1 must be an instance of Q with the specified actual parameters:with
package
P1is
new
Q (Param1 => X, Param2 => Y); -- P2 must be an instance of Q where the actuals are the defaults:with
package
P2is
new
Q;
可以指定一个默认参数、无参数或只指定一些参数。默认值用方框 (“ => <> ”) 表示,可以使用 “ others => <> ”) 表示 “ 对所有未提及的参数使用默认值”。当然,实际包必须与这些约束匹配。
泛型可以看到实际包的公共部分和泛型参数(以上示例中的 Param1 和 Param2)。
此功能允许程序员将任意复杂的类型作为参数传递给泛型单元,同时保持完整的类型安全性和封装。(需要示例)
包不能将自身列为泛型形式,因此无法进行泛型递归。以下操作是非法的
with
A;generic
with
package
Pis
new
A (<>);package
A; -- illegal: A references itself
事实上,这只是
with
A; -- illegal: A does not exist yet at this point!package
A;
的一个特例,这也是非法的,尽管 A 已经不再是泛型。
实例化泛型
[edit | edit source]要实例化泛型单元,请使用关键字 new
function
Square_Matrixis
new
Square (Element_T => Matrices.Matrix_T, "*" => Matrices.Product);
对 C++ 程序员特别感兴趣的注意事项
- 泛型形式类型完全定义了哪些类型可以作为实际参数;因此,编译器可以在不查看泛型主体的情况下实例化泛型。
- 每个实例都有一个名称,与所有其他实例不同。特别是,如果一个泛型包声明了一个类型,并且创建了该包的两个实例,那么即使实际参数相同,也将获得两个不同的、不兼容的类型。
- Ada 要求所有实例化必须是显式的。
- 无法创建泛型的特殊情况实例(在 C++ 中称为“模板特化”。
作为上述内容的结果,Ada 不允许模板元编程。但是,这种设计具有显著的优势
- 除非程序员要求内联子程序,否则所有泛型实例都可以共享目标代码;不存在代码膨胀的风险。
- 在阅读其他人编写的程序时,没有隐藏的实例化,也没有特殊情况需要担心。Ada 遵循最小惊奇原则。
高级泛型
[edit | edit source]泛型和嵌套
[edit | edit source]泛型单元可以嵌套在另一个单元中,该单元本身可能是泛型的。即使没有特殊的规则适用(只有关于泛型的正常规则和关于嵌套单元的规则),新手也可能感到困惑。理解泛型单元和泛型单元的实例之间的区别很重要。
示例 1. 嵌套在非泛型包中的泛型子程序。
package
Bag_Of_Stringsis
type
Bagis
private
;generic
with
procedure
Operator (S :in
out
String);procedure
Apply_To_All (B :in
out
Bag);private
-- omittedend
Bag_Of_Strings;
要使用 Apply_To_All,首先定义要应用于 Bag 中每个 String 的过程。然后,实例化 Apply_To_All,最后调用实例。
with
Bag_Of_Strings;procedure
Example_1is
procedure
Capitalize (S :in
out
String)is
separate
; -- omittedprocedure
Capitalize_Allis
new
Bag_Of_Strings.Apply_To_All (Operator => Capitalize); B : Bag_Of_Strings.Bag;begin
Capitalize_All (B);end
Example_1;
示例 2. 嵌套在泛型包中的泛型子程序
这与上面的示例相同,只是现在 Bag 本身是泛型的
generic
type
Element_Type (<>)is
private
;package
Generic_Bagis
type
Bagis
private
;generic
with
procedure
Operator (S :in
out
Element_Type);procedure
Apply_To_All (B :in
out
Bag);private
-- omittedend
Generic_Bag;
如您所见,泛型形式子程序 Operator 接受泛型形式类型 Element_Type 的参数。这是可以的:嵌套泛型可以看到其封闭单元中的所有内容。
您不能直接实例化 Generic_Bag.Apply_To_All,因此必须首先创建 Generic_Bag 的实例,例如 Bag_Of_Strings,然后实例化 Bag_Of_Strings.Apply_To_All。
with
Generic_Bag;procedure
Example_2is
procedure
Capitalize (S :in
out
String)is
separate
; -- omittedpackage
Bag_Of_Stringsis
new
Generic_Bag (Element_Type => String);procedure
Capitalize_Allis
new
Bag_Of_Strings.Apply_To_All (Operator => Capitalize); B : Bag_Of_Strings.Bag;begin
Capitalize_All (B);end
Example_2;
泛型和子单元
[edit | edit source]示例 3. 作为非泛型单元的子单元的泛型单元。
泛型子单元的每个实例都是父单元的子单元,因此它可以看到父单元的公共部分和私有部分。
package
Bag_Of_Stringsis
type
Bagis
private
;private
-- omittedend
Bag_Of_Strings;generic
with
procedure
Operator (S :in
out
String);procedure
Bag_Of_Strings.Apply_To_All (B :in
out
Bag);
与示例 1 的区别在于
- Bag_Of_Strings.Apply_To_All 是单独编译的。特别是,Bag_Of_Strings.Apply_To_All 可能由没有访问 Bag_Of_Strings 源文本的人员编写。
- 在使用 Bag_Of_Strings.Apply_To_All 之前,必须显式地 with 它;with 父单元 Bag_Of_Strings 不足以。
- 如果不使用 Bag_Of_Strings.Apply_To_All,程序中将不包含其目标代码。
- 由于 Bag_Of_Strings.Apply_To_All 位于库级别,因此它可以声明受控类型;嵌套包在 Ada 95 中无法做到这一点。在 Ada 2005 中,可以在任何级别声明受控类型。
with
Bag_Of_Strings.Apply_To_All; -- implicitly withs Bag_Of_Strings, tooprocedure
Example_3is
procedure
Capitalize (S :in
out
String)is
separate
; -- omittedprocedure
Capitalize_Allis
new
Bag_Of_Strings.Apply_To_All (Operator => Capitalize); B : Bag_Of_Strings.Bag;begin
Capitalize_All (B);end
Example_3;
示例 4. 作为泛型单元的子单元的泛型单元
这与示例 3 相同,只是现在 Bag 也是泛型的。
generic
type
Element_Type (<>)is
private
;package
Generic_Bagis
type
Bagis
private
;private
-- omittedend
Generic_Bag;generic
with
procedure
Operator (S :in
out
Element_Type);procedure
Generic_Bag.Apply_To_All (B :in
out
Bag);with
Generic_Bag.Apply_To_All;procedure
Example_4is
procedure
Capitalize (S :in
out
String)is
separate
; -- omittedpackage
Bag_Of_Stringsis
new
Generic_Bag (Element_Type => String);procedure
Capitalize_Allis
new
Bag_Of_Strings.Apply_To_All (Operator => Capitalize); B : Bag_Of_Strings.Bag;begin
Capitalize_All (B);end
Example_4;
示例 5. 无参数泛型子单元
泛型单元的子单元 必须 是泛型的,无论如何。仔细想想,这是非常合乎逻辑的:子单元可以看到其父单元的公共部分和私有部分,包括在父单元中声明的变量。如果父单元是泛型的,那么子单元应该看到哪个实例?答案是,子单元必须是父单元的单个实例的子单元,因此子单元也必须是泛型的。
generic
type
Element_Type (<>)is
private
;type
Hash_Typeis
(<>);with
function
Hash_Function (E : Element_Type)return
Hash_Type;package
Generic_Hash_Mapis
type
Mapis
private
;private
-- omittedend
Generic_Hash_Map;
假设我们想要 Generic_Hash_Map 的一个子单元,它可以将映射序列化到磁盘;为此,它需要按哈希值对映射进行排序。这很容易做到,因为我们知道 Hash_Type 是离散类型,因此它具有小于运算符。执行序列化的子单元不需要任何额外的泛型参数,但它必须是泛型的,因此它可以看到其父单元的泛型参数、公共部分和私有部分。
generic
package
Generic_Hash_Map.Serializeris
procedure
Dump (Item :in
Map; To_File :in
String);procedure
Restore (Item :out
Map; From_File :in
String);end
Generic_Hash_Map.Serializer;
要将映射读写到磁盘,首先要创建 Generic_Hash_Map 的实例,例如 Map_Of_Unbounded_Strings,然后创建 Map_Of_Unbounded_Strings.Serializer 的实例
with
Ada.Strings.Unbounded;with
Generic_Hash_Map.Serializer;procedure
Example_5is
use
Ada.Strings.Unbounded;function
Hash (S :in
Unbounded_String)return
Integeris
separate
; -- omittedpackage
Map_Of_Unbounded_Stringsis
new
Generic_Hash_Map (Element_Type => Unbounded_String, Hash_Type => Integer, Hash_Function => Hash);package
Serializeris
new
Map_Of_Unbounded_Strings.Serializer; M : Map_Of_Unbounded_Strings.Map;begin
Serializer.Restore (Item => M, From_File => "map.dat");end
Example_5;
另请参阅
[edit | edit source]维基教科书
[edit | edit source]- Ada 编程
- Ada 编程/面向对象:带标记类型在 Ada 中提供了其他多态性手段。