第叁章: 复杂资料型态

MudOS v21c2

中阶 LPC

 Descartes of Borg
 November 1993

第叁章: 复杂资料型态

3.1 简单的资料型态

在基础 LPC  课本里, 你学到常见的基本 LPC  资料型态: 整数 (int)、字串 (string)、物件 (object) 、无传回值 (void).  重要的是, 你学到很多运算式(operation) 和函式 (function) 会因为运算不同的变数资料型态而有不同的行为. 如果你用错资料型态, 有的运算子 (operator) 和函式会给你错误讯息. 例如: "a" + "b" 处理起来就跟 1 + 1  不同. "a" + "b" 把 "b"  加在 "a"  的後面, 得到 "ab".  另一方面, 1 + 1 你不会得到 11,  你会得到你所期望的 2.

我把这些资料型态归类为简单资料型态, 因为它们基本到无法拆成更小的资料型态元件. 物件资料型态是个例外, 但是你实际上也没办法知道它由什麽元素组成. 所以我把它归类为简单资料型态.

本章介绍复杂资料型态的概念, 它是由许多简单资料型态单元所组成的. LPC 有两种常见的复杂资料型态, 两种都属於阵列. 第一种, 传统的阵列 (array), 以连续的各个元素储存数值, 并以数字代表所储存的值在第几号元素 (element) 中. 第二种是称为映射 (mapping)  的关联性阵列 (associative array). 映射把一些数值结合起来, 让资料处理起来更接近一般人的习性.


3.2 数值: NULL  (虚无) 和 0

深入了解阵列以前, 第一个要先彻底了解的观念是 NULL 的观念和 0  的观念. 在 LPC  中, 一个虚无值 (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.

回到上一页