跳至內容

Ada 程式設計/常量

來自華夏公益教科書,為開放世界提供開放書籍

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

變數 和常量都儲存資料。它們之間的區別在於,儲存在變數中的資料可以在程式執行期間更改,而儲存在常量中的資料則不能更改。兩者都必須宣告為特定型別,但只有常量需要在宣告時分配(初始化)值(資料)。

在 Ada 中,變數和常量實際上被稱為物件(Ada RM 3.3 [註釋])。這與面向物件程式設計無關。Ada 中的變數是一個物件,它被宣告為給定型別,並具有可選的初始值。常量是相同的,只是宣告現在包含保留字 **constant**,並且初始值不再是可選的,而是必需的。

變數和常量都在塊的宣告部分宣告。

變數和常量的宣告方式如下

X, Y: T := Some_Value;  -- Initialization optional
C: constant T := Some_Value;

X 和 Y 的初始化值是分別針對每個值計算的——因此,如果這是一個函式呼叫,它們可能具有不同的值。

最好將不更改的本地物件宣告為常量。

有關如何命名變數和常量的通用建議,請參閱 此處

常量是幫助使程式更可靠和可維護的重要工具。常量與變數一樣,是對特定 type 資料的引用,但它們與變數的不同之處在於它們的常量性質。變數是變化的,常量是恆定的。當宣告和初始化時,它們引用的資料是靜態的,不能再更改。如果您嘗試更改常量,編譯器將大聲抱怨。

讓我們看看如何宣告和初始化常量

Name : constant String (1 .. 12) := "Thomas Løcke";

Name : constant String := "Thomas Løcke";

與變數唯一的區別在於保留 constant 關鍵字,以及常量 **必須** 在宣告時初始化的事實。除此之外,宣告和初始化常量與變數完全相同,並且適用於所有 types

A_Array : constant array (1 .. 3) of Integer := (1, 2, 3);
An_Positive : constant Positive := 10;

type Colors is (Red, Blue, Green);
Color : constant Colors := Red;
type Person is record
   Name : String (1 .. 12);
   Age : Natural;
end record;
B_Person : constant Person := (Name => "Thomas Løcke", Age => 37);

如果您嘗試在程式中更改常量,編譯器將使用類似以下訊息進行抱怨

 constants.adb:15:04: left hand side of assignment must be a variable
 gnatmake: "/path/to/constants.adb" compilation error

當資料應該保持靜態時,您應該使用常量。不要低估人類犯錯的能力,例如用以下方式宣告 Pi

Pi : Float := 3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37511;

Pi 應該在程式執行期間更改嗎?可能不會。那麼為什麼要允許這樣做呢?為什麼要冒錯誤發生這種風險呢?相反,請執行以下操作

Pi : constant := 3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37511;

好了。現在您可以放心,無論發生什麼,Pi 都將是 Pi。

順便說一句,對於 Pi 本身(以及最著名的常量),您無需自己宣告它:它已經在 Ada.Numerics 包中為您完成了。

命名數字

[編輯 | 編輯原始碼]

細心的讀者會注意到,上面使用的 Pi 常量是在沒有 type 的情況下宣告的。這種常量被稱為命名數字 (3.3 [註釋]),它們的 type 被稱為通用型別 (3.4.1 [註釋])。有四種 通用型別

  • universal_integer
  • universal_real
  • universal_fixed
  • universal_access

您不能顯式地宣告一個物件為 universal_integer 型別。但您可以做的是宣告一個物件為常量 命名數字。與從這些型別派生的型別(例如 IntegerFloat)不同,這些型別可以採用任何大小或精度的值。通用型別不受從它們派生的型別所繫結的相同約束

Large_Int : Integer := 4_294_967_296;        --  Value not in range on a 32 bit machine
Named_Large_Int : constant := 4_294_967_296; --  OK on a 32 bit machine

Named_Large_Int 常量是 universal_integer 型別,因為文字 4_294_967_296 是一個整數。如果我們寫

