跳轉到內容

Ada 程式設計/型別/訪問

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

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

什麼是訪問型別?

[編輯 | 編輯原始碼]

Ada 中的訪問型別是其他語言中稱為指標的東西。它們指向位於特定地址的物件。因此,通常可以將訪問型別視為簡單的地址(這種簡化觀點存在例外情況)。Ada 不說“指向”,而是說“授予訪問許可權”或“指定”某個物件。

訪問型別的物件隱式地初始化為null,即,如果未顯式初始化,它們將不指向任何內容。

在 Ada 中應該很少使用訪問型別。在其他語言中使用指標的許多情況下,還有其他不使用指標的方法。如果您需要動態資料結構,請先檢查是否可以使用 Ada 容器庫。特別是對於不定記錄或陣列元件,Ada 2012 包 Ada.Containers.Indefinite_Holders(RM A.18.18 [註釋])可以用來代替指標。

Ada 中有四種訪問型別:池訪問型別 - 一般訪問型別 - 匿名訪問型別 - 訪問子程式型別。

池訪問

[編輯 | 編輯原始碼]

池訪問型別處理對在特定堆(或 Ada 中稱為儲存池)上建立的物件的訪問。這些型別的指標不能指向堆疊或庫級別(靜態)物件,也不能指向其他儲存池中的物件。因此,池訪問型別之間的轉換是非法的。(可以使用 Unchecked_Conversion,但請注意,透過與分配池不同的儲存池的訪問物件進行釋放是錯誤的。)

type Person is record
  First_Name : String (1..30);
  Last_Name  : String (1..20);
end record;

type Person_Access is access Person;

可以使用儲存大小子句來限制相應的(實現定義的匿名)儲存池。儲存大小子句為 0 將停用分配器的呼叫。

for Person_Access'Storage_Size use 0;

如果沒有指定,儲存池是實現定義的。Ada 支援使用者定義的儲存池,因此可以使用以下方法定義儲存池:

for Person_Access'Storage_Pool use Pool_Name;

儲存池中的物件是用關鍵字建立的new:

Father: Person_Access := new Person;                                          -- uninitialized
Mother: Person_Access := new Person'(Mothers_First_Name, Mothers_Last_Name);  -- initialized

透過附加 .all 訪問儲存池中的物件。Mother.all 是完整的記錄;元件用通常的點表示法表示:Mother.all.First_Name。訪問元件時,隱式解引用(即省略all)可以作為一種便捷的簡寫

Mother.all := (Last_Name => Father.Last_Name, First_Name => Mother.First_Name);  -- marriage

隱式解引用也適用於陣列

  type Vector is array (1 .. 3) of Complex;
  type Vector_Access is access Vector;

  VA: Vector_Access := new Vector;
  VB: array (1 .. 3) of Vector_Access := (others => new Vector);

  C1: Complex := VA (3);    -- a shorter equivalent for VA   .all (3)
  C2: Complex := VB (3)(1); -- a shorter equivalent for VB(3).all (1)

使用訪問物件進行復制時,請注意區分深複製和淺複製

Obj1.all := Obj2.all;  -- Deep copy: Obj1 still refers to an object different from Obj2, but it has the same content
Obj1 := Obj2;          -- Shallow copy: Obj1 now refers to the same object as Obj2

從儲存池中刪除物件

[編輯 | 編輯原始碼]

雖然 Ada 標準提到了垃圾收集器,它會自動刪除所有在堆上建立的無用物件(當沒有定義儲存池時),但只有針對像 Java 或 .NET 這樣的虛擬機器的 Ada 編譯器實際上具有垃圾收集器。

當訪問型別超出範圍時,相應仍然分配的資料項將以任意順序被最終化(即它們不再存在);但是,分配的記憶體只有在透過屬性定義子句為訪問型別定義了屬性 Storage_Size 後才會釋放。(注意:最終化釋放是不同的!)

以下是 Ada 參考手冊的摘錄。省略號代表與本案無關的部分。

