精華區beta mud_sanc 關於我們 聯絡資訊
中階 LPC Descartes of Borg November 1993 第三章: 複雜資料型態 3.1 簡單的資料型態 在基礎 LPC 課本裡, 你學到常見的基本 LPC 資料型態: 整數 (int)、字串 (string)、物件 (object) 、無傳回值 (void). 重要的是, 你學到很多運算式 (operation) 和函式 (function) 會因為運算不同的變數資料型態而有不同的行 為. 如果"b" 處理起來就跟 1 + 1 不同. "a" + "b" 把 "b" 加在 "a" 的 如: "a" + "b" 處理起來就跟 1 + 1 不同. "a" + "b" 把 "b" 加在 "a" 的 後面, 得到 "ab". 另一方面, 1 + 1 你不會得到 11, 你會得到你所期望的 2. 我把這些資料型態歸類為簡單資料型態, 因為它們基本到無法拆成更小的資料型 態元件. 物件資料型態是個例外, 但是你實際上也沒辦法知道它由什麼元素組成. 所以我把它歸類為簡單資料型態. 本章介紹複雜資料型態的概念, 它是由許多簡單資料型態單元所組成的. LPC 有 兩種常見的複雜資料型態, 兩種都屬於陣列. 第一種, 傳統的陣列 (array), 以 連續的各個元素儲存數值, 並以數字代表所儲存的值在第幾號元素 (element) 中. 第二種是稱為映射 (mapping) 的關聯性陣列 (associative array). 映射 把一些數值結合起來, 讓資料處理起來更接近一般人的習性. 3.2 數值: NULL (虛無無值 (null value) 由整數 0 代表之. 雖然整數 0 和 NULL 常常隨意轉換, 在你進入複雜資料型態的領域時, 這種情況常會導致莫大 的困擾. 你可能在使用字串時, 已經碰過此種困擾. 0 對整數來說, 表示你把任何數值加上 0 還是原來的數值. 對任何資料型態的 加法運算來說, ZERO (零值) 對此資料型態來講, 就是你把任何值加上去都維持 原值. 所以: A 加 ZERO 等於 A. A 是一個已定資料型態的值, 而且 ZERO 是該 資料型態的零值. 這並不算是任何一種正式的數學定義. 雖然還是有一種定義, 但是我不是數學家, 所以我也不知道它的名詞是什麼. 總之對整數來說, 0 是零 值, 因為 1 + 0 等於 1. 另一方面來說, NULL 表示沒有任何值或沒有意義. LPC driver 如果能了解 NULL 在該處的意義, 就把 NULL 解釋成整數 0. 除了整數的加法以外, 加上 NULL 會導致錯誤. NULL 產生錯誤的原因是, 把那些資料型態加上其他沒有值的資料 型態沒有意義. 從另一個觀點來看, 我們知道 "a" 加上什麼值結果會得到 "a", 所以我們可以 得出字串的零值. 答案不是 0, 而是 "". 對整數來說, NULL 和 0 可以互換 是因為 0 代表整數資料型態沒有其值. 這種可互換性對其他的資料型態並不適 用, 因為其他資料型態的零值並不代表沒有其值. 換句話說, "" 表示一個沒有 長度的字串, 而它與 0 完全不一樣. 當你第一次宣告任何型態的變數, 它都沒有值. 除了整數以外, 在執行任何運算 之前都需要經過初始化. 通常, 全域變數在 create() 函式中初始化, 而區域變 數在區域函數的開端指定某些值, 通常是該資料型態的零值. 舉例來說, 在以下 的程式碼中, 我想要作出一個由隨機單字組成的字串: string build_nonsense() { string str; int i; str = ""; /* 在此, str 以字串的零值初始化 */ for(i=0; i<6; i++) { switch(random(3)+1) { case 1: str += "bing"; break; case 2: str += "borg"; break; case 3: str += "foo"; break; } if(i==5) str += ".\n"; else str += " "; } return capitalize(str); } 如果我們沒有對 str 初始化, 嘗試把一個字串加上零值會導致錯誤. 不過, 在 此段程式碼中將 str 以字串的零值 "" 初始化. 之後, 程式進入一個有六次週 期的迴圈, 每次把字串加上三個單字的其中一個. 除了最後一個單字之外, 每個 單字後面均加上一個空白字元. 此函式最後離開迴圈, 把這個無意義的字串轉換 成大寫, 然後結束. 3.3 LPC 的陣列 (array) 字串是 LPC 一種強大的複雜資料型態, 讓你在一個單一變數中存取多個值. 舉 例來說, Nightmare mud 中, 玩家交易時使用多種貨幣. 但是, 其中只有五種貨 幣是硬貨幣 (hard currency). 在此, 硬貨幣隨時可以兌換成其他種類的硬貨幣 , 但是軟貨幣 (soft currency) 只能購買之, 不能出售. 在銀行裡, 有一張硬 貨幣表讓銀行老闆知道哪種貨幣屬於硬貨幣. 使用簡單資料型態, 每次處理貨幣 兌換交易時, 我們必須要執行以下難看的運算: int exchange(string str) { string from, to; int amt; if(!str) return 0; if(sscanf(str, "%d %s for %s", amt, from, to) != 3) return 0; if(from != "platinum" && from != "gold" && from != "silver" && from != "electrum" && from != "copper") { notify_fail("我們不接受軟貨幣 !\n"); return 0; } ... } 以五種硬貨幣來說, 我們有一個相當簡單的例子. 全部只需要兩行的程式碼, 用 於 if 敘述中過濾不接受兌換的貨幣種類. 但是, 如果你必須檢查所有遊戲中不 能使用的貨幣種類, 怎麼辦 ? 遊戲中可能有 100 種; 你想寫一百條 if 敘述 ? 如果你想在硬貨幣表上加上一種新的貨幣呢 ? 這表示, 你必須把遊戲中每一項 檢查硬貨幣的 if 子句加入新的部分. 陣列讓你簡易地存取一組相關的資料, 讓 你每次執行運算時, 不用分別處理每一個值. 一個陣列常數看起來大概像這樣: ({ "platinum", "gold", "silver", "electrum", "copper" }) 這是一個字串陣列. 陣列中個別的資料值稱為元素 (element), 或是有時候稱為 成員 (member). 在程式碼裡, 作為常數的字串前後以 "" 表示, 陣列常數前後 以 ({ }) 表示, 陣列中個別的元素以 , (逗號) 分開. 你可以使用任何簡單的或複雜的 LPC 資料型態陣列. 由不同種類的值所組成的 陣列稱作混合 (mixed) 型態陣列. 在多數的 LPC driver 中, 你使用一種 C 語言的陣列語法來宣告陣列. 這種語法常常困擾撰寫 LPC 程式的人, 因為這種 語法在 C 中的意義並不能轉用到 LPC 中. 無論如何, 如果我們想用一個字串 型態的陣列, 我們要用以下的方式宣告它: string *arr; 換句話說, 陣列中包含的元素, 其資料型態之後跟著一個空白字元和一個星號. 不過請你記住, 新宣告的字串陣列, 其宣告時裡頭是 NULL 值. 3.4 使用陣列 你應該了解如何宣告並認識程式碼中的陣列. 要了解它們在程式碼中如何運作, 讓我們回顧一下前面銀行的程式碼, 這次我們用陣列: string *hard_currencies; int exchange(string str) { string from, to; int amt; if(!str) return 0; if(sscanf(str, "%d %s for %s", amt, from, to) != 3) return 0; if(member_array(from, hard_currencies) == -1) { notify_fail("我們不接受軟貨幣 !\n"); return 0; } ... } 這段程式碼假設 hard_currencies 是一個全域變數, 並且在 create() 中初始 化: hard_currencies = ({ "platinum", "gold", "electrum", "silver", "copper" }); 最佳的做法是把硬貨幣在標頭檔 (header file) 中定義為 #define, 讓所有的 物件都能使用之, 不過 #define 在以後的章節會提到. 一旦你知道 member_array() 外部函式的功能後, 這種方式就比較容易讀懂, 也 比較容易撰寫. 實際上, 你大概已經猜到 member_array() 外部函式的功能: 它 告訴你一個指定的值是否在某個陣列中. 此處特別是指, 我們想知道玩家想賣出 的貨幣是否為 hard_currencies 陣列中的元素. 你可能會感到混淆的是, member_array() 不只告訴我們特定值是否為陣列中的元素, 實際上還告訴我們 陣列中的哪一個元素是此值. 它要怎麼告訴你是哪個元素 ? 如果你把陣列變數當作是擁有一個數字, 就比較 容易瞭解它. 對上面的參數舉例來說, 我們假設 hard_currencies 擁有 179000 的值. 這個值告訴 driver 要到哪裡尋找 hard_currencies 所代表的陣列. 所 以, hard_currencies 指向一個可以找到陣列值的地方. 當有人談到陣列的第一 個元素時, 它們希望該元素位於 179000. 當一個物件需要陣列第二個元素的值 時, 它就找 179000 + 一個值, 然後 179000 加上兩個值就是第三個, 以此類推. 我們因此可以藉由陣列元素的索引來存取個別的陣列元素, 索引就是在陣列起點 之後第幾個值, 而我們在陣列中找尋數值. 對 hard_currencies 陣列來說: "platinum" 索引為 0. "gold" 索引為 1. "electrum" 索引為 2. "silver" 索引為 3. "copper" 索引為 4. 如果在陣列中有此種貨幣, member_array() 傳回其元素的索引, 如果陣列中沒 有則傳回 0. 要參考一個陣列中的單獨元素時, 你要照著以下的方式使用之: 陣列名稱[索引號] 範例: hard_currencies[3] hard_currencies[3] 會是 "silver". 所以, 你現在應該知道陣列以全體或個別元素出現的方式. 全體而言, 你用它的 名稱參考 (reference) 之, 而一個陣列常數前後以 ({ }) 圍住, 並且用 , (逗號) 分隔其元素. 對個別的元素而言, 你用陣列名稱跟著前後加上 [] 的索 引號碼來參考陣列變數, 而對陣列常數來說, 你可以如同相同型態的簡單資料型 態常數般參考之. 整個陣列: 變數: arr 常數: ({ "platinum", "gold", "electrum", "silver", "copper" }) 陣列中個別的元素: 變數: arr[2] 常數: "electrum" 你可以將這些參考的方式, 用於你以前習慣其他資料型態的方法. 你可以指定其 值、將其值用於運算式中、將其值當成參數傳入函式中、用其值當作傳回值. 請 記得一件很重要的事, 當你單獨處理一個元素時, 單獨的元素本身不是陣列 (除 非你處理的是陣列的陣列). 在上述的範例中, 單獨的元素是字串. 所以: str = arr[3] + " and " + arr[1]; 會造出一個字串等於 "silver and gold". 雖然這看起來很簡單, 很多剛開始接 觸陣列的人試著在陣列中加入新元素時, 就遇到麻煩. 當你處理整個陣列, 並想 要加入新元素時, 你必須用另一個陣列加上去. 注意以下的例子: string str1, str2; string *arr; str1 = "hi"; str2 = "bye"; /* str1 + str2 等於 "hibye" */ arr = ({ str1 }) + ({ str2 }); /* arr 等於 ({ str1, str2 }) */ 更深入以前, 我必須說明這個製作陣列的例子是極為恐怖的方法. 你應該這樣來 設定陣列: arr = ({ str1, str2 }). 不過, 這個例子的重點是, 你必須以同樣 的資料型態進行加法. 如果你試著把一個元素以其資料型態加入一個陣列, 你會 得到錯誤. 你必須將它視為一個只有單一元素的陣列處理之. 3.5 映射 (mapping) LPMud 中, 一個最重要的進步是創立了映射資料型態. 大家亦稱它為關聯性陣列. 實際上來說, 一個陣列讓你不用像陣列般使用數字索引一個值. 映射讓你使用實 際上對你有意義的值當作其值的索引, 比較像一個相關的資料庫 (relational database). 在一個有五個元素的陣列中, 你個別使用它們 0 到 4 的整數索引存取這些值. 想像一下, 再回到錢幣的範例中. 玩家有不同數量、不同種類的錢幣. 在玩家物 件中, 你需要一個方法儲存這些錢幣的種類, 並把該種貨幣與玩家有多少數量連 結起來. 對陣列來說, 最好的方法就是儲存一個表示錢幣種類的字串陣列, 和另 一個整數陣列代表有多少錢. 這樣會產生一段吃光 CPU 的難看程式碼: int query_money(string type) { int i; i = member_array(type, currencies); if(i>-1 && i < sizeof(amounts)) /* sizeof 外部函式傳回元素的總數 */ return amounts[i]; else return 0; } 這是一個簡單的查詢函式. 接下來看一個加法函式: void add_money(string type, int amt) { string *tmp1; int * tmp2; int i, x, j, maxj; i = member_array(type, currencies); if(i >= sizeof(amounts)) /* 錯誤的資料, 我們用了一個爛方法 */ return; else if(i== -1) { currencies += ({ type }); amounts += ({ amt }); return; } else { amounts[i] += amt; if(amounts[i] < 1) { tmp1 = allocate(sizeof(currencies)-1); tmp2 = allocate(sizeof(amounts)-1); for(j=0, x =0, maxj=sizeof(tmp1); j < maxj; j++) { if(j==i) x = 1; tmp1[j] = currencies[j+x]; tmp2[j] = amounts[j+x]; } currencies = tmp1; amounts = tmp2; } } } 這實在是一些很爛的程式碼, 只為了增加錢這種簡單的概念. 首先, 我們要得知 玩家有哪些種類的錢幣, 如果有, 它是貨幣陣列中的哪一個元素. 之後, 我們必 須檢查更動過之貨幣資料是否完整. 如果在貨幣陣列中, 貨幣種類的索引大於錢 幣數量陣列的元素總數, 則我們就出了問題. 因為這兩個陣列之間僅靠索引連結 其關係. 只要我們知道資料正確無誤, 如果玩家手上目前沒有該種貨幣, 我們僅 把這種貨幣當作新的元素加入貨幣陣列, 並把其數量也當作新元素加入數量陣列. 最後, 如果玩家手上持有該種貨幣, 我們就把其數量加在數量陣列中相對的索引 上. 如果錢幣數量小於 1, 表示用完該種貨幣, 我們想把該種貨幣從記憶體中清 除之. 從一個陣列中減去一個陣列不是一件簡單的事. 舉個例子, 下面的結果: string *arr; arr = ({ "a", "b", "a" }); arr -= ({ arr[2] }); 你認為 arr 最後的值是多少 ? 唔, 它是: ({ "b", "a" }) 從原來的陣列減去 arr[2] 並不會從該陣列中除去第三個元素. 反之, 它從該陣 列減去其第三個元素的值. 而陣列的減法是把該陣列中第一次出現的該值刪除之. 既然我們不想被迫去計算該元素在陣列中是否唯一, 我們就被迫要翻幾個觔斗以 從兩個陣列中同時除去正確的元素. 如此才能保持兩個陣列索引的關聯性. 映射提供了一個比較好的方式. 它們讓你直接把錢幣種類和其總數連結在一起. 有些人認為映射就相當於, 一種不限制你只能用整數當索引的陣列. 事實上, 映 射是一種徹底不同的概念, 用於儲存多個集團資訊. 陣列強迫你選擇一種對機器 才有意義的索引, 該索引用於尋找正確資料位置之用. 這種索引告訴機器在首值 之後第幾個元素才是你想要找的值. 而映射, 你可以選擇對你有意義的索引, 不 用擔心機器要怎麼去尋找和儲存它. 以下是映射的格式: 常數: 整個: ([ 索引:值, 索引:值 ]) 例: ([ "gold":10, "silver":20 ]) 元素: 10 變數值: 整個: map (map 是映射變數的名稱) 元素: map["gold"] 所以現在我的貨幣函式看起來像: int query_money(string type) { return money[type]; } void add_money(string type, int amt) { if(!money[type]) money[type] = amt; else money[type] += amt; if(money[type] < 1) map_delete(money, type); /* 用於 MudOS */ ...或... money = m_delete(money, type) /* 用於 LPMud 3.* 衍生版本 */ ... 或... m_delete(money, type); /* 用於 LPMud 3.* 衍生版本 */ } 請先注意, 從一個映射中清除一個映射元素的外部函式, 每種 driver 都不同. 查詢你的 driver 文件說明, 以得知適當的外部函式名稱及語法. 你可以馬上看到, 你不需要檢查你資料的完整性, 因為你想得知的兩個值密不 可分地結合在一起. 另外, 刪除無用的值只需要一個簡單的外部函式呼叫, 不 用一個繁雜而耗費 CPU 的迴圈. 最後, 查詢的函式只需要一行 return 指令. 使用映射以前, 你必須宣告並將其初始化. 宣告看來如下: mapping map; 而通常初始化看來如下: map = ([]); map = allocate_mapping(10) ...OR... map = m_allocate(10); map = ([ "gold": 20, "silver": 15 ]); 跟其他的資料型態一樣, 它們通常的運算也有其規則定義, 像是加法和減法: ([ "gold":20, "silver":30 ]) + ([ "electrum":5 ]) 得到: (["gold":20, "silver":30, "electrum":5]) 雖然我的示範顯示出映射有個順序, 但是實際上, 映射在儲存元素時, 不保證會 遵照其順序. 所以, 最好別比較兩個映射是否相等. 3.6 總結 映射和陣列可以依照你的需求, 要有多複雜就有多複雜. 你可以造出一個陣列的 映射的陣列. 這種東西可以宣告如下: mapping *map_of_arrs; 它看起來像: ({ ([ ind1: ({valA1, valA2}), ind2: ({valB1, valB2}) ]), ([ indX: ({valX1,valX2}) ]) }) 映射可以使用任何一種資料型態作為索引, 包括物件. 映射索引常常稱作關鍵 (key), 是來自資料庫的名詞. 你隨時要謹記在心, 對於任何非整數的資料型 態而言, 作一般像是加法或減法的運算使用之前, 你必須先將其變數初始化. 雖然利用映射和陣列撰寫 LPC 程式變得簡單又方便, 沒有正確地將其初始化 所產生的錯誤, 常常把剛接觸這種資料型態的新手逼瘋. 我敢說大家最常碰到 映射和陣列的錯誤, 是以下三者之一: Indexing on illegal type. Illegal index. Bad argument 1 to (+ += - -=) /* 看你最喜歡哪一種運算 */ 第一個和第三個幾乎都是因為出問題的陣列或映射沒有正確初始化. 第二種錯誤 訊息通常是當你試著使用一個已初始化過的陣列中所沒有的索引. 另外, 對陣列 來說, 剛接觸陣列的人常得到第三種錯誤訊息, 因為他們常試著將一個單獨的元 素加入一個陣列, 把初始的陣列與單一的元素值相加, 而沒有把一個含有該單一 元素的陣列與初始的陣列相加. 請記住, 只能把陣列加上陣列. 行文至此, 你應該覺得能自在地使用映射和陣列. 剛開始使用它們時, 應會碰上 以上的錯誤訊息. 使用映射成功的關鍵, 在於除去這些錯誤訊息, 並找出你程式 設計上, 何處使你試著使用沒有初始化的映射和陣列. 最後, 回到最基本的房間 程式碼, 並看看像是 set_exits() 之類的函式 (或在你的 mudlib 上相當的函 式). 它有可能使用映射. 在某些情況下, 它會使用陣列以保持與 mudlib.h 的 相容性. Copyright (c) George Reese 1993 譯者: Spock of the Final Fronier 98.Jul.24.