Named_Real : constant := 4.294_967_296;

那麼 Named_Realuniversal_real 型別,因為文字 4.294_967_296 包含一個點 (2.4 [註釋])。

高階主題:常量是常量嗎?

[編輯 | 編輯原始碼]

愚蠢的問題:什麼是“常量”?

procedure Swap (X, Y: in out T) is
  Temp: constant T := X;
begin
  X := Y;
  Y := Temp;
end Swap;

在 Swap 示例中,X 的臨時副本當然是一個常量——我們沒有觸碰它。但是(你會說這微不足道),在 Swap 結束時的構建期間或銷燬期間,它並不是一個常量。

等等!

在以下內容中,您將瞭解有關“常量”(帶有關鍵字 constant 的物件)的一些驚人事實。

首次疑問

[編輯 | 編輯原始碼]

讓我們先對常量性產生懷疑。

受控型別(RM 7.6)允許程式設計師透過三種操作訪問構建、複製和銷燬的過程:Initialize、Adjust 和 Finalize。

給定受控型別 T。

X: constant T := F;

在構建 X 並使用 F 的副本進行初始化後,操作 Adjust 被呼叫,引數模式為 in out,用於常量物件 X。(由於給定了初始值,因此不會呼叫 Initialize。)

在 Ada.*_IO(RM A.6-A.10)中,Create 和 Close 的檔案引數的模式為 in out,而 Read 和 Write(分別是 Put 和 Get)的模式為 in。但這些操作中的每一個都會改變檔案引數的狀態。

這反映了一種設計,其中(內部)File 物件的“狀態”表示 File 物件的開啟狀態,而不是(外部)檔案的內容。同意這種選擇是可以爭論的,但它在整個 I/O 包中是一致的。

假定的實現模型隱含了一定程度的間接性,其中 File 引數表示一個引用,當檔案關閉時該引用本質上是“null”,而當檔案開啟時則是非空。其他一切都被埋在某種神秘的國度中,其狀態沒有反映在 File 引數的模式中。

這是任何具有指標引數的語言的症狀,這些引數的模式對更新指向的物件的能力沒有影響。

所以

物件不是常量,至少在構建和銷燬期間不是。

但正如您在 Ada.*_IO 中看到的那樣,常量物件確實也可以在生命週期內改變其狀態。(in 引數就像子程式中的一個常量。)

**“constant 關鍵字表示值永遠不會改變”——這對 Ada 來說是一個錯誤的陳述**(細節很痛苦);它對基本型別和其他一些型別是正確的,但對大多數複合型別不正確。

您可以在 Ada RM 中找到一些關於此的有趣陳述:RM 3.3(13/3, 25.1/3), 13.9.1(13/3)。相應的術語是 *不可變限制* 和 *固有可變*。

指標示例