RM 3.10(7/1) 存在 ... 訪問物件型別,其值指定物件... 與訪問物件型別相關聯的是一個儲存池;幾個訪問型別可以共享同一個儲存池。... 儲存池是用於儲存動態分配的物件(稱為池元素)的儲存區域,由分配器建立。

(8) 訪問物件型別進一步細分為特定池訪問型別,其值只能指定其關聯儲存池的元素...

RM 7.6(1) ... 每個物件在被銷燬之前都會被最終化(例如,透過離開包含物件宣告的子程式體,或透過呼叫 Unchecked_Deallocation 的例項)...

RM 7.6.1(5) 對於物件的最終化

(6/3) 如果物件的完整型別是基本型別,則最終化沒有效果;

(7/3) 如果物件的完整型別是標記型別,並且物件的標記標識受控型別,則將呼叫該受控型別的 Finalize 過程;

(10) 在 Unchecked_Deallocation 的例項回收物件的儲存空間之前,將對該物件進行最終化。如果從未對由分配器建立的物件應用 Unchecked_Deallocation 的例項,則該物件在相應的 master 完成時仍然存在,並且將在那時被最終化。

(11.1/3) 每個非派生訪問型別 T 都有一個關聯的集合,它是透過 T 的分配器(或從 T 派生的型別)建立的物件集。Unchecked_Deallocation 從其集合中刪除物件。集合的最終化包括對集合中每個物件的最終化,順序任意...

RM 13.11(1) 每個訪問物件型別都有一個關聯的儲存池。分配器分配的儲存空間來自該池;Unchecked_Deallocation 例項將儲存空間返回該池。多個訪問型別可以共享同一個池。

(2/2) 儲存池是根植於 Root_Storage_Pool 的型別的變數,Root_Storage_Pool 是一個抽象受限受控型別。預設情況下,實現為每個訪問物件型別選擇一個標準儲存池...

(11) 儲存池型別(或池型別)是 Root_Storage_Pool 的後代。儲存池的元素是由分配器在池中分配的物件。

(15) 可以透過屬性定義子句為非派生訪問物件型別指定 Storage_Size 或 Storage_Pool...

(17) 如果未為由訪問物件定義定義的型別指定 Storage_Pool,則實現將以實現定義的方式為其選擇一個標準儲存池...

(18/4) 如果為訪問型別 T 指定了 Storage_Size,則為該型別使用一個實現定義的池 PP 的 Storage_Size 至少為請求的大小,並且在包含訪問型別宣告的主體退出時回收 P 的儲存空間...

以下程式可以編譯,但在執行時會因異常而無法透過可訪問性檢查。

with Ada.Text_IO;
use Ada.Text_IO;

procedure Main is
   function Accessibility_Check_Fail
     return access String
   is
      -- Declare a new access type locally.
      -- All memory with this type will be finalized but not freed
      -- when the this type goes out of scope.
      type A_Type is access String;  -- no Storage_Size defined
      
      X : A_Type := new String'("x");  -- storage will be lost
      Y : access String;  -- defined locally
   begin
      Y := X;  -- data defined in a local pool will be finalized when function returns
      return Y;  -- exception should be raised
   end Accessibility_Check_Fail;
begin
   -- Accessibility check will fail because the accessiblity level associated
   -- with Y is deeper than the accessibility level of this scope.
   Put_Line(Accessibility_Check_Fail.all);
end Main;

還有一個pragma Controlled,當應用於此類訪問型別時,可以防止使用它建立的物件被自動垃圾回收。 請注意,pragma Controlled 已從 Ada 2012 中移除,儲存管理的子池已取代它。請參見 RM 2012 13.11.3 [帶註釋的]13.11.4 [帶註釋的]

因此,要從堆中刪除物件,您需要通用單元 Ada.Unchecked_Deallocation。在釋放物件時,要格外小心,不要建立懸空指標,如以下示例所示。(請注意,當相應的儲存池不同時,使用與建立物件時不同的訪問型別釋放物件是錯誤的。)

