Lua 5.3 拾遺——基本概念
對照: Lua 5.3 參考手冊 、Lua 5.3參考手冊(英文原文)
學的語言一多,語言的細節就容易被忽略。
此係列文章,將對照以上手冊,以相對較精簡的方式進行重新描述,便於自身加深理解和快速回顧。
紅色表示關鍵點
綠色表示個人註解
------------------------------------------------------------------------
一. 值與型別:
動態型別語言,型別存在於執行時,即變數無型別,值有型別。
庫函式 type() 以字串形式返回給定值的型別。
------------------------------ 型別一覽 ------------------------------
nil(表示空,條件語句中同false)、
boolean、
number(64 位整數和雙精度(64 位)浮點數)、
string(不可變的位元組序列)、
function
userdata(使用者資料)、
thread (一個獨立的執行序列,被用於實現協程)
table (是一個關聯陣列, 可以以除了 nil 和 NaN 之外的所有 Lua 值做索引。表是 Lua 中唯一的資料結構,可被用於表示普通陣列、序列、符號表、集合、記錄、圖、樹等等)。
---------------------------------------------------------------------------
二. 環境與全域性環境:
_ENV: 每個被編譯的 Lua 程式碼塊都會生成一個外部區域性變數 _ENV,它是一個上值(upvalue),作為
_ENV
的值的表都被稱為環境。_ENV的預設值是_G(所以_G才能在全域性任何地方被訪問)。
_G:全域性變數。Lua維護了一個“全域性環境”,它的值被儲存在 C 登錄檔的一個特別索引中。_G的初始值與這個全域性環境的值相同。所有的標準庫都被載入入全域性環境。
三. 錯誤處理:
error() 用於顯式地丟擲一個錯誤。
pcall() 或 xcall() 用於捕獲異常。
四. 元表及元方法:
Lua 中的每個值都可以有一個元表。 這個元表就是一個普通的 Lua 表, 它決定了原始值在特定操作下的行為(數學運算、位運算、比較、連線、 取長度、呼叫、索引等)(元表中定義了對各操作事件的處理方法)。 元表中還可以定義一個元方法__gc,當表物件或使用者資料物件在垃圾回收時呼叫它。
getmetatable() 用來獲取任何值的元表。setmetatable() 用來設定/改變一張表的元表。
lua中只可設定/改變表的元表(除非使用除錯庫)。
表和完全使用者資料有獨立的元表, 其它型別的值按型別共享元表,預設情況下,值沒有元表,但字串庫在初始化的時候為字串型別設定了元表。
元表可控制的操作的元方法,均以 " __ " 為字首。當兩個運算元不能直接進行要做的操作時(如,想加法時,但其中一個或兩個為非數字),Lua會依次檢查它們兩個值是否存在為對應的元方法。如果存在,則將這兩個運算元傳入元方法,結果作為操作的結果。如果不存在,則丟擲一個錯誤。
------------------------------ 元方法及呼叫時機一覽 ------------------------------
- "add": + (加)操作。 "sub":
-
(減)操作。 "mul":*
(乘以)操作。 "div":/
(除以)操作。 "mod":%
(取餘)操作。 "pow":^
(次方)操作。 "unm":-
(取負)操作。 "idiv"://
(向下取整除法)操作。 任一運算元不是number(能轉換為數字的字串除外)時事件被觸發。 - "band":
&
(按位與)操作。"bor":|
(按位或)操作。 "bxor":~
(按位異或)操作。"bnot":~
(按位非)操作。 "shl":<<
(左移)操作。"shr":>>
(右移)操作。任一運算元無法轉換為整數時事件被觸發。 - "concat":
..
(連線)操作。任一運算元既不是字串也不是數字時事件被觸發。 - "len":
#
(取長度)操作。 運算元不是字串時事件被觸發。 如果運算元是一張表且沒有元方法, Lua 使用表的取長度操作 - "eq":
==
(等於)操作。 兩個運算元都是表或都是完全使用者資料且它們不是同一個物件時事件被觸發。 呼叫的結果總會被轉換為布林量。 - "lt":
<
(小於)操作。 兩個運算元不全為整數也不全為字串時事件被觸發。 呼叫的結果總會被轉換為布林量。 - "le":
<=
(小於等於)操作。 兩個運算元不全為整數也不全為字串時事件被觸發。和其它操作不同, 此操作可能用到兩個不同的事件。 首先,Lua 在兩個運算元中查詢 "__le
" 元方法。 如果一個元方法都找不到,就會再次查詢 "__lt
" 元方法, 它會假設a <= b
等價於not (b < a)
。 呼叫的結果總會被轉換為布林量。 - "index": 索引
table[key]
。 當table
不是表或是表table
中不存在key
這個鍵時事件被觸發。 __index可以是一個方法也可以是一張表,是函式時,table
和key
將以引數傳入;是一張表時,則用key
對這個表取索引。 - "newindex": 索引賦值
table[key] = value
。 當table
不是表或是表table
中不存在key
這個鍵時事件被觸發。__newindex可以是一個方法也可以是一張表。是函式時,table
、key 和 value
將以引數傳入;是一張表時,對這張表做索引賦值操作。注意,這裡是對這張表賦值,而不是對原來的table表賦值。(如果有必要,在元方法內部可以呼叫 rawset 來做賦值。) - "call": 函式呼叫操作
func(args)
。 當呼叫一個非函式的值的時事件被觸發 (即func
不是一個函式)。 查詢func
的元方法__call, 如果存在,func
作為第一個引數傳入,原來呼叫的引數(args
)後依次排在後面。
------------------------------------------------------------------------------------------
五. 垃圾收集
Lua 在內部維護了一個 增量標記-掃描收集器 以此自動管理記憶體。 垃圾收集器間歇率 和 垃圾收集器步進倍率,控制著垃圾收集的迴圈。
垃圾收集器間歇率 控制著收集器開啟新的迴圈前要等待多久。數值100相當於1次迴圈。
垃圾收集器步進倍率 控制著收集器運作速度相對於記憶體分配速度的倍率。數值100相當於記憶體分配速度。
可以通過在 C 中呼叫 lua_gc() 或在 Lua 中呼叫 collectgarbage() 來改變這倆數字。 這兩個函式也可直接控制收集器(例如停止它或重啟它)。
在為一個物件設定元表時,在其中定義 "__gc
" 元方法,就標記了這個物件需要觸發終結器。 終結器允許你配合 Lua 的垃圾收集器做一些額外的資源管理工作 (例如關閉檔案、網路或資料庫連線,或是釋放一些你自己的記憶體)。NRatel個人認為其類似於C#中實現IDispose介面,對非託管記憶體的管理的機制。
終結器的觸發次序為標記次序的逆序。 即,先標後調,後標先調,類似棧序。
先被回收,後又因為終結器用到的物件,會被lua復活,一般情況下是短暫復活(在下次垃圾收集迴圈釋放),但如果在終結器中將物件存到了全域性,則會永久復活。
如果在終結器中對一個正進入終結流程的物件再次做一次標記讓它觸發終結器, 只要這個物件在下個迴圈中依舊不可達(unreachable, NRatel個人理解為:不可訪問,無引用),它的終結函式還會再呼叫一次。 無論是哪種情況, 物件所屬記憶體僅在垃圾收集迴圈中該物件不可達 且 沒有被標記成需要觸發終結器才會被釋放。
當使用 lua_close() 關閉一個狀態機時, Lua 將呼叫所有標記為終結的終結器。在這個過程中,任何終結器再次標記物件的行為都不會生效。
------------------------------------------------------------------------------------------
弱表 指內部元素為 弱引用 的表。可以是弱鍵-強值、強鍵-弱值、弱鍵-弱值。 垃圾收集器會忽略掉弱引用(即,按未引用處理)。
強鍵-弱值的表允許值的回收,但會阻止鍵的回收;弱鍵-弱值的表,收集器可以回收其中的任意鍵和值。
任何情況下,只要鍵或值的任意一項被回收, 相關聯的鍵值對都會從表中移除。
通過元表中的__mode
欄位(field)控制表中的弱屬性。__mode
包含字元 'k
' 時,表中所有鍵都為弱引用,包含字元 'v
' 時表中所有值都為弱引用。
弱鍵-強值的表,稱為暫時表。 暫時表中,只有當鍵可訪問時,它的值才可被訪問。特別注意,如果一個鍵只被它的值引用,那麼這個鍵值對將被刪除。
對一張表的弱屬性的修改僅在下次收集迴圈才生效。 尤其是,當你把表由弱改強時,Lua 可能在修改生效前回收表內一些專案。
只有那些有顯式構造的物件才會從弱表中移除。 某些如數字和輕量C函式的值,不受垃圾收集器管轄, 因此不會從弱表中移除 (除非它們關聯的值被回收)。 雖然字串受垃圾回收器管轄, 但它們沒有顯式的構造過程,所以也不會從弱表中移除。
弱錶針對復活的物件 (指那些正在走終結流程,僅能被終結器訪問的物件) 有著特殊的行為。 弱值引用的物件,在執行它們的終結器前就被移除了, 而弱鍵引用的物件則要等到終結器執行完畢後,到下次收集當物件真的被釋放時才被移除。 這個行為使得終結器執行時得以訪問到由該物件在弱表中所關聯的屬性。
如果一張弱表在當次收集迴圈內的復活物件中, 那麼在下個迴圈前這張表有可能未被正確地清理。
六. 協程
協程有三個狀態:suspended(掛起)、running(執行)、dead(函式走完後的狀態,這時候不能再重新resume)。
coroutine.create() 建立協程。引數是協程主函式。 返回值是其控制代碼 (一個 thread 型別的物件)。建立後,協程處於掛起狀態。
coroutine.resume() 執行協程,使協程從掛起變為執行。首個引數是 coroutine.create 建立所得的控制代碼,之後的引數是傳遞給協程函式的引數。返回值在正常結束時,返回true和協程主函式的返回值;在發生錯誤時,返回false和錯誤資訊。
coroutine.yield() 使協程掛起,並可傳入引數。 協程掛起時, coroutine.resume() 返回 true和傳入的引數。 當下次重啟此協程時, 會接著從掛起點繼續執行。這時,之前的 coroutine.yield 會返回,返回值是傳給重啟協程的方法 coroutine.resume() 的第一個引數之外的其他引數。
coroutine.wrap() 也會建立一個協程。 不同的是,它返回一個函式。 啟動協程直接呼叫這個函式即可,而不是再使用coroutine.resume 。傳入引數相當於 coroutine.resume 的額外引數。 函式只返回協程主函式的返回值,不會捕獲錯誤,不會返回是否正常結束的bool標誌。
額外注意點:
1. 當一個可以完全表示為整數的浮點數做為鍵值時, 都會被轉換為對應的整數儲存。 例如,t[2.0]=1, 實際被插入表t中的鍵是整數 2,另一方面,2 與 "2" 是兩個不同的 Lua 值, 故而它們可以是同一張表中的不同項。
2. 表、函式、執行緒、以及完全使用者資料在 Lua 中被稱為 物件: 變數並不真的 持有 它們的值,而僅儲存了對這些物件的 引用,賦值、引數傳遞、函式返回,都是針對引用而不是針對值的操作, 這些操作均不會做任何形式的隱式拷貝。
3. 訪問元表中的元方法永遠不會觸發另一次元方法。
4. 對於一元操作符(取負、求長度、位反), 元方法呼叫的時候,第二個引數是個啞元,其值等於第一個引數。這樣處理僅僅是為了所有的操作都和二元操作一致。
5. 在為一個物件設定元表時,如果設定時沒有 __gc
,之後才給元表加上 __gc, 那這個物件是沒有被成功標記的。 即,__gc 必須在一開始設定元表時就定義進去,不能後來加入。不用擔心的是,__gc
在一開始設定標記後,還是可以修改的。