第一章: LPC 的基本概念
1.1 LP 系统纯物件导向的设计概念
LPMud 的世界为一纯物件的世界,构成此世界的最基本元素就是物件。一个物件的产生,代表该物件被载入了记忆体中,但是并不一定经过编译。系统给予每个物件独一无二的识别名称□物件名称 (object name),在一个世界里每个物件只有一个物件名称,永远不会和其他的物件重复。物件导向的世界里,同样类型的物件拥有相同的属性(property),改变属性的值代表变更了物件的特徵及外观,譬如人类类型的物件,有著相同的姓名属性,改变姓名属性的值可以代表不同的人,同样的改变性别属性可以区分男女,以职业属性来区分每个人所会做的工作;我们使用物件方法(method)来存取及变更属性,物件方法也能依照属性值的不同,而使物件能够做不同的工作。
1.2 物件的编译(compile)、载入(load)与复制(clone)
一个物件原则上具有一个实际存在的 .c 档案,当我们希望载入或复制此物件,而此物件之原始物件 (original object)并不存在于记忆体中时,此档案会经过编译并被载入。一个物件可以透过以下几种几个外部函式 (efun, external function)载入:
- load_object(filename) - 只载入原始物件
- find_object(filename, 1) - 结果同上
- new(filename, ...) - 载入原始物件(如果未被载入),并从原始物件复制一份出来,产生一复制物件
- clone_object(filename, ...) - 结果同上
- call_other(filename, func, ...) 或 filename->func() -与 1, 2 相同, 但载入或寻找到原始物件后,会立即呼叫该物件的方法函式func, 使用 call_other(filename, "???") 或是在 ??? 处填入任意不存在的方法函式名称,则只会载入物件,亦即与 1, 2 结果相同,值得注意的是,filename 必须是一字串型态才可进行载入物件的动作
注:标出 ... 的部分目前不必理会,只需要知道该处可送入一群参数,也可以完全不需要送入参数,如 new("/obj/test"), new("/obj/test", 10) 、 new("/obj/test", 10, "abcd", 1234, "test")
在上面我们提到了原始物件(original object)和复制物件(cloned object),这两个物件的差别在于原始物件含有经过编译后的执行码(opcode),占据较大的记忆体空间,而每个物件身上的属性均占有不同的记忆体空间,也就是互不相干。要注意的是无论原始物件在被复制时其中的属性值为何,其值并不会被复制,而是依程式撰写的方式决定其初始值(initial value) 。 当一个变数被摧毁时,如果其方法函式正在运作,并不会立即停止程式的执行,但是其物件属性已被完全消灭,如果摧毁后的执行过程存取到物件属性,则会发生执行期错误(runtime error)。
原始物件被摧毁之后,如果仍有其复制物件存在,则指令码仍被暂存于记忆体的某处,被这些复制物件所参考著。要注意,当新的原始物件被载入后,产生了新的指令码,但是那些旧的复制物件依然参考著旧的指令码。当然,由此新物件所复制出来的物件,参考到的是新的指令码。
原始物件的物件名称通常是原始档名去掉最后的 .c 部分,如果是 .c.c 的档名则仅去掉最后一个 .c ,如果该原始物件已存在,将不会进行编译和载入的动作。而复制物件则的物件名称均有一个 # 号,后面接著一个数字编号,这个编号在整个 mud 中无论是否为同一个物件所复制出来的,都不会重复,复制物件的特徵就是其物件名称具有一个 # 号。
当然,更精确的判断方式是使用 clonep() 这个外部函式判断。
1.3 虚拟物件(virtual object)
前面提到,原则上一个物件均会有一个实际存在的 .c 档案,但是也是有例外的,这种物件我们称作虚拟物件,虚拟物件是当系统尝试载入一实际 .c 档案不存在之物件时,经过一些处理之后,将其他物件的物件名称变更为此档案对应之原始或复制物件之名称,完成虚拟物件的载入或复制动作。虚拟物件在大多数的应用下均有复制物件的特性,以上一小节最后提到的精确判断方式将可察觉,当然某些虚拟物件并不具备复制物件的特性,原因是拿来取代的物件本身就是原始物件。具有复制物件特性的虚拟物件,其名称并不一定有# 号,虚拟物件将会在后面的章节作详细的介绍。
1.4 本章总结
1. LPMud 的基本构成元素为物件(object)。
2. 每个物件具有属性(property)及物件方法(method)。
3. 每个物件均有一独一无二的物件名称(object name)。
3. 依是否包含指令码(opcode)可区分为 ---┬ 原始物件(original object)
└ 复制物件(cloned object)
4. 编译(compile)的动作仅在原始物件不存在时才会进行。
5. 原始物件被摧毁后如果其复制物件依然存在,其旧版之指令码依然存在且被参考。
6. 物件被摧毁并不会停止其方法函式之执行,但不得存取其物件属性,否则会发生错误。
7. 复制物件被复制时其属性初始值(initial value)与原始物件属性现存值无关。
8. 依实际的 .c 档案是否存在可分为 ---┬ 实体物件(real object)
└ 虚拟物件(virtual object)
第二章: 物件的空间概念
2.1 环境(environment)与内容物(inventory)
LPC 的世界里,大多数的物件均有一个环境,而该环境的内容物即包含了该物件。举一个简单的例子,玩家 player A、player B 目前站在一个房间 room A ,而此房间放置了一个物品 item A ,那么 player A、player B 及 item A 均为 room A 的内容物,相对的room A 为 player A、player B 及 item A 的环境。实际状况如图所示:
room A
┌———————————┐
│ │
│ item A │
│ player A ○ │
│ ☆ │
│ │
│ player B │
│ ☆ │
│ │
│ │
└———————————┘
任何物件均能被当作其他物件的环境,我们可以将任何物件视为一个容器,如 player A将 item A 捡起,实际情形如下:
room A
┌—————————————┐
│ player A │
│┌————————┐ │
││ ○ item A │ │
││ │ │
│└————————┘ │
│ player B │
│ ┌—————————┐ │
│ │ 空 的 │ │
│ └—————————┘ │
└—————————————┘
此时,我们可以执行 environment(player A 或 player B) 来得到环境物件 room A,可以执行 environment(item A) 得到 player A,而透过执行 all_inventory(room A),可以得到 player A 及 player B 的物件集合(LPC 程式里即为一物件阵列),要注意的是,这样无法取得 item A,除非执行了 all_inventory(player A)。
2.2 物件的移动
延续上一节,这里开始说明物件的移动和移动的规则。
将 item A 移动到 player A 身上的动作,我们可以呼叫 item A 身上的物件方法,其中有一行会执行 move(player A) ,如此便能将 item A 移动到 player A 物件中。要注意物件的移动有一个规则,就是不能将自己移动到自己身上,譬如说 player A 身上的一方法函式执行了 move(player A) ,此时将会发生错误,另外,因为 player A 目前是位于room A 之中,因此如果呼叫了 room A 中的某一物件方法,使其执行了 move(player A),此时亦会发生 player A 被移动至自己身上的状况,因此亦会产生错误。当然,如果在player A 并不位于 room A 的情形下,那么是可以将 room A 移动到 player A 身上的,但是以正常角度去想,一个人是不可能搬得动一间房子的,因此我们应当在与移动相关的物件方法上检查物件的重量属性,以及目标物的负重量属性来判断这个移动是否合理。
必须注意,如果 room A 外层有一 area A 存在,那么此 area A 亦不能被移动至内部任一内容物身上,其示意图如下,当然,此时将 room B 移动到 room A 甚至是 player A中,是被允许的,因为 room A 和 player A 并不是 room B 本身或其内容物。
area A
┌————————————————————————┐
│ room A room B │
│ ┌—————————————┐ ┌——————┐│
│ │ player A │ │ ││
│ │┌————————┐ │ │ 空 ││
│ ││ ○ item A │ │ │ ││
│ ││ │ │ │ ││
│ │└————————┘ │ │ ││
│ │ player B │ │ 的 ││
│ │ ┌—————————┐ │ │ ││
│ │ │ 空 的 │ │ │ ││
│ │ └—————————┘ │ └——————┘│
│ └—————————————┘ │
│ │
└————————————————————————┘
物件的移动在使用 add_action() 的系统上,将会产生一连串的 init() 事件呼叫;另外,移动物件基本上必须呼叫被移动物件本身的物件方法来执行 move 的动作,但是我们也能够透过创造一 move 之函式指标(function pointer),再使用 bind 改变该函式指标之拥有者(function owner)为欲移动之物件,将之执行,亦能得到相同的效果。这些部分将留在后面的章节讲解。
2.3 本章总结
1. LPC 的世界里大多数可见的物件均有一环境,该物件为该环境物件的内容物之一。
2. 透过执行 environment(object A),可以取得 object A 的环境物件。
3. 透过执行 all_invenotry(object A),可以取得 object A 中所有的内容物。
4. 在一个物件 object A 的物件方法中执行了 move(object B),则此物件会被移动到
object B 中。
5. 物件不能被移动到自己或是其所属的内容物身上。
第三章: 副程式(subroutine)、 函式(function)与物件方法(object method)
3.1 副程式(subroutine)
副程式的直接意义,就是子程序,亦即其为主程序所附属,在主程序中呼叫子程序,然后程式跳到该处做一些事情,而后返回主程序。在 LPC,副程式可以被视为是一个无传回值的函式,以下是一个副程式的例子:
int main()
{
┌———→ print_sum(3, 5); ———————┐
│ return 1; │
│ } │(1) 跳至 print_sum 副程式,
│ │ 将 3, 5 分别送入 a, b
│(3) 返回 │
│ │
│ │
│ void print_sum(int a, int b) ←——┘
│ {
│ printf("%d\n", a + b); (2) 将 a + b 之结果 print 出来
└———— return;
}
必须注意的是,返回的位置依然在同一行,而并非是其下一行,原因是该行若有其他副程式或是函式,则应当继续执行。其中,main 是 print_sum 的主程式,函式 main 的副程式为 print_sum,必须注意的是,主程式和副程式属于相对关系的名词,而不是绝对的。
3.2 函式(function)
函式在数学上的中文被翻译作函数,其实是相同的意思,这里先看一个简单的例子:
int main()
{
┌—→ printf("%d\n", f(4, 5)); ——┐
│ return 1; │
│ } │(1) 呼叫 sum 函式,将参数4, 5
│ │
│(2) 将 x + y 的结果传回 │
│ │
│ int f(int x, int y) ←—————┘
│ {
└—— return x + y;
}
在数学领域中,这种情形我们可以写作一函数式: f(x, y) = x + y。
在 f(4, 5) 被执行时,则以 x = 4, y = 5 代入,函数 f 将 x + y 之结果 9 传回,依括号由内向外拆的原则,原程式变为printf("%d\n", 9) ,此时会将 9 print 出来。
在程式语言中,一般将 function 翻译作函式,但是在原文上是相同的字。一个函式会有一执行结果,并被传回。而函式的内容可被视为一个黑箱 (black box),示意图如下:
Input Output
┌————————┐
x ——┤ │
│ │
输入参数 y ——┤ Black Box ├——→ 将结果输出
│ │
z ——┤ │
└————————┘
在把需要的功能写成一个一个函式之后,此后撰写程式就不需要去考虑到函式的内容是什么,我们只需要提供一个说明,告诉别人这个函式是做什么的,该输入什么型态的参数资料,每个参数会被用来做什么,以及输出的资料型态和执行结果是什么,简单说,往后只需要使用这个现成函式而已。这种程式设计方式被称做模组化程式设计(modularity pro-gramming),而直接使用现存的函式,不需要了解其细部处理动作,将函式本身视为一个黑箱,只留下使用函式的介面,这种情形被称做资讯隐藏(information hiding)。
函式所做的工作不一定是数学运算,可能是进行一些其他的动作,譬如一个自动贩卖机,传入的参数可以是钱和所按的按钮,传出的参数则是所购得的商品。函式与数学函数一样,在一对一、多对一,都是合法的情形,但是不允许有多对多的状况发生,事实上这种情形非常容易理解,因为 return 叙述只会被执行一次,该函式即传回后面的值,并结束执行,以前面的例子而言,可以想像成每次购买的行为只会使自动贩卖机每次只会掉出一样商品。一个资讯隐藏的例子是,假设自动贩卖机的辨伪及交易等功能已被制成现存函式,我们将不需要再去考虑那些细节,只要将该函式拿来使用即可。
前面提到,副程式在 LPC 中可视为一无传回值之函式,因此以后我们将其统称为函式。
3.3 物件的功能□物件方法(object method)
在纯物件导向的世界里,每个物件可能会有一些功能,这些由一个个函式写成的功能,我们将其称为这个物件的方法。有些物件方法只允许物件自己使用,属于自身的运作,不希望被外界呼叫干涉,这种物件方法被称作为私有的 (private)。而一个物件的方法如果希望能让外界呼叫以提供一些服务(services),这种物件方法被称作公共的(public)。我们将由下面范例说明,如何以物件本身的方法来变更或提供查询其名称的功能。
object A
┌——————————————┐
│string name; │ □ 物件属性
├——————————————┤
│void change_name(string arg)│ ┐
│{ │ │
│ name = arg; │ │
│} │ │
-¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨- ├ 物件方法
│string query_name() │ │
│{ │ │
│ return name; │ │
│} │ ┘
└——————————————┘
我们可以在 object A 中执行 change_name("abcd"),来将 object A 本身的 name 属性设定为 abcd ,也可以执行 query_name() 来查询 object A 本身的 name 属性。在此介绍资讯隐藏的另一个概念,就是保护物件本身的资料,不被任意的使用和更改,任何修改属性的动作均需透过物件方法,我们可以在物件方法上加入其他检查来设限。
物件和物件间的沟通透过讯息(messages)的传递,物件的方法函式接收讯息后,会开始执行相关的功能,并视需求对呼叫者回应 (respond)。讯息的传递可视为非物件导向语言中函式间所传递的参数 (arguments)。物件讯息的传递可示意如下:
object A object B
┌———————┐ ┌——————————————┐
│ 属性 A │ │ name │
-¨¨¨¨¨¨¨¨- -¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨-
│ 属性 B │ │ gender │
├———————┤ 改变 name 的讯息 ├——————————————┤
│ 方法 A ├—————————→│void change_name(string arg)│
-¨¨¨¨¨¨¨¨- 查询 name 的讯息 -¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨-
│ 方法 B ├—————————→│string query_name() ├—┐
│ │←——┐ └——————————————┘ │
-¨¨¨¨¨¨¨¨- │ │
│ 方法 C │ └———————————————————————┘
└———————┘ 回应 object A 的查询动作
在 LPC 中,物件传递讯息的方式被称作 call other。我们可以使用 call_other() 这个外部函式,或是使用呼叫运算子(call other operator): ->
上图中 object A 的方法 A 欲改变 object B 的 name 属性,可以使用:
1. call_other(B, "change_name", "abcd");
2. B->change_name("abcd")
这两行程式码的意义是相同的。
因为 object B 的 query_name 之方法函式有传回值,而 object A 的方法 B 呼叫它来询问 name 属性的值,所以 query_name 这个方法将会回应一个查询结果,此时我们通常会在 object A 的方法 B 中将此值暂存或是直接处理,如:
1. printf("%s\n", B->query_name()); 将 B 的 name 属性 print 出来
2. temp_name = B->query_name(); 将 B 的 name 属性存入 temp_name 变数中
使用 call_other 的方式呼叫其他物件中的方法,如果该方法函式并不存在,将不会有任何事情发生,将其视为一无效的呼叫,但是之前提过,如果 B 的部分填入的是原始物件名称,该物件如果不存在,将会进行载入的动作。 call_other 传递参数如果超过目标方法函式的参数个数,多馀的部分将被自动忽略,如果个数不足,将会自动补上 0,要注意这个 0 是一个 undefined zero,undefined zero 将在后面的章节中介绍。
要注意,如果是物件本身的函式呼叫自己本身的函式,传递的参数个数必须完全符合,否则将会在编译期间产生错误。我们可以使用 varargs (可变参数:variable arguments)来告诉编译器这个方法函式的参数是可变的,不足的部分将会被自动补 0,而不会产生错误,varargs 是一个函式的修饰字(modifier),这些也留在后面的章节作介绍。
3.4 本章总结
- 副程式在 LPC 中被视为一无传回值的函式,视为函式的一种。
- 函式通常会传回一个值,每次呼叫函式只能传回一个值。
- 将问题切割成一个个较小的问题,针对每个小问题设计一个处理函式,这种方式被称为模组化程式设计。
- 资讯隐藏的两个目的:
- 不需要知道执行动作每一个的细节,只需要提供资讯,就能取得结果。
- 保护物件本身的资料,避免被任意使用和修改。
- 物件中提供物件运作的功能或是改变属性的函式被称作方法函式或物件的方法。
- 物件和物件之间透过讯息来沟通,LPC 中使用 call_other 传递讯息。
- 不希望被外界使用的物件方法称私有的,提供外界呼叫的方法称为公共的。
- 物件间传递讯息时,送给方法函式的参数个数不相符也不会发生错误。
- 物件内部函式的参数传递,参数个数必须相符,除非加上 varargs 修饰字,否则会发生编译期错误。