【LPC】Object 概念
第一章: LPC 的基本概念
1.1 LP 系統(tǒng)純物件導(dǎo)向的設(shè)計(jì)概念
LPMud 的世界為一純物件的世界,構(gòu)成此世界的最基本元素就是物件。一個物件的產(chǎn)生,代表該物件被載入了記憶體中,但是并不一定經(jīng)過編譯。系統(tǒng)給予每個物件獨(dú)一無二的識別名稱□物件名稱 (object name),在一個世界里每個物件只有一個物件名稱,永遠(yuǎn)不會和其他的物件重復(fù)。物件導(dǎo)向的世界里,同樣類型的物件擁有相同的屬性(property),改變屬性的值代表變更了物件的特徵及外觀,譬如人類類型的物件,有著相同的姓名屬性,改變姓名屬性的值可以代表不同的人,同樣的改變性別屬性可以區(qū)分男女,以職業(yè)屬性來區(qū)分每個人所會做的工作;我們使用物件方法(method)來存取及變更屬性,物件方法也能依照屬性值的不同,而使物件能夠做不同的工作。
1.2 物件的編譯(compile)、載入(load)與復(fù)制(clone)
一個物件原則上具有一個實(shí)際存在的 .c 檔案,當(dāng)我們希望載入或復(fù)制此物件,而此物件之原始物件 (original object)并不存在于記憶體中時,此檔案會經(jīng)過編譯并被載入。一個物件可以透過以下幾種幾個外部函式 (efun, external function)載入:
- load_object(filename) - 只載入原始物件
- find_object(filename, 1) - 結(jié)果同上
- new(filename, ...) - 載入原始物件(如果未被載入),并從原始物件復(fù)制一份出來,產(chǎn)生一復(fù)制物件
- clone_object(filename, ...) - 結(jié)果同上
- call_other(filename, func, ...) 或 filename->func() -與 1, 2 相同, 但載入或?qū)ふ业皆嘉锛?,會立即呼叫該物件的方法函式func, 使用 call_other(filename, "???") 或是在 ??? 處填入任意不存在的方法函式名稱,則只會載入物件,亦即與 1, 2 結(jié)果相同,值得注意的是,filename 必須是一字串型態(tài)才可進(jìn)行載入物件的動作
注:標(biāo)出 ... 的部分目前不必理會,只需要知道該處可送入一群參數(shù),也可以完全不需要送入?yún)?shù),如 new("/obj/test"), new("/obj/test", 10) 、 new("/obj/test", 10, "abcd", 1234, "test")
在上面我們提到了原始物件(original object)和復(fù)制物件(cloned object),這兩個物件的差別在于原始物件含有經(jīng)過編譯后的執(zhí)行碼(opcode),占據(jù)較大的記憶體空間,而每個物件身上的屬性均占有不同的記憶體空間,也就是互不相干。要注意的是無論原始物件在被復(fù)制時其中的屬性值為何,其值并不會被復(fù)制,而是依程式撰寫的方式?jīng)Q定其初始值(initial value) 。 當(dāng)一個變數(shù)被摧毀時,如果其方法函式正在運(yùn)作,并不會立即停止程式的執(zhí)行,但是其物件屬性已被完全消滅,如果摧毀后的執(zhí)行過程存取到物件屬性,則會發(fā)生執(zhí)行期錯誤(runtime error)。
原始物件被摧毀之后,如果仍有其復(fù)制物件存在,則指令碼仍被暫存于記憶體的某處,被這些復(fù)制物件所參考著。要注意,當(dāng)新的原始物件被載入后,產(chǎn)生了新的指令碼,但是那些舊的復(fù)制物件依然參考著舊的指令碼。當(dāng)然,由此新物件所復(fù)制出來的物件,參考到的是新的指令碼。
原始物件的物件名稱通常是原始檔名去掉最后的 .c 部分,如果是 .c.c 的檔名則僅去掉最后一個 .c ,如果該原始物件已存在,將不會進(jìn)行編譯和載入的動作。而復(fù)制物件則的物件名稱均有一個 # 號,后面接著一個數(shù)字編號,這個編號在整個 mud 中無論是否為同一個物件所復(fù)制出來的,都不會重復(fù),復(fù)制物件的特徵就是其物件名稱具有一個 # 號。
當(dāng)然,更精確的判斷方式是使用 clonep() 這個外部函式判斷。
1.3 虛擬物件(virtual object)
前面提到,原則上一個物件均會有一個實(shí)際存在的 .c 檔案,但是也是有例外的,這種物件我們稱作虛擬物件,虛擬物件是當(dāng)系統(tǒng)嘗試載入一實(shí)際 .c 檔案不存在之物件時,經(jīng)過一些處理之后,將其他物件的物件名稱變更為此檔案對應(yīng)之原始或復(fù)制物件之名稱,完成虛擬物件的載入或復(fù)制動作。虛擬物件在大多數(shù)的應(yīng)用下均有復(fù)制物件的特性,以上一小節(jié)最后提到的精確判斷方式將可察覺,當(dāng)然某些虛擬物件并不具備復(fù)制物件的特性,原因是拿來取代的物件本身就是原始物件。具有復(fù)制物件特性的虛擬物件,其名稱并不一定有# 號,虛擬物件將會在后面的章節(jié)作詳細(xì)的介紹。
1.4 本章總結(jié)
1. LPMud 的基本構(gòu)成元素為物件(object)。
2. 每個物件具有屬性(property)及物件方法(method)。
3. 每個物件均有一獨(dú)一無二的物件名稱(object name)。
3. 依是否包含指令碼(opcode)可區(qū)分為 ---┬ 原始物件(original object)
└ 復(fù)制物件(cloned object)
4. 編譯(compile)的動作僅在原始物件不存在時才會進(jìn)行。
5. 原始物件被摧毀后如果其復(fù)制物件依然存在,其舊版之指令碼依然存在且被參考。
6. 物件被摧毀并不會停止其方法函式之執(zhí)行,但不得存取其物件屬性,否則會發(fā)生錯誤。
7. 復(fù)制物件被復(fù)制時其屬性初始值(initial value)與原始物件屬性現(xiàn)存值無關(guān)。
8. 依實(shí)際的 .c 檔案是否存在可分為 ---┬ 實(shí)體物件(real object)
└ 虛擬物件(virtual object)
第二章: 物件的空間概念
2.1 環(huán)境(environment)與內(nèi)容物(inventory)
LPC 的世界里,大多數(shù)的物件均有一個環(huán)境,而該環(huán)境的內(nèi)容物即包含了該物件。舉一個簡單的例子,玩家 player A、player B 目前站在一個房間 room A ,而此房間放置了一個物品 item A ,那么 player A、player B 及 item A 均為 room A 的內(nèi)容物,相對的room A 為 player A、player B 及 item A 的環(huán)境。實(shí)際狀況如圖所示:
room A┌———————————┐
│ │
│ item A │
│ player A ○ │
│ ☆ │
│ │
│ player B │
│ ☆ │
│ │
│ │
└———————————┘
任何物件均能被當(dāng)作其他物件的環(huán)境,我們可以將任何物件視為一個容器,如 player A將 item A 撿起,實(shí)際情形如下:
room A
┌—————————————┐
│ player A │
│┌————————┐ │
││ ○ item A │ │
││ │ │
│└————————┘ │
│ player B │
│ ┌—————————┐ │
│ │ 空 的 │ │
│ └—————————┘ │
└—————————————┘
此時,我們可以執(zhí)行 environment(player A 或 player B) 來得到環(huán)境物件 room A,可以執(zhí)行 environment(item A) 得到 player A,而透過執(zhí)行 all_inventory(room A),可以得到 player A 及 player B 的物件集合(LPC 程式里即為一物件陣列),要注意的是,這樣無法取得 item A,除非執(zhí)行了 all_inventory(player A)。
2.2 物件的移動
延續(xù)上一節(jié),這里開始說明物件的移動和移動的規(guī)則。
將 item A 移動到 player A 身上的動作,我們可以呼叫 item A 身上的物件方法,其中有一行會執(zhí)行 move(player A) ,如此便能將 item A 移動到 player A 物件中。要注意物件的移動有一個規(guī)則,就是不能將自己移動到自己身上,譬如說 player A 身上的一方法函式執(zhí)行了 move(player A) ,此時將會發(fā)生錯誤,另外,因?yàn)?player A 目前是位于room A 之中,因此如果呼叫了 room A 中的某一物件方法,使其執(zhí)行了 move(player A),此時亦會發(fā)生 player A 被移動至自己身上的狀況,因此亦會產(chǎn)生錯誤。當(dāng)然,如果在player A 并不位于 room A 的情形下,那么是可以將 room A 移動到 player A 身上的,但是以正常角度去想,一個人是不可能搬得動一間房子的,因此我們應(yīng)當(dāng)在與移動相關(guān)的物件方法上檢查物件的重量屬性,以及目標(biāo)物的負(fù)重量屬性來判斷這個移動是否合理。
必須注意,如果 room A 外層有一 area A 存在,那么此 area A 亦不能被移動至內(nèi)部任一內(nèi)容物身上,其示意圖如下,當(dāng)然,此時將 room B 移動到 room A 甚至是 player A中,是被允許的,因?yàn)? room A 和 player A 并不是 room B 本身或其內(nèi)容物。
area A
┌————————————————————————┐
│ room A room B │
│ ┌—————————————┐ ┌——————┐│
│ │ player A │ │ ││
│ │┌————————┐ │ │ 空 ││
│ ││ ○ item A │ │ │ ││
│ ││ │ │ │ ││
│ │└————————┘ │ │ ││
│ │ player B │ │ 的 ││
│ │ ┌—————————┐ │ │ ││
│ │ │ 空 的 │ │ │ ││
│ │ └—————————┘ │ └——————┘│
│ └—————————————┘ │
│ │
└————————————————————————┘
物件的移動在使用 add_action() 的系統(tǒng)上,將會產(chǎn)生一連串的 init() 事件呼叫;另外,移動物件基本上必須呼叫被移動物件本身的物件方法來執(zhí)行 move 的動作,但是我們也能夠透過創(chuàng)造一 move 之函式指標(biāo)(function pointer),再使用 bind 改變該函式指標(biāo)之擁有者(function owner)為欲移動之物件,將之執(zhí)行,亦能得到相同的效果。這些部分將留在后面的章節(jié)講解。
2.3 本章總結(jié)
1. LPC 的世界里大多數(shù)可見的物件均有一環(huán)境,該物件為該環(huán)境物件的內(nèi)容物之一。
2. 透過執(zhí)行 environment(object A),可以取得 object A 的環(huán)境物件。
3. 透過執(zhí)行 all_invenotry(object A),可以取得 object A 中所有的內(nèi)容物。
4. 在一個物件 object A 的物件方法中執(zhí)行了 move(object B),則此物件會被移動到
object B 中。
5. 物件不能被移動到自己或是其所屬的內(nèi)容物身上。
第三章: 副程式(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 之結(jié)果 print 出來
└———— return;
}
必須注意的是,返回的位置依然在同一行,而并非是其下一行,原因是該行若有其他副程式或是函式,則應(yīng)當(dāng)繼續(xù)執(zhí)行。其中,main 是 print_sum 的主程式,函式 main 的副程式為 print_sum,必須注意的是,主程式和副程式屬于相對關(guān)系的名詞,而不是絕對的。
3.2 函式(function)
函式在數(shù)學(xué)上的中文被翻譯作函數(shù),其實(shí)是相同的意思,這里先看一個簡單的例子:
int main()
{
┌—→ printf("%d\n", f(4, 5)); ——┐
│ return 1; │
│ } │(1) 呼叫 sum 函式,將參數(shù)4, 5
│ │
│(2) 將 x + y 的結(jié)果傳回 │
│ │
│ int f(int x, int y) ←—————┘
│ {
└—— return x + y;
}
在數(shù)學(xué)領(lǐng)域中,這種情形我們可以寫作一函數(shù)式: f(x, y) = x + y。
在 f(4, 5) 被執(zhí)行時,則以 x = 4, y = 5 代入,函數(shù) f 將 x + y 之結(jié)果 9 傳回,依括號由內(nèi)向外拆的原則,原程式變?yōu)閜rintf("%d\n", 9) ,此時會將 9 print 出來。
在程式語言中,一般將 function 翻譯作函式,但是在原文上是相同的字。一個函式會有一執(zhí)行結(jié)果,并被傳回。而函式的內(nèi)容可被視為一個黑箱 (black box),示意圖如下:
Input Output
┌————————┐
x ——┤ │
│ │
輸入?yún)?shù) y ——┤ Black Box ├——→ 將結(jié)果輸出
│ │
z ——┤ │
└————————┘
在把需要的功能寫成一個一個函式之后,此后撰寫程式就不需要去考慮到函式的內(nèi)容是什么,我們只需要提供一個說明,告訴別人這個函式是做什么的,該輸入什么型態(tài)的參數(shù)資料,每個參數(shù)會被用來做什么,以及輸出的資料型態(tài)和執(zhí)行結(jié)果是什么,簡單說,往后只需要使用這個現(xiàn)成函式而已。這種程式設(shè)計(jì)方式被稱做模組化程式設(shè)計(jì)(modularity pro-gramming),而直接使用現(xiàn)存的函式,不需要了解其細(xì)部處理動作,將函式本身視為一個黑箱,只留下使用函式的介面,這種情形被稱做資訊隱藏(information hiding)。
函式所做的工作不一定是數(shù)學(xué)運(yùn)算,可能是進(jìn)行一些其他的動作,譬如一個自動販賣機(jī),傳入的參數(shù)可以是錢和所按的按鈕,傳出的參數(shù)則是所購得的商品。函式與數(shù)學(xué)函數(shù)一樣,在一對一、多對一,都是合法的情形,但是不允許有多對多的狀況發(fā)生,事實(shí)上這種情形非常容易理解,因?yàn)? return 敘述只會被執(zhí)行一次,該函式即傳回后面的值,并結(jié)束執(zhí)行,以前面的例子而言,可以想像成每次購買的行為只會使自動販賣機(jī)每次只會掉出一樣商品。一個資訊隱藏的例子是,假設(shè)自動販賣機(jī)的辨?zhèn)渭敖灰椎裙δ芤驯恢瞥涩F(xiàn)存函式,我們將不需要再去考慮那些細(xì)節(jié),只要將該函式拿來使用即可。
前面提到,副程式在 LPC 中可視為一無傳回值之函式,因此以后我們將其統(tǒng)稱為函式。
3.3 物件的功能□物件方法(object method)
在純物件導(dǎo)向的世界里,每個物件可能會有一些功能,這些由一個個函式寫成的功能,我們將其稱為這個物件的方法。有些物件方法只允許物件自己使用,屬于自身的運(yùn)作,不希望被外界呼叫干涉,這種物件方法被稱作為私有的 (private)。而一個物件的方法如果希望能讓外界呼叫以提供一些服務(wù)(services),這種物件方法被稱作公共的(public)。我們將由下面范例說明,如何以物件本身的方法來變更或提供查詢其名稱的功能。
object A
┌——————————————┐
│string name; │ □ 物件屬性
├——————————————┤
│void change_name(string arg)│ ┐
│{ │ │
│ name = arg; │ │
│} │ │
-¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨- ├ 物件方法
│string query_name() │ │
│{ │ │
│ return name; │ │
│} │ ┘
└——————————————┘
我們可以在 object A 中執(zhí)行 change_name("abcd"),來將 object A 本身的 name 屬性設(shè)定為 abcd ,也可以執(zhí)行 query_name() 來查詢 object A 本身的 name 屬性。在此介紹資訊隱藏的另一個概念,就是保護(hù)物件本身的資料,不被任意的使用和更改,任何修改屬性的動作均需透過物件方法,我們可以在物件方法上加入其他檢查來設(shè)限。
物件和物件間的溝通透過訊息(messages)的傳遞,物件的方法函式接收訊息后,會開始執(zhí)行相關(guān)的功能,并視需求對呼叫者回應(yīng) (respond)。訊息的傳遞可視為非物件導(dǎo)向語言中函式間所傳遞的參數(shù) (arguments)。物件訊息的傳遞可示意如下:
object A object B
┌———————┐ ┌——————————————┐
│ 屬性 A │ │ name │
-¨¨¨¨¨¨¨¨- -¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨-
│ 屬性 B │ │ gender │
├———————┤ 改變 name 的訊息 ├——————————————┤
│ 方法 A ├—————————→│void change_name(string arg)│
-¨¨¨¨¨¨¨¨- 查詢 name 的訊息 -¨¨¨¨¨¨¨¨¨¨¨¨¨¨¨-
│ 方法 B ├—————————→│string query_name() ├—┐
│ │←——┐ └——————————————┘ │
-¨¨¨¨¨¨¨¨- │ │
│ 方法 C │ └———————————————————————┘
└———————┘ 回應(yīng) object A 的查詢動作
在 LPC 中,物件傳遞訊息的方式被稱作 call other。我們可以使用 call_other() 這個外部函式,或是使用呼叫運(yùn)算子(call other operator): ->
上圖中 object A 的方法 A 欲改變 object B 的 name 屬性,可以使用:
1. call_other(B, "change_name", "abcd");
2. B->change_name("abcd")
這兩行程式碼的意義是相同的。
因?yàn)?object B 的 query_name 之方法函式有傳回值,而 object A 的方法 B 呼叫它來詢問 name 屬性的值,所以 query_name 這個方法將會回應(yīng)一個查詢結(jié)果,此時我們通常會在 object A 的方法 B 中將此值暫存或是直接處理,如:
1. printf("%s\n", B->query_name()); 將 B 的 name 屬性 print 出來
2. temp_name = B->query_name(); 將 B 的 name 屬性存入 temp_name 變數(shù)中 使用 call_other 的方式呼叫其他物件中的方法,如果該方法函式并不存在,將不會有任何事情發(fā)生,將其視為一無效的呼叫,但是之前提過,如果 B 的部分填入的是原始物件名稱,該物件如果不存在,將會進(jìn)行載入的動作。 call_other 傳遞參數(shù)如果超過目標(biāo)方法函式的參數(shù)個數(shù),多馀的部分將被自動忽略,如果個數(shù)不足,將會自動補(bǔ)上 0,要注意這個 0 是一個 undefined zero,undefined zero 將在后面的章節(jié)中介紹。
要注意,如果是物件本身的函式呼叫自己本身的函式,傳遞的參數(shù)個數(shù)必須完全符合,否則將會在編譯期間產(chǎn)生錯誤。我們可以使用 varargs (可變參數(shù):variable arguments)來告訴編譯器這個方法函式的參數(shù)是可變的,不足的部分將會被自動補(bǔ) 0,而不會產(chǎn)生錯誤,varargs 是一個函式的修飾字(modifier),這些也留在后面的章節(jié)作介紹。
3.4 本章總結(jié)
- 副程式在 LPC 中被視為一無傳回值的函式,視為函式的一種。
- 函式通常會傳回一個值,每次呼叫函式只能傳回一個值。
- 將問題切割成一個個較小的問題,針對每個小問題設(shè)計(jì)一個處理函式,這種方式被稱為模組化程式設(shè)計(jì)。
- 資訊隱藏的兩個目的:
- 不需要知道執(zhí)行動作每一個的細(xì)節(jié),只需要提供資訊,就能取得結(jié)果。
- 保護(hù)物件本身的資料,避免被任意使用和修改。
- 物件中提供物件運(yùn)作的功能或是改變屬性的函式被稱作方法函式或物件的方法。
- 物件和物件之間透過訊息來溝通,LPC 中使用 call_other 傳遞訊息。
- 不希望被外界使用的物件方法稱私有的,提供外界呼叫的方法稱為公共的。
- 物件間傳遞訊息時,送給方法函式的參數(shù)個數(shù)不相符也不會發(fā)生錯誤。
- 物件內(nèi)部函式的參數(shù)傳遞,參數(shù)個數(shù)必須相符,除非加上 varargs 修飾字,否則會發(fā)生編譯期錯誤。
