跳轉到內容

Ada 程式設計/面向物件

來自 Wikibooks,開放世界中的開放書籍

Ada. Time-tested, safe and secure.
Ada。經久耐用、安全可靠。

Ada 中的面向物件

[編輯 | 編輯原始碼]

面向物件程式設計是指以“物件”為單位構建軟體。一個“物件”包含資料並具有行為。資料通常由常量和變數組成,如本書其他部分所述,但也可以在程式之外,例如磁碟或網路上。行為由對資料進行操作的子程式組成。與程序式程式設計相比,面向物件程式設計的獨特之處不在於單個特性,而是幾個特性的組合。

  • 封裝,即能夠將物件的實現與其介面分離;這反過來又將物件的“客戶端”(只能以某些預定義方式使用物件)與物件的內部(對外部客戶端一無所知)分離。
  • 繼承,一種型別的物件能夠繼承另一種型別的物件的資料和行為(子程式),而無需打破封裝;
  • 型別擴充套件,物件能夠在繼承的物件的基礎上新增新的資料元件和新的子程式,並用自己的版本替換一些繼承的子程式;這稱為覆蓋
  • 多型性,"客戶端"能夠在不知道物件的確切型別的情況下使用物件的服務,即以抽象的方式。實際上,在執行時,實際物件在每次呼叫時可能具有不同的型別。

任何語言都可以進行面向物件程式設計,即使是組合語言。然而,如果沒有語言支援,型別擴充套件和多型性很難實現。

在 Ada 中,每個概念都有一個匹配的構造;這就是 Ada 直接支援面向物件程式設計的原因。

  • 包提供封裝;
  • 派生型別提供繼承;
  • 記錄擴充套件(如下所述)提供型別擴充套件;
  • 類範圍型別(如下所述)提供多型性。

Ada 從第一個版本(1980 年的 MIL-STD-1815)開始就擁有封裝和派生型別,這導致一些人以非常狹義的意義將該語言歸類為“面向物件”的。記錄擴充套件和類範圍型別是在 Ada 95 中新增的。Ada 2005 進一步增加了介面。本章的其餘部分將涵蓋這些方面。

最簡單的物件:單例

[編輯 | 編輯原始碼]
package Directory is
  function Present (Name_Pattern: String) return Boolean;
  generic
     with procedure Visit (Full_Name, Phone_Number, Address: String;
                           Stop: out Boolean);
  procedure Iterate (Name_Pattern: String);
end Directory;

目錄是一個物件,包含資料(電話號碼和地址,可能儲存在外部檔案或資料庫中)和行為(它可以查詢條目並遍歷與 Name_Pattern 匹配的所有條目,對每個條目呼叫 Visit)。

一個簡單的包提供封裝(目錄的內部機制被隱藏),一對子程式提供行為。

這種模式適用於只允許存在一個特定型別物件的場景;因此,不需要型別擴充套件或多型性。

基本操作

[編輯 | 編輯原始碼]

在 Ada 中,方法通常被稱為帶標記型別的基本子程式或等效術語帶標記型別的基本操作。型別的基本操作是始終在使用型別的地方可用的操作。對於面向物件程式設計中使用的帶標記型別,它們也可以被派生型別繼承和覆蓋,並且可以動態分派。

型別的基本操作需要在與型別相同的包中宣告(不能在巢狀包或子包中)。對於帶標記型別,在型別凍結點之前,還需要宣告新的基本操作和對繼承的基本操作的覆蓋。在凍結點之後宣告的任何子程式都不會被視為基本程式,因此不能被繼承,也不會進行動態分派。凍結點將在下面更詳細地討論,但將所有基本操作宣告在初始型別宣告之後,這個簡單的做法將確保這些子程式確實被識別為基本程式。

型別 T 的基本操作至少需要有一個型別為Taccess T的引數。雖然大多數面嚮物件語言會自動提供thisself指標,但 Ada 要求顯式宣告一個形式引數來接收當前物件。該引數通常是引數列表中的第一個引數,它允許object.subprogram呼叫語法(從 Ada 2005 開始可用),但它可以位於任何引數位置。帶標記型別始終按引用傳遞;引數傳遞方式與引數模式inout無關,這些模式描述了資料流。對於Taccess T,引數傳遞方式相同。

對於帶標記型別,引數列表中不能使用其他直接可分派型別,因為 Ada 不提供多重分派。以下示例是非法的。

package P is
   type A is tagged private;
   type B is tagged private;
   procedure Proc (This: B; That: A); -- illegal: can't dispatch on both A and B
end P;

當需要傳遞額外的可分派物件時,引數列表應使用它們的類範圍型別T'Class來宣告它們。例如