[編輯 | 編輯原始碼]
Pointer:
declare
  type Rec is record
    I: access Integer;
  end record;
  Ob: constant Rec := (I => new Integer'(42));
  -- Ob.I is constant, but not Ob.I.all!
begin
  Ob.I.all := 53;
end Pointer;

在這裡,您一定會同意我的觀點,這很清楚明瞭。

但如果內部指標訪問整個物件本身呢?

Reflexive:
declare
  type Rec is limited record  -- immutably limited
    -- Rosen technique (only possible on immutably limited types);
    -- Ref is a variable view (Rec is aliased since it is limited).
    Ref: access Rec := Rec'Unchecked_Access;
    I  : Integer;
  end record;
  Ob: constant Rec := (I => 42, Ref => <>);
begin
  Ob.I := 53;  -- illegal
  Ob.Ref.I := 53;  -- erroneous until Ada 2005, legal for Ada 2012
end Reflexive;

該物件必須是有限的,因為複製會破壞引用。

Ada 2012 AARM 13.9.1(14.e/3):當實際物件是常量時,Rosen 技巧(以 Jean Pierre Rosen 命名)不再是錯誤的,但這是最佳實踐。

這適用於在物件生命週期的某個點上一定存在一個變數檢視,您可以將其“儲存起來”以供以後使用。這當然不會“錯誤地”發生,程式設計師是故意這麼做的。

package Controlled is
  type Rec is new Ada.Finalization.Controlled with record
    Ref: access Rec;  -- variable view
    I  : Integer;
  end record;
  overriding procedure Initialize (T: in out Rec);
  overriding procedure Adjust     (T: in out Rec);
  -- "in out" means they see a variable view of the object
  -- (even if it's a constant)
end Controlled;

您可以像這樣使用它

declare
  function Create return Controlled.Rec isend Create;
  Ob: constant Controlled.Rec := Create;
  -- here, Adjust works on a constant on an assignment operation
begin
  Ob.Ref.I := 53;  -- Ob.I := 53;  illegal
end;

將返回物件 Create 的副本賦值給 Ob,物件 Create 已完成。現在,元件 Ref 指向了一個不再存在的物件。(注意賦值語句賦值操作之間的區別。)Adjust 必須糾正錯誤的目標。

Create 可能看起來像這樣

function Create return Controlled.Rec is
  X: Controlled.Rec;  -- Initialize called
begin
  return X;
end Create;

變數 X 在宣告時沒有初始值,因此呼叫 Initialize。Create 返回此自引用物件。

這是包體

package body Controlled is
  procedure Initialize (T: in out Rec) is
  begin
    T := (Ada.Finalization.Controlled with
          Ref => T'Unchecked_Access,
          I   => 42);
  end Initialize;
  procedure Adjust (T: in out Rec) is
  begin
    T.Ref := T'Unchecked_Access;
  end Adjust;
end Controlled;

如果在物件宣告中沒有給出初始值,則僅呼叫 Initiallize。T 被視為別名,以便元件 Ref 是物件本身的變數檢視。

示例任務和受保護物件

[編輯 | 編輯原始碼]

任務是主動物件,即每個任務都有自己的控制執行緒:所有任務併發執行。任務透過呼叫其入口點之一(大致對應於子程式呼叫)與其他任務進行通訊。

由於 Ada 是一種面向塊的語言,因此任務可以定義為陣列和記錄的元件,而這些元件可以是常量。

task type TT is
  entry Set (I: Integer);
end TT;
task body TT is      -- local objects are not
  I: Integer := 42;  -- considered part of constant
begin
  accept Set (I: Integer) do
    TT.I := I;
  end Set;
end TT;
type Rec is record
  T: TT;  -- active object that changes state (even if "constant")
end record;
Ob: constant Rec := (T => <>);
Ob.T.Set (53);  -- execution of an entry of a constant task

受保護物件與任務非常相似。

protected type Prot is
  procedure Set (I: Integer);
private
  I: Integer := 42;  -- *
end Prot;
protected body Prot is
  procedure Set (I: Integer) is
  begin
    Prot.I := I;  -- Prot.I is *
  end Set;
end Prot;
type Rec is limited record
  Ref: access Rec := Rec'Unchecked_Access;  -- reflexive
  P  : Prot;
end record;
Ob: constant Rec := (others => <>);
Ob.P.Set (53);      -- illegal
Ob.Ref.P.Set (53);  -- variable view required

常量是這樣

[編輯 | 編輯原始碼]

如果宣告一個物件為“常量”,其含義只有兩方面

  • 該物件不能用於賦值(如果它尚未受限)。
  • 該物件只能用作輸入引數。

在任何情況下,這都不意味著物件的邏輯狀態是不可變的。事實上,這種型別應該是私有的,這意味著客戶端對物件內部發生的事情沒有任何影響。提供者負責正確的行為。不可變受限受控物件固有可變的。

另請參閱

[編輯 | 編輯原始碼]

華夏公益教科書

[編輯 | 編輯原始碼]

Ada 參考手冊

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