with Ada.Unchecked_Deallocation;

procedure Deallocation_Sample is

   type Vector     is array (Integer range <>) of Float;
   type Vector_Ref is access Vector;

   procedure Free_Vector is new Ada.Unchecked_Deallocation
      (Object => Vector, Name => Vector_Ref);
  
   VA, VB: Vector_Ref;
   V     : Vector;

begin

   VA     := new Vector (1 .. 10);
   VB     := VA;  -- points to the same location as VA

   VA.all := (others => 0.0);

   --  ... Do whatever you need to do with the vector

   Free_Vector (VA); -- The memory is deallocated and VA is now null

   V := VB.all;  -- VB is not null, access to a dangling pointer is erroneous

end Deallocation_Sample;

正是由於存在懸空指標問題,釋放操作被稱為unchecked。程式設計師有責任確保這種情況不會發生。

由於 Ada 允許使用者定義儲存池,您也可以嘗試使用 垃圾收集器庫

構建引用計數指標

[edit | edit source]

您可以在網上找到一些引用計數指標的實現,稱為安全智慧指標。使用這種型別可以避免擔心釋放,因為當不再有指向物件的指標時,將自動執行釋放。但請注意,大多數這些實現並不能阻止故意釋放,因此會破壞使用它們所獲得的所謂安全性。

在 AdaCore 網站上的一系列 Gems 中可以找到有關如何構建這種型別的良好教程。

Gem #97:Ada 中的引用計數 - 第 1 部分 此小寶石構建了一個簡單的引用計數指標,它不阻止釋放,即本質上是不安全的。

Gem #107:防止引用計數型別的釋放 此寶石進一步描述瞭如何獲得一種指標型別,其安全性無法被破壞(除任務問題外)。這種改進的安全性的代價是笨拙的語法。

Gem #123:Ada 2012 中的隱式解引用 此寶石展示瞭如何使用新的 Ada 2012 生成簡化語法。(誠然,此寶石與引用計數有點關係,因為新的語言功能可以應用於任何型別的容器。)

通用訪問

[edit | edit source]

通用訪問型別允許訪問在任何儲存池、堆疊或庫級別(靜態)建立的物件。它們有兩種版本,分別授予讀寫訪問許可權或只讀訪問許可權。允許在通用訪問型別之間進行轉換,但要遵守某些訪問級別檢查。

解引用與池訪問型別類似。要引用的物件(池物件除外)必須宣告為aliased,並使用屬性 'Access 建立對它們的引用。訪問級別限制可以防止訪問超出被訪問物件生存期的物件,這會導致程式出錯。屬性 'Unchecked_Access 會省略相應的檢查。

訪問變數

[edit | edit source]

當關鍵字all 用於定義時,它們會授予讀寫訪問許可權。

type Day_Of_Month is range 1 .. 31;            
type Day_Of_Month_Access is access all Day_Of_Month;

訪問常量

[edit | edit source]

授予對被引用物件只讀訪問許可權的通用訪問型別使用關鍵字constant 在其定義中。被引用物件可以是常量或變數。

type Day_Of_Month is range 1 .. 31;            
type Day_Of_Month_Access is access constant Day_Of_Month;

一些示例

[edit | edit source]
 type General_Pointer  is access all      Integer;
 type Constant_Pointer is access constant Integer;

 I1: aliased constant Integer := 10;
 I2: aliased Integer;

 P1: General_Pointer  := I1'Access;  -- illegal
 P2: Constant_Pointer := I1'Access;  -- OK, read only
 P3: General_Pointer  := I2'Access;  -- OK, read and write
 P4: Constant_Pointer := I2'Access;  -- OK, read only

 P5: constant General_Pointer := I2'Access;  -- read and write only to I2

匿名訪問

[edit | edit source]

匿名訪問型別也有兩種版本,類似於通用訪問型別,分別授予讀寫訪問許可權或只讀訪問許可權,具體取決於關鍵字constant 是否出現。

匿名訪問可以用作子程式的引數或作為區分符。以下是一些示例

