Ada 程式設計/型別/訪問
Ada 中的訪問型別是其他語言中稱為指標的東西。它們指向位於特定地址的物件。因此,通常可以將訪問型別視為簡單的地址(這種簡化觀點存在例外情況)。Ada 不說“指向”,而是說“授予訪問許可權”或“指定”某個物件。
訪問型別的物件隱式地初始化為null,即,如果未顯式初始化,它們將不指向任何內容。
在 Ada 中應該很少使用訪問型別。在其他語言中使用指標的許多情況下,還有其他不使用指標的方法。如果您需要動態資料結構,請先檢查是否可以使用 Ada 容器庫。特別是對於不定記錄或陣列元件,Ada 2012 包 Ada.Containers.Indefinite_Holders(RM A.18.18 [註釋])可以用來代替指標。
Ada 中有四種訪問型別:池訪問型別 - 一般訪問型別 - 匿名訪問型別 - 訪問子程式型別。
池訪問型別處理對在特定堆(或 Ada 中稱為儲存池)上建立的物件的訪問。這些型別的指標不能指向堆疊或庫級別(靜態)物件,也不能指向其他儲存池中的物件。因此,池訪問型別之間的轉換是非法的。(可以使用 Unchecked_Conversion,但請注意,透過與分配池不同的儲存池的訪問物件進行釋放是錯誤的。)
typePersonisrecordFirst_Name : String (1..30); Last_Name : String (1..20);endrecord;typePerson_AccessisaccessPerson;
可以使用儲存大小子句來限制相應的(實現定義的匿名)儲存池。儲存大小子句為 0 將停用分配器的呼叫。
forPerson_Access'Storage_Sizeuse0;
如果沒有指定,儲存池是實現定義的。Ada 支援使用者定義的儲存池,因此可以使用以下方法定義儲存池:
forPerson_Access'Storage_PoolusePool_Name;
儲存池中的物件是用關鍵字建立的new:
Father: Person_Access :=newPerson; -- uninitialized Mother: Person_Access :=newPerson'(Mothers_First_Name, Mothers_Last_Name); -- initialized
透過附加 . 訪問儲存池中的物件。allMother. 是完整的記錄;元件用通常的點表示法表示:allMother.。訪問元件時,隱式解引用(即省略all.First_Nameall)可以作為一種便捷的簡寫
Mother.all := (Last_Name => Father.Last_Name, First_Name => Mother.First_Name); -- marriage
隱式解引用也適用於陣列
typeVectorisarray(1 .. 3)ofComplex;typeVector_AccessisaccessVector; VA: Vector_Access :=newVector; VB:array(1 .. 3)ofVector_Access := (others=>newVector); 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,則為該型別使用一個實現定義的池 P。P 的 Storage_Size 至少為請求的大小,並且在包含訪問型別宣告的主體退出時回收 P 的儲存空間...
以下程式可以編譯,但在執行時會因異常而無法透過可訪問性檢查。
withAda.Text_IO;useAda.Text_IO;procedureMainisfunctionAccessibility_Check_FailreturnaccessStringis-- 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.typeA_TypeisaccessString; -- no Storage_Size defined X : A_Type :=newString'("x"); -- storage will be lost Y :accessString; -- defined locallybeginY := X; -- data defined in a local pool will be finalized when function returnsreturnY; -- exception should be raisedendAccessibility_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);endMain;
還有一個 請注意,,當應用於此類訪問型別時,可以防止使用它建立的物件被自動垃圾回收。pragma Controlled 已從 Ada 2012 中移除,儲存管理的子池已取代它。請參見 RM 2012 13.11.3 [帶註釋的] 和 13.11.4 [帶註釋的]。pragma Controlled
因此,要從堆中刪除物件,您需要通用單元 Ada.Unchecked_Deallocation。在釋放物件時,要格外小心,不要建立懸空指標,如以下示例所示。(請注意,當相應的儲存池不同時,使用與建立物件時不同的訪問型別釋放物件是錯誤的。)
withAda.Unchecked_Deallocation;procedureDeallocation_SampleistypeVectorisarray(Integerrange<>)ofFloat;typeVector_RefisaccessVector;procedureFree_VectorisnewAda.Unchecked_Deallocation (Object => Vector, Name => Vector_Ref); VA, VB: Vector_Ref; V : Vector;beginVA :=newVector (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 erroneousendDeallocation_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 用於定義時,它們會授予讀寫訪問許可權。
typeDay_Of_Monthisrange1 .. 31;typeDay_Of_Month_AccessisaccessallDay_Of_Month;
訪問常量
[edit | edit source]授予對被引用物件只讀訪問許可權的通用訪問型別使用關鍵字constant 在其定義中。被引用物件可以是常量或變數。
typeDay_Of_Monthisrange1 .. 31;typeDay_Of_Month_AccessisaccessconstantDay_Of_Month;
一些示例
[edit | edit source]typeGeneral_PointerisaccessallInteger;typeConstant_PointerisaccessconstantInteger; I1:aliasedconstantInteger := 10; I2:aliasedInteger; 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:constantGeneral_Pointer := I2'Access; -- read and write only to I2
匿名訪問
[edit | edit source]匿名訪問型別也有兩種版本,類似於通用訪問型別,分別授予讀寫訪問許可權或只讀訪問許可權,具體取決於關鍵字constant 是否出現。
匿名訪問可以用作子程式的引數或作為區分符。以下是一些示例
procedureModify (Some_Day:accessDay_Of_Month);procedureTest (Some_Day:accessconstantDay_Of_Month); -- Ada 2005 only
tasktypeThread (Execute_For_Day:accessDay_Of_Month)is...endThread;
typeDay_Data (Store_For_Day:accessDay_Of_Month)isrecord-- componentsendrecord;
在使用匿名訪問之前,您應該考慮命名訪問型別,或者更好的是,考慮是否“out”或“in out”修飾符更合適。
此語言功能僅從 Ada 2005 開始可用。
在 Ada 2005 中,匿名訪問在更多情況下是允許的
typeObjectisrecordM : Integer; Next:accessObject;endrecord; X:accessInteger;functionFreturnaccessconstantFloat;
隱式解引用
[edit | edit source]此語言功能已在 Ada 2012 中引入。
Ada 2012 使用新的語法簡化了透過指標訪問物件。
假設您有一個包含某種元素的容器。
typeContainerisprivate;typeElement_PtrisaccessElement;procedurePut (X: Element; Into:inoutContainer);
現在,如何訪問儲存在容器中的元素。當然,您可以透過
functionGet (From: Container)returnElement;
來檢索它們,但這會複製元素,如果元素很大,這是不利的。您可以使用
functionGet (From: Container)returnElement_Ptr;
獲得直接訪問許可權,但指標很危險,因為您很容易建立懸空指標,例如
P: Element_Ptr := Get (Cont);
P.all := E;
Free (P);
... Get (Cont) -- this is now a dangling pointer
使用訪問器物件而不是訪問型別可以防止意外釋放(這仍然是 Ada 2005)
typeAccessor (Data:notnullaccessElement)islimitedprivate; -- read/write accessfunctionGet (From: Container)returnAccessor;
(對於空排除not null 在區分符的宣告中,請參見下文)。透過此類訪問器進行的訪問是安全的:區分符只能用於解引用,不能將其複製到 Element_Ptr 型別的物件,因為其訪問級別更深。在上面的形式中,訪問器提供了讀寫訪問許可權。如果新增關鍵字constant,則只能進行讀訪問。
typeAccessor (Data:notnullaccessconstantElement)islimitedprivate; -- 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 方面
typeAccessor (Data:notnullaccessElement)islimitedprivatewithImplicit_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 修改,此類子型別的物件永遠不會有空值,因此必須進行初始化。
typeDay_Of_Month_AccessisaccessDay_Of_Month;subtypeDay_Of_Month_Not_Null_AccessisnotnullDay_Of_Month_Access;
該語言還允許直接使用空排除宣告第一個子型別
typeDay_Of_Month_AccessisnotnullaccessDay_Of_Month;
但是,在幾乎所有情況下,這不是一個好主意,因為它會使該型別物件的可用性變得很差(例如,您無法釋放分配的記憶體)。非空訪問旨在用於訪問子型別、物件宣告和子程式引數。[1]
訪問子程式
[edit | edit source]訪問子程式允許呼叫方呼叫 子程式,而無需知道其名稱或宣告位置。這種訪問方式的一種應用是眾所周知的回撥。
typeCallback_Procedureisaccessprocedure(Id : Integer; Text: String);typeCallback_Functionisaccessfunction(The_Alarm: Alarm)returnNatural;
要獲取對子程式的訪問權,需要將屬性 Access 應用於子程式名稱,並使用適當的引數和結果概要。
procedure Process_Event (Id : Integer;
Text: String);
My_Callback: Callback_Procedure := Process_Event'Access;
此語言功能僅從 Ada 2005 開始可用。
procedureTest (Call_Back:accessprocedure(Id: Integer; Text: String));
現在,一個序列中關鍵字的數量不再受限制。
functionFreturnaccessfunctionreturnaccessfunctionreturnaccessSome_Type;
這是一個函式,它返回對一個函式的訪問,該函式又返回對一個函式的訪問,該函式返回對某種型別的訪問。
關於 Ada 的訪問型別,一些 "常見問題" 和 "常見問題" (主要來自 C 使用者)。
一個訪問 all 可以執行任何一個簡單的access 可以執行的操作。因此有人可能會問:"為什麼還要使用簡單的access 呢?" - 實際上,一些程式設計師從來不使用簡單的訪問.
Unchecked_Deallocation 如果使用不當,始終是危險的。將池特定的物件釋放兩次和釋放堆疊物件一樣容易,也同樣危險。"訪問所有" 的優勢在於,你可能根本不需要使用 Unchecked_Deallocation。
道德:如果你有(或可能會有)將 '訪問或 'Unchecked_Access 儲存到訪問物件的有效理由,那麼使用 "訪問所有" 並且不要擔心。如果沒有,"最小許可權" 的口號建議應該省略 "所有" (不要啟用你不會使用的功能)。
以下(可能災難性的)示例將嘗試釋放一個堆疊物件
declaretypeDay_Of_Monthisrange1 .. 31;typeDay_Of_Month_AccessisaccessallDay_Of_Month;procedureFreeisnewAda.Unchecked_Deallocation (Object => Day_Of_Month, Name => Day_Of_Month_Access); A :aliasedDay_Of_Month; Ptr: Day_Of_Month_Access := A'Access;beginFree(Ptr);end;
使用一個簡單的access 你至少知道你不會嘗試釋放一個堆疊物件。原因是access 不允許從堆疊物件建立指標。
訪問可以與一個簡單的記憶體地址不同,它可能包含更多內容。例如,"對字串的訪問" 通常還需要某種方法來儲存字串大小。如果你需要一個簡單的地址並且不關心強型別,請使用 System.Address 型別。
建立 C 相容訪問的正確方法是使用pragma Convention
typeDay_Of_Monthisrange1 .. 31;forDay_Of_Month'SizeuseInterfaces.C.int'Size;pragmaConvention (Convention => C, Entity => Day_Of_Month);typeDay_Of_Month_AccessisaccessDay_Of_Month;pragmaConvention (Convention => C, Entity => Day_Of_Month_Access);
pragma Convention 應該用於你想要在 C 中使用的任何型別。如果該型別無法與 C 相容,編譯器會發出警告。
在宣告 Day_Of_Month 時,你也可以考慮以下更短的替代方法
typeDay_Of_MonthisnewInterfaces.C.intrange1 .. 31;
在 C 中使用訪問型別之前,你應該考慮使用普通的 "in"、"out" 和 "in out" 修飾符。pragma Export 和pragma Import 知道引數通常如何在 C 中傳遞,並且會在 C 使用指標傳遞引數的情況下自動使用指標來傳遞引數。當然,RM 包含關於何時為 "in"、"out" 和 "in out" 使用指標的精確規則 - 請參閱 "B.3: Interfacing with C [Annotated]"。
雖然實際上是 "與 C 互動" 的問題,這裡有一些可能的解決方案
procedureTestissubtypePvoidisSystem.Address; -- the declaration in C looks like this: -- int C_fun(int *)functionC_fun (pv: Pvoid)returnInteger;pragmaImport (Convention => C, Entity => C_fun, -- any Ada name External_Name => "C_fun"); -- the C name Pointer: Pvoid; Input_Parameter:aliasedInteger := 32; Return_Value : Integer;beginPointer := Input_Parameter'Address; Return_Value := C_fun (Pointer);endTest;
可移植性較差,但可能更易用 (對於 32 位 CPU)
typevoidismod2 ** 32;forvoid'Sizeuse32;
使用 GNAT,你可以透過使用以下方法獲得 32/64 位可移植性
typevoidismodSystem.Memory_Size;forvoid'SizeuseSystem.Word_Size;
更接近 void 的本質 - 指向大小為零的元素的指標是指向空記錄的指標。這也具有為 void 和 void* 提供表示的優勢
typeVoidisnullrecord;pragmaConvention (C, Void);typeVoid_PtrisaccessallVoid;pragmaConvention (C, Void_Ptr);
訪問型別和地址之間的區別將在下面詳細說明。使用術語 指標 是因為這是常用的術語。
有一個預定義的單元 System.Address_to_Access_Conversion 用於在訪問值和地址之間來回轉換。請謹慎使用這些轉換,如下文所述。
瘦指標允許訪問約束子型別。
typeIntisrange-100 .. +500;typeAcc_IntisaccessInt;typeArrisarray(1 .. 80)ofCharacter;typeAcc_ArrisaccessArr;
此類子型別的物件具有靜態大小,因此只需一個簡單的地址即可訪問它們。在陣列的情況下,這通常是第一個元素的地址。
對於這種型別的指標,使用 System.Address_to_Access_Conversion 是安全的。
typeUncisarray(Integerrange<>)ofCharacter;typeAcc_UncisaccessUnc;
子型別 Unc 的物件需要約束,即起始和終止索引,因此指向它們的指標也需要包含這些索引。因此,像第一個元件的地址這樣的簡單地址是不夠的。請注意,對於任何陣列物件,A'Address 與 A(A'First)'Address 相同。
對於這種型別的指標,System.Address_to_Access_Conversion 可能無法正常工作。
CO:aliasedUnc (-1 .. +1) := (-1 .. +1 => ' '); UO:aliasedUnc := (-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。在非法情況下,指定子型別和名義子型別在靜態上不匹配。
- 4.8: 分配器 [註釋]
- 13.11: 儲存管理 [註釋]
- 13.11.2: 未檢查儲存釋放 [註釋]
- 3.7: 辨別式 [註釋]
- 3.10: 訪問型別 [註釋]
- 6.1: 子程式宣告 [註釋]
- B.3: 與 C 互動 [註釋]
- 4.8: 分配器 [註釋]
- 13.11: 儲存管理 [註釋]
- 13.11.2: 未檢查儲存釋放 [註釋]
- 3.7: 辨別式 [註釋]
- 3.10: 訪問型別 [註釋]
- 6.1: 子程式宣告 [註釋]
- B.3: 與 C 互動 [註釋]
- 3.10: 訪問型別 [註釋]
- 7.6: 賦值和終結 [註釋]
- 7.6.1: 完成和終結 [註釋]
- 13.11: 儲存管理 [註釋]
