跳轉到內容

Pascal 程式設計/列舉

來自華夏公益教科書,開放的書籍,開放的世界

Pascal 的一個強大的符號和語法工具是自定義列舉資料型別的宣告。

列舉資料型別是有限的命名離散值列表。列舉實際上給單個整數值起了名字,但是,你不能(直接)對其進行算術運算。

列舉資料型別透過在資料型別識別符號後面加上一個非空的逗號分隔的(新的、以前未使用過的)識別符號列表來宣告。

type
	weekday = (Monday, Tuesday, Wednesday, Thursday, Friday,
		Saturday, Sunday);

列表中的每個專案都代表資料型別可以取的特定值。資料型別識別符號標識整個資料型別。

一旦列舉資料型別被宣告,你就可以像使用其他任何資料型別一樣使用它

var
	startOfWeek: weekday;
begin
	startOfWeek := Sunday;
end.

變數 startOfWeek 被限制為只能取資料型別 weekday 的合法值。請注意,Sunday 不用打字機引號 (') 括起來,而打字機引號通常表示字串字面量。識別符號 Sunday 表示一個本身的值。

序數值

[編輯 | 編輯原始碼]

每個列舉資料型別宣告都隱式地定義了一個順序。逗號分隔列表本質上是一個排序列表。內建函式 ord,是序數值的縮寫,使你有機會獲取列舉元素的序數值,即該列舉成員的唯一的/integer 值。

列舉的第一個元素編號為 0。第二個元素,如果有的話,編號為 1,依此類推。

某些編譯器,例如 FPC,允許你為列舉中的一些甚至所有元素指定顯式索引

type
	month = (January := 1, February, March, April, May, June,
		July, August, September, October, November, December);

這裡,January 的序數值將為 1。所有後續專案的序數值都大於 1。數字的自動分配仍然確保每個列舉成員在整個列舉資料型別中都有一個唯一的數字。 February 的序數值將為 2March 的序數值為 3,依此類推。但是,值 0 不會分配給該列舉中的任何元素。

Note 如果你為特定元素指定顯式索引,你需要確保所有數字都是升序。你不能兩次分配同一個數字。如果你跳過分配數字,自動編號系統會分配並“保留”數字。你不能使用自動系統使用的數字。

指定顯式索引是非標準擴充套件。在 FPC{$mode Delphi} 中,你需要使用一個簡單的等號 (=) 而不是 :=。這也稱為“C 風格列舉宣告”,因為程式語言 C 使用這種語法。

Pascal 沒有提供通用的函式,讓你根據數字確定列舉元素。例如,沒有函式可以返回 January,如果它被提供 integer1[fn 1]

標準函式 predsucc,分別是前驅後繼的縮寫,是自動每個列舉資料型別定義的。這些函式返回列舉值的先前值或下一個值。例如, succ(January) 將返回 February,因為它是值 January 的後繼。

但是, pred(January) 將會失敗,因為技術上不存在先於 January 的成員。列舉列表不是迴圈的。雖然在現實生活中,1 月份緊隨 12 月份,但列舉資料型別 month “不知道”這一點。

EP (EP) 標準允許在 predsucc 中提供一個可選的第二個 integer 引數。 succ(January, 2) 等同於 succ(succ(January)),更方便且更短,但是 pred(January, -2) 會返回相同的值。

利用此功能,您可以獲取給定索引的列舉值。 succ(Monday, 3) 評估為 weekday 值,該值具有序數值 3,從而實際上提供了一種 逆向 ord 函式的方式。但是,有必要知道列舉的第一個元素,並且列舉可能在其宣告中不使用任何 顯式索引(除非所有索引都與自動編號模式一致)。

運算子

[編輯 | 編輯原始碼]

列舉資料型別值自動可以與多個運算子一起使用。由於每個列舉值都具有序數值,因此可以對它們進行排序,並且您可以測試該排序。關係運算符

  • <
  • >
  • <=
  • >=
  • =
  • <>

與列舉值一起使用。例如,January < February 將評估為 true,因為 January 的序數值小於 February

但是,從技術上講,您可以比較蘋果和橘子(劇透警報:它們是不等的),所有關係運算符僅與兩種相同型別的值一起使用。在 Pascal 中,您不能將 weekday 值與 month 值進行比較。但是,像 ord(August) > ord(Monday) 合法的,因為實際上您是在比較 integer 值。

請注意,算術運算子 (+, -, 等等) 不適用於列舉資料型別,即使它們具有序數值。

Boolean 作為列舉資料型別

[編輯 | 編輯原始碼]

資料型別 Boolean 是一個內建的特殊列舉資料型別。保證

  • ord(false) = 0,
  • ord(true) = 1,因此
  • pred(true) = false

邏輯運算子

[編輯 | 編輯原始碼]

Boolean 是唯一可以直接使用邏輯運算子執行操作的列舉資料型別。

最基本的運算子是否定。它是一個一元運算子,這意味著它只期望一個運算元。在 Pascal 中,它使用關鍵字 not。透過在 Boolean 表示式前面加上 not(以及一些分隔符,例如空格字元),表示式將被否定。

表示式 結果
not true false
not false true
not

雖然這可能很直觀,但所謂的邏輯合取(用 and 表示)可能並非如此。它的真值表如下所示

tired 的值 intoxicated 的值 tired and intoxicated 的結果
false false false
false true false
true false false
true true true
合取真值表

EE 中,這通常寫成 (“乘”)甚至省略,因為(就像數學一樣)假設一個不可見的“乘”。鑑於 falsetrue 的序數值如上所述,您可以透過將它們相乘來計算 and 結果。

有點令人困惑,因為這可能與某人的自然語言相矛盾,是 or 一詞。如果任一運算元是 true,則整個表示式的結果將變為 true

raining 的值 snowing 的值 raining or snowing 的結果
false false false
false true true
true false true
true true true
析取真值表

電子工程師經常使用 符號來表示這個操作。但是,關於 Boolean 的序數值,你必須 “定義” 仍然是 .

優先順序

[edit | edit source]

像數學中的常規規則 “先乘除後加減” 一樣,連線運算子先於析取運算子進行求值。但是,由於否定運算子是一元運算子,所以無論如何它都會先進行求值。這意味著你必須非常小心,不要忘記放置括號。表示式

not hungry or thirsty

not (hungry or thirsty)

範圍

[edit | edit source]

序數型別

[edit | edit source]

列舉資料型別屬於 序數資料型別 類別。其他序數資料型別包括

  • 整數,
  • 字元,
  • 以及所有列舉資料型別,包括 Boolean

它們都具有一個共同點,即它們的值可以對映到一個唯一的 integer 值。 ord 函式允許你檢索該值。

區間

[edit | edit source]

有時,將一組值限制在某個範圍內是有意義的。例如,軍事時間時鐘上的小時數可能顯示從 0 到包括 23 的值。但是資料型別 integer 也將允許其他值。

Pascal 允許你宣告(子)範圍資料型別。一個(子)範圍資料型別有一個宿主資料型別,例如 integer,以及兩個限制。一個下限和一個上限。範圍透過按升序給出限制,並用兩個句點分隔來指定 (..)

type
	majuscule = 'A'..'Z';

限制可以作為任何可計算的表示式給出,只要它不依賴於執行時資料即可。 [fn 2] 例如,可以使用常量(已經定義的)

type
	integerNonNegative = 0..maxInt;

注意,我們將此範圍命名為 integerNonNegative 而不是 nonNegativeInteger,因為這將有助於某些文件工具或 IDEs 中的字母排序。

限制

[edit | edit source]

擁有一個(子)範圍資料型別的變數只能取 範圍內的 值。如果變數超出其合法範圍,程式將中止。可能會出現以下錯誤訊息(末尾的記憶體地址可能不同)

./a.out: value out of range (error #300 at 402a54)

相應的測試程式已使用 GPC 編譯。其他編譯器可能會發出不同的訊息。

然而,FPC 的預設配置會忽略這一點。將超出範圍的值分配給變數不會產生錯誤(如果它依賴於執行時資料)。 FPC 的開發者引用了其他編譯器的相容性原因,這些編譯器為了速度原因決定忽略超出範圍的值。 [fn 3] 你需要明確要求不能將非法值分配給序數型別變數。這可以透過在任何(關鍵)分配之前放置一個精心製作的註釋來實現: {$rangeChecks on} (不區分大小寫)或 {$R+} (區分大小寫)用於簡寫,將確保不會分配非法值,並且如果嘗試進行任何分配,程式將中止。在你的原始碼檔案中 一次 指定此編譯器開關就足夠了。 FPC‑Cr 命令列開關具有相同的效果。

Note 範圍限制必須在 儲存 值時滿足,例如在將值儲存到變數時。然而,中間 計算(例如在計算表示式時)可能會超出範圍。

選擇

[edit | edit source]

隨著列舉資料型別的出現,僅僅使用 if 分支檢查值可能會變得繁瑣和乏味。

解釋

[edit | edit source]

case 選擇語句將多個 互斥if 分支合併為一個語言結構。 [fn 4]

case sign(x) of
	-1:
	begin
		writeLn('You have entered a negative number.');
	end;
	0:
	begin
		writeLn('The numbered you have entered is sign-less.');
	end;
	1:
	begin
		writeLn('That is a positive number.');
	end;
end;

caseof 之間,可以出現任何求值為序數值的表示式。之後, -1:0:1:case 標籤。這些 case 標籤標記著備選方案的開始。每個 case 標籤之後是一個語句。

-101 表示 case 值。每個 case 標籤由一個非空的以逗號分隔的 case 值列表組成,後跟一個冒號 (:)。所有 case 值都必須是合法的常量值,常量 表示式,它們與上面的比較表示式相容,即 caseof 之間寫入的內容。每個指定的 case 值需要 唯一 出現在一個 case 標籤中。沒有 case 標籤值可以出現兩次。沒有必要按照它們的值進行排序,儘管這可以讓你的原始碼更易讀。

許多 case 的簡寫

[編輯 | 編輯原始碼]

EP 中,case 標籤可以包含範圍。

program letterReport(input, output);
var
	c: char;
begin
	write('Give me a letter: ');
	readLn(c);
	
	case c of
		'A'..'Z':
		begin
			writeLn('Wow! That’s a big letter!');
		end;
		'a'..'z':
		begin
			writeLn('What a nice small letter.');
		end;
	end;
end.

這種簡寫符號允許你捕獲許多情況。case 標籤 'A'..'Z': 包含所有大寫字母,無需單獨列出它們。

注意,任何範圍都不應與其他 case 標籤值重疊。這是禁止的。好的處理器會對這種錯誤提出警告。 GPC 會給出錯誤資訊 duplicate case-constant in `case' statementFPC 會報告 duplicate case label[fn 5],兩者都會提供一些有關你原始碼中位置的資訊。

重要的是,比較表示式的任何(預期)值都應匹配一個 case 標籤。如果比較表示式計算出的值不在任何 case 標籤包含的值範圍內,程式將中止。[fn 6] 如果不希望出現這種情況,“擴充套件 Pascal”標準允許使用一個名為 otherwise(注意,沒有冒號)的特殊 case 標籤。此 case 會處理所有沒有與之關聯的顯式 case 標籤的值。

program asciiTest(input, output);
var
	c: char;
begin
	write('Supply any character: ');
	readLn(c);
	case c of
		// empty statement, so the control characters are not
		// considered by the otherwise-branch as non-ASCII characters
		#0..#31, #127: ;
		#32..#126:
		begin
			writeLn('You entered an ASCII printable character.');
		end;
		otherwise
		begin
			writeLn('You entered a non-ASCII character.');
		end;
	end;
end.

otherwise 只能在末尾出現。之前必須至少有一個 case 標籤,否則(雙關語) otherwise case 將始終被執行,從而導致整個 case 語句失效。

BP(即 Delphi)重新使用單詞 else,其語義與 otherwise 相同。 FPCGPC 都支援兩者,儘管 GPC 可以被指示接受 otherwise

編寫一個簡明的 function,返回 month 的後繼,但對於 December,返回的值為 January
使用你在本單元中學到的知識,case 語句非常適合
function successor(start: month): month;
begin
	case start of
		January..November:
		begin
			successor := succ(start);
		end;
		December:
		begin
			successor := January;
		end;
	end;
end;

出於本練習的目的(演示關係運算符如 < 是如何自動為列舉資料型別定義的),以下也是可以接受的

function successor(start: month): month;
begin
	if start < December then
	begin
		successor := succ(start);
	end
	else
	begin
		successor := January;
	end;
end;

然而,從數學角度講,case 實現更加精確。在第一個實現中,如果引數錯誤、超出範圍,程式將中止。

使用 if then else 的第二個實現也會對非法值“錯誤地”定義。
使用你在本單元中學到的知識,case 語句非常適合
function successor(start: month): month;
begin
	case start of
		January..November:
		begin
			successor := succ(start);
		end;
		December:
		begin
			successor := January;
		end;
	end;
end;

出於本練習的目的(演示關係運算符如 < 是如何自動為列舉資料型別定義的),以下也是可以接受的

function successor(start: month): month;
begin
	if start < December then
	begin
		successor := succ(start);
	end
	else
	begin
		successor := January;
	end;
end;

然而,從數學角度講,case 實現更加精確。在第一個實現中,如果引數錯誤、超出範圍,程式將中止。

使用 if then else 的第二個實現也會對非法值“錯誤地”定義。


蘊涵 是一個邏輯運算子。在數學中,它寫成 (不同的作者更喜歡不同的箭頭)。蘊涵顯示以下行為
hasRained 的值 streetWet 的值 的結果
false false true
false true true
true false false
true true true
蘊涵真值表
用 Pascal 編寫一個 Boolean 表示式,使其得到相同的真值。假設 hasRainedstreetWetBoolean 變數;你將如何將它們關聯起來,以便整個 Boolean 表示式與數學表示式 相同?
請記住,Boolean 是一個內建列舉資料型別。這意味著它是有序的,因此這種資料型別的成員可以按關係順序排列。在程式設計中,你遇到的 的最常見翻譯是
not hasRained or streetWet
但在 Pascal 中,你可以寫
hasRained <= streetWet
因為 Boolean 是一個列舉資料型別。然而,一些沒有使用帕斯卡語言程式設計的人 (例如,編寫個人文字訊息) 可能使用 <= 來表示 ,這與 正好相反 (這意味著在數學中,,即交換了 ,是另一種同樣有效的表示 的方式)。如果你屬於這一類人,你可能會發現這種簡短的表達方式不直觀,因為在帕斯卡語言中,<= 事實上是 ≤ (小於或等於),而不是 ⇐。
請記住,Boolean 是一個內建列舉資料型別。這意味著它是有序的,因此這種資料型別的成員可以按關係順序排列。在程式設計中,你遇到的 的最常見翻譯是
not hasRained or streetWet
但在 Pascal 中,你可以寫
hasRained <= streetWet
因為 Boolean 是一個列舉資料型別。然而,一些沒有使用帕斯卡語言程式設計的人 (例如,編寫個人文字訊息) 可能使用 <= 來表示 ,這與 正好相反 (這意味著在數學中,,即交換了 ,是另一種同樣有效的表示 的方式)。如果你屬於這一類人,你可能會發現這種簡短的表達方式不直觀,因為在帕斯卡語言中,<= 事實上是 ≤ (小於或等於),而不是 ⇐。

註釋

  1. 一些編譯器,例如 FPC,允許“型別轉換”,實際上將 1 轉換為 January。然而,這種型別轉換不是一個函式,尤其是當值超出範圍時,型別轉換無法正常工作 (不會產生執行時錯誤,也不會採取其他措施)。
  2. 這是“擴充套件帕斯卡” (ISO 10206) 的擴充套件。在標準的“非擴充套件”帕斯卡 (ISO 7185) 中,只允許使用常量。
  3. 帕斯卡 ISO 標準確實允許這樣做。編譯器程式設計師可以選擇忽略這些錯誤。但是,附帶的文件 (手冊等) 應該指出這一點。
  4. 這是一個類比。case 語句通常不會被轉換為一系列 if 分支。
  5. 這個錯誤訊息是不準確的。GNU Pascal 編譯器的錯誤訊息更準確。問題在於某個值“case-constant”出現了多次。
  6. 許多編譯器在其預設配置中不遵守此要求。GNU Pascal 編譯器需要被指示為“完全”符合 ISO 標準 (‑‑classic‑pascal, ‑‑extended‑pascal 或僅 ‑‑case‑value‑checking)。在 BP 中,Delphi 將會繼續執行,並忽略缺失的 case。截至 3.2.0 版本,FreePascal 編譯器完全不考慮此要求。


下一頁: 集合 | 上一頁: 例程
主頁: 帕斯卡程式設計
華夏公益教科書