procedure Modify (Some_Day: access          Day_Of_Month);
procedure Test   (Some_Day: access constant Day_Of_Month);  -- Ada 2005 only
task type Thread (Execute_For_Day: access Day_Of_Month) is
   ...
end Thread;
type Day_Data (Store_For_Day: access Day_Of_Month) is record
  -- components
end record;

在使用匿名訪問之前,您應該考慮命名訪問型別,或者更好的是,考慮是否“out”或“in out”修飾符更合適。

此語言功能僅從 Ada 2005 開始可用。

在 Ada 2005 中,匿名訪問在更多情況下是允許的

type Object is record
  M   : Integer;
  Next: access Object;
end record;

X: access Integer;

function F return access constant Float;

隱式解引用

[edit | edit source]

此語言功能已在 Ada 2012 中引入。

Ada 2012 使用新的語法簡化了透過指標訪問物件。

假設您有一個包含某種元素的容器。

type Container   is private;
type Element_Ptr is access Element;

procedure Put (X: Element; Into: in out Container);

現在,如何訪問儲存在容器中的元素。當然,您可以透過

function Get (From: Container) return Element;

來檢索它們,但這會複製元素,如果元素很大,這是不利的。您可以使用

function Get (From: Container) return Element_Ptr;

獲得直接訪問許可權,但指標很危險,因為您很容易建立懸空指標,例如

P: Element_Ptr := Get (Cont);
P.all := E;
Free (P);
... Get (Cont) -- this is now a dangling pointer

使用訪問器物件而不是訪問型別可以防止意外釋放(這仍然是 Ada 2005)

type Accessor (Data: not null access Element) is limited private;  -- read/write access
function Get (From: Container) return Accessor;

(對於空排除not null 在區分符的宣告中,請參見下文)。透過此類訪問器進行的訪問是安全的:區分符只能用於解引用,不能將其複製到 Element_Ptr 型別的物件,因為其訪問級別更深。在上面的形式中,訪問器提供了讀寫訪問許可權。如果新增關鍵字constant,則只能進行讀訪問。

type Accessor (Data: not null access constant Element) is limited private;  -- only read access

現在訪問容器物件的方式如下

Get (Cont).all      := E;  -- via access type: dangerous
Get (Cont).Data.all := E;  -- via accessor: safe, but ugly

這裡,新的 Ada 2012 方面功能非常有用;對於這種情況,我們需要使用 Implicit_Dereference 方面

type Accessor (Data: not null access Element) is limited private
   with Implicit_Dereference => Data;

現在,無需再編寫上面冗長且難看的函式呼叫,我們可以簡單地省略區分符及其解引用,如下所示

Get (Cont).Data.all := E;  -- Ada 2005 via accessor: safe, but ugly
Get (Cont)          := E;  -- Ada 2012 implicit dereference

請注意,呼叫 Get (Cont) 是過載的 - 它可以表示訪問器物件或元素,編譯器會根據上下文選擇正確的解釋。

空排除

[edit | edit source]

此語言功能僅從 Ada 2005 開始可用。

所有訪問子型別都可以用not null 修改,此類子型別的物件永遠不會有空值,因此必須進行初始化。

type    Day_Of_Month_Access          is access   Day_Of_Month;
subtype Day_Of_Month_Not_Null_Access is not null Day_Of_Month_Access;

該語言還允許直接使用空排除宣告第一個子型別

type Day_Of_Month_Access is not null access Day_Of_Month;

但是,在幾乎所有情況下,這不是一個好主意,因為它會使該型別物件的可用性變得很差(例如,您無法釋放分配的記憶體)。非空訪問旨在用於訪問子型別、物件宣告和子程式引數[1]

訪問子程式

[edit | edit source]

訪問子程式允許呼叫方呼叫 子程式,而無需知道其名稱或宣告位置。這種訪問方式的一種應用是眾所周知的回撥。

type Callback_Procedure is access procedure (Id  : Integer;
                                             Text: String);

