Ada 樣式指南/程式設計實踐
軟體總是處於變化之中。這種變化的需求,委婉地說叫做“維護”,來自各種來源。錯誤需要在發現時被糾正。系統功能可能需要以計劃或非計劃的方式進行增強。不可避免的是,在系統的整個生命週期中,需求都會發生變化,迫使系統不斷演進。通常,這些修改是在軟體最初編寫很久之後進行的,通常由除最初編寫者之外的人進行。
輕鬆、成功的修改要求軟體可讀、易懂,並按照既定的實踐進行結構化。如果一個軟體元件不能被熟悉其預期功能的程式設計師輕易理解,那麼該軟體元件就不是可維護的。使程式碼可讀和易懂的技術可以提高其可維護性。前面的章節介紹了諸如一致地使用命名約定、清晰且組織良好的註釋以及適當的模組化等技術。本章介紹了對語言特性的持續使用和邏輯使用。
正確性是可靠性的一個方面。雖然樣式指南無法強制使用正確的演算法,但它們可以建議使用已知可以減少錯誤數量或可能性或透過定義錯誤發生時的行為來提高程式可預測性的技術和語言特性。這些技術包括可以減少錯誤可能性或透過定義錯誤發生時的行為來提高程式可預測性的程式構造方法。
Ada 語法中的部分內容雖然是可選的,但可以增強程式碼的可讀性。以下給出的指南涉及對其中一些可選功能的使用。
- 當迴圈巢狀時,將名稱與迴圈關聯(Booch 1986、1987)。
- 將名稱與包含任何exit語句的迴圈關聯。
Process_Each_Page:
loop
Process_All_The_Lines_On_This_Page:
loop
...
exit Process_All_The_Lines_On_This_Page when Line_Number = Max_Lines_On_Page;
...
Look_For_Sentinel_Value:
loop
...
exit Look_For_Sentinel_Value when Current_Symbol = Sentinel;
...
end loop Look_For_Sentinel_Value;
...
end loop Process_All_The_Lines_On_This_Page;
...
exit Process_Each_Page when Page_Number = Maximum_Pages;
...
end loop Process_Each_Page;
當您將名稱與迴圈關聯時,您必須將該名稱與該迴圈的關聯
endexit一起包含(Ada 參考手冊 1995)。這有助於讀者找到任何給定迴圈的關聯
end
。這在迴圈跨越螢幕或頁面邊界時尤其重要。為迴圈選擇一個好名字可以記錄其目的,減少對解釋性註釋的需求。如果為迴圈選擇名稱非常困難,這可能表明需要對演算法進行更多思考。
定期為迴圈命名有助於您遵循指南 5.1.3。即使在程式碼發生更改的情況下,例如,新增外部或內部迴圈,指南
end- 語句也不會變得模稜兩可。
示例
可能很難為每個迴圈想出一個名稱;因此,指南指定了巢狀迴圈。在可讀性和二次思考方面的益處超過了為迴圈命名帶來的不便。Trip:
declare
...
begin -- Trip
Arrive_At_Airport:
declare
...
begin -- Arrive_At_Airport
Rent_Car;
Claim_Baggage;
Reserve_Hotel;
...
end Arrive_At_Airport;
Visit_Customer:
declare
...
begin -- Visit_Customer
-- again a set of activities...
...
end Visit_Customer;
Departure_Preparation:
declare
...
begin -- Departure_Preparation
Return_Car;
Check_Baggage;
Wait_For_Flight;
...
end Departure_Preparation;
Board_Return_Flight;
end Trip;
原理
塊名稱[編輯 | 編輯原始碼][編輯 | 編輯原始碼]當塊巢狀時,將名稱與塊關聯。
- 在所有exit來自巢狀迴圈的語句中使用迴圈名稱。
參見 5.1.1 中的示例。
一個exit語句是一個隱式的goto. 它應該明確地指定其源。當存在巢狀迴圈結構並且一個exit語句被使用時,可能很難確定退出的是哪個迴圈。此外,將來可能引入巢狀迴圈的更改很可能會引入錯誤,因為exit意外地從錯誤的迴圈中退出。命名迴圈及其退出將減輕這種混亂。如果巢狀迴圈跨越螢幕或頁面邊界,此指南也很有用。
- 在包規範和主體末尾包含定義的程式單元名稱。
- 在任務規範和主體末尾包含定義的識別符號。
- 在accept語句的迴圈關聯。
- 末尾包含入口識別符號。
- 在子程式主體末尾包含設計器。
示例
在受保護單元宣告末尾包含定義的識別符號。------------------------------------------------------------------------
package Autopilot is
function Is_Engaged return Boolean;
procedure Engage;
procedure Disengage;
end Autopilot;
------------------------------------------------------------------------
package body Autopilot is
...
---------------------------------------------------------------------
task Course_Monitor is
entry Reset (Engage : in Boolean);
end Course_Monitor;
---------------------------------------------------------------------
function Is_Engaged return Boolean is
...
end Is_Engaged;
---------------------------------------------------------------------
procedure Engage is
...
end Engage;
---------------------------------------------------------------------
procedure Disengage is
...
end Disengage;
---------------------------------------------------------------------
task body Course_Monitor is
...
accept Reset (Engage : in Boolean) do
...
end Reset;
...
end Course_Monitor;
---------------------------------------------------------------------
end Autopilot;
------------------------------------------------------------------------
[編輯 | 編輯原始碼][編輯 | 編輯原始碼]在這些複合語句末尾重複名稱可確保程式碼的一致性。此外,命名的
如果單元跨越頁面或螢幕邊界,或者如果它包含巢狀單元,則為讀者提供參考。
引數列表
子程式或入口引數列表是子程式或入口實現的抽象的介面。重要的是它清晰且以一致的風格表達。關於形式引數命名和排序的慎重決定可以使子程式的目的更容易理解,從而可以使子程式更容易使用。
形式引數示例
用描述性的名稱命名形式引數,以減少對註釋的需求。List_Manager.Insert (Element => New_Employee,
Into_List => Probationary_Employees,
At_Position => 1);
遵循形式引數的變數命名指南 ( 3.2.1 和 3.2.3 ) 可以使對子程式的呼叫更像普通散文,如上面的示例所示,其中不需要任何註釋。這種型別的描述性名稱還可以使子程式主體中的程式碼更清晰。
命名關聯為可選引數提供非預設值時使用命名關聯。
例項化示例
在從單個原始檔中的不到五個地方呼叫或具有超過兩個形式引數的子程式或入口的呼叫中使用命名引數關聯。Encode_Telemetry_Packet (Source => Power_Electronics,
Content => Temperature,
Value => Read_Temperature_Sensor(Power_Electronics),
Time => Current_Time,
Sequence => Next_Packet_ID,
Vehicle => This_Spacecraft,
Primary_Module => True);
很少使用或具有許多形式引數的子程式或入口的呼叫,如果不參考子程式或入口程式碼就可能難以理解。命名引數關聯可以使這些呼叫更具可讀性。
當形式引數已適當地命名時,可以更輕鬆地確定子程式的確切用途,而無需檢視其程式碼。這減少了僅為使呼叫更具可讀性而存在的命名常量的需求。它還允許用作實際引數的變數被賦予指示其用途的名稱,而無需考慮它們在呼叫中傳遞的原因。實際引數(是表示式而不是變數)不能以其他方式命名。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
筆記
命名引數關聯是否提高可讀性的判斷是主觀的。當然,簡單的或熟悉的子程式,例如交換例程或正弦函式,不需要在過程呼叫中使用命名關聯的額外說明。
警告
命名引數關聯的結果是,形式引數名稱可能無法在不修改每個呼叫文字的情況下更改。
預設引數指南
[edit | edit source]- 提供預設引數,以便偶爾對廣泛使用的子程式或條目進行特殊使用。
- 將預設引數放在形式引數列表的末尾。
- 考慮為新增到現有子程式的新引數提供預設值。
示例
[edit | edit source]Ada 參考手冊 (1995) 包含許多關於這種實踐的示例。
原理
[edit | edit source]通常,子程式或條目的大多數使用都需要對給定引數使用相同的值。提供該值作為引數的預設值,會使引數在大多數呼叫中成為可選的。它還允許剩餘的呼叫透過為該引數提供不同的值來自定義子程式或條目。
將預設引數放在形式引數列表的末尾,允許呼叫者在呼叫時使用位置關聯;否則,只有在使用命名關聯時,預設值才可用。
通常在維護活動期間,您會增加子程式或條目的功能。這需要比原始形式為某些呼叫提供更多引數。可能需要新的引數來控制此新功能。為新引數提供指定舊功能的預設值。需要舊功能的呼叫不需要更改;它們採用預設值。如果將新引數新增到引數列表的末尾,或者在所有呼叫中使用命名關聯,則情況也是如此。需要新功能的新呼叫可以透過為新引數提供其他值來指定該功能。
這提高了可維護性,因為使用修改後的例程的位置本身不需要修改,而例程的先前功能級別可以“重用”。
例外情況
[edit | edit source]不要過度。如果功能上的變化非常激進,那麼您應該準備一個單獨的例程,而不是修改現有的例程。這種情況的一個指標是難以確定用於預設值的價值組合,這些組合唯一且自然地需要兩種功能中更嚴格的一種。在這種情況下,最好繼續建立單獨的例程。
模式指示
[edit | edit source]指南
[edit | edit source]- 顯示所有過程和條目引數的模式指示(Nissen 和 Wallis 1984)。
- 使用適用於您的應用程式的最嚴格的引數模式。
示例
[edit | edit source]procedure Open_File (File_Name : in String;
Open_Status : out Status_Codes);
entry Acquire (Key : in Capability;
Resource : out Tape_Drive);
原理
[edit | edit source]透過顯示引數的模式,您可以幫助閱讀者。如果您沒有指定引數模式,則預設模式為in。明確顯示所有引數的模式指示比僅僅使用預設模式是一種更肯定的操作。任何以後審查程式碼的人都會更有信心,您希望引數模式為in.
使用反映引數實際使用的模式。您應該避免將所有引數都設定為in out模式的傾向,因為out模式引數既可以檢查也可以更新。
例外情況
[edit | edit source]可能需要考慮給定抽象的幾種替代實現。例如,有界堆疊可以實現為指向陣列的指標。即使對被指向物件的更新不需要更改指標值本身,您可能也希望考慮將模式設為in out以允許對實現進行更改,並更準確地記錄操作正在執行的操作。如果您以後將實現更改為簡單陣列,則模式將必須為in out,這可能會導致對呼叫例程的所有位置進行更改。
型別
[edit | edit source]除了確定變數和子型別名稱的可能值外,型別區分還可以成為開發安全、可讀和易於理解程式碼的寶貴工具。型別闡明瞭資料的結構,可以限制或限制對該資料執行的操作。 “保持型別區分已被證明是發現程式編寫時邏輯錯誤的非常有效的手段,並在程式隨後維護時提供寶貴的幫助”(Pyle 1985)。利用 Ada 的強型別功能,例如子型別、派生型別、任務型別、受保護型別、私有型別和受限私有型別。
這些指南鼓勵編寫大量程式碼以確保強型別。雖然可能看起來這種程式碼量會帶來執行效能上的損失,但實際上通常並非如此。與其他傳統語言不同,Ada 在編寫的程式碼量與生成的執行程式的大小之間沒有直接的關係。大多數強型別檢查是在編譯時而不是執行時執行的,因此執行程式碼的大小不會受到很大影響。
有關特定型別的資料結構和標記型別的指南,請分別參見 9.2.1。
派生型別和子型別
[edit | edit source]指南
[edit | edit source]- 透過從現有型別派生新型別來使用現有型別作為構建塊。
- 對子型別使用範圍約束。
- 定義新型別,尤其是派生型別,以包含最大可能的價值集合,包括邊界值。
- 使用子型別限制派生型別的範圍,排除邊界值。
- 當沒有有意義的元件新增到型別時,使用型別派生而不是型別擴充套件。
示例
[edit | edit source]型別Table是建立新型別的構建塊
type Table is
record
Count : List_Size := Empty;
List : Entry_List := Empty_List;
end record;
type Telephone_Directory is new Table;
type Department_Inventory is new Table;
以下是不能在未明確程式設計使用它們的運算中混合使用的不同型別
type Dollars is new Number;
type Cents is new Number;
下面,Source_Tail的值在Listing_Paper的範圍內,當行為空時。只要結果落在正確的子類型範圍內,所有索引都可以在表示式中混合使用
type Columns is range First_Column - 1 .. Listing_Width + 1;
subtype Listing_Paper is Columns range First_Column .. Listing_Width;
subtype Dumb_Terminal is Columns range First_Column .. Dumb_Terminal_Width;
type Line is array (Columns range <>) of Bytes;
subtype Listing_Line is Line (Listing_Paper);
subtype Terminal_Line is Line (Dumb_Terminal);
Source_Tail : Columns := Columns'First;
Source : Listing_Line;
Destination : Terminal_Line;
...
Destination(Destination'First .. Source_Tail - Destination'Last) :=
Source(Columns'Succ(Destination'Last) .. Source_Tail);
原理
[edit | edit source]派生型別的名稱可以清楚地表明其預期用途,並避免類似型別定義的激增。兩個派生型別的物件,即使派生自相同的型別,也不能在操作中混合使用,除非明確提供這些操作,或者其中一個被明確轉換為另一個。這種禁止是強型別的強制執行。
謹慎而有意地定義新型別、派生型別和子型別。子型別和派生型別不是等效的概念,但它們可以協同使用以獲得優勢。子型別限制了型別可能值的範圍,但沒有定義新型別。
型別可以具有高度受限的值集,而不會消除有用的值。協同使用派生型別和子型別可以消除可執行語句中的許多標誌變數和型別轉換。這使程式更具可讀性,強制執行抽象,並允許編譯器強制執行強型別約束。
許多演算法以正常範圍之外的值開始或結束。如果邊界值在子表示式中不相容,演算法可能會變得不必要地複雜。當程式可以簡單地測試零或其他哨兵值(位於正常範圍之外)時,程式可能會因標誌變數和特殊情況而變得雜亂無章。
型別Columns和子型別Listing_Paper在上面的示例中演示瞭如何允許哨兵值。子型別Listing_Paper可以用作在包規範中宣告的子程式引數的型別。這將限制呼叫者可以指定的值得範圍。同時,型別Columns可以用作在包體中內部儲存此類值,允許First_Column - 1用作哨兵值。這種型別和子型別的組合允許子表示式中的子型別之間相容,而無需型別轉換(就像使用派生型別時那樣)。
型別派生和型別擴充套件之間的選擇取決於您希望對型別中的物件進行何種更改。一般來說,型別派生是一種非常簡單的繼承形式:派生型別繼承了父型別的結構、操作和值(Rationale 1995,§4.2)。雖然您可以新增操作,但不能擴充資料結構。您可以從標量型別或複合型別派生。
型別擴充套件是一種更強大的繼承形式,僅適用於標記記錄,您可以在其中擴充型別的元件和操作。當記錄實現具有重用和/或擴充套件潛力的抽象時,它是將其設定為標記的良好候選者。類似地,如果抽象是具有明確定義變數和通用屬性的抽象系列的成員,則應考慮使用標記記錄。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]減少獨立型別宣告數量的代價是,當基本型別重新定義時,子型別和派生型別會發生變化。這種級聯變化有時是福,有時是禍。但是,通常它是有意且有益的。
匿名型別
[edit | edit source]指南
[edit | edit source]- 避免使用匿名陣列型別。
- 僅當不存在或無法建立合適的型別,並且陣列不會被整體引用(例如,用作子程式引數)時,才為陣列變數使用匿名陣列型別。
- 使用訪問引數和訪問判別式來確保引數或判別式被視為常量。
示例
[edit | edit source]使用
type Buffer_Index is range 1 .. 80;
type Buffer is array (Buffer_Index) of Character;
Input_Line : Buffer;
而不是
Input_Line : array (Buffer_Index) of Character;
原理
[edit | edit source]雖然 Ada 允許匿名型別,但它們的使用有限,會使程式修改變得複雜。例如,除了陣列之外,匿名型別的變數永遠不能用作實際引數,因為不可能定義相同型別的形式引數。即使這可能不是最初的限制,它也排除了將程式碼段更改為子程式的修改。雖然您可以將匿名陣列宣告為別名,但您不能將此訪問值用作子程式中的實際引數,因為子程式的形式引數宣告需要型別標記。此外,使用相同的匿名型別宣告宣告的兩個變數實際上是不同型別的。
即使 Ada 支援引數傳遞期間的陣列型別隱式轉換,也很難證明不使用引數型別。在大多數情況下,引數型別是可見的,並且可以輕鬆地替代匿名陣列型別。使用匿名陣列型別意味著陣列僅用作實現值集合的便捷方式。使用匿名型別,然後將變數視為物件,這是具有誤導性的。
當您使用訪問引數或訪問判別式時,匿名型別本質上是在子程式或物件本身內宣告的(Rationale 1995,§3.7.1)。因此,您無法宣告相同型別的其他物件,並且物件被視為常量。對於自引用資料結構(參見指南 5.4.6),您需要訪問引數才能操作判別式訪問的資料(Rationale 1995,§3.7.1)。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]有關匿名任務型別,請參見指南 6.1.4。
例外情況
[edit | edit source]如果您要建立一個唯一的表,例如元素週期表,請考慮使用匿名陣列型別。
私有型別
[edit | edit source]指南
[edit | edit source]- 優先從受控型別派生,而不是使用受限私有型別。
- 優先使用受限私有型別,而不是私有型別。
- 優先使用私有型別,而不是非私有型別。
- 顯式匯出所需的操作,而不是放寬限制。
示例
[edit | edit source]------------------------------------------------------------------------
with Ada.Finalization;
package Packet_Telemetry is
type Frame_Header is new Ada.Finalization.Controlled with private;
type Frame_Data is private;
type Frame_Codes is (Main_Bus_Voltage, Transmitter_1_Power);
...
private
type Frame_Header is new Ada.Finalization.Controlled with
record
...
end record;
-- override adjustment and finalization to get correct assignment semantics
procedure Adjust (Object : in out Frame_Header);
procedure Finalize (Object : in out Frame_Header);
type Frame_Data is
record
...
end record;
...
end Packet_Telemetry;
------------------------------------------------------------------------
原理
[edit | edit source]受限私有型別和私有型別比非私有型別更好地支援抽象和資訊隱藏。型別越受限制,資訊隱藏就越好。反過來,這使得實現能夠更改,而不會影響程式的其餘部分。雖然有許多正當理由匯出型別,但最好先嚐試首選路線,僅在必要時放寬限制。如果包的使用者需要使用幾個受限操作,最好透過匯出的子程式顯式地單獨匯出操作,而不是降低限制級別。這種做法保留了其他操作的限制。
受限私有型別具有一組最受限制的操作,這些操作可供包的使用者使用。在必須提供給包使用者的型別中,儘可能多地應從受控型別或受限私有型別派生。受控型別使您能夠調整賦值和最終確定值,因此您不再需要建立受限私有型別來保證客戶端賦值和相等性服從深層複製/比較語義。因此,可以匯出一個稍微不太受限制的型別(即擴充套件Ada.Finalization.Controlled)的私有型別,它具有可調整的賦值運算子和可覆蓋的相等運算子。另請參見指南 5.4.5。
受限私有型別可供使用者使用的操作是成員測試、選定元件、任何判別式的選擇元件、限定和顯式轉換,以及屬性'Base和'Size. 受限私有型別的物件還具有屬性'Constrained(如果有判別式)。這些操作都不允許包使用者以依賴型別結構的方式操作物件。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]預定義包Direct_IO和Sequential_IO不接受有限私有型別作為泛型引數。在需要對型別進行 I/O 操作時,應考慮此限制。
有關在泛型單元中使用私有型別和有限私有型別的討論,請參見指南 8.3.3。
- 使用訪問子程式型別來間接訪問子程式。
- 在可能的情況下,使用抽象標記型別和分派,而不是訪問子程式型別來實現子程式的動態選擇和呼叫。
以下示例摘自《原理》(1995 年,第 3.7.2 節)
generic
type Float_Type is digits <>;
package Generic_Integration is
type Integrand is access function (X : Float_Type) return Float_Type;
function Integrate (F : Integrand;
From : Float_Type;
To : Float_Type;
Accuracy : Float_Type := 10.0*Float_Type'Model_Epsilon)
return Float_Type;
end Generic_Integration;
with Generic_Integration;
procedure Try_Estimate (External_Data : in Data_Type;
Lower : in Float;
Upper : in Float;
Answer : out Float) is
-- external data set by other means
function Residue (X : Float) return Float is
Result : Float;
begin -- Residue
-- compute function value dependent upon external data
return Result;
end Residue;
package Float_Integration is
new Generic_Integration (Float_Type => Float);
use Float_Integration;
begin -- Try_Estimate
...
Answer := Integrate (F => Residue'Access,
From => Lower,
To => Upper);
end Try_Estimate;
訪問子程式型別允許您建立包含子程式引用的資料結構。此功能有許多用途,例如實現狀態機、X 視窗系統中的回撥、迭代器(應用於列表中每個元素的操作)以及數值演算法(例如積分函式)(《原理》,1995 年,第 3.7.2 節)。
您可以透過使用抽象標記型別來實現與訪問子程式型別相同的動態選擇效果。您可以宣告一個帶有一個抽象操作的抽象型別,然後使用訪問類範圍型別來獲得分派效果。與訪問子程式型別相比,這種技術提供了更大的靈活性和型別安全性(Ada 語言參考手冊 1995 年,第 3.10.2 節 [註釋])。
訪問子程式型別在實現動態選擇方面很有用。子程式的引用可以直接儲存在資料結構中。例如,在有限狀態機中,一個數據結構可以描述在狀態轉換時要採取的操作。由於 Ada 95 要求指定的子程式具有與子程式訪問型別中指定的引數/結果配置檔案相同的配置檔案,因此可以維護強型別檢查。
另請參見指南 7.3.2。
Ada 的資料結構化功能是一種強大的資源;因此,使用它們儘可能地對資料進行建模。可以對邏輯相關的組資料,並讓語言控制資料的抽象和操作,而不是要求程式設計師或維護人員這樣做。資料也可以以構建塊的方式組織。除了顯示資料結構的組織方式(並可能向讀者暗示為什麼以這種方式組織)之外,從更小的元件建立資料結構還允許重用這些元件。使用 Ada 提供的功能可以提高程式碼的可維護性。
- 在宣告判別式時,使用盡可能受約束的子型別(即,具有儘可能具體的範圍約束的子型別)。
- 使用帶判別的記錄,而不是受約束的陣列來表示實際值不受約束的陣列。
型別為Name_Holder_1的物件可能包含長度為Natural'Last:
type Number_List is array (Integer range <>) of Integer;
type Number_Holder_1 (Current_Length : Natural := 0) is
record
Numbers : Number_List (1 .. Current_Length);
end record;
型別為的字串Name_Holder_2
type Number_List is array (Integer range <>) of Integer;
subtype Max_Numbers is Natural range 0 .. 42;
type Number_Holder_2 (Current_Length : Max_Numbers := 0) is
record
Numbers : Number_List (1 .. Current_Length);
end record;
原理
對字串元件的長度施加了更合理的限制當您使用判別式來約束帶判別的記錄內的陣列時,判別式可以取值的範圍越大,型別的物件可能需要的空間就越大。儘管您的程式可以編譯和連結,但當執行時系統無法建立所需潛在大小的物件時,它會在執行時失敗。帶判別的記錄捕獲了邊界在執行時可能變化的陣列的意圖。簡單的受約束的陣列定義(例如,type Number_List is array (1 .. 42) of Integer;
) 無法捕獲列表中最多可能只有 42 個數字的意圖。
異構相關資料示例
考慮將記錄對映到 I/O 裝置資料。type Propulsion_Method is (Sail, Diesel, Nuclear);
type Craft is
record
Name : Common_Name;
Plant : Propulsion_Method;
Length : Feet;
Beam : Feet;
Draft : Feet;
end record;
type Fleet is array (1 .. Fleet_Size) of Craft;
透過將相關資料收集到同一個構造中,您可以幫助維護人員找到所有相關資料,簡化對所有資料而不是部分資料的任何修改。這反過來又會提高可靠性。您或未知的維護人員都不太可能忘記在可執行語句中處理所有資訊片段,尤其是在儘可能使用聚合賦值進行更新的情況下。想法是將維護人員需要了解的資訊放在最容易找到的地方。例如,如果與給定Craft
相關的所有資訊都位於同一個地方,那麼這種關係在宣告中以及在訪問和更新該資訊的程式碼中就非常清楚。但是,如果它分散在幾個資料結構中,那麼就不太明顯這是有意關係還是偶然關係。在後一種情況下,宣告可以分組在一起以暗示意圖,但可能無法以這種方式分組訪問和更新程式碼。確保使用相同的索引訪問幾個並行陣列中的對應元素非常困難,尤其是在訪問分散的情況下。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
如果應用程式必須直接與硬體介面,則使用記錄,尤其是在與記錄表示子句結合使用時,可能有助於對映到相關硬體的佈局。
例外情況
將異構資料儲存在並行陣列中,這相當於 FORTRAN 風格,似乎是可取的。這種風格是 FORTRAN 資料結構化限制的產物。FORTRAN 只提供構建同構陣列的功能。
如果應用程式必須直接與硬體介面,並且硬體要求資訊分佈在各個位置,則可能無法使用記錄。
異構多型資料- 使用訪問型別指向類範圍內的型別來實現異構多型資料結構。
- 使用標記型別和型別擴充套件,而不是變體記錄(與列舉型別和 case 語句結合使用)。
一個型別為Employee_List的陣列可以包含指向兼職和全職員工的指標(將來可能還會包含其他型別的員工)。
-----------------------------------------------------------------------------------
package Personnel is
type Employee is tagged limited private;
type Reference is access all Employee'Class;
...
private
...
end Personnel;
-----------------------------------------------------------------------------------
with Personnel;
package Part_Time_Staff is
type Part_Time_Employee is new Personnel.Employee with
record
...
end record;
...
end Part_Time_Staff;
-----------------------------------------------------------------------------------
with Personnel;
package Full_Time_Staff is
type Full_Time_Employee is new Personnel.Employee with
record
...
end record;
...
end Full_Time_Staff;
-----------------------------------------------------------------------------------
...
type Employee_List is array (Positive range <>) of Personnel.Reference;
Current_Employees : Employee_List (1..10);
...
Current_Employees(1) := new Full_Time_Staff.Full_Time_Employee;
Current_Employees(2) := new Part_Time_Staff.Part_Time_Employee;
...
多型性是一種將一組抽象之間的差異提取出來的方法,以便程式可以根據共同的屬性進行編寫。多型性允許異構資料結構中不同的物件以相同的方式進行處理,基於對根標記型別上定義的排程操作。這樣就無需使用case語句來選擇每個特定型別所需的處理。指南 5.6.3 討論了使用case語句帶來的維護影響。
列舉型別、變體記錄和 case 語句難以維護,因為對資料型別特定變體的專業知識往往分散在整個程式中。當您建立標記型別層次結構(標記型別和型別擴充套件)時,可以避免變體記錄、case 語句和僅支援變體記錄判別的單個列舉型別。此外,透過將與單個操作相關的所有原語都呼叫共同的“操作特定”程式碼,您可以將有關變體的“專業知識”定位在資料結構內。
有關標記型別的更詳細討論,請參見指南 9.2.1。
在某些情況下,您可能希望使用變體記錄方法來圍繞操作組織模組化。例如,對於圖形輸出,您可能會發現使用變體記錄更易於維護。您必須權衡新增新操作是否比新增新變體工作量更小。
- 記錄結構不應總是扁平的。提取出公共部分。
- 對於大型記錄結構,將相關元件分組到較小的子記錄中。
- 對於巢狀記錄,選擇在引用內部元素時可讀性良好的元素名稱。
- 考慮使用型別擴充套件來組織大型資料結構。
type Coordinate is
record
Row : Local_Float;
Column : Local_Float;
end record;
type Window is
record
Top_Left : Coordinate;
Bottom_Right : Coordinate;
end record;
您可以透過將複雜資料結構組合成熟悉的構建塊來使其易於理解和理解。這種技術對於具有自然分組部分的大型記錄型別尤其有效。基於共同質量或目的而分解到單獨宣告的記錄中的元件,對應於比大型記錄所代表的更低級別的抽象。
在設計複雜資料結構時,您必須考慮型別組合或型別擴充套件是否是最合適的技術。型別組合指的是建立型別本身為記錄的記錄元件。您通常需要這兩種技術的混合,即,透過型別組合包含一些元件,透過型別擴充套件建立其他元件。如果“中間”記錄都是同一抽象家族的例項,則型別擴充套件可能會提供更簡潔的設計。另請參見指南 5.4.2 和 9.2.1。
仔細選擇的較大記錄元件的名稱(用於選擇較小的記錄),可以提高可讀性,例如
if Window1.Bottom_Right.Row > Window2.Top_Left.Row then . . .
- 區分靜態資料和動態資料。謹慎使用動態分配的物件。
- 僅當需要動態建立和銷燬動態分配的資料結構,或需要透過不同的名稱引用它們時,才使用動態分配的資料結構。
- 不要丟棄指向未分配物件的指標。
- 不要留下指向已分配物件的懸空引用。
- 初始化記錄中所有訪問變數和元件。
- 不要依賴記憶體釋放。
- 顯式釋放記憶體。
- 使用長度子句來指定總分配大小。
- 為Storage_Error.
- 提供處理程式。
- 使用受控型別來實現操作動態資料的私有型別。
- 除非您的執行時環境可靠地回收動態堆儲存,否則避免使用無約束記錄物件。
- 除非您的執行時環境可靠地回收動態堆儲存,否則僅在庫包、主子程式或永久任務的最外層、未巢狀的宣告部分宣告以下專案
- 訪問型別
- 具有非靜態邊界的約束複合物件
- 除無約束記錄之外的其他無約束複合型別物件
- 複合物件足夠大(在編譯時),以便編譯器在堆上隱式分配
- 除非您的執行時環境可靠地回收動態堆儲存,或者您正在建立永久的、動態分配的任務,否則請避免在以下情況下宣告任務
- 元件為任務的無約束陣列子型別
- 包含任務陣列的元件的判別記錄子型別,其中陣列大小取決於判別的值
- 除庫包或主子程式的最外層、未巢狀的宣告部分之外的任何宣告區域
示例
未靜態約束的任務陣列P1 := new Object;
P2 := P1;
Unchecked_Object_Deallocation(P2);
這些行展示瞭如何建立懸空引用。
X := P1.all;
由於引用了已分配物件,因此此行可能會引發異常。在以下三行中,如果P1
P1 := new Object;
...
P1 := P2;
的值沒有分配給任何其他指標,則第一行建立的物件在第三行之後將不再可訪問。指向已分配物件的唯一指標已被丟棄。以下程式碼展示了使用Finalize
with Ada.Finalization;
package List is
type Object is private;
function "=" (Left, Right : Object) return Boolean; -- element-by-element comparison
... -- Operations go here
private
type Handle is access List.Object;
type Object is new Ada.Finalization.Controlled with
record
Next : List.Handle;
... -- Useful information go here
end record;
procedure Adjust (L : in out List.Object);
procedure Finalize (L : in out List.Object);
end List;
package body List is
Free_List : List.Handle;
...
procedure Adjust (L : in out List.Object) is
begin
L := Deep_Copy (L);
end Adjust;
procedure Finalize (L : in out List.Object) is
begin
-- Chain L to Free_List
end Finalize;
end List;
原理
來確保在物件被終結(即超出範圍)時,動態分配的元素被連結到一個空閒列表上。[編輯 | 編輯原始碼]另請參見 6.3.2 中有關這些問題的變體。動態分配的物件是由分配器執行建立的物件(new
)。透過訪問變數引用的已分配物件允許您生成別名,即對同一物件的多個引用。當您透過另一個名稱引用已分配的物件時,可能會出現異常行為。這被稱為懸空引用。完全將仍然有效的物件與所有名稱分離被稱為丟棄指標。沒有與名稱關聯的動態分配物件無法被引用或顯式釋放。
丟棄指標依賴於隱式記憶體管理器來回收空間。它還引發了讀者對失去對物件的訪問是故意的還是意外的疑問。Ada 環境不需要提供動態分配物件的釋放。如果提供,它可能被隱式提供(當它們的訪問型別超出範圍時,物件會被釋放),顯式提供(當Ada.Unchecked_DeallocationAda 環境不需要提供動態分配物件的釋放。如果提供,它可能被隱式提供(當它們的訪問型別超出範圍時,物件會被釋放),顯式提供(當被呼叫時,物件會被釋放),或兩者兼而有之。為了提高儲存空間被回收的可能性,最好在您完成使用每個動態建立的物件時顯式呼叫Ada 環境不需要提供動態分配物件的釋放。如果提供,它可能被隱式提供(當它們的訪問型別超出範圍時,物件會被釋放),顯式提供(當。對
的呼叫還記錄了要放棄物件的故意決定,從而使程式碼更易於閱讀和理解。為了絕對確保空間被回收並重新使用,請管理您自己的“空閒列表”。跟蹤您已完成使用哪些物件,並在以後重新使用它們,而不是動態分配新物件。
懸空引用的危險在於您可能會嘗試使用它們,從而訪問已釋放給記憶體管理器的記憶體,該記憶體可能隨後已分配給程式中其他部分的另一個用途。當您從這樣的記憶體讀取資料時,可能會發生意外錯誤,因為程式的其他部分可能之前已在其中寫入完全無關的資料。更糟糕的是,當您寫入這樣的記憶體時,您可以透過更改該程式碼動態分配的變數的值來導致程式碼中看似無關部分的錯誤。這種型別的錯誤可能非常難以找到。最後,這些錯誤可能在您沒有編寫的環境部分中觸發,例如,在記憶體管理系統本身中,它可能動態分配記憶體以記錄有關您的動態分配記憶體的資訊。請記住,記錄或陣列的任何未重置元件也可能是懸空引用,或者可能承載表示不一致資料的位模式。訪問型別的元件始終預設初始化為null
無論何時使用動態分配,都有可能耗盡空間。Ada 提供了一種機制(長度子句)用於在編譯時請求分配空間池的大小。但請預期,在執行時仍有可能耗盡空間。為異常準備處理程式Storage_Error,並仔細考慮在每種情況下程式中可以包含哪些替代方案。
有一種觀點認為應該避免所有動態分配。這種觀點主要基於對執行過程中記憶體耗盡的恐懼。諸如長度子句和異常處理程式之類的機制為Storage_Error提供了對記憶體分割槽和錯誤恢復的顯式控制,使得這種恐懼毫無根據。
在實現複雜的資料結構(樹、列表、稀疏矩陣等)時,通常會使用訪問型別。如果不注意,可能會用這些動態分配的物件耗盡所有儲存空間。可以匯出一個釋放操作,但無法確保它會在適當的地方被呼叫;實際上,你是在信任客戶端。如果從受控型別派生(有關更多資訊,請參見 8.3.3 和 9.2.3),可以使用終結來處理動態資料的釋放,從而避免儲存耗盡。使用者定義的儲存池可以更好地控制分配策略。
一個相關但不同的問題是共享語義與複製語義:即使資料結構是使用訪問型別實現的,也不一定希望共享語義。在某些情況下,你真正想要的是 :=建立一個副本,而不是一個新的引用,並且你真正想要的是=比較內容,而不是引用。應該將結構實現為受控型別。如果需要複製語義,可以重新定義調整以執行深層複製,以及=以執行對內容的比較。還可以重新定義以下程式碼展示了使用以確保當物件被終結(即超出範圍)時,動態分配的元素被連結到一個空閒列表(或者透過Ada 環境不需要提供動態分配物件的釋放。如果提供,它可能被隱式提供(當它們的訪問型別超出範圍時,物件會被釋放),顯式提供(當).
Ada 程式在執行過程中對動態(堆)儲存的隱式使用會帶來重大風險,可能會導致軟體故障。Ada 執行時環境可能會在與複合物件、動態建立的任務和連線相關的操作中使用隱式動態(堆)儲存。通常,用於管理動態分配和回收堆儲存的演算法會導致碎片或洩漏,這會導致儲存耗盡。通常很難或不可能從儲存耗盡或Storage_Error中恢復,除非重新載入並重新啟動 Ada 程式。避免所有隱式分配的使用將非常嚴格。另一方面,阻止顯式和隱式釋放可以顯著降低碎片和洩漏的風險,而不會過度限制對複合物件、訪問值、任務物件和連線的使用。
例外情況
[edit | edit source]如果複合物件足夠大,可以分配到堆中,仍然可以將其宣告為in或in out形式引數。該指南旨在避免在物件宣告、形式out引數或函式返回的值中宣告物件。
應該監控堆中的洩漏和/或碎片。如果它們達到穩態,並且在程式或分割槽執行期間沒有持續增加,可以使用指南中描述的結構。
別名物件
[edit | edit source]指南
[edit | edit source]- 儘量減少對別名變數的使用。
- 對靜態建立的、不規則陣列使用別名(理由 1995,§3.7.1)。
- 當想要隱藏內部連線和簿記資訊時,使用別名來引用資料結構的一部分。
示例
[edit | edit source]package Message_Services is
type Message_Code_Type is range 0 .. 100;
subtype Message is String;
function Get_Message (Message_Code: Message_Code_Type)
return Message;
pragma Inline (Get_Message);
end Message_Services;
package body Message_Services is
type Message_Handle is access constant Message;
Message_0 : aliased constant Message := "OK";
Message_1 : aliased constant Message := "Up";
Message_2 : aliased constant Message := "Shutdown";
Message_3 : aliased constant Message := "Shutup";
. . .
type Message_Table_Type is array (Message_Code_Type) of Message_Handle;
Message_Table : Message_Table_Type :=
(0 => Message_0'Access,
1 => Message_1'Access,
2 => Message_2'Access,
3 => Message_3'Access,
-- etc.
);
function Get_Message (Message_Code : Message_Code_Type)
return Message is
begin
return Message_Table (Message_Code).all;
end Get_Message;
end Message_Services;
以下程式碼片段展示了別名物件的使用,使用屬性'Access來實現一個管理物件散列表的通用元件
generic
type Hash_Index is mod <>;
type Object is tagged private;
type Handle is access all Object;
with function Hash (The_Object : in Object) return Hash_Index;
package Collection is
function Insert (Object : in Collection.Object) return Collection.Handle;
function Find (Object : in Collection.Object) return Collection.Handle;
Object_Not_Found : exception;
...
private
type Cell;
type Access_Cell is access Cell;
end Collection;
package body Collection is
type Cell is
record
Value : aliased Collection.Object;
Link : Access_Cell;
end record;
type Table_Type is array (Hash_Index) of Access_Cell;
Table : Table_Type;
-- Go through the collision chain and return an access to the useful data.
function Find (Object : in Collection.Object;
Index : in Hash_Index) return Handle is
Current : Access_Cell := Table (Index);
begin
while Current /= null loop
if Current.Value = Object then
return Current.Value'Access;
else
Current := Current.Link;
end if;
end loop;
raise Object_Not_Found;
end Find;
-- The exported one
function Find (Object : in Collection.Object) return Collection.Handle is
Index : constant Hash_Index := Hash (Object);
begin
return Find (Object, Index);
end Find;
...
end Collection;
原理
[edit | edit source]別名允許程式設計師透過間接方式訪問宣告的物件。由於可以透過多個路徑更新別名物件,因此必須謹慎操作以避免意外更新。當將別名物件限制為常量時,可以避免物件被意外修改。在上面的示例中,單個訊息物件是被別名的常量訊息字串,因此它們的值不能更改。然後,不規則陣列被初始化為對每個常量字串的引用。
別名允許你透過間接方式操作物件,同時避免動態分配。例如,可以將物件插入到連結列表中,而無需動態分配該物件的空間(理由 1995,§3.7.1)。
別名的另一種用途是在連結資料結構中,試圖隱藏封閉容器。這本質上是自引用資料結構的逆運算(參見指南 5.4.7)。如果一個包使用連結資料結構管理一些資料,你可能只想匯出表示“有用”資料的訪問值。可以使用指向物件的訪問來返回指向有用資料的訪問,排除用於連結物件的指標。
訪問辨別式
[edit | edit source]指南
[edit | edit source]- 使用訪問辨別式建立自引用資料結構,即資料結構的其中一個元件指向封閉結構。
示例
[edit | edit source]參見指南 8.3.6(使用訪問辨別式構建迭代器)和 9.5.1(在多重繼承中使用訪問辨別式)中的示例。
原理
[edit | edit source]訪問辨別式本質上是一個匿名型別的指標,被用作辨別式。由於訪問辨別式是匿名訪問型別,因此無法宣告該型別的其他物件。因此,一旦初始化了辨別式,就會在辨別式及其訪問的物件之間建立一個“永久”(對於物件的生命週期)關聯。當建立自引用結構時,即結構的某個元件被初始化為指向封閉物件,訪問辨別式的“常量”行為會提供正確的行為,以幫助維護結構的完整性。
另見理由(1995,§4.6.3)中有關使用訪問辨別式實現物件的多個檢視的討論。
另見指南 6.1.3 中關於任務型別的訪問辨別式的示例。
模組型別
[edit | edit source]指南
[edit | edit source]- 當建立需要按位操作的資料結構時,使用模組型別而不是布林陣列,例如和和或.
示例
[edit | edit source]with Interfaces;
procedure Main is
type Unsigned_Byte is mod 255;
X : Unsigned_Byte;
Y : Unsigned_Byte;
Z : Unsigned_Byte;
X1 : Interfaces.Unsigned_16;
begin -- Main
Z := X or Y; -- does not cause overflow
-- Show example of left shift
X1 := 16#FFFF#;
for Counter in 1 .. 16 loop
X1 := Interfaces.Shift_Left (Value => X1, Amount => 1);
end loop;
end Main;
原理
[edit | edit source]當位元數已知少於一個字中的位元數和/或效能是一個嚴重問題時,首選模組型別。當位元數事先未知且效能不是一個嚴重問題時,布林陣列是合適的。另見指南 10.6.3。
表示式
[edit | edit source]正確編碼的表示式可以增強程式的可讀性和可理解性。編碼不當的表示式會將程式變成維護者的噩夢。
範圍值
[edit | edit source]指南
[edit | edit source]- 使用'第一個或'最後一個而不是使用數字字面量來表示範圍的第一個或最後一個值。
- 使用'範圍或範圍的子型別名稱,而不是'第一個 .. '最後一個.
type Temperature is range All_Time_Low .. All_Time_High;
type Weather_Stations is range 1 .. Max_Stations;
Current_Temperature : Temperature := 60;
Offset : Temperature;
...
for I in Weather_Stations loop
Offset := Current_Temperature - Temperature'First;
...
end loop;
在上面的例子中,最好使用Weather_Stations在for迴圈中,而不是使用Weather_Stations'First .. Weather_Stations'Last或1 .. Max_Stations因為它更清晰、不易出錯,並且對型別的定義依賴性更小Weather_Stations。類似地,在偏移量計算中,最好使用Temperature'First而不是使用All_Time_Low因為即使子型別的定義Temperature發生變化,程式碼仍然是正確的。這增強了程式的可靠性。
當您以這種方式隱式指定範圍和屬性時,請注意使用正確的子型別名稱。很容易在沒有意識到的情況下引用一個非常大的範圍。例如,給出以下宣告
type Large_Range is new Integer;
subtype Small_Range is Large_Range range 1 .. 10;
type Large_Array is array (Large_Range) of Integer;
type Small_Array is array (Small_Range) of Integer;
則下面的第一個宣告工作正常,但第二個宣告可能是一個意外,並在大多數機器上引發異常,因為它請求一個巨大的陣列(從最小的整數索引到最大的整數)
Array_1 : Small_Array;
Array_2 : Large_Array;
- 使用陣列屬性'第一個, '最後一個,或'長度而不是使用數字字面量來訪問陣列。
- 使用'範圍陣列的,而不是索引子型別的名稱來表達範圍。
- 使用'範圍而不是'第一個 .. '最後一個來表達範圍。
subtype Name_String is String (1 .. Name_Length);
File_Path : Name_String := (others => ' ');
...
for I in File_Path'Range loop
...
end loop;
在上面的例子中,最好使用Name_String'Range在for迴圈中,而不是使用Name_String_Size, Name_String'First .. Name_String'Last,或1 .. 30因為它更清晰、不易出錯,並且對Name_String和Name_String_Size的定義依賴性更小。如果Name_String被更改為具有不同的索引型別,或者如果陣列的邊界被更改,這仍然可以正常工作。這增強了程式的可靠性。
- 使用圓括號來指定子表示式求值的順序以澄清表示式(NASA 1987)。
- 使用圓括號來指定子表示式的求值順序,其正確性取決於從左到右的求值。
(1.5 * X**2)/A - (6.5*X + 47.0)
2*I + 4*Y + 8*Z + C
Ada 的運算子優先順序規則在 Ada 參考手冊 1995,第 4.5 節 [帶註釋的] 中定義,並遵循相同的普遍接受的代數運算子優先順序。Ada 中的強型別機制與常見的優先順序規則相結合,使許多圓括號變得不必要。但是,當出現不常見的運算子組合時,即使優先順序規則適用,新增圓括號也可能會有所幫助。表示式
5 + ((Y ** 3) mod 10)
更清晰,等同於
5 + Y**3 mod 10
求值規則確實指定了對具有相同優先順序級別的運算子從左到右進行求值。但是,在檢查表示式的正確性時,它是最常被忽視的求值規則。
- 避免依賴於負值使用的名稱和結構。
- 選擇標誌的名稱,使它們表示可以以肯定形式使用的狀態。
使用
if Operator_Missing then
而不是兩者
if not Operator_Found then
或
if not Operator_Missing then
當以肯定形式陳述時,關係表示式可能更易讀且更易理解。作為選擇名稱的輔助方法,請考慮在條件構造中,最常用的分支應首先遇到。
在某些情況下,負形式是不可避免的。如果關係表示式更好地反映了程式碼中的實際情況,那麼不建議反轉測試以遵守此指南。
- 使用邏輯運算子的短路形式來指定條件的順序,當一個條件失敗意味著另一個條件將引發異常時。
使用
if Y /= 0 or else (X/Y) /= 10 then
或
if Y /= 0 then
if (X/Y) /= 10 then
而不是兩者
if Y /= 0 and (X/Y) /= 10 then
或
if (X/Y) /= 10 then
以避免Constraint_Error。
使用
if Target /= null and then Target.Distance < Threshold then
而不是
if Target.Distance < Threshold then
以避免引用不存在的物件中的欄位。
使用短路控制形式可以防止一類資料相關的錯誤或異常,這些錯誤或異常可能是表示式求值的結果。短路形式保證了求值的順序,並保證了exit從關係表示式序列中退出,只要可以確定表示式的結果。
在沒有短路形式的情況下,Ada 不保證表示式求值的順序,也不保證在關係表示式求值為False(對於和)或True(對於或).
如果某個表示式的所有部分都必須始終被計算,那麼該表示式可能違反了指南 4.1.4,該指南限制了函式中的副作用。
- 使用<=和>=在包含實數運算元的關係表示式中,而不是=.
Current_Temperature : Temperature := 0.0;
Temperature_Increment : Temperature := 1.0 / 3.0;
Maximum_Temperature : constant := 100.0;
...
loop
...
Current_Temperature :=
Current_Temperature + Temperature_Increment;
...
exit when Current_Temperature >= Maximum_Temperature;
...
end loop;
固定點和浮點數,即使從相似的表示式派生而來,也可能並不完全相等。硬體中實數的不精確、有限表示總是存在舍入誤差,因此,兩個實數的構建路徑或歷史記錄的任何變化都有可能導致不同的數字,即使這些路徑或歷史記錄在數學上是等價的。
Ada 對模型區間的定義也意味著使用<=比使用<或=.
指南 7.2.7 中介紹了浮點運算。
如果您的應用程式必須測試實數的精確值(例如,測試特定機器上算術的精度),則必須使用=。但是,永遠不要在實數運算元上使用=作為退出迴圈的條件。
即使程式的全域性結構組織良好,但語句使用不當或過於複雜,也會使程式難以閱讀和維護。您應該努力使語句的使用簡單一致,以實現本地程式結構的清晰度。本節中的某些指南建議使用或避免使用特定的語句。正如各個指南中指出的那樣,嚴格遵守這些指南將是過分的,但經驗表明,它們通常會導致程式碼具有更高的可靠性和可維護性。
- 將巢狀表示式的深度最小化 (Nissen 和 Wallis 1984)。
- 將巢狀控制結構的深度最小化 (Nissen 和 Wallis 1984)。
- 嘗試使用簡化啟發式方法(參見以下注釋)。
- 不要將表示式或控制結構巢狀到超過五層的巢狀級別。
以下程式碼部分
if not Condition_1 then
if Condition_2 then
Action_A;
else -- not Condition_2
Action_B;
end if;
else -- Condition_1
Action_C;
end if;
可以更清晰地重寫,並且巢狀更少
if Condition_1 then
Action_C;
elsif Condition_2 then
Action_A;
else -- not (Condition_1 or Condition_2)
Action_B;
end if;
深度巢狀的結構令人困惑,難以理解,難以維護。問題在於難以確定程式的哪個部分包含在任何給定級別。對於表示式,這在實現正確的位置的平衡分組符號以及實現所需的運算子優先順序方面非常重要。對於控制結構,問題涉及控制的哪個部分。具體來說,某個語句是否處於適當的巢狀級別,也就是說,它是否巢狀得太深或太淺,或者某個語句是否與正確的選擇相關聯,例如,對於if或case語句?縮排很有幫助,但它不是萬能的。從視覺上檢查縮排程式碼的對齊方式(主要是中間級別)充其量是一個不確定的工作。為了最大限度地降低程式碼的複雜性,請將最大巢狀級別限制在三到五層之間。
問問自己以下問題,以幫助您簡化程式碼
- 表示式的一部分能否放入常量或變數中?
- 較低巢狀控制結構的一部分是否代表一個重要的,可能可重複使用的計算,我可以將其分解為子程式?
- 我可以將這些巢狀的if語句轉換為case語句嗎?
- 我是否在使用else if,而我本可以使用elsif?
- 嗎?我可以重新排序控制此巢狀結構的條件表示式嗎?
- 是否有其他更簡單的設計?
如果深度巢狀經常需要,那麼程式碼的整體設計決策可能應該更改。某些演算法需要深度巢狀的迴圈和由條件分支控制的段。它們可以繼續使用,歸因於它們的效率、熟悉度和時間證明的效用。當需要巢狀時,請謹慎行事,並特別注意識別符號以及迴圈和塊名稱的選擇。
- 使用切片而不是迴圈來複制陣列的一部分。
First : constant Index := Index'First;
Second : constant Index := Index'Succ(First);
Third : constant Index := Index'Succ(Second);
type Vector is array (Index range <>) of Element;
subtype Column_Vector is Vector (Index);
type Square_Matrix is array (Index) of Column_Vector;
subtype Small_Range is Index range First .. Third;
subtype Diagonals is Vector (Small_Range);
type Tri_Diagonal is array (Index) of Diagonals;
Markov_Probabilities : Square_Matrix;
Diagonal_Data : Tri_Diagonal;
...
-- Remove diagonal and off diagonal elements.
Diagonal_Data(Index'First)(First) := Null_Value;
Diagonal_Data(Index'First)(Second .. Third) :=
Markov_Probabilities(Index'First)(First .. Second);
for I in Second .. Index'Pred(Index'Last) loop
Diagonal_Data(I) :=
Markov_Probabilities(I)(Index'Pred(I) .. Index'Succ(I));
end loop;
Diagonal_Data(Index'Last)(First .. Second) :=
Markov_Probabilities(Index'Last)(Index'Pred(Index'Last) .. Index'Last);
Diagonal_Data(Index'Last)(Third) := Null_Value;
使用切片的賦值語句比迴圈更簡單、更清晰,可以幫助讀者瞭解預期的操作。另請參見指南 10.5.7,瞭解切片賦值與迴圈的潛在效能問題。
- 儘量減少使用others選擇在case語句的迴圈關聯。
- 中。不要在case語句帶來的維護影響。
- 使用case語句中使用列舉文字的範圍,而應使用if/elsif語句(如果可能)。
- 使用型別擴充套件和分派,而不是case語句(如果可能)。
type Color is (Red, Green, Blue, Purple);
Car_Color : Color := Red;
...
case Car_Color is
when Red .. Blue => ...
when Purple => ...
end case; -- Car_Color
現在考慮對型別進行更改
type Color is (Red, Yellow, Green, Blue, Purple);
此更改可能在case語句中產生未被注意到且不受歡迎的影響。如果選擇被明確列舉,如下所示when Red | Green | Blue =>而不是when Red .. Blue =>,那麼case語句將無法編譯。這將迫使維護人員在以下情況下做出關於如何處理的明智決定黃色.
在下面的示例中,假設已釋出了一個選單,並且使用者應輸入四個選項之一。假設User_Choice宣告為字元並且Terminal_IO.Get處理使用者輸入中的錯誤。使用if/elsif語句的較不直觀的替代方法顯示在case語句之後
Do_Menu_Choices_1:
loop
...
case User_Choice is
when 'A' => Item := Terminal_IO.Get ("Item to add");
when 'D' => Item := Terminal_IO.Get ("Item to delete");
when 'M' => Item := Terminal_IO.Get ("Item to modify");
when 'Q' => exit Do_Menu_Choices_1;
when others => -- error has already been signaled to user
null;
end case;
end loop Do_Menu_Choices_1;
Do_Menu_Choices_2:
loop
...
if User_Choice = 'A' then
Item := Terminal_IO.Get ("Item to add");
elsif User_Choice = 'D' then
Item := Terminal_IO.Get ("Item to delete");
elsif User_Choice = 'M' then
Item := Terminal_IO.Get ("Item to modify");
elsif User_Choice = 'Q' then
exit Do_Menu_Choices_2;
end if;
end loop Do_Menu_Choices_2;
原理
[edit | edit source]應瞭解物件的全部可能值,併為每個值分配特定的操作。使用others子句可能會阻止開發人員仔細考慮每個值的相應操作。如果未使用others子句,編譯器會警告使用者遺漏的值。
如果others表示式子型別具有許多值,例如,則可能無法避免在case語句中使用通用整數, 寬字元,或字元)。如果選擇的值範圍比子型別的範圍小,則應考慮使用if/elsif語句。請注意,您必須提供others備選方案,當您的case表示式是泛型型別時。
應明確列舉每個可能的值。範圍可能很危險,因為範圍可能會發生變化,並且case語句可能不會重新檢查。如果您已宣告子型別以對應於感興趣的範圍,則可以考慮使用此命名子型別。
在許多情況下,case語句可提高程式碼的可讀性。有關效能注意事項的討論,請參見指南 10.5.3。在許多實現中,case語句可能更有效。
當您向資料結構新增新的變體時,型別擴充套件和分派會減輕維護負擔。另請參見指南 5.4.2 和 5.4.4。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]需要在case語句中使用的範圍可以使用受約束的子型別來增強可維護性。它更容易維護,因為範圍的宣告可以放置在邏輯上屬於抽象的一部分的地方,而不是隱藏在可執行程式碼的case語句中
subtype Lower_Case is Character range 'a' .. 'z';
subtype Upper_Case is Character range 'A' .. 'Z';
subtype Control is Character range Ada.Characters.Latin_1.NUL ..
Ada.Characters.Latin_1.US;
subtype Numbers is Character range '0' .. '9';
...
case Input_Char is
when Lower_Case => Capitalize(Input_Char);
when Upper_Case => null;
when Control => raise Invalid_Input;
when Numbers => null;
...
end case;
例外情況
[edit | edit source]僅當用戶確信永遠不會在舊值之間插入新值時,才可以使用範圍作為可能值,例如在 ASCII 字元範圍中'a' .. 'z'.
迴圈
[edit | edit source]指南
[edit | edit source]- 使用for迴圈,儘可能地使用。
- 使用while迴圈,當無法在進入迴圈之前計算迭代次數,但可以在迴圈頂部應用簡單的延續條件時。
- 使用帶有exit語句的普通迴圈來處理更復雜的情況。
- 避免在exit迴圈中使用while和for語句。
- 最大程度地減少exit迴圈的方法。
示例
[edit | edit source]要遍歷陣列的所有元素
for I in Array_Name'Range loop
...
end loop;
要遍歷連結串列中的所有元素
Pointer := Head_Of_List;
while Pointer /= null loop
...
Pointer := Pointer.Next;
end loop;
經常會出現需要“迴圈和一半”的情況。為此,請使用
P_And_Q_Processing:
loop
P;
exit P_And_Q_Processing when Condition_Dependent_On_P;
Q;
end loop P_And_Q_Processing;
而不是
P;
while not Condition_Dependent_On_P loop
Q;
P;
end loop;
原理
[edit | edit source]一個for迴圈是有界的,因此不能是“無限迴圈”。Ada 語言強制執行此操作,它要求在迴圈規範中使用有限範圍,並且不允許修改for迴圈的迴圈計數器,該迴圈計數器由迴圈內執行的語句修改。這為讀者和作者提供了與其他形式迴圈無關的確定性理解。一個for迴圈也更容易維護,因為迭代範圍可以使用迴圈操作的資料結構的屬性來表示,如上面的示例所示,其中每次修改陣列宣告時,範圍都會自動更改。出於這些原因,最好儘可能地使用for迴圈,也就是說,只要可以使用簡單表示式來描述迴圈計數器的第一個值和最後一個值,就可以使用它。
該while迴圈已成為大多數程式設計師非常熟悉的結構。一目瞭然,它指示迴圈繼續的條件。如果無法使用while迴圈,但存在描述迴圈應繼續的條件的簡單布林表示式時,請使用for迴圈,如上面的示例所示。
應在更復雜的情況下使用普通迴圈語句,即使可以使用for或while迴圈結合額外的標誌變數或exit語句來設計解決方案。選擇迴圈結構的標準是儘可能清晰且易於維護。使用exit語句從for或while迴圈中退出不是一個好主意,因為在顯然已在迴圈頂部描述了完整的迴圈條件集之後,它會誤導讀者。遇到普通迴圈語句的讀者希望看到exit語句帶來的維護影響。
有一些熟悉的迴圈情況最適合使用普通迴圈語句來實現。例如,Pascal 的repeat until迴圈的語義,其中迴圈在終止測試發生之前始終至少執行一次,最適合使用帶有一個exit語句的普通迴圈來實現,該語句位於迴圈末尾。另一種常見情況是“迴圈和一半”結構,如上面的示例所示,其中迴圈必須在語句體序列中的某個位置終止。使用while迴圈模擬的複雜的“迴圈和一半”結構通常需要引入標誌變數或在迴圈之前和期間複製程式碼,如示例所示。這種扭曲使程式碼更加複雜,可靠性降低。
最大程度地減少exit迴圈,以使迴圈更易於讀者理解。您需要從迴圈中退出超過兩種方式的情況應該很少見。如果需要,請務必對所有情況使用exit語句,而不是向exit迴圈新增for或while語句。
當存在巢狀塊結構時,可能難以確定哪個
[edit | edit source]指南
[edit | edit source]- 使用exit語句,以增強迴圈終止程式碼的可讀性 (NASA 1987)。
- 使用exit when ...而不是if ... then exit儘可能地使用 (NASA 1987)。
- 複查exit語句放置。
示例
[edit | edit source]參見指南 5.1.1 和指南 5.6.4 中的示例。
原理
[edit | edit source]使用exit語句比嘗試向while迴圈條件添加布爾標誌來模擬從迴圈中間退出更易讀。即使所有exit語句都集中在迴圈體頂部,將複雜條件分解為多個exit語句可以簡化程式碼,並使其更易讀、更清晰。兩個exit語句的順序執行通常比短路控制形式更清晰。
該exit when形式優於if ... then exit形式,因為它透過不將其巢狀在任何控制結構中,使單詞exit更醒目。僅當除了if ... then exit語句之外,還必須有條件地執行其他語句時,才需要exit形式。例如
Process_Requests:
loop
if Status = Done then
Shut_Down;
exit Process_Requests;
end if;
...
end loop Process_Requests;
具有許多分散的exit語句的迴圈可能表明對演算法中迴圈目的的模糊思考。這種演算法可能可以透過其他方式更好地編寫程式碼,例如使用一系列迴圈。一些返工通常可以減少exit語句的數量,並使程式碼更清晰。
另請參見指南 5.1.3 和 5.6.4。
遞迴和迭代邊界
[edit | edit source]指南
[edit | edit source]- 考慮為迴圈指定邊界。
- 考慮為遞迴指定邊界。
示例
[edit | edit source]建立迭代邊界
Safety_Counter := 0;
Process_List:
loop
exit when Current_Item = null;
...
Current_Item := Current_Item.Next;
...
Safety_Counter := Safety_Counter + 1;
if Safety_Counter > 1_000_000 then
raise Safety_Error;
end if;
end loop Process_List;
建立遞迴邊界
subtype Recursion_Bound is Natural range 0 .. 1_000;
procedure Depth_First (Root : in Tree;
Safety_Counter : in Recursion_Bound
:= Recursion_Bound'Last) is
begin
if Root /= null then
if Safety_Counter = 0 then
raise Recursion_Error;
end if;
Depth_First (Root => Root.Left_Branch,
Safety_Counter => Safety_Counter - 1);
Depth_First (Root => Root.Right_Branch,
Safety_Counter => Safety_Counter - 1);
... -- normal subprogram body
end if;
end Depth_First;
以下是此子程式使用示例。 一個呼叫指定了最大遞迴深度為 50。第二個使用預設值 (1,000)。第三個使用計算的邊界
Depth_First(Root => Tree_1, Safety_Counter => 50);
Depth_First(Tree_2);
Depth_First(Root => Tree_3, Safety_Counter => Current_Tree_Height);
原理
[edit | edit source]遞迴和使用除for語句以外的結構的迭代可能是無限的,因為預期終止條件沒有出現。 這種故障有時非常微妙,可能很少出現,並且可能難以檢測,因為外部表現可能不存在或延遲相當長的時間。
透過除了迴圈本身之外還包括計數器和對計數器值的檢查,可以防止許多形式的無限迴圈。 包括此類檢查是安全程式設計技術 (Anderson 和 Witty 1978) 的一個方面。
這些檢查的邊界不必準確,只要符合實際情況即可。 這種計數器和檢查不是程式主要控制結構的一部分,而是作為執行時“安全網”的良性新增,允許錯誤檢測,並可能從潛在的無限迴圈或無限遞迴中恢復。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]如果迴圈使用for迭代方案 (指南 5.6.4),則它遵循此指南。
例外情況
[edit | edit source]嵌入式控制應用程式具有旨在無限迴圈的迴圈。 這些應用程式中只有少數迴圈應該作為此指南的例外。 這些例外應該是經過深思熟慮的 (並經過記錄的) 策略決定。
此指南對於安全關鍵系統至關重要。 對於其他系統,它可能過於繁瑣。
Goto 語句
[edit | edit source]指南
[edit | edit source]不要使用goto語句帶來的維護影響。
原理
[edit | edit source]一個goto語句是非結構化的控制流更改。 更糟糕的是,該標籤不需要指示相應goto語句的位置。 這使得程式碼難以閱讀,並使其正確執行存在疑問。
其他語言使用goto語句來實現迴圈退出和異常處理。 Ada 對這些構造的支援使得goto語句極其罕見。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]如果您必須使用goto語句,則使用空白將其和標籤突出顯示。 在標籤處指示相應goto語句的位置。
Return 語句
[edit | edit source]指南
[edit | edit source]- 最小化return語句 從子程式 (NASA 1987) 中退出。
- 突出顯示return語句使用註釋或空白,以防止它們在其他程式碼中丟失。
示例
[edit | edit source]以下程式碼片段比必要更長更復雜
if Pointer /= null then
if Pointer.Count > 0 then
return True;
else -- Pointer.Count = 0
return False;
end if;
else -- Pointer = null
return False;
end if;
它應該用更短、更簡潔和更清晰的等效行替換
return Pointer /= null and then Pointer.Count > 0;
原理
[edit | edit source]過度使用 return 會使程式碼混亂且難以閱讀。 僅在必要時使用return語句。 子程式中過多的 return 可能表明邏輯混亂。 如果應用程式需要多個 return,則在同一級別使用它們 (即,像在case語句的不同分支中一樣),而不是分散在整個子程式程式碼中。 一些修改通常可以將return語句的數量減少到一個,並使程式碼更清晰。
例外情況
[edit | edit source]如果這樣做會影響自然結構和程式碼可讀性,則不要避免使用return語句。
塊
[edit | edit source]指南
[edit | edit source]- 使用塊來區域性化宣告的範圍。
- 使用塊來執行區域性重新命名。
- 使用塊來定義區域性異常處理程式。
示例
[edit | edit source]with Motion;
with Accelerometer_Device;
...
---------------------------------------------------------------------
function Maximum_Velocity return Motion.Velocity is
Cumulative : Motion.Velocity := 0.0;
begin -- Maximum_Velocity
-- Initialize the needed devices
...
Calculate_Velocity_From_Sample_Data:
declare
use type Motion.Acceleration;
Current : Motion.Acceleration := 0.0;
Time_Delta : Duration;
begin -- Calculate_Velocity_From_Sample_Data
for I in 1 .. Accelerometer_Device.Sample_Limit loop
Get_Samples_And_Ignore_Invalid_Data:
begin
Accelerometer_Device.Get_Value(Current, Time_Delta);
exception
when Constraint_Error =>
null; -- Continue trying
when Accelerometer_Device.Failure =>
raise Accelerometer_Device_Failed;
end Get_Samples_And_Ignore_Invalid_Data;
exit when Current <= 0.0; -- Slowing down
Update_Velocity:
declare
use type Motion.Velocity;
use type Motion.Acceleration;
begin
Cumulative := Cumulative + Current * Time_Delta;
exception
when Constraint_Error =>
raise Maximum_Velocity_Exceeded;
end Update_Velocity;
end loop;
end Calculate_Velocity_From_Sample_Data;
return Cumulative;
end Maximum_Velocity;
---------------------------------------------------------------------
...
原理
[edit | edit source]塊可以分解大型程式碼段,並隔離與每個程式碼子部分相關的細節。 當宣告性塊描述該程式碼時,僅在特定程式碼部分中使用的變數將清晰可見。
重新命名可以簡化演算法的表達,並提高對給定程式碼部分的可讀性。 但當重新命名子句在視覺上與它適用的程式碼分離時,就會令人困惑。 宣告性區域允許重新命名在讀者檢查使用該縮寫的程式碼時立即可見。 指南 5.7.1 討論了關於use子句的類似指南。
區域性異常處理程式可以在靠近起源點的地方捕獲異常,並允許對它們進行處理、傳播或轉換。
- 使用聚合而不是一系列賦值來為記錄的所有元件賦值。
- 在構建要作為實際引數傳遞的記錄時,使用聚合而不是臨時變數。
- 僅在引數存在常規排序時使用位置關聯。
最好使用聚合
Set_Position((X, Y));
Employee_Record := (Number => 42,
Age => 51,
Department => Software_Engineering);
而不是使用連續賦值或臨時變數
Temporary_Position.X := 100;
Temporary_Position.Y := 200;
Set_Position(Temporary_Position);
Employee_Record.Number := 42;
Employee_Record.Age := 51;
Employee_Record.Department := Software_Engineering;
在維護期間使用聚合是有益的。如果記錄結構被更改,但相應的聚合未被更改,編譯器會標記聚合賦值中缺少的欄位。它將無法檢測到應該向賦值語句列表新增新的賦值語句這一事實。
聚合也可以真正方便地將資料項組合成作為引數傳遞資訊的記錄或陣列結構。命名元件關聯使聚合更具可讀性。
有關聚合的效能影響,請參見指南 10.4.5。
如指南 4.2 所述,Ada 透過其可見性控制功能強制實施資訊隱藏和關注點分離的能力是該語言最重要的優勢之一。破壞這些功能,例如,透過過於自由地使用use子句,是浪費和危險的。
- 當您需要為運算子提供可見性時,請使用use type子句的類似指南。
- 避免/最小化使用use子句 (Nissen 和 Wallis 1984)。
- 考慮使用包重新命名子句而不是use子句用於包。
- 考慮在以下情況下使用use子句
- 當需要標準包且沒有引入歧義引用時
- 當需要對列舉文字的引用時
- 本地化所有use子句的效果。
這是對指南 4.2.3 中示例的修改。的影響use子句是本地化的
----------------------------------------------------------------------------------
package Rational_Numbers is
type Rational is private;
function "=" (X, Y : Rational) return Boolean;
function "/" (X, Y : Integer) return Rational; -- construct a rational number
function "+" (X, Y : Rational) return Rational;
function "-" (X, Y : Rational) return Rational;
function "*" (X, Y : Rational) return Rational;
function "/" (X, Y : Rational) return Rational; -- rational division
private
...
end Rational_Numbers;
----------------------------------------------------------------------------------
package body Rational_Numbers is
procedure Reduce (R : in out Rational) is . . . end Reduce;
. . .
end Rational_Numbers;
----------------------------------------------------------------------------------
package Rational_Numbers.IO is
...
procedure Put (R : in Rational);
procedure Get (R : out Rational);
end Rational_Numbers.IO;
----------------------------------------------------------------------------------
with Rational_Numbers;
with Rational_Numbers.IO;
with Ada.Text_IO;
procedure Demo_Rationals is
package R_IO renames Rational_Numbers.IO;
use type Rational_Numbers.Rational;
use R_IO;
use Ada.Text_IO;
X : Rational_Numbers.Rational;
Y : Rational_Numbers.Rational;
begin -- Demo_Rationals
Put ("Please input two rational numbers: ");
Get (X);
Skip_Line;
Get (Y);
Skip_Line;
Put ("X / Y = ");
Put (X / Y);
New_Line;
Put ("X * Y = ");
Put (X * Y);
New_Line;
Put ("X + Y = ");
Put (X + Y);
New_Line;
Put ("X - Y = ");
Put (X - Y);
New_Line;
end Demo_Rationals;
這些指南允許您在可維護性和可讀性之間保持謹慎的平衡。使用use子句確實可以使程式碼讀起來更像散文文字。但是,維護人員可能還需要解析引用並識別模稜兩可的操作。在沒有解析這些引用和識別更改 use 子句影響的工具的情況下,完全限定名稱是最好的替代方案。
避免use子句會強制您使用完全限定名稱。在大型系統中,可能會有許多庫單元在with子句中命名。當相應的use子句伴隨with子句,並且庫包的簡單名稱被省略(如use子句所允許的那樣),對外部實體的引用會被掩蓋,並且很難識別外部依賴項。
在某些情況下,use子句的益處是顯而易見的。標準包可以與顯而易見的假設一起使用,即讀者非常熟悉這些包,並且不會引入額外的過載。
該use type子句使中綴和字首運算子都可見,而無需重新命名子句。您可以使用use type子句來提高可讀性,因為您可以使用更自然的中綴運算子表示法來編寫語句。另請參見指南 5.7.2。
您可以透過將use子句放置在包或子程式的主體中,或將其封裝在塊中以限制可見性來最小化其範圍。
避免use子句完全會導致列舉文字出現問題,列舉文字必須完全限定。這個問題可以透過宣告以列舉文字為值的常量來解決,只是這些常量不能像列舉文字那樣過載。
可以在 Rosen (1987) 中找到支援使用 use 子句的論點。
有一些工具可以分析您的 Ada 原始碼,解析名稱過載,並在use子句或完全限定名稱之間自動轉換。
- 將重新命名宣告的範圍限制為必要的最小範圍。
- 重新命名一個長而完全限定的名稱以減少複雜性,如果它變得難以處理(參見指南 3.1.4)。
- 如果此子程式僅僅呼叫第一個子程式,則使用重新命名來提供子程式的主體。
- 為了可見性目的而進行重新命名宣告,而不是使用 use 子句,除了運算子(參見指南 5.7.1)。
- 當您的程式碼與使用非描述性或不適用的命名法的可重用元件互動時,請重新命名部分。
- 使用專案範圍內的標準縮略詞列表來重新命名常用包。
- 提供一個use type而不是一個重新命名子句來為運算子提供可見性。
procedure Disk_Write (Track_Name : in Track;
Item : in Data) renames
System_Specific.Device_Drivers.Disk_Head_Scheduler.Transmit;
另請參見指南 5.7.1 中的示例,其中包級別的重新命名子句為包提供了縮寫Rational_Numbers_IO.
如果濫用重新命名功能,程式碼可能難以閱讀。一個重新命名子句可以將縮寫替換為限定符或長包名稱的本地。這可以使程式碼更具可讀性,但將程式碼固定在完整名稱上。您可以使用重新命名子句來一次評估複雜的名稱,或者提供對物件的新“檢視”(無論它是否被標記)。但是,使用重新命名子句通常可以透過仔細選擇名稱來避免或使其明顯不可取,以便完全限定的名稱讀起來很好。
當子程式主體呼叫另一個子程式而不新增本地資料或其他演算法內容時,讓此子程式主體重新命名實際執行工作的子程式會更具可讀性。因此,您避免必須編寫程式碼來“透過”子程式呼叫(原理 1995,第 II.12 節)。
重新命名宣告列表充當縮寫定義列表(參見指南 3.1.4)。作為替代方案,您可以在庫級別重新命名包以定義包的專案範圍內的縮寫,然後with重新命名的包。通常從重用庫中呼叫的部分沒有像可能的那樣通用或與新應用程式的命名方案匹配的名稱。匯出重新命名子程式的介面包可以對映到您的專案的命名法。另請參見指南 5.7.1。
在 Ada 參考手冊 1995,第 8.5 節 [帶註釋的] 中描述的重新命名型別的方法是使用子型別(參見指南 3.4.1)。
該use type子句消除了重新命名中綴運算子的必要性。由於您不再需要顯式重新命名每個運算子,因此您可以避免錯誤,例如將+重新命名為-。另請參見指南 5.7.1。
包名應儘量具有意義,並考慮到包名將在很多地方用作字首(例如:Pkg.Operation或Object : Pkg.Type_Name;)。如果將每個包都重新命名為某個縮寫,則會失去選擇有意義名稱的意義,並且難以跟蹤所有縮寫代表的內容。
為了在 Ada 95 環境中向上相容 Ada 83 程式,該環境包括 Ada 83 庫級包的庫級重新命名(Ada 參考手冊 1995,§J.1 [帶註釋的])。不建議您在 Ada 95 程式碼中使用這些重新命名。
過載子程式
[edit | edit source]指南
[edit | edit source]將過載限制為對不同型別引數執行類似操作的廣泛使用子程式(Nissen 和 Wallis 1984)。
示例
[edit | edit source]function Sin (Angles : in Matrix_Of_Radians) return Matrix;
function Sin (Angles : in Vector_Of_Radians) return Vector;
function Sin (Angle : in Radians) return Small_Real;
function Sin (Angle : in Degrees) return Small_Real;
原理
[edit | edit source]過度過載會讓維護人員感到困惑(Nissen 和 Wallis 1984,65)。如果過載成為習慣,還存在隱藏宣告的危險。如果引數配置檔案不唯一,嘗試過載操作實際上可能會隱藏原始操作。從那時起,就無法確定呼叫新操作是否符合程式設計師的意願,或者程式設計師是否打算呼叫隱藏的操作,而意外地隱藏了它。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]本指南並不禁止在不同包中宣告具有相同名稱的子程式。
過載運算子
[edit | edit source]指南
[edit | edit source]- 保留過載運算子的傳統意義(Nissen 和 Wallis 1984)。
- 使用“+”來標識新增、聯接、增加和增強型別的函式。
- 使用“-“”來標識減法、分離、減少和消耗型別的函式。
- 當應用於標記型別時,謹慎而一致地使用運算子過載。
示例
[edit | edit source]function "+" (X : in Matrix;
Y : in Matrix)
return Matrix;
...
Sum := A + B;
原理
[edit | edit source]破壞運算子的傳統解釋會導致程式碼混亂。
運算子過載的優點是,當使用它時,程式碼可以變得更清晰,並且可以更緊湊(更易讀)地編寫。這可以使語義簡單自然。但是,很容易誤解過載運算子的含義,尤其是在應用於後代時。如果程式設計師沒有應用自然的語義,情況尤其如此。因此,如果無法一致地使用過載,並且很容易誤解,請不要使用過載。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]任何過載都存在潛在問題。例如,如果有幾個版本的"+"運算子,並且對其中一個運算子的更改影響其引數的數量或順序,則查詢必須更改的出現的操作可能會很困難。
過載相等運算子
[edit | edit source]指南
[edit | edit source]- 為私有型別定義適當的相等運算子。
- 考慮重新定義私有型別的相等運算子。
- 當為型別過載相等運算子時,請維護代數等價關係的屬性。
原理
[edit | edit source]與私有型別一起提供的預定義相等操作取決於用於實現該型別的 資料結構。如果使用訪問型別,則相等意味著運算元具有相同的指標值。如果使用離散型別,則相等意味著運算元具有相同的值。如果使用浮點型別,則相等基於 Ada 模型間隔(請參見指南 7.2.7)。因此,您應重新定義相等以提供客戶端預期的含義。如果使用訪問型別實現私有型別,則應重新定義相等以提供深度相等。對於浮點型別,您可能希望提供一個在某個應用程式相關 epsilon 值內測試相等的相等性。
對私有型別相等含義的任何假設都會在該型別的實現上產生依賴關係。有關詳細討論,請參見 Gonzalez(1991)。
當定義“=”時,此符號隱含了傳統的代數含義。正如 Baker(1991)中所述,相等運算子應保持以下屬性:
- 自反a = a
- 對稱a = b ==> b = a
- 傳遞a = b 並且 b = c ==> a = c
在重新定義相等時,您不需要具有結果型別為Standard.Boolean。理由(1995,§6.3)給出了兩個結果型別為使用者定義型別的示例。在三值邏輯抽象中,您重新定義相等以返回以下之一:True, False,或Unknown。在向量處理應用程式中,您可以定義一個返回布林值向量的逐分量相等運算子。在這兩種情況下,您還應重新定義不相等,因為它不是相等函式的布林補碼。
使用異常
[edit | edit source]Ada 異常是一種增強可靠性的語言特性,旨在幫助在出現錯誤或意外事件的情況下指定程式行為。異常不打算提供通用控制結構。此外,不應將過度使用異常視為提供完整軟體容錯的充分條件(Melliar-Smith 和 Randell 1987)。
本節討論瞭如何以及何時避免引發異常、如何以及在何處處理異常以及是否應傳播異常。有關如何將異常用作單元介面的一部分的資訊包括要宣告和引發的異常以及在什麼條件下引發異常。其他問題在第 4.3 節和第 7.5 節的指南中討論。
處理與避免異常
[edit | edit source]指南
[edit | edit source]- 如果可以輕鬆高效地做到這一點,請避免導致引發異常。
- 為無法避免的異常提供處理程式。
- 使用異常處理程式透過將錯誤處理與正常執行分離來增強可讀性。
- 不要使用異常和異常處理程式作為goto語句帶來的維護影響。
- 不要評估由於語言定義的檢查失敗而變得異常的物件(或物件的一部分)的值。
原理
[edit | edit source]在許多情況下,可以輕鬆高效地檢測到您即將執行的操作將引發異常。在這種情況下,最好進行檢查,而不是允許引發異常並使用異常處理程式進行處理。例如,檢查每個指標是否為請記住,記錄或陣列的任何未重置元件也可能是懸空引用,或者可能承載表示不一致資料的位模式。訪問型別的元件始終預設初始化為當遍歷由指標連線的記錄的連結串列時。此外,在除以整數之前先測試它是否為 0,並呼叫詢問函式Stack_Is_Empty在呼叫pop棧包的程式之前。當可以輕鬆高效地將這些測試作為正在實現的演算法的自然組成部分執行時,這些測試是合適的。
然而,提前進行錯誤檢測並不總是那麼簡單。在某些情況下,這種測試過於昂貴或不可靠。在這種情況下,最好在異常處理程式的範圍內嘗試操作,以便在異常發生時處理異常。例如,在使用連結串列實現列表的情況下,在每次呼叫過程之前呼叫函式Entry_Exists僅僅為了避免引發異常Modify_Entry效率非常低下Entry_Not_Found. 為了避免異常而搜尋列表所花費的時間與執行更新而搜尋列表所花費的時間一樣多。類似地,在異常處理程式的範圍內嘗試對實數進行除法以處理數值溢位,要比事先測試被除數是否過大或除數是否過小以便商可以在機器上表示要容易得多。
在併發情況下,提前進行的測試也可能不可靠。例如,如果您想在多使用者系統上修改現有檔案,最好在異常處理程式的範圍內嘗試這樣做,而不是事先測試檔案是否存在,檔案是否受到保護,檔案系統是否有足夠的空間來擴充套件檔案等等。即使您測試了所有可能的錯誤情況,也不能保證在測試之後和修改操作之前不會有任何變化。您仍然需要異常處理程式,因此提前測試毫無意義。
只要不適用這種情況,正常的和可預測的事件應該由程式碼處理,而不需要異常所代表的異常控制轉移。當異常處理程式中只包含故障處理程式碼時,這種分離使程式碼更容易閱讀。讀者可以跳過所有異常處理程式,仍然理解程式碼的正常控制流程。出於這個原因,異常永遠不應該在同一個單元內被引發和處理,作為一種goto從迴圈,if, case,或塊語句的迴圈關聯。
退出語句的形式。評估異常物件會導致錯誤執行(Ada 參考手冊 1995,第 13.9.1 節 [註釋])。語言定義的檢查失敗會引發異常。在相應的異常處理程式中,您需要執行適當的清理操作,包括記錄錯誤(參見準則 5.8.2 中關於異常發生的討論)和/或重新引發異常。評估將您帶入異常處理程式碼的物件會導致錯誤執行,您不知道您的異常處理程式是否已完全或正確執行。另請參見準則 5.9.1,該準則在Ada.Unchecked_Conversion.
的上下文中討論了異常物件
[edit | edit source]指南
[edit | edit source]- 在為others編寫異常處理程式時,透過Exception_Name, Exception_Message,或Exception_Information捕獲並返回有關異常的額外資訊,這些子程式在預定義包中宣告Ada.Exceptions.
- 使用others中宣告,僅用於捕獲不能顯式列舉的異常,最好僅用於標記潛在的中止。
- 在開發過程中,捕獲others,捕獲正在處理的異常,並考慮為該異常新增一個顯式處理程式。
示例
[edit | edit source]以下簡化的示例讓使用者有機會輸入 1 到 3 之間的整數。如果發生錯誤,它會向用戶提供資訊。對於超出預期範圍的整數值,該函式會報告異常的名稱。對於任何其他錯誤,該函式會提供更完整的跟蹤資訊。跟蹤資訊的量是實現相關的。
with Ada.Exceptions;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
function Valid_Choice return Positive is
subtype Choice_Range is Positive range 1..3;
Choice : Choice_Range;
begin
Ada.Text_IO.Put ("Please enter your choice: 1, 2, or 3: ");
Ada.Integer_Text_IO.Get (Choice);
if Choice in Choice_Range then -- else garbage returned
return Choice;
end if;
when Out_of_Bounds : Constraint_Error =>
Ada.Text_IO.Put_Line ("Input choice not in range.");
Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Name (Out_of_Bounds));
Ada.Text_IO.Skip_Line;
when The_Error : others =>
Ada.Text_IO.Put_Line ("Unexpected error.");
Ada.Text_IO.Put_Line (Ada.Exceptions.Exception_Information (The_Error));
Ada.Text_IO.Skip_Line;
end Valid_Choice;
原理
[edit | edit source]預定義包Ada.Exceptions允許您記錄異常,包括其名稱和跟蹤資訊。在為others編寫處理程式時,您應該提供有關異常的資訊以促進除錯。因為您可以訪問有關異常發生的資訊,所以您可以以標準方式儲存適合以後分析的資訊。透過使用異常發生,您可以識別特定的異常,並記錄詳細資訊或採取糾正措施。
提供對others的處理程式,使您可以遵循本節中的其他準則。它提供了一個位置來捕獲和轉換真正意外的異常,這些異常沒有被顯式處理程式捕獲。雖然有可能提供“防火牆”以防止意外異常在沒有提供處理程式的情況下傳播,但您可以轉換出現的意外異常。該others處理程式無法區分不同的異常,因此,任何此類處理程式都必須將異常視為災難。即使這種災難仍然可以在此時轉換為使用者定義的異常。因為對others的處理程式會捕獲任何未被顯式處理的異常,所以在任務或主子程式的框架中放置一個處理程式可以提供執行最終清理並乾淨關閉的機會。
為others編寫處理程式需要謹慎。您應該在處理程式中為它命名(例如,Error : others;),以便區分實際引發的異常或異常引發的確切位置。通常,others處理程式不能對可以做什麼或甚至需要“修復”什麼做出任何假設。
在開發過程中使用對others的處理程式,當預計異常發生頻繁時,可能會阻礙除錯,除非您利用Ada.Exceptions中的功能。對於開發人員來說,檢視帶有實際異常資訊的跟蹤更有資訊量,這些資訊由Ada.Exceptions子程式捕獲。編寫沒有這些子程式的處理程式會限制您可能看到的錯誤資訊的量。例如,您可能只會在跟蹤中看到轉換後的異常,而不會列出引發原始異常的位置。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]可以使用Exception_Id在others處理程式中區分不同的異常,但這不推薦。型別Exception_Id是實現定義的。操作型別為Exception_Id的值會降低程式的可移植性,並使其更難理解。
傳播
[edit | edit source]指南
[edit | edit source]- 處理所有異常,包括使用者定義的異常和預定義的異常。
- 對於可能引發的每個異常,在合適的框架中提供一個處理程式,以防止異常意外傳播到抽象之外。
原理
[edit | edit source]“不可能發生”的說法不是一種可接受的程式設計方法。您必須假設它可能發生,並且在發生時處於控制之中。您應該為“不可能到達這裡”的情況提供防禦性程式碼例程。
一些現有的建議要求捕獲和將任何異常傳播到呼叫單元。這種建議可能會停止程式。您應該捕獲異常並傳播它或一個替代異常,只有當您的處理程式在錯誤的抽象級別上進行恢復時。進行恢復可能很困難,但替代方案是程式無法滿足其規範。
顯式請求終止意味著您的程式碼控制著這種情況,並已確定這是唯一安全的行動方針。處於控制之中可以提供機會以受控方式關閉(清理鬆散的結,關閉檔案,將表面釋放到手動控制,發出警報),這意味著所有可用的程式設計恢復嘗試都已經完成。
定位異常的起因
[edit | edit source]指南
[edit | edit source]- 不要依賴於能夠識別引發故障的預定義異常或實現定義的異常。
- 使用Ada.Exceptions中定義的功能來捕獲有關異常的儘可能多的資訊。
- 使用塊將程式碼的區域性區域與其自己的異常處理程式相關聯。
示例
[edit | edit source]參見準則 5.6.9。
原理
[edit | edit source]在異常處理程式中,很難確定到底是哪條語句和語句中的哪個操作引發了異常,特別是預定義異常和實現定義的異常。預定義異常和實現定義的異常是轉換為更高級別抽象以進行處理的候選物件。使用者定義的異常與應用程式更緊密相關,更適合在處理程式中進行恢復。
使用者定義的異常也很難本地化。將處理程式與小的程式碼塊關聯有助於縮小可能性,從而更容易編寫恢復操作。在子程式或任務體內的較小塊中放置處理程式,還可以允許在恢復操作後恢復子程式或任務。如果您不在塊內處理異常,則處理程式可用的唯一操作是按照準則 5.8.3 關閉任務或子程式。
如準則 5.8.2 所述,您可以記錄有關異常的執行時系統資訊。您還可以向異常附加訊息。在程式碼開發、除錯和維護過程中,此資訊應該有助於定位異常的原因。
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
[edit | edit source]透過塊及其異常處理程式來保護您選擇的程式碼部分的最佳大小非常依賴於應用程式。粒度太小,會導致您在為異常操作程式設計時比為正常演算法花費更多精力。粒度太大,會重新引入確定問題出在哪裡以及恢復正常流程的問題。
錯誤執行和有界錯誤
[edit | edit source]Ada 95 引入了有界錯誤類別。有界錯誤是指行為不確定但處於定義明確的範圍內的案例(原理 1995,第 1.4 節)。有界錯誤的結果是限制編譯器的行為,以便在出現錯誤的情況下,Ada 環境不會隨意執行任何操作。《Ada 參考手冊 1995,第 1.1.5 節》[Annotated] 定義了一組可能的輸出結果,用於處理未定義行為的後果,例如未初始化的值或超出其子類型範圍的值。例如,正在執行的程式可能會引發預定義的異常Program_Error, Constraint_Error,或者它可能什麼也不做。
當 Ada 程式生成編譯器或執行時環境不需要檢測的錯誤時,該程式就是錯誤的。如《Ada 參考手冊 1995,第 1.1.5 節》[Annotated] 中所述,“錯誤執行的影響是不可預測的”。如果編譯器檢測到錯誤程式的例項,它的選擇是指示編譯時錯誤;插入程式碼以引發Program_Error,可能還會寫一條相關訊息;或者什麼也不做。
錯誤性不是 Ada 獨有的概念。以下準則描述或解釋了《Ada 參考手冊 1995,第 1.1.5 節》[Annotated] 中定義的錯誤性的某些特定例項。這些準則並非意在面面俱到,而是著重強調一些常被忽視的問題領域。嚴格來說,任意順序依賴關係並不屬於錯誤執行的範疇;因此,在準則 7.1.9 中將它們作為可移植性問題進行了討論。
未經檢查的轉換
[edit | edit source]指南
[edit | edit source]- 使用Ada.Unchecked_Conversion僅在萬不得已的情況下才使用(《Ada 參考手冊 1995,第 13.9 節》[Annotated])。
- 考慮在以下情況下使用'Valid屬性檢查標量資料的有效性。
- 確保從Ada.Unchecked_Conversion正確地表示引數子型別的某個值。
- 隔離Ada.Unchecked_Conversion的使用,放在包體中。
示例
[edit | edit source]以下示例展示瞭如何使用'Valid屬性檢查標量資料的有效性
------------------------------------------------------------------------
with Ada.Unchecked_Conversion;
with Ada.Text_IO;
with Ada.Integer_Text_IO;
procedure Test is
type Color is (Red, Yellow, Blue);
for Color'Size use Integer'Size;
function Integer_To_Color is
new Ada.Unchecked_Conversion (Source => Integer,
Target => Color);
Possible_Color : Color;
Number : Integer;
begin -- Test
Ada.Integer_Text_IO.Get (Number);
Possible_Color := Integer_To_Color (Number);
if Possible_Color'Valid then
Ada.Text_IO.Put_Line(Color'Image(Possible_Color));
else
Ada.Text_IO.Put_Line("Number does not correspond to a color.");
end if;
end Test;
------------------------------------------------------------------------
原理
[edit | edit source]未經檢查的轉換是不考慮源型別或目標型別對這些位和位位置的含義的按位複製。源位模式在目標型別的上下文中很容易毫無意義。未經檢查的轉換會建立違反後續操作型別約束的值。對大小不匹配的物件進行未經檢查的轉換將產生與實現相關的結果。
使用'Valid屬性對標量資料進行檢查,可以檢查它是否在範圍內,如果超出範圍也不會引發異常。在以下幾種情況下,此類有效性檢查可以提高程式碼的可讀性和可維護性
- 透過未經檢查的轉換生成的資料
- 輸入資料
- 從外部語言介面返回的引數值
- 中止賦值(在非同步控制轉移期間或執行abort語句期間)
- 由於語言定義的檢查失敗而導致的中斷賦值
- 使用'Address屬性指定地址的資料
在沒有編譯器或執行時檢查的情況下獲得訪問值時,不應該假定它正確。在處理訪問值時,使用'Valid屬性有助於防止使用Ada 環境不需要提供動態分配物件的釋放。如果提供,它可能被隱式提供(當它們的訪問型別超出範圍時,物件會被釋放),顯式提供(當, Unchecked_Access,或Ada.Unchecked_Conversion.
後可能發生的錯誤取消引用。在將非標量物件用作未經檢查的轉換中的實際引數的情況下,應確保其從過程返回時的值正確地表示子型別中的值。這種情況發生在引數處於模式時out或in out。在與外部語言介面或使用語言定義的輸入過程時,檢查值非常重要。《Ada 參考手冊 1995,第 13.9.1 節》[Annotated] 列出了有關資料有效性的完整規則。
未經檢查的釋放
[edit | edit source]指南
[edit | edit source]- 隔離Ada 環境不需要提供動態分配物件的釋放。如果提供,它可能被隱式提供(當它們的訪問型別超出範圍時,物件會被釋放),顯式提供(當的使用,放在包體中。
- 確保在退出本地物件的範圍後,不存在對本地物件的懸空引用。
原理
[edit | edit source]準則 5.4.5 中已經給出了大多數使用Ada 環境不需要提供動態分配物件的釋放。如果提供,它可能被隱式提供(當它們的訪問型別超出範圍時,物件會被釋放),顯式提供(當的原因。使用此功能時,不會進行檢查以驗證是否只有一個訪問路徑指向正在釋放的儲存。因此,任何其他訪問路徑都不會被請記住,記錄或陣列的任何未重置元件也可能是懸空引用,或者可能承載表示不一致資料的位模式。訪問型別的元件始終預設初始化為。這些其他訪問路徑的值可能導致錯誤執行。
如果您的 Ada 環境隱式地使用動態堆儲存,但不能完全可靠地回收和重用堆儲存,則不應該使用Ada 環境不需要提供動態分配物件的釋放。如果提供,它可能被隱式提供(當它們的訪問型別超出範圍時,物件會被釋放),顯式提供(當.
Unchecked Access
[edit | edit source]指南
[edit | edit source]- 儘量減少Unchecked_Access屬性的使用,最好將其隔離到包體中。
- 只對壽命/範圍為“庫級別”的資料使用Unchecked_Access屬性。
原理
[edit | edit source]可訪問性規則在編譯時以靜態方式進行檢查(訪問引數除外,它們在執行時進行檢查)。這些規則確保訪問值不會超出其所指定的物件的壽命。由於這些規則在Unchecked_Access的情況下不適用,因此可能會透過訪問路徑訪問不再處於範圍內的物件。
隔離Unchecked_Access意味著將其使用與包的客戶端隔離。您不應該將其應用於訪問值僅僅是為了向客戶端返回一個現在不安全的價值。
當您使用屬性Unchecked_Access時,您正在以不安全的方式建立訪問值。您面臨著懸空引用的風險,這反過來會導致執行錯誤 (Ada 參考手冊 1995,§13.9.1 [帶註釋的])。
該 Ada 參考手冊 1995,§13.10 [帶註釋的]) 為此危險屬性定義了以下潛在用途。“此屬性提供支援以下情況:當一個區域性物件需要被插入到一個全域性連結資料結構中時,程式設計師知道該物件將在退出其作用域之前始終從資料結構中刪除。”
- 使用地址子句將變數和條目對映到硬體裝置或記憶體,而不是模擬 FORTRAN 的“等價”功能。
- 確保在屬性定義子句中指定的地址有效,並且不與對齊衝突。
- 如果您的 Ada 環境中可用,請使用包Ada.Interrupts將處理程式與中斷關聯。
- 避免將地址子句用於未匯入的程式單元。
Single_Address : constant System.Address := System.Storage_Elements.To_Address(...);
Interrupt_Vector_Table : Hardware_Array;
for Interrupt_Vector_Table'Address use Single_Address;
為多個物件或程式單元指定單個地址的結果是未定義的,與為單個物件或程式單元指定多個地址一樣。為中斷指定多個地址子句也是未定義的。它不一定覆蓋物件或程式單元,也不一定將單個條目與多箇中斷關聯起來。
您有責任確保您指定的地址的有效性。Ada 要求地址物件是其對齊的整數倍數。
在 Ada 83 (Ada 參考手冊 1983) 中,您必須使用型別的值System.Address將中斷條目附加到中斷。雖然這種技術在 Ada 95 中是允許的,但您正在使用一個過時的功能。您應該使用受保護的過程和相應的編譯指示 (參考手冊 1995,§C.3.2)。
- 在開發期間不要抑制異常檢查。
- 如果需要,在執行期間,引入包含可以安全地移除異常檢查的最小語句範圍的塊。
如果您停用了異常檢查,並且程式執行導致一個原本會引發異常的條件,那麼程式執行將是錯誤的。結果是不可預測的。此外,您仍然必須做好準備,如果這些異常在您呼叫的子程式、任務和包的體中被引發並從其中傳播,則必須處理這些被抑制的異常。
透過最小化移除異常檢查的程式碼,您可以提高程式的可靠性。有一個經驗法則表明,20% 的程式碼佔了 80% 的 CPU 時間。因此,一旦您確定了實際需要移除異常檢查的程式碼,明智的做法是將它隔離在一個塊中(並新增適當的註釋),並將周圍的程式碼保留異常檢查。
- 在使用之前初始化所有物件。
- 在初始化訪問值時要謹慎。
- 不要依賴於不屬於語言的一部分的預設初始化。
- 從受控型別派生並覆蓋原始過程以確保自動初始化。
- 在使用實體之前確保其已細化。
- 在宣告中謹慎使用函式呼叫。
第一個示例說明了初始化訪問值的潛在問題
procedure Mix_Letters (Of_String : in out String) is
type String_Ptr is access String;
Ptr : String_Ptr := new String'(Of_String); -- could raise Storage_Error in caller
begin -- Mix_Letters
...
exception
... -- cannot trap Storage_Error raised during elaboration of Ptr declaration
end Mix_Letters;
第二個示例說明了確保實體在使用之前細化的重要性
------------------------------------------------------------------------
package Robot_Controller is
...
function Sense return Position;
...
end Robot_Controller;
------------------------------------------------------------------------
package body Robot_Controller is
...
Goal : Position := Sense; -- This raises Program_Error
...
---------------------------------------------------------------------
function Sense return Position is
begin
...
end Sense;
---------------------------------------------------------------------
begin -- Robot_Controller
Goal := Sense; -- The function has been elaborated.
...
end Robot_Controller;
------------------------------------------------------------------------
Ada 除了訪問型別之外,沒有為任何型別的物件的初始預設值定義初始預設值,訪問型別的初始預設值為 null。如果您在宣告訪問值時初始化它,並且分配引發異常Storage_Error,則異常將在呼叫過程中而不是被呼叫過程中引發。呼叫者沒有準備處理此異常,因為它對導致問題分配的原因一無所知。
作業系統在分配記憶體頁時所做的事情會有所不同:一個作業系統可能會將整個頁面清零;第二個作業系統可能什麼都不做。因此,在物件被賦予值之前使用該物件的值會導致不可預測的行為(但有限制),可能會引發異常。物件可以透過宣告隱式初始化,也可以透過賦值語句顯式初始化。在宣告時初始化是最安全的,也是維護人員最容易理解的。您還可以為記錄的元件指定預設值,作為這些記錄的型別宣告的一部分。
確保初始化並不意味著在宣告時初始化。在上面的示例中,目標必須透過函式呼叫初始化。這不能在宣告時發生,因為函式感測尚未細化,但它可以在以後作為封閉包體的語句序列的一部分發生。
在宣告(初始化)中呼叫一個未細化的函式會引發異常,Program_Error,必須在包含宣告的單元之外處理。這對函式引發的任何異常都是如此,即使它已被細化。
如果函式呼叫在宣告中引發異常,則不會在該直接作用域中處理該異常。它會被引發到封閉作用域。這可以透過巢狀塊來控制。
另請參見指南 9.2.3。
有時,細化順序可以用編譯指示來控制Elaborate_All。編譯指示Elaborate_All應用於庫單元會導致該單元及其依賴項的傳遞閉包的細化。換句話說,從該庫單元體可達的所有庫單元體都會被細化,從而防止在細化之前訪問錯誤 (參考手冊 1995,§10.3)。使用編譯指示Elaborate_Body當您希望在包宣告之後立即細化包體時。
5.9.7 直接 I/O 和順序 I/O
- 確保從Ada.Direct_IO和Ada.Sequential_IO獲得的值在範圍內。
- 使用'Valid屬性來檢查透過Ada.Direct_IO和Ada.Sequential_IO
原理
獲得的標量值的有效性。[編輯 | 編輯原始碼]異常Data_Error可能由Read異常這些包中找到的程式傳播,如果讀取的元素不能被解釋為所需子型別的值(Ada 參考手冊 1995,§A.13 [註釋])。但是,如果相關的檢查過於複雜,實現可能不會傳播異常。在讀取的元素不能被解釋為所需子型別的值,但
命名關聯允許在對現有呼叫的影響最小的情況下向子程式插入新引數。
沒有傳播的情況下,結果值可能異常,對該值的後續引用會導致錯誤執行。
有時很難迫使最佳化編譯器對編譯器認為在範圍內的值執行必要的檢查。大多數編譯器供應商允許抑制最佳化的選項,這可能會有所幫助。
異常傳播[編輯 | 編輯原始碼]以下程式碼展示了使用或調整防止異常傳播到任何使用者定義的
原理
過程之外,在每個過程的末尾為所有預定義和使用者定義的異常提供處理程式。[編輯 | 編輯原始碼]以下程式碼展示了使用或調整使用Program_Error來傳播異常會導致有界錯誤(Ada 參考手冊 1995,§7.6.1 [註釋])。要麼異常將被忽略,要麼
將引發異常。
受保護的物件原理
不要在受保護的入口、受保護的過程或受保護的函式中呼叫可能阻塞的操作。- 該Ada 參考手冊 1995,§9.5.1 [註釋] 列出了可能阻塞的操作語句之後
- Select語句之後
- Accept
- 入口呼叫語句語句之後
- Delay語句之後
- Abort
- 任務建立或啟用
- 對受保護的子程式(或外部重新排隊)的外部呼叫,其目標物件與受保護操作的目標物件相同
對子程式的呼叫,其主體包含可能阻塞的操作Program_Error呼叫受保護入口、過程或函式內的任何這些可能阻塞的操作,可能會導致檢測到有界錯誤或死鎖情況。在有界錯誤的情況下,異常
將被引發。此外,避免在受保護的入口、過程或函式中呼叫可能直接或間接呼叫作業系統原語或類似操作的例程,這些操作會導致 Ada 執行時系統無法識別的阻塞。
Abort 語句原理
不要建立依賴於完全包含在中止延遲操作執行內的主任務的任務。- 中止延遲操作是以下操作之一
- 受保護的入口、受保護的過程或受保護的函式使用者定義的Initialize
- 受保護的入口、受保護的過程或受保護的函式以下程式碼展示了使用作為受控物件的預設初始化的最後一步使用的過程
- 受保護的入口、受保護的過程或受保護的函式調整用於受控物件最終化的過程
用於受控物件賦值的過程Program_Error該Ada 參考手冊 1995,§9.8 [註釋] 指出指南中不鼓勵的做法會導致有界錯誤。如果實現檢測到錯誤,將引發異常abort。如果實現沒有檢測到錯誤,操作將按中止延遲操作之外的方式進行。一個
語句本身可能沒有效果。
摘要- 當迴圈巢狀時,將名稱與迴圈關聯(Booch 1986、1987)。
- 將名稱與包含任何exit語句的迴圈關聯。
- [編輯 | 編輯原始碼]
- 在所有exit來自巢狀迴圈的語句中使用迴圈名稱。
- 在包規範和主體末尾包含定義的程式單元名稱。
- 在任務規範和主體末尾包含定義的識別符號。
- 在accept語句的迴圈關聯。
- 末尾包含入口識別符號。
- 在子程式主體末尾包含設計器。
當塊巢狀時,將名稱與塊關聯。
引數列表- [編輯 | 編輯原始碼]
- 用描述性的方式命名形式引數名形式引數,以減少對註釋的需求。
- 在很少使用或具有許多形式引數的子程式或入口的呼叫中使用命名引數關聯。
- 在例項化泛型時使用命名關聯。
- 當實際引數是任何字面量或表示式時,使用命名關聯進行澄清。
- 提供預設引數,以便偶爾對廣泛使用的子程式或條目進行特殊使用。
- 將預設引數放在形式引數列表的末尾。
- 考慮為新增到現有子程式的新引數提供預設值。
- 在很少使用或具有許多形式引數的子程式或入口的呼叫中使用命名引數關聯。
- 使用適用於您的應用程式的最嚴格的引數模式。
顯示所有過程和入口引數的模式指示(Nissen 和 Wallis 1984)。
型別- 透過從現有型別派生新型別來使用現有型別作為構建塊。
- 對子型別使用範圍約束。
- 定義新型別,尤其是派生型別,以包含最大可能的價值集合,包括邊界值。
- 使用子型別限制派生型別的範圍,排除邊界值。
- 當沒有有意義的元件新增到型別時,使用型別派生而不是型別擴充套件。
- 避免使用匿名陣列型別。
- 僅當不存在或無法建立合適的型別,並且陣列不會被整體引用(例如,用作子程式引數)時,才為陣列變數使用匿名陣列型別。
- 使用訪問引數和訪問判別式來確保引數或判別式被視為常量。
- 優先從受控型別派生,而不是使用受限私有型別。
- 優先使用受限私有型別,而不是私有型別。
- 優先使用私有型別,而不是非私有型別。
- 顯式匯出所需的操作,而不是放寬限制。
- 使用訪問子程式型別來間接訪問子程式。
- 在可能的情況下,使用抽象標記型別和分派,而不是訪問子程式型別來實現子程式的動態選擇和呼叫。
- 在宣告判別式時,使用盡可能受約束的子型別(即,具有儘可能具體的範圍約束的子型別)。
- 使用帶判別的記錄,而不是受約束的陣列來表示實際值不受約束的陣列。
- [編輯 | 編輯原始碼]
- 使用記錄來分組異構但相關的資料。
- 使用訪問型別指向類範圍內的型別來實現異構多型資料結構。
- 使用標記型別和型別擴充套件,而不是變體記錄(與列舉型別和 case 語句結合使用)。
- 記錄結構不應總是扁平的。提取出公共部分。
- 對於大型記錄結構,將相關元件分組到較小的子記錄中。
- 對於巢狀記錄,選擇在引用內部元素時可讀性良好的元素名稱。
- 考慮使用型別擴充套件來組織大型資料結構。
- 區分靜態資料和動態資料。謹慎使用動態分配的物件。
- 僅當需要動態建立和銷燬動態分配的資料結構,或需要透過不同的名稱引用它們時,才使用動態分配的資料結構。
- 不要丟棄指向未分配物件的指標。
- 不要留下指向已分配物件的懸空引用。
- 初始化記錄中所有訪問變數和元件。
- 不要依賴記憶體釋放。
- 顯式釋放記憶體。
- 使用長度子句來指定總分配大小。
- 為Storage_Error.
- 提供處理程式。
- 使用受控型別來實現操作動態資料的私有型別。
- 除非您的執行時環境可靠地回收動態堆儲存,否則避免使用無約束記錄物件。
- 複合物件足夠大(在編譯時),以便編譯器在堆上隱式分配
- 除非您的執行時環境可靠地回收動態堆儲存,或者您正在建立永久的、動態分配的任務,否則請避免在以下情況下宣告任務
- 元件為任務的無約束陣列子型別
- 包含任務陣列的元件的判別記錄子型別,其中陣列大小取決於判別的值
- 除庫包或主子程式的最外層、未巢狀的宣告部分之外的任何宣告區域
- 儘量減少對別名變數的使用。
- 除無約束記錄之外的無約束複合型別的物件
- 當想要隱藏內部連線和簿記資訊時,使用別名來引用資料結構的一部分。
- 使用訪問辨別式建立自引用資料結構,即資料結構的其中一個元件指向封閉結構。
- 對靜態建立的參差不齊的陣列使用別名(Rationale 1995,§3.7.1)。和和或.
當你建立需要按位操作的資料結構(例如
表示式- 使用'第一個或'最後一個而不是使用數字字面量來表示範圍的第一個或最後一個值。
- 使用'範圍或範圍的子型別名稱,而不是'第一個 .. '最後一個.
- 使用陣列屬性'第一個, '最後一個,或'長度而不是使用數字字面量來訪問陣列。
- 使用'範圍陣列的,而不是索引子型別的名稱來表達範圍。
- 使用'範圍而不是'第一個 .. '最後一個來表達範圍。
- [編輯 | 編輯原始碼]
- 使用圓括號來指定子表示式的求值順序,其正確性取決於從左到右的求值。
- 使用括號指定子表示式求值的順序以澄清表示式(NASA 1987)。
- 選擇標誌的名稱,使它們表示可以以肯定形式使用的狀態。
- 使用邏輯運算子的短路形式來指定條件的順序,當一個條件失敗意味著另一個條件將引發異常時。
- 使用<=和>=在包含實數運算元的關係表示式中,而不是=.
語句
避免依賴於使用負數的名稱和構造。- [編輯 | 編輯原始碼]
- 最大程度地減少巢狀表示式的深度(Nissen 和 Wallis 1984)。
- 最大程度地減少巢狀控制結構的深度(Nissen 和 Wallis 1984)。
- 使用切片而不是迴圈來複制陣列的一部分。
- 儘量減少使用others選擇在case語句的迴圈關聯。
- 中。不要在case語句帶來的維護影響。
- 使用case語句中使用列舉文字的範圍,而應使用if/elsif語句(如果可能)。
- 使用型別擴充套件和分派,而不是case嘗試使用簡化啟發式方法。
- 使用for迴圈,儘可能地使用。
- 使用while迴圈,當無法在進入迴圈之前計算迭代次數,但可以在迴圈頂部應用簡單的延續條件時。
- 使用帶有exit語句的普通迴圈來處理更復雜的情況。
- 避免在exit迴圈中使用while和for語句。
- 語句,如果可能的話。
- 使用exit語句,以增強迴圈終止程式碼的可讀性 (NASA 1987)。
- 使用exit when ...而不是if ... then exit儘可能地使用 (NASA 1987)。
- 複查exit語句放置。
- 最大程度地減少退出迴圈的方式。
- 考慮指定迴圈的界限。
- 不要使用goto語句帶來的維護影響。
- 最小化return語句 從子程式 (NASA 1987) 中退出。
- 突出顯示return語句使用註釋或空白,以防止它們在其他程式碼中丟失。
- 使用塊來區域性化宣告的範圍。
- 使用塊來執行區域性重新命名。
- 使用塊來定義區域性異常處理程式。
- 考慮指定遞迴的界限。
- 使用聚合而不是一系列賦值來將值分配給記錄的所有元件
- 僅在引數存在常規排序時使用位置關聯。
- 當您需要為運算子提供可見性時,請使用use type子句的類似指南。
- 避免/最小化使用use子句 (Nissen 和 Wallis 1984)。
- 考慮使用包重新命名子句而不是use子句用於包。
- 考慮在以下情況下使用use子句
- 當需要標準包且沒有引入歧義引用時
- 當需要對列舉文字的引用時
- 本地化所有use子句的效果。
- 將重新命名宣告的範圍限制為必要的最小範圍。
- 如果完全限定的長名稱變得笨拙,請重新命名以減少複雜性。
- 如果此子程式僅僅呼叫第一個子程式,則使用重新命名來提供子程式的主體。
- 為了可見性目的,請重新命名宣告,而不是使用 use 子句,運算子除外。
- 當您的程式碼與使用非描述性或不適用的命名法的可重用元件互動時,請重新命名部分。
- 使用專案範圍內的標準縮略詞列表來重新命名常用包。
- 提供一個use type而不是一個重新命名子句來為運算子提供可見性。
- 將過載限制為對不同型別引數執行類似操作的廣泛使用子程式(Nissen 和 Wallis 1984)。
- 保留過載運算子的傳統意義(Nissen 和 Wallis 1984)。
- 使用“+”來標識新增、聯接、增加和增強型別的函式。
- 使用“-“”來標識減法、分離、減少和消耗型別的函式。
- 當應用於標記型別時,謹慎而一致地使用運算子過載。
- 為私有型別定義適當的相等運算子。
- 考慮重新定義私有型別的相等運算子。
- 當為型別過載相等運算子時,請維護代數等價關係的屬性。
- 如果可以輕鬆高效地做到這一點,請避免導致引發異常。
- 為無法避免的異常提供處理程式。
- 使用異常處理程式透過將錯誤處理與正常執行分離來增強可讀性。
- 不要使用異常和異常處理程式作為goto語句帶來的維護影響。
- 不要評估由於語言定義的檢查失敗而變得異常的物件(或物件的一部分)的值。
- 在為others編寫異常處理程式時,透過Exception_Name, Exception_Message,或Exception_Information捕獲並返回有關異常的額外資訊,這些子程式在預定義包中宣告Ada.Exceptions.
- 使用others中宣告,僅用於捕獲不能顯式列舉的異常,最好僅用於標記潛在的中止。
- 在開發過程中,捕獲others,捕獲正在處理的異常,並考慮為該異常新增一個顯式處理程式。
- 處理所有異常,包括使用者定義的異常和預定義的異常。
- 對於可能引發的每個異常,在合適的框架中提供一個處理程式,以防止異常意外傳播到抽象之外。
- 不要依賴於能夠識別引發故障的預定義異常或實現定義的異常。
- 使用Ada.Exceptions中定義的功能來捕獲有關異常的儘可能多的資訊。
- 使用塊將程式碼的區域性區域與其自己的異常處理程式相關聯。
- 使用Ada.Unchecked_Conversion僅在萬不得已的情況下才使用(《Ada 參考手冊 1995,第 13.9 節》[Annotated])。
- 考慮在以下情況下使用'Valid屬性用於檢查標量資料的有效性。
- 確保從Ada.Unchecked_Conversion正確地表示引數子型別的某個值。
- 隔離Ada.Unchecked_Conversion的使用,放在包體中。
- 隔離Ada 環境不需要提供動態分配物件的釋放。如果提供,它可能被隱式提供(當它們的訪問型別超出範圍時,物件會被釋放),顯式提供(當的使用,放在包體中。
- 確保在退出本地物件的範圍後,不存在對本地物件的懸空引用。
- 儘量減少Unchecked_Access屬性的使用,最好將其隔離到包體中。
- 只對壽命/範圍為“庫級別”的資料使用Unchecked_Access屬性。
- 使用地址子句將變數和條目對映到硬體裝置或記憶體,而不是模擬 FORTRAN 的“等價”功能。
- 確保在屬性定義子句中指定的地址有效,並且不與對齊衝突。
- 如果您的 Ada 環境中可用,請使用包Ada.Interrupts將處理程式與中斷關聯。
- 避免將地址子句用於未匯入的程式單元。
- 在開發期間不要抑制異常檢查。
- 如果需要,在執行期間,引入包含可以安全地移除異常檢查的最小語句範圍的塊。
- 在使用之前初始化所有物件,包括訪問值。
- 在初始化訪問值時要謹慎。
- 不要依賴於不屬於語言的一部分的預設初始化。
- 從受控型別派生並覆蓋原始過程以確保自動初始化。
- 在使用實體之前確保其已細化。
- 在宣告中謹慎使用函式呼叫。
- 確保從Ada.Direct_IO和Ada.Sequential_IO獲得的值在範圍內。
- 使用'Valid屬性來檢查透過Ada.Direct_IO和Ada.Sequential_IO
- [編輯 | 編輯原始碼]以下程式碼展示了使用或調整防止異常傳播到任何使用者定義的
- [編輯 | 編輯原始碼]
- 不要在中止延遲操作中使用非同步 select 語句。
- 語句。