package P is
   type A is tagged private;
   type B is tagged private;
   procedure Proc (This: B; That: A'Class); -- dispatching only on B
end P;

但是,這並不限制相同帶標記型別引數的數量。例如,以下定義是合法的。

package P is
   type A is tagged private;
   procedure Proc (This, That: A); -- dispatching only on A
end P;

帶標記型別的基本操作是分派操作。對這種基本操作的呼叫實際上是分派呼叫還是靜態繫結,取決於上下文(見下文)。請注意,在分派呼叫中,最後一個示例的兩個實際引數必須具有相同的標記(即相同的型別);如果標記檢查失敗,將呼叫 Constraint_Error。

派生型別

[編輯 | 編輯原始碼]

型別派生一直是 Ada 的核心部分。

package P is
  type T is private;
  function Create (Data: Boolean) return T;  -- primitive
  procedure Work (Object : in out T);        -- primitive
  procedure Work (Pointer: access T);        -- primitive
  type Acc_T is access T;
  procedure Proc (Pointer: Acc_T);           -- not primitive
private
  type T is record
    Data: Boolean;
  end record;
end P;

上面的示例建立了一個包含資料(這裡只是一個布林值,但可以是任何東西)和行為的型別 T,行為包括一些子程式。它還透過將型別 T 的詳細資訊放在包的 private 部分來演示封裝。

T 的基本操作是函式 Create、過載過程 Work 和預定義的“=”運算子;Proc 不是基本程式,因為它使用 T 的訪問型別作為引數——不要將此與訪問引數混淆,如第二個過程 Work 中使用的那樣。從 T 派生時,會繼承基本操作。

with P;
package Q is
  type Derived is new P.T;
end Q;

型別 Q.Derived 具有與 P.T 相同的資料以及相同行為;它繼承了資料和子程式。因此,可以編寫以下程式碼

with Q;
procedure Main is
  Object: Q.Derived := Q.Create (Data => False);
begin
  Q.Work (Object);
end Main;

繼承的操作可以被覆蓋,也可以新增新的操作,但規則(Ada 83)不幸地與帶標記型別(Ada 95)的規則有所不同。

誠然,編寫此程式碼的原因可能看起來很模糊。這種程式碼的目的是擁有型別 P.T 和 Q.Derived 的物件,它們是不相容的

Ob1: P.T;
Ob2: Q.Derived;
Ob1 := Ob2;              -- illegal
Ob1 := P.T (Ob2);        -- but convertible
Ob2 := Q.Derived (Ob1);  -- in both directions

這種特性並不經常使用(例如,用於宣告反映物理維度的型別),但我在這裡介紹它,以便引入下一步:型別擴充套件。

型別擴充套件

[編輯 | 編輯原始碼]

型別擴充套件是 Ada 95 的一個修正。

帶標籤型別支援動態多型和型別擴充套件。帶標籤型別包含一個隱藏的標籤,在執行時識別型別。除了標籤之外,帶標籤記錄就像任何其他記錄一樣,因此它可以包含任意資料。

package Person is
   type Object is tagged
     record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
     end record;
   procedure Put (O : Object);
end Person;

如您所見,Person.Object 在某種意義上是一個物件,因為它具有資料和行為(過程 Put)。但是,此物件不會隱藏其資料;任何具有 with Person 子句的程式單元都可以直接讀取和寫入 Person.Object 中的資料。這破壞了封裝,也說明了 Ada 完全將封裝型別的概念分開。以下是一個封裝了其資料的 Person.Object 版本

package Person is
   type Object is tagged private;
   procedure Put (O : Object);
private
   type Object is tagged
     record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
     end record;
end Person;

因為型別 Person.Object 帶有標籤,所以可以建立記錄擴充套件,它是一個具有額外資料的派生型別。

with Person;
package Programmer is
   type Object is new Person.Object with private;
private
   type Object is new Person.Object with
     record
        Skilled_In : Language_List;
     end record;
end Programmer;

型別 Programmer.Object 繼承了 Person.Object 的資料和行為,即型別的基本操作;因此可以編寫

with Programmer;
procedure Main is
   Me : Programmer.Object;
begin
   Programmer.Put (Me);
   Me.Put; -- equivalent to the above, Ada 2005 only
end Main;

因此,型別 Programmer.Object 作為 Person.Object 的記錄擴充套件的宣告,隱式聲明瞭一個 procedure Put,它適用於 Programmer.Object

與無標籤型別一樣,Person 和 Programmer 型別的物件是可轉換的。但是,在無標籤物件可以雙向轉換的情況下,帶標籤型別的轉換僅適用於根方向。(遠離根的轉換將必須憑空新增元件。)這種轉換稱為檢視轉換,因為元件不會丟失,它們只是變得不可見。

如果您要離開根,則必須使用擴充套件聚合。

現在我們已經引入了帶標籤型別、記錄擴充套件和基本操作,就能夠理解覆蓋了。在上面的示例中,我們引入了一個名為 Person.Object 的型別,它有一個名為 Put 的基本操作。以下是包的主體

with Ada.Text_IO;
package body Person is
   procedure Put (O : Object) is
   begin
      Ada.Text_IO.Put (O.Name);
      Ada.Text_IO.Put (" is a ");
      Ada.Text_IO.Put_Line (Gender_Type'Image (O.Gender));
   end Put;
end Person;

如您所見,此簡單操作會將記錄型別的兩個資料元件都列印到標準輸出。現在,請記住記錄擴充套件 Programmer.Object 具有一個額外的資料成員。如果我們編寫

with Programmer;
procedure Main is
   Me : Programmer.Object;
begin
   Programmer.Put (Me);
   Me.Put; -- equivalent to the above, Ada 2005 only
end Main;

那麼程式將呼叫繼承的基本操作 Put,它將列印姓名和性別但不會列印額外資料。為了提供此額外行為,我們必須覆蓋繼承的過程 Put,如下所示

with Person;
package Programmer is
   type Object is new Person.Object with private;
   overriding -- Optional keyword, new in Ada 2005
   procedure Put (O : Object);
private
   type Object is new Person.Object with
     record
        Skilled_In : Language_List;
     end record;
end Programmer;
package body Programmer is
   procedure Put (O : Object) is
   begin
      Person.Put (Person.Object (O)); -- view conversion to the ancestor type
      Put (O.Skilled_In); -- presumably declared in the same package as Language_List
   end Put;
end Programmer;

Programmer.Put 覆蓋Person.Put;換句話說,它完全替換了它。由於目的是擴充套件行為而不是替換行為,Programmer.PutPerson.Put 作為其行為的一部分呼叫。它透過將引數從型別 Programmer.Object 轉換為其祖先型別 Person.Object 來實現這一點。此結構是一個檢視轉換;與普通型別轉換相反,它不會建立新物件,不會產生任何執行時成本(實際上,如果這種檢視轉換的運算元實際上是一個變數,則結果可以在需要輸出引數時使用(例如過程呼叫)。當然,覆蓋操作是否呼叫其祖先是可以選擇的;在某些情況下,目的是確實要替換,而不是擴充套件繼承的行為。

(請注意,對於無標籤型別,也可以覆蓋繼承的操作。之所以在這裡討論它,是因為無標籤型別的派生很少見。)

多型、類範圍程式設計和動態分派

[編輯 | 編輯原始碼]

面向物件技術的全部力量是透過多型、類範圍程式設計和動態分派實現的,它們是同一概念的不同說法。為了解釋此概念,讓我們擴充套件前面部分的示例,在該示例中,我們聲明瞭一個名為 Person.Object 的基本帶標籤型別,它有一個名為 Put 的基本操作,以及一個名為 Programmer.Object 的記錄擴充套件,它具有額外資料和一個覆蓋的基本操作 Put

現在,讓我們想象一個包含多個人的集合。在該集合中,有些人是程式設計師。我們想要遍歷集合,並對每個人呼叫 Put。當被考慮的人是程式設計師時,我們想要呼叫 Programmer.Put;當該人不是程式設計師時,我們想要呼叫 Person.Put。從本質上講,這就是多型、類範圍程式設計和動態分派。

有了 Ada 的強型別,普通呼叫無法進行動態分派;對宣告型別上的操作的呼叫必須始終靜態繫結到該特定型別定義的操作。動態分派(在 Ada 術語中被稱為分派)是透過單獨的類範圍型別提供的,這些型別是多型的。每個帶標籤型別(例如 Person.Object)都有一個相應的型別類,它是由 Person.Object 本身以及擴充套件 Person.Object 的所有型別組成的型別集。在我們的示例中,此類包含兩種型別

  • Person.Object
  • Programmer.Object

Ada 95 定義了 Person.Object'Class 屬性來表示相應的類範圍型別。換句話說

declare
   Someone : Person.Object'Class := ...; -- to be expanded later
begin
   Someone.Put; -- dynamic dispatching
end;

Someone 的宣告表示一個可能是任何型別的物件,Person.ObjectProgrammer.Object。因此,對基本操作 Put 的呼叫將動態分派到 Person.PutProgrammer.Put

唯一的問題是,由於我們不知道 Someone 是否是程式設計師,所以我們也不知道 Someone 擁有多少個數據元件,因此我們也不知道 Someone 在記憶體中佔用了多少位元組。出於這個原因,類範圍型別 Person.Object'Class不定的。在不指定任何約束的情況下,不可能宣告此型別的物件。但是,可以

  • 宣告一個具有初始值的類範圍物件(如上)。然後,該物件會受到其初始值的約束。
  • 宣告對該物件的訪問值(因為訪問值具有已知大小);
  • 將類範圍型別的物件作為引數傳遞給子程式
  • 將特定型別的物件(特別是函式呼叫的結果)分配給類範圍型別的變數。

有了這些知識,我們現在可以構建一個包含多個人的多型集合;在這個例子中,我們將簡單地建立一個包含對人的訪問值的陣列

with Person;
procedure Main is
   type Person_Access is access Person.Object'Class;
   type Array_Of_Persons  is array (Positive range <>) of Person_Access;

   function Read_From_Disk return Array_Of_Persons is separate;

   Everyone : constant Array_Of_Persons := Read_From_Disk;
begin -- Main
   for K in Everyone'Range loop
      Everyone (K).all.Put; -- dereference followed by dynamic dispatching
   end loop;
end Main;

上面的過程實現了我們想要的目標:它遍歷 Persons 陣列,並呼叫適合每個人的過程 Put

高階主題:動態分派的工作原理

[編輯 | 編輯原始碼]

您不需要了解動態分派的工作原理就能有效地使用它,但如果您好奇,以下是一個解釋。

記憶體中每個物件的第一個元件是標籤;這就是為什麼物件是帶標籤型別而不是普通記錄的原因。標籤實際上是對錶的訪問值;每個特定型別都有一個表。表包含對該型別每個基本操作的訪問值。在我們的示例中,由於存在兩種型別 Person.ObjectProgrammer.Object,因此存在兩個表,每個表包含一個訪問值。Person.Object 的表包含對 Person.Put 的訪問值,而 Programmer.Object 的表包含對 Programmer.Put 的訪問值。當您編譯程式時,編譯器會構建這兩個表並將它們放在程式可執行程式碼中。

程式每次建立特定型別的新物件時,都會自動將其標籤設定為指向相應的表。

程式每次執行基本操作的分派呼叫時,編譯器都會插入以下物件程式碼

  • 取消對標籤的引用以查詢當前物件特定型別的基本操作表
  • 取消對基本操作訪問值的引用
  • 呼叫基本操作。

相反,當程式執行引數為對祖先型別的檢視轉換的呼叫時,編譯器會在編譯時而不是執行時執行這兩個取消引用操作:此類呼叫是靜態繫結的;編譯器會發出直接呼叫檢視轉換中指定的祖先型別基本操作的程式碼。

重新分派

[編輯 | 編輯原始碼]

分派由物件的(隱藏)標籤控制。那麼,當基本操作 Op1 對同一個物件呼叫另一個基本操作 Op2 時會發生什麼呢?

 type Root is tagged private;
 procedure Op1 (This: Root);
 procedure Op2 (This: Root);

 type Derived is new Root with private;
 -- Derived inherits Op1
 overriding procedure Op2 (This: Derived);

 procedure Op1 (This: Root) is
 begin
   ...
   Op2 (This);               -- not redispatching
   Op2 (Root'Class (This));  -- redispatching
   This.Op2;                 -- not redispatching (new syntax since Ada 2005)
   (Root'Class (This)).Op2;  -- redispatching (new syntax since Ada 2005)
   ...
 end Op1;

 D: Derived;
 C: Root'Class := D;

 Op1 (D);  -- statically bound call
 Op1 (C);  -- dispatching call
 D.Op1;    -- statically bound call (new syntax since Ada 2005)
 C.Op1;    -- dispatching call (new syntax since Ada 2005)

在此片段中,Op1 沒有被覆蓋,而 Op2 被覆蓋了。Op1 的主體呼叫 Op2,那麼如果 Op1 被用於 Derived 型別的物件呼叫,將會呼叫哪個 Op2 呢?

分派的規則仍然適用。對 Op2 的呼叫將在使用類範圍型別的物件呼叫時進行分派。

操作的正式引數列表指定了This的型別為一個特定型別,而不是類範圍型別。事實上,該引數必須是特定型別,以便在該型別的物件上分派操作,並允許操作的程式碼訪問與該型別相關聯的任何附加資料項。如果您希望重新分派,則必須透過將特定型別的引數再次轉換為類範圍型別來顯式宣告。 (記住:檢視轉換永遠不會丟失元件,它們只是隱藏它們。轉換為類範圍型別可以再次取消隱藏它們。)第一個呼叫Op1 (D)(靜態繫結,即不分派)執行繼承的Op1 - 並在Op1中,對Op2的第一次呼叫也靜態繫結(沒有重新分派),因為引數This是到特定型別Root的檢視轉換。但是,第二次呼叫是分派,因為引數This被轉換為類範圍型別。該呼叫分派到重寫Op2

由於傳統的This.Op2呼叫不是分派,因此即使物件本身是Derived型別,並且Op2操作被重寫,該呼叫也會呼叫Root.Op2這與其他面嚮物件語言的行為有很大不同。在其他面嚮物件語言中,方法要麼是分派的,要麼不是。在 Ada 中,操作要麼可用於分派,要麼不可用。對於給定呼叫是否實際使用分派取決於在該呼叫點指定物件型別的方式。對於習慣於其他面嚮物件語言的程式設計師來說,來自可分派操作對同一物件上的其他操作的呼叫預設情況下(動態)分派可能會讓人感到意外。

如果所有操作都已被重寫,則不重新分派預設值不會成為問題,因為它們都將對預期的物件型別進行操作。但是,在為將來可能被另一種型別擴充套件的型別編寫程式碼時,它會產生影響。如果新型別沒有覆蓋所有呼叫其他基本操作的基本操作,則新型別可能無法按預期工作。最安全的策略是對物件使用類範圍轉換以強制分派呼叫。實現此目標的一種方法是在每個分派方法中定義一個類範圍常量

 procedure Op2 (This: Derived) is
   This_Class: constant Root'Class := This;
 begin

This用於訪問資料項並進行任何非分派呼叫。This_Class用於進行分派呼叫。

不太常見,或許不那麼令人驚訝的是,來自針對標記型別的不可分派(類範圍)例程對同一物件上的其他例程的呼叫預設情況下是分派的

 type Root is tagged private;
 procedure Op1 (This: Root'Class);
 procedure Op2 (This: Root);

 type Derived is new Root with private;
 -- Derived does not inherit Op1, rather Op1 is applicable to Derived.
 overriding procedure Op2 (This: Derived);

 procedure Op1 (This: Root'Class) is
 begin
   ...
   Op2 (This);               -- dispatching
   Op2 (Root (This));        -- static call
   This.Op2;                 -- dispatching (new syntax since Ada 2005)
   (Root (This)).Op2;        -- static call (new syntax since Ada 2005)
   ...
 end Op1;

 D: Derived;
 C: Root'Class := D;

 Op1 (D);  -- static call
 Op1 (C);  -- static call
 D.Op1;    -- static call (new syntax since Ada 2005)
 C.Op1;    -- static call (new syntax since Ada 2005)

請注意,對Op1的呼叫始終是靜態的,因為Op1沒有被繼承。它的引數型別是類範圍型別,因此該操作適用於從 Root 派生的所有型別。(Op2 在分派表中為從Root派生的每個型別都有一項條目。Op1沒有這樣的分派表;相反,所有型別只有一個這樣的操作。)

來自Op1的正常呼叫是分派的,因為This的宣告型別是類範圍型別。分派的預設值通常不會造成麻煩,因為類範圍操作通常用於執行涉及對一個或多個分派操作的呼叫的指令碼。

執行時型別識別

[edit | edit source]

執行時型別識別允許程式在執行時(間接或直接)查詢物件的標記以確定該物件屬於哪個型別。此功能顯然只有在多型性和動態分派的情況下才有意義,因此僅適用於標記型別。

您可以透過成員測試來確定物件是否屬於某個型別類或特定型別in,例如

type Base    is tagged private;
type Derived is new Base    with private;
type Leaf    is new Derived with private;

...
procedure Explicit_Dispatch (This : in Base'Class) is
begin
   if This in Leaf then ... end if;
   if This in Derived'Class then ... end if;
end Explicit_Dispatch;

由於 Ada 的強型別規則,執行時型別識別實際上很少需要;類範圍型別和特定型別之間的區別通常允許程式設計師確保物件是適當的型別,而無需使用此功能。

此外,參考手冊定義了package Ada.Tags(RM 3.9(6/2))、屬性'Tag(RM 3.9(16,18))和function Ada.Tags.Generic_Dispatching_Constructor(RM 3.9(18.2/2)),它們使直接操作標記成為可能。

建立物件

[edit | edit source]

語言參考手冊中有關3.3:物件和命名數字 [註釋]的部分說明了何時建立物件以及何時再次銷燬物件。本小節說明了如何建立物件。

LRM 部分開頭寫著:

物件在執行時建立,幷包含給定型別的值。物件可以在細化宣告、評估分配器、聚合或函式呼叫時建立和初始化.

例如,假設一個典型的面向物件型別層次結構:一個頂層型別Person、一個從Person派生的Programmer型別,以及可能更多的其他人型別。每個人都有一個名字;假設Person物件具有一個Name元件。同樣,每個人都有一個Gender元件。Programmer型別繼承了Person型別的元件和操作,因此Programmer物件也具有一個Name和一個Gender元件。Programmer物件可能具有特定於程式設計師的附加元件。

標記型別的物件與任何型別的物件以相同的方式建立。LRM 的第二個句子說,例如,當您宣告一個型別變數或常量時,將建立一個物件。對於標記型別Person

declare
   P: Person;
begin
   Text_IO.Put_Line("The name is " & P.Name);
end;

到目前為止沒有特殊之處。就像任何普通的變數宣告一樣,這個 O-O 變數被細化了。細化結果是一個名為P的型別為Person的物件。但是,P只有預設的姓名和性別值元件。這些可能不是有用的值。為物件元件提供初始值的一種方法是分配一個聚合。

declare
   P: Person := (Name => "Scorsese", Gender => Male);
begin
   Text_IO.Put_Line("The name is " & P.Name);
end;

:=後的括號表示式稱為聚合4.3:聚合 [註釋])。

LRM 段落中提到的另一種建立物件的 方法是呼叫函式。將建立一個物件作為函式呼叫的返回值。因此,我們可以呼叫一個返回物件的函式,而不是使用初始值的聚合。

引入適當的 O-O 資訊隱藏,我們更改包含Person型別的包,以便Person成為一個私有型別。為了使包的客戶端能夠構造Person物件,我們宣告一個返回它們的函式。(該函式可能會對物件執行一些有趣的構造工作。例如,上面的聚合很可能會根據提供的姓名字串引發 Constraint_Error 異常;該函式可以對姓名進行混淆,使其與元件的宣告相匹配。)我們還宣告一個返回Person物件姓名的函式。

package Persons is

   type Person is tagged private;

   function Make (Name: String; Sex: Gender_Type) return Person;

   function Name (P: Person) return String;

private
   type Person is tagged
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Persons;

呼叫Make函式會產生一個可用於初始化的物件。由於Person型別是私有的,因此我們不能再引用PName元件。但是,有一個對應的函式Name被宣告為型別Person,使其成為所謂的原始操作。(此示例中的元件和函式都名為Name 但是,如果需要,我們可以為兩者選擇不同的名稱。)

declare
   P: Person := Make (Name => "Orwell", Sex => Male);
begin
   Text_IO.Put_Line("The name is " & Name(P));
end;

物件可以複製到另一個物件中。目標物件首先被銷燬。然後,源物件的元件值被分配給目標物件的相應元件。在以下示例中,預設初始化的P獲得從Make呼叫建立的物件之一的副本。

declare
   P: Person;
begin
   if 2001 > 1984 then
      P := Make (Name => "Kubrick", Sex => Male);
   else
      P := Make (Name => "Orwell", Sex => Male);
   end if;

   Text_IO.Put_Line("The name is " & Name(P));
end;

到目前為止,還沒有提到從Person派生的Programmer型別。還沒有多型性,同樣,初始化也沒有提到繼承。在處理Programmer物件及其初始化之前,有必要簡單介紹一下類範圍型別。

關於基本操作的更多詳細資訊

[edit | edit source]

請記住我們之前對"基本操作" 的說法。基本操作是

  • 接受標記型別引數的子程式;
  • 返回標記型別物件的函式;
  • 接受指向標記型別的匿名訪問型別的子程式;
  • 僅在 Ada 2005 中,返回指向標記型別的匿名訪問型別的函式;

此外,基本操作必須在型別凍結之前宣告(凍結的概念將在後面解釋)

示例

package X is
   type Object is tagged null record;

   procedure Primitive_1 (This : in     Object);
   procedure Primitive_2 (That :    out Object);
   procedure Primitive_3 (Me   : in out Object);
   procedure Primitive_4 (Them : access Object);
   function  Primitive_5 return Object;
   function  Primitive_6 (Everyone : Boolean) return access Object;
end X;

所有這些子程式都是基本操作。

基本操作還可以接受相同型別或其他型別的引數;此外,控制運算元不必是第一個引數

package X is
   type Object is tagged null record;

   procedure Primitive_1 (This : in Object; Number : in Integer);
   procedure Primitive_2 (You  : in Boolean; That : out Object);
   procedure Primitive_3 (Me, Her : in out Object);
end X;

基本操作的定義明確排除了命名訪問型別和類範圍型別,以及不在同一宣告區域中立即定義的操作。反例

package X is
   type Object is tagged null record;
   type Object_Access is access Object;
   type Object_Class_Access is access Object'Class;

   procedure Not_Primitive_1 (This : in     Object'Class);
   procedure Not_Primitive_2 (This : in out Object_Access);
   procedure Not_Primitive_3 (This :    out Object_Class_Access);
   function  Not_Primitive_4 return Object'Class;

   package Inner is
       procedure Not_Primitive_5 (This : in Object);
   end Inner;
end X;

高階主題:凍結規則

[edit | edit source]

凍結規則(ARM 13.14)可能是 Ada 語言定義中最複雜的部分;這是因為該標準試圖儘可能明確地描述凍結。此外,語言定義的這一部分涉及所有實體的凍結,包括複雜的場景,如泛型和透過取消引用訪問值訪問的物件。但是,如果您瞭解動態分派的工作原理,您可以直觀地瞭解標記型別的凍結。在那一節中,我們看到編譯器為每個標記型別發出一個基本操作表。程式文字中發生此事件的點是標記型別凍結的點,即表變得完整的點。型別凍結後,就不能再向其中新增基本操作。

此點是以下最早的點:

  • 宣告標記型別的包規範的末尾
  • 從標記型別派生的第一個型別的出現

示例

package X is

  type Object is tagged null record;
  procedure Primitive_1 (This: in Object);

  -- this declaration freezes Object
  type Derived is new Object with null record;

  -- illegal: declared after Object is frozen
  procedure Primitive_2 (This: in Object);

end X;

直觀地:在宣告 Derived 的時候,編譯器開始為派生型別建立一個新的基本操作表。最初,此新表等於父型別Object的基本操作表。因此,Object必須凍結。

  • 標記型別變數的宣告

示例

package X is

  type Object is tagged null record;
  procedure Primitive_1 (This: in Object);

  V: Object;  -- this declaration freezes Object

  -- illegal: Primitive operation declared after Object is frozen
  procedure Primitive_2 (This: in Object);

end X;

直觀地:在宣告V之後,就可以在V上呼叫該型別的任何基本操作。因此,基本操作列表必須已知且完整,即凍結。

  • 帶標記型別的常量的完成(不是宣告,如果有的話)
package X is

  type Object is tagged null record;
  procedure Primitive_1 (This: in Object);

  -- this declaration does NOT freeze Object
  Deferred_Constant: constant Object;

  procedure Primitive_2 (This : in Object); -- OK

private

  -- only the completion freezes Object
  Deferred_Constant: constant Object := (null record);

  -- illegal: declared after Object is frozen
  procedure Primitive_3 (This: in Object);

 end X;

Ada 2005 的新特性

[edit | edit source]

此語言特性僅從 Ada 2005 開始可用。

Ada 2005 添加了覆蓋指示器,允許在更多地方使用匿名訪問型別,並提供 object.method 表示法。

覆蓋指示器

[edit | edit source]

新關鍵字overriding 可用於指示操作是否覆蓋繼承的子程式。由於與 Ada 95 的向上相容性,它的使用是可選的。例如

package X is
    type Object is tagged null record;

   function  Primitive return access Object; -- new in Ada 2005

   type Derived_Object is new Object with null record;

   not overriding -- new optional keywords in Ada 2005
   procedure Primitive (This : in Derived_Object); -- new primitive operation

   overriding
   function  Primitive return access Derived_Object;
end X;

編譯器將檢查所需的行為。

這是一個良好的程式設計實踐,因為它可以避免一些討厭的錯誤,例如由於程式設計師拼寫識別符號錯誤,或者由於後來在父型別中添加了新的引數而導致沒有覆蓋繼承的子程式。

它也可以用於抽象操作、重新命名或例項化泛型子程式

not overriding
procedure Primitive_X (This : in Object) is abstract;

overriding
function  Primitive_Y return Object renames Some_Other_Subprogram;

not overriding
procedure Primitive_Z (This : out Object)
      is new Generic_Procedure (Element => Integer);

Object.Method 表示法

[edit | edit source]

我們已經看到了這種表示法

package X is
   type Object is tagged null record;

   procedure Primitive (This: in Object; That: in Boolean);
end X;
with X;
procedure Main is
   Obj : X.Object;
begin
   Obj.Primitive (That => True); -- Ada 2005 object.method notation
end Main;

這種表示法僅適用於控制引數是第一個引數的原始操作。

抽象型別

[edit | edit source]

帶標記型別也可以是抽象的(因此可以有抽象操作)

package X is

   type Object is abstract tagged …;

   procedure One_Class_Member      (This : in     Object);
   procedure Another_Class_Member  (This : in out Object);
   function  Abstract_Class_Member return Object  is abstract;

end X;

抽象操作不能有任何主體,因此派生型別被迫覆蓋它(除非這些派生型別也是抽象的)。有關這方面的更多資訊,請參閱下一節關於介面的內容。

與非抽象帶標記型別不同的是,你不能宣告任何此型別的變數。但是,你可以宣告對它的訪問,並將其用作類範圍操作的引數。

透過介面實現多重繼承

[edit | edit source]

此語言特性僅從 Ada 2005 開始可用。

介面允許有限形式的多重繼承(取自 Java)。從語義上講,它們類似於“抽象帶標記的空記錄”,因為它們可能具有原始操作,但不能儲存任何資料,因此這些操作不能有主體,它們要麼被宣告為abstractnull抽象表示操作必須被覆蓋,表示預設實現為空主體,即不執行任何操作的實現。

介面用以下方式宣告

package Printable is
   type Object is interface;
   procedure Class_Member_1 (This : in     Object) is abstract;
   procedure Class_Member_2 (This :    out Object) is null;
end Printable;

你透過將其新增到具體的中來實現一個interface

with Person;
package Programmer is
   type Object is new Person.Object
                  and Printable.Object
   with
      record
         Skilled_In : Language_List;
      end record;
   overriding
   procedure Class_Member_1   (This : in Object);
   not overriding
   procedure New_Class_Member (This : Object; That : String);
end Programmer;

像往常一樣,所有繼承的抽象操作都必須被覆蓋,儘管空子程式不需要。

這種型別可以實現介面列表(稱為祖先),但只能有一個父類。父類可以是具體型別,也可以是介面。

type Derived is new Parent and Progenitor_1 and Progenitor_2 ... with ...;

透過 mix-in 實現多重繼承

[edit | edit source]

Ada 支援對介面的多重繼承(見上文),但只支援對實現的單一繼承。這意味著帶標記型別可以實現多個介面,但只能擴充套件一個祖先帶標記型別。

如果你想將行為新增到一個已經擴充套件了另一個型別的型別,這可能會成為問題;例如,假設你有

type Base is tagged private;
type Derived is new Base with private;

並且你想使Derived 受控,即新增Derived 控制其初始化、賦值和終結的行為。可惜你不能寫

type Derived is new Base and Ada.Finalization.Controlled with private; -- illegal

因為Ada.Finalization 由於歷史原因,沒有定義介面ControlledLimited_Controlled,而是定義了抽象型別。

如果你的基本型別不是受限的,那麼沒有很好的解決方案;你必須回到類的根部並使它受控。(原因將在稍後變得很明顯。)

但是,對於受限型別,另一種解決方案是使用 mix-in

type Base is tagged limited private;
type Derived;

type Controlled_Mix_In (Enclosing: access Derived) is
  new Ada.Finalization.Limited_Controlled with null record;

overriding procedure Initialize (This: in out Controlled_Mix_In);
overriding procedure Finalize   (This: in out Controlled_Mix_In);

type Derived is new Base with record
  Mix_In: Controlled_Mix_In (Enclosing => Derived'Access); -- special syntax here
  -- other components here...
end record;

這種特殊型別的 mix-in 是一個物件,它有一個訪問判別式,該判別式引用其封閉物件(也稱為Rosen 技巧)。在Derived 型別的宣告中,我們使用特殊語法初始化此判別式:Derived'Access 實際上引用的是Derived 型別當前例項的訪問值。因此,訪問判別式允許 mix-in 檢視其封閉物件及其所有元件;因此它可以初始化和終結其封閉物件

overriding procedure Initialize (This: in out Controlled_Mix_In) is
  Enclosing: Derived renames This.Enclosing.all;
begin
  -- initialize Enclosing...
end Initialize;

以及Finalize 的類似情況。

這對於非受限型別不起作用的原因是透過判別式的自引用。想象一下,你擁有兩個這樣的非受限型別的變數,並將一個賦值給另一個

X := Y;

在賦值語句中,Adjust 只在XFinalize 之後被呼叫,因此不能提供判別式的新的值。因此X.Mixin_In.Enclosing 將不可避免地引用Y

現在讓我們進一步擴充套件我們的層次結構

type Further is new Derived with null record;

overriding procedure Initialize (This: in out Further);
overriding procedure Finalize   (This: in out Further);

哎呀,這不起作用,因為還沒有Derived 的相應過程 - 因此讓我們快速新增它們。

type Base is tagged limited private;
type Derived;

type Controlled_Mix_In (Enclosing: access Derived) is
  new Ada.Finalization.Limited_Controlled with null record;

overriding procedure Initialize (This: in out Controlled_Mix_In);
overriding procedure Finalize   (This: in out Controlled_Mix_In);

type Derived is new Base with record
  Mix_In: Controlled_Mix_In (Enclosing => Derived'Access);  -- special syntax here
  -- other components here...
end record;

not overriding procedure Initialize (This: in out Derived);  -- sic, they are new
not overriding procedure Finalize   (This: in out Derived);

type Further is new Derived with null record;
overriding procedure Initialize (This: in out Further);
overriding procedure Finalize   (This: in out Further);

當然,我們必須為Derived 上的過程寫入not overriding,因為它們實際上沒有可以覆蓋的內容。主體是

not overriding procedure Initialize (This: in out Derived) is
begin
  -- initialize Derived...
end Initialize;
overriding procedure Initialize (This: in out Controlled_Mix_In) is
  Enclosing: Derived renames This.Enclosing.all;
begin
  Initialize (Enclosing);
end Initialize;

令我們沮喪的是,我們必須瞭解到,型別為Further 的物件的Initialize/Finalize 不會被呼叫,而是會呼叫其父類DerivedInitialize/Finalize。為什麼?

declare
  X: Further;  -- Initialize (Derived (X)) is called here
begin
  null;
end;  -- Finalize (Derived (X)) is called here

原因是 mix-in 在上面的重新命名語句中定義了局部物件Enclosing 的型別為Derived。為了解決這個問題,我們必須使用令人討厭的重新分派(以不同的但等效的表示法顯示)

overriding procedure Initialize (This: in out Controlled_Mix_In) is
  Enclosing: Derived renames This.Enclosing.all;
begin
  Initialize (Derived'Class (Enclosing));
end Initialize;
overriding procedure Finalize (This: in out Controlled_Mix_In) is
  Enclosing: Derived'Class renames Derived'Class (This.Enclosing.all);
begin
  Enclosing.Finalize;
end Finalize;
declare
  X: Further;  -- Initialize (X) is called here
begin
  null;
end;  -- Finalize (X) is called here

或者(可能更好)寫

type Controlled_Mix_In (Enclosing: access Derived'Class) is
  new Ada.Finalization.Limited_Controlled with null record;

然後我們自動獲得重新分派,並且可以省略Enclosing 上的型別轉換。

類名

[edit | edit source]

類包和類記錄都需要一個名稱。理論上它們可以有相同的名稱,但實際上,當使用use 子句時,這會導致討厭的(由於直觀錯誤訊息)名稱衝突。因此,隨著時間的推移,三種事實上的命名標準已被廣泛使用。

類/類

[edit | edit source]

包以複數名詞命名,記錄以相應的單數形式命名。

package Persons is

   type Person is tagged
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Persons;

此約定是 Ada 內建庫中通常使用的約定。

缺點:一些“複數”很難拼寫,尤其是對於那些不是以英語為母語的人來說。

類/物件

[edit | edit source]

包以類命名,記錄只命名為 Object。

package Person is

   type Object is tagged
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Person;

大多數 UMLIDL 程式碼生成器使用這種技術。

缺點:你不能在任何時候對多個這樣的類包使用use 子句。但是,你始終可以使用“型別”而不是包。

類/類_型別

[edit | edit source]

包以類命名,記錄以_Type 為字尾。

package Person is

   type Person_Type is tagged
      record
         Name   : String (1 .. 10);
         Gender : Gender_Type;
      end record;

end Person;

缺點:很多難看的“_Type”字尾。

面向物件的 Ada 適用於 C++ 程式設計師

[edit | edit source]

在 C++ 中,結構

 struct C {
   virtual void v();
   void w();
   static void u();
 };

與 Ada 中的以下內容嚴格等效

package P is
  type C is tagged null record;
  procedure V (This : in out C);        -- primitive operation, will be inherited upon derivation
  procedure W (This : in out C'Class);  -- not primitive, will not be inherited upon derivation
  procedure U;
end P;

在 C++ 中,成員函式隱式地接受一個型別為 C*this 引數。在 Ada 中,所有引數都是顯式的。因此,u() 接受引數這一事實,在 C++ 中是隱式的,而在 Ada 中是顯式的。

在 C++ 中,this 是一個指標。在 Ada 中,顯式的 This 引數不必是指標;所有標記型別引數都隱式地按引用傳遞。

靜態分派

[edit | edit source]

在 C++ 中,函式呼叫在以下情況下靜態分派

  • 呼叫的目標是物件型別
  • 成員函式是非虛擬的

例如

 C object;
 object.v();
 object.w();

都靜態分派。特別是,對 v() 的靜態分派可能會令人困惑;這是因為物件既不是指標也不是引用。Ada 在這方面行為完全相同,只是 Ada 將此稱為靜態繫結而不是分派

declare
   Object : P.C;
begin
   Object.V; -- statically bound
   Object.W; -- statically bound
end;

動態分派

[edit | edit source]

在 C++ 中,如果同時滿足以下兩個條件,函式呼叫會動態分派

  • 呼叫的目標是指標或引用
  • 成員函式是虛擬的。

例如

 C* object;
 object->v(); // dynamic dispatch
 object->w(); // static, non-virtual member function
 object->u(); // illegal: static member function
 C::u(); // static dispatch

在 Ada 中,原始子程式呼叫(動態地)分派,當且僅當

  • 目標物件是類範圍型別;

注意:在 Ada 行話中,術語分派始終表示動態

例如

declare
   Object : P.C'Class := ...;
begin
   P.V (Object); -- dispatching
   P.W (Object); -- statically bound: not a primitive operation
   P.U; -- statically bound
end;

如您所見,不需要訪問型別或指標來在 Ada 中進行分派。在 Ada 中,標記型別總是按引用傳遞給子程式,而不需要顯式的訪問值。

還要注意,在 C++ 中,類充當

  • 封裝單元(Ada 使用包和可見性來實現這一點)
  • 型別,如 Ada 中一樣。

因此,您在 C++ 中呼叫 C::u(),因為 u() 封裝在 C 中,但在 Ada 中呼叫 P.U,因為 U 封裝在 P 中,而不是型別 C 中。

類範圍型別和特定型別

[edit | edit source]

對於 C++ 程式設計師來說,最令人困惑的部分是“類範圍型別”的概念。為了幫助您理解

  • C++ 中的指標和引用實際上是隱式的類範圍;
  • C++ 中的物件型別實際上是特定的;
  • C++ 不提供宣告等效於以下內容的方法
type C_Specific_Access is access C;
  • C++ 不提供宣告等效於以下內容的方法
type C_Specific_Access_One is access C;
type C_Specific_Access_Two is access C;

這在 Ada 中是兩種不同的、不相容的型別,可能從不同的儲存池分配它們的記憶體!

  • 在 Ada 中,您不需要訪問值來進行動態分派。
  • 在 Ada 中,您使用訪問值來進行動態記憶體管理(僅限),使用類範圍型別來進行動態分派(僅限)。
  • 在 C++ 中,您使用指標和引用來進行動態記憶體管理和動態分派。
  • 在 Ada 中,類範圍型別是顯式的(使用 'Class)。
  • 在 C++ 中,類範圍型別是隱式的(使用 *&)。

建構函式

[edit | edit source]

在 C++ 中,一個特殊的語法聲明瞭一個建構函式

 class C {
    C(/* optional parameters */); // constructor
 };

建構函式不能是虛擬的。一個類可以根據需要擁有任意數量的建構函式,這些建構函式由它們的形參區分。

Ada 沒有這樣的建構函式。它們可能被認為是不必要的,因為在 Ada 中,任何返回標記型別物件的函式都可以充當一種建構函式。然而,這與 C++ 中的真實建構函式不同;這種區別在派生樹的情況下最為明顯(參見下面的 Finalization)。Ada 建構函式子程式不必具有特殊的名稱,並且可以根據需要擁有任意數量的建構函式;每個函式都可以根據需要接受形參。

package P is
  type T is tagged private;
  function Make                 return T;  -- constructor
  function To_T (From: Integer) return T;  -- another constructor
  procedure Make (This: out T);            -- not a constructor
private
  ...
end P;

如果 Ada 建構函式也是一個原始操作(如上面的示例),那麼它在派生時會成為抽象的,並且如果派生型別本身不是抽象的,則必須重寫它。如果您不希望這樣,請在巢狀範圍內宣告此類函式。

在 C++ 中,一種慣例是複製建構函式及其同類賦值運算子

 class C {
    C(const C& that); // copies "that" into "this"
    C& operator= (const C& right); // assigns "right" to "this", which is "left"
 };

該複製建構函式在初始化時隱式呼叫,例如

 C a = b; // calls the copy constructor
 C c;
 a = c;   // calls the assignment operator

Ada 透過受控型別提供類似的功能。受控型別是擴充套件了預定義型別 Ada.Finalization.Controlled 的型別

with Ada.Finalization;
package P is
  type T is new Ada.Finalization.Controlled with private;
  function Make return T;  -- constructor
private
  type T is ... end record;
  overriding procedure Initialize (This: in out T);
  overriding procedure Adjust     (This: in out T); -- copy constructor
end P;

請注意,Initialize 不是建構函式;它在某種程度上類似於 C++ 建構函式,但也有很大不同。假設您有一個從 T 派生的型別 T1,其中包含對 Initialize 的適當重寫。一個真實的建構函式(如 C++ 中的建構函式)會自動首先構造父元件(T),然後構造子元件。在 Ada 中,這不是自動的。為了在 Ada 中模擬這一點,我們必須編寫

procedure Initialize (This: in out T1) is
begin
  Initialize (T (This));  -- Don't forget this part!
  ...  -- handle the new components here
end Initialize;

編譯器在每個型別為 T 的物件在未給出初始值時被分配時,會在每個物件之後插入對 Initialize 的呼叫。它還會在對物件的每次賦值後插入對 Adjust 的呼叫。因此,宣告

A: T;
B: T := X;

  • 為 A 分配記憶體
  • 呼叫 Initialize (A)
  • 為 B 分配記憶體
  • 將 X 的內容複製到 B
  • 呼叫 Adjust (B)

由於顯式初始化,不會呼叫 Initialize (B)。

因此,複製建構函式的等效項是對 Adjust 的重寫。

如果您想為擴充套件另一個非受控型別的型別提供此功能,請參見 "多重繼承"

解構函式

[edit | edit source]

在 C++ 中,解構函式是一個僅具有隱式 this 形參的成員函式

 class C {
    virtual ~C(); // destructor
 }

建構函式不能是虛擬的,而解構函式必須是虛擬的,如果該類要與動態分派一起使用(具有虛擬方法或從具有虛擬方法的類派生)。C++ 類預設情況下不使用動態分派,因此它可能會透過簡單地忘記關鍵字 virtual 來捕獲一些程式設計師,並在他們的程式中造成破壞。

在 Ada 中,等效的功能再次由受控型別提供,透過重寫過程 Finalize

with Ada.Finalization;
package P is
   type T is new Ada.Finalization.Controlled with private;
   function Make return T;  -- constructor
private
   type T is ... end record;
   overriding procedure Finalize (This: in out T);  -- destructor
end P;

由於 Finalize 是一個原始操作,因此它是自動“虛擬”的;在 Ada 中,您不能忘記使解構函式成為虛擬的。

封裝:公共、私有和受保護的成員

[edit | edit source]

在 C++ 中,封裝的單元是類;在 Ada 中,封裝的單元是包。這對 Ada 程式設計師如何放置物件型別的各個元件有影響。

 class C {
 public:
    int a;
    void public_proc();
 protected:
    int b;
    int protected_func();
 private:
    bool c;
    void private_proc();
 };

在 Ada 中模擬 C++ 類的另一種方法是定義一個型別層次結構,其中基型別是公共部分,它必須是抽象的,這樣才能防止定義這種基型別的獨立物件。它看起來像這樣

private with Ada.Finalization;

package CPP is

  type Public_Part is abstract tagged record  -- no objects of this type
    A: Integer;
  end record;

  procedure Public_Proc (This: in out Public_Part);

  type Complete_Type is new Public_Part with private;

  -- procedure Public_Proc (This: in out Complete_Type);  -- inherited, implicitly defined

private  -- visible for children

  type Private_Part;  -- declaration stub
  type Private_Part_Pointer is access Private_Part;

  type Private_Component is new Ada.Finalization.Controlled with record
    P: Private_Part_Pointer;
  end record;

  overriding procedure Initialize (X: in out Private_Component);
  overriding procedure Adjust     (X: in out Private_Component);
  overriding procedure Finalize   (X: in out Private_Component);

  type Complete_Type is new Public_Part with record
    B: Integer;
    P: Private_Component;  -- must be controlled to avoid storage leaks
  end record;

  not overriding procedure Protected_Proc (This: Complete_Type);

end CPP;

私有部分僅定義為存根,其完成隱藏在主體中。為了使它成為完整型別的元件,我們必須使用指標,因為元件的大小仍然未知(指標的大小對編譯器來說是已知的)。不幸的是,使用指標,我們會冒記憶體洩漏的風險,因此我們必須使私有元件受控。

為了進行一個小測試,這是主體,其中子程式主體透過識別列印提供。

with Ada.Unchecked_Deallocation;
with Ada.Text_IO;

package body CPP is

  procedure Public_Proc (This: in out Public_Part) is  -- primitive
  begin
    Ada.Text_IO.Put_Line ("Public_Proc" & Integer'Image (This.A));
  end Public_Proc;

  type Private_Part is record  -- complete declaration
    C: Boolean;
  end record;

  overriding procedure Initialize (X: in out Private_Component) is
  begin
    X.P := new Private_Part'(C => True);
    Ada.Text_IO.Put_Line ("Initialize " & Boolean'Image (X.P.C));
  end Initialize;

  overriding procedure Adjust (X: in out Private_Component) is
  begin
    Ada.Text_IO.Put_Line ("Adjust " & Boolean'Image (X.P.C));
    X.P := new Private_Part'(C => X.P.C);  -- deep copy
  end Adjust;

  overriding procedure Finalize (X: in out Private_Component) is
    procedure Free is new Ada.Unchecked_Deallocation (Private_Part, Private_Part_Pointer);
  begin
    Ada.Text_IO.Put_Line ("Finalize " & Boolean'Image (X.P.C));
    Free (X.P);
  end Finalize;

  procedure Private_Proc (This: in out Complete_Type) is  -- not primitive
  begin
    Ada.Text_IO.Put_Line ("Private_Proc" & Integer'Image (This.A) & Integer'Image (This.B) & ' ' & Boolean'Image (This.P.P.C));
  end Private_Proc;

  not overriding procedure Protected_Proc (This: Complete_Type) is  -- primitive
    X: Complete_Type := This;
  begin
    Ada.Text_IO.Put_Line ("Protected_Proc" & Integer'Image (This.A) & Integer'Image (This.B));
    Private_Proc (X);
  end Protected_Proc;

end CPP;

我們看到,由於構造,私有過程不是一個原始操作。

讓我們定義一個子類,以便可以訪問受保護的操作

package CPP.Child is
 
  procedure Do_It (X: Complete_Type);  -- not primitive

end CPP.Child;

子類可以檢視父類的私有部分,因此可以檢視受保護的過程

with Ada.Text_IO;

package body CPP.Child is

  procedure Do_It (X: Complete_Type) is
  begin
    Ada.Text_IO.Put_Line ("Do_It" & Integer'Image (X.A) & Integer'Image (X.B));
    Protected_Proc (X);
  end Do_It;

end CPP.Child;

這是一個簡單的測試程式,其輸出顯示在下面。

with CPP.Child;
use  CPP.Child, CPP;

procedure Test_CPP is

  X, Y: Complete_Type;

begin

  X.A := +1;
  Y.A := -1;

  Public_Proc (X);  Do_It (X);
  Public_Proc (Y);  Do_It (Y);

  X := Y;

  Public_Proc (X);  Do_It (X);

end Test_CPP;

這是測試程式的註釋輸出

Initialize TRUE                     Test_CPP: Initialize X
Initialize TRUE                                      and Y
Public_Proc 1                       |  Public_Proc (X):  A=1
Do_It 1-1073746208                  |  Do_It (X):        B uninitialized
Adjust TRUE                         |  |  Protected_Proc (X): Adjust local copy X of This
Protected_Proc 1-1073746208         |  |  |
Private_Proc 1-1073746208 TRUE      |  |  |  Private_Proc on local copy of This
Finalize TRUE                       |  |  Protected_Proc (X): Finalize local copy X
Public_Proc-1                       |  ditto for Y
Do_It-1 65536                       |  |
Adjust TRUE                         |  |
Protected_Proc-1 65536              |  |
Private_Proc-1 65536 TRUE           |  |
Finalize TRUE                       |  |
Finalize TRUE                       |  Assignment: Finalize target X.P.C
Adjust TRUE                         |  |           Adjust: deep copy
Public_Proc-1                       |  again for X, i.e. copy of Y
Do_It-1 65536                       |  |
Adjust TRUE                         |  |
Protected_Proc-1 65536              |  |
Private_Proc-1 65536 TRUE           |  |
Finalize TRUE                       |  |
Finalize TRUE                       Finalize Y
Finalize TRUE                            and X

您看到將 C++ 行為直接翻譯成 Ada 是很困難的,即使可行。我認為,原始 Ada 子程式更像是虛擬的 C++ 方法(在本例中,它們不是)。每種語言都有其自身的特性,必須考慮這些特性,因此嘗試將程式碼從一種語言直接翻譯成另一種語言可能不是最好的方法。

反封裝:朋友和流輸入輸出

[edit | edit source]

在 C++ 中,朋友函式或類可以檢視它所成為朋友的類的所有成員。朋友會破壞封裝,因此應該避免使用。在 Ada 中,由於包而不是類是封裝的單元,因此“朋友”子程式只是在與標記型別相同的包中宣告的子程式。

在 C++ 中,流輸入輸出是通常需要朋友的特殊情況

 #include <iostream>
 class C {
 public:
    C();
    friend ostream& operator<<(ostream& output, C& arg);
 private:
    int a, b;
    bool c;
 };

 #include <iostream>
 int main() {
    C object;
    cout << object;
    return 0;
 };

Ada 不需要這種構造,因為它預設定義流輸入和輸出操作:可以重寫 InputOutputReadWrite 屬性的預設實現(以 Write 為例)。重寫必須在型別凍結之前發生,即(在本示例的情況下)在包規範中。

private with Ada.Streams;  -- needed only in the private part
package P is
   type C is tagged private;
private
   type C is tagged record
      A, B : Integer;
      C : Boolean;
   end record;
   procedure My_Write (Stream : not null access Ada.Streams.Root_Stream_Type'Class;
                       Item   : in C);
   for C'Write use My_Write;  -- override the default attribute
end P;

預設情況下,Write 屬性會按照宣告中給出的順序將元件傳送到流,即 A、B 然後 C,因此我們更改了順序。

package body P is
   procedure My_Write (Stream : not null access Ada.Streams.Root_Stream_Type'Class;
                       Item : in C) is
   begin
      -- The default implementation is to write A then B then C; here we change the order.
      Boolean'Write (Stream, Item.C);  -- call the
      Integer'Write (Stream, Item.B);  --   default attributes
      Integer'Write (Stream, Item.A);  --      for the components
   end My_Write;
end P;

現在 P.C'Write 呼叫包的重寫版本。

 with Ada.Text_IO.Text_Streams;
 with P;
 procedure Main is
    Object : P.C;
 begin
    P.C'Write (Ada.Text_IO.Text_Streams.Stream (Ada.Text_IO.Standard_Output),
               Object);
 end Main;

請注意,流 IO 屬性不是標記型別的原始操作;在 C++ 中也是如此,朋友運算子實際上不是型別的成員函式。

術語

[edit | edit source]
Ada C++
類(作為封裝單元)
帶標記型別 類(物件)(作為型別)( *不* 指標或引用,它們是類範圍的)
基本操作 虛成員函式
標記 指向虛表的指標
類(型別) 一個類樹,以基類為根,包括該基類的所有(遞迴)派生類
類範圍型別 -
類範圍操作 靜態成員函式
訪問特定帶標記型別的值 -
訪問類範圍型別的值 指向類的指標或引用

華夏公益教科書

[編輯 | 編輯原始碼]

維基百科

[編輯 | 編輯原始碼]

Ada 參考手冊

[編輯 | 編輯原始碼]

Ada 質量和風格指南

[編輯 | 編輯原始碼]
華夏公益教科書