type Callback_Function is access function (The_Alarm: Alarm) return Natural;

要獲取對子程式的訪問權,需要將屬性 Access 應用於子程式名稱,並使用適當的引數和結果概要。

procedure Process_Event (Id  : Integer;
                         Text: String);

My_Callback: Callback_Procedure := Process_Event'Access;

匿名訪問子程式

[編輯 | 編輯原始碼]

此語言功能僅從 Ada 2005 開始可用。

procedure Test (Call_Back: access procedure (Id: Integer; Text: String));

現在,一個序列中關鍵字的數量不再受限制。

function F return access function return access function return access Some_Type;

這是一個函式,它返回對一個函式的訪問,該函式又返回對一個函式的訪問,該函式返回對某種型別的訪問。

訪問常見問題解答

[編輯 | 編輯原始碼]

關於 Ada 的訪問型別,一些 "常見問題" 和 "常見問題" (主要來自 C 使用者)。

訪問 vs. 訪問所有

[編輯 | 編輯原始碼]

一個訪問 all 可以執行任何一個簡單的access 可以執行的操作。因此有人可能會問:"為什麼還要使用簡單的access 呢?" - 實際上,一些程式設計師從來不使用簡單的訪問.

Unchecked_Deallocation 如果使用不當,始終是危險的。將池特定的物件釋放兩次和釋放堆疊物件一樣容易,也同樣危險。"訪問所有" 的優勢在於,你可能根本不需要使用 Unchecked_Deallocation。

道德:如果你有(或可能會有)將 '訪問或 'Unchecked_Access 儲存到訪問物件的有效理由,那麼使用 "訪問所有" 並且不要擔心。如果沒有,"最小許可權" 的口號建議應該省略 "所有" (不要啟用你不會使用的功能)。

以下(可能災難性的)示例將嘗試釋放一個堆疊物件

declare

  type Day_Of_Month is range 1 .. 31;            
  type Day_Of_Month_Access is access all Day_Of_Month;

  procedure Free is new Ada.Unchecked_Deallocation
      (Object => Day_Of_Month,
       Name   => Day_Of_Month_Access);

  A  : aliased Day_Of_Month;
  Ptr: Day_Of_Month_Access := A'Access;

begin

   Free(Ptr);

end;

使用一個簡單的access 你至少知道你不會嘗試釋放一個堆疊物件。原因是access 不允許從堆疊物件建立指標。

訪問 vs. System.Address

[編輯 | 編輯原始碼]

訪問可以與一個簡單的記憶體地址不同,它可能包含更多內容。例如,"對字串的訪問" 通常還需要某種方法來儲存字串大小。如果你需要一個簡單的地址並且不關心強型別,請使用 System.Address 型別。

C 相容指標

[編輯 | 編輯原始碼]

建立 C 相容訪問的正確方法是使用pragma Convention

type Day_Of_Month is range 1 .. 31;
for  Day_Of_Month'Size use Interfaces.C.int'Size;

pragma Convention (Convention => C,
                   Entity     => Day_Of_Month);

type Day_Of_Month_Access is access Day_Of_Month;

pragma Convention (Convention => C,
                   Entity     => Day_Of_Month_Access);

pragma Convention 應該用於你想要在 C 中使用的任何型別。如果該型別無法與 C 相容,編譯器會發出警告。

在宣告 Day_Of_Month 時,你也可以考慮以下更短的替代方法

type Day_Of_Month is new Interfaces.C.int range 1 .. 31;

在 C 中使用訪問型別之前,你應該考慮使用普通的 "in"、"out" 和 "in out" 修飾符。pragma Exportpragma Import 知道引數通常如何在 C 中傳遞,並且會在 C 使用指標傳遞引數的情況下自動使用指標來傳遞引數。當然,RM 包含關於何時為 "in"、"out" 和 "in out" 使用指標的精確規則 - 請參閱 "B.3: Interfacing with C [Annotated]"。

void* 在哪裡?

[編輯 | 編輯原始碼]

雖然實際上是 "與 C 互動" 的問題,這裡有一些可能的解決方案

procedure Test is

  subtype Pvoid is System.Address;

  -- the declaration in C looks like this:
  -- int C_fun(int *)
  function C_fun (pv: Pvoid) return Integer;
  pragma Import (Convention    => C,
                 Entity        => C_fun,     -- any Ada name
                 External_Name => "C_fun");  -- the C name

  Pointer: Pvoid;

  Input_Parameter: aliased Integer := 32;
  Return_Value   : Integer;

begin

  Pointer      := Input_Parameter'Address;
  Return_Value := C_fun (Pointer);

end Test;

可移植性較差,但可能更易用 (對於 32 位 CPU)

type void is mod 2 ** 32;
for void'Size use 32;

使用 GNAT,你可以透過使用以下方法獲得 32/64 位可移植性

type void is mod System.Memory_Size;
for void'Size use System.Word_Size;

更接近 void 的本質 - 指向大小為零的元素的指標是指向空記錄的指標。這也具有為 voidvoid* 提供表示的優勢

type Void is null record;
pragma Convention (C, Void);

type Void_Ptr is access all Void;
pragma Convention (C, Void_Ptr);

瘦訪問型別和胖訪問型別

[編輯 | 編輯原始碼]

訪問型別和地址之間的區別將在下面詳細說明。使用術語 指標 是因為這是常用的術語。

有一個預定義的單元 System.Address_to_Access_Conversion 用於在訪問值和地址之間來回轉換。請謹慎使用這些轉換,如下文所述。

瘦指標

[編輯 | 編輯原始碼]

瘦指標允許訪問約束子型別。

type Int     is range -100 .. +500;
type Acc_Int is access Int;

type Arr     is array (1 .. 80) of Character;
type Acc_Arr is access Arr;

此類子型別的物件具有靜態大小,因此只需一個簡單的地址即可訪問它們。在陣列的情況下,這通常是第一個元素的地址。

對於這種型別的指標,使用 System.Address_to_Access_Conversion 是安全的。

胖指標

[編輯 | 編輯原始碼]
type Unc     is array (Integer range <>) of Character;
type Acc_Unc is access Unc;

子型別 Unc 的物件需要約束,即起始和終止索引,因此指向它們的指標也需要包含這些索引。因此,像第一個元件的地址這樣的簡單地址是不夠的。請注意,對於任何陣列物件,A'Address 與 A(A'First)'Address 相同。

對於這種型別的指標,System.Address_to_Access_Conversion 可能無法正常工作。

CO: aliased Unc (-1 .. +1) := (-1 .. +1 => ' ');
UO: aliased Unc            := (-1 .. +1 => ' ');

在這裡,CO 是一個 名義約束 物件,指向它的指標不需要儲存約束,即一個瘦指標就足夠了。相反,UO 是一個 名義未約束 子型別的物件,它的 實際子型別 由初始值約束。

A: Acc_Unc            := CO'Access;  -- illegal
B: Acc_Unc            := UO'Access;  -- OK
C: Acc_Unc (CO'Range) := CO'Access;  -- also illegal

RM 中的相關段落很難理解。簡而言之

訪問型別的目標型別稱為 指定子型別,在我們這個示例中是 Unc。RM 3.10.2 [Annotated](27.1/2) 要求 Acc_Unc 的指定子型別在靜態上與物件的 名義子型別 匹配。

現在,CO 的名義子型別是約束匿名子型別 Unc (-1 .. +1)UO 的名義子型別是未約束子型別 Unc。在非法情況下,指定子型別和名義子型別在靜態上不匹配。

另請參閱

[編輯 | 編輯原始碼]

華夏公益教科書

[編輯 | 編輯原始碼]

Ada 參考手冊

[編輯 | 編輯原始碼]

最新 RM

[編輯 | 編輯原始碼]

Ada 質量和風格指南

[編輯 | 編輯原始碼]

參考資料

[編輯 | 編輯原始碼]


華夏公益教科書