Programming in Lua
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
Programming in Lua
做者:Roberto Ierusalimschy
翻譯:www.luachina.net
Simple is beautiful
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
i
版權聲明
《Programming in Lua》的翻譯由www.luachina.net完成。本站已經徵得做者Mr.
Roberto Ierusalimschy的贊成,能夠翻譯他的著做並在本站發佈,本書的版權歸Mr. Roberto
Ierusalimschy 全部,有關版權請參考下面引自官方網站的聲明,未經許可不得擅自轉貼
或者以任何形式發佈本書,不然後果自負。
Copyright © 2003-2004 Roberto Ierusalimschy. All rights reserved.
This online book is for personal use only. It cannot be copied
to other web sites or further distributed in any form.
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
i
譯序
「袁承志知道若再謙遜,那就是瞧人不起,展開五行拳,發拳當胸打去。榮彩和旁
觀三人原本都覺得他武功有獨到之祕,哪知使出來的竟是武林中最尋常不過的五行拳。
敵對三人登時意存輕視,溫青臉上不自禁露出失望的神色。
「榮彩心中暗喜,雙拳如風,連搶三下攻勢,滿擬本身的大力魔爪手江南獨步,三
四招之間就可破去對方五行拳,那知袁承志輕描淡寫的一一化解。再拆數招,榮彩暗暗
吃驚,原來對方所使雖是極尋常的拳術,但每一招均是含勁不吐,意在拳先,舉手擡足
之間隱含極渾厚的內力。」
——金庸《碧血劍》
編程語言之於程序員,若武功招式之於習武之人,招式雖重要,但在於使用之人。
勝者之道,武功只行於表,高手用劍,片草只葉亦威力無窮。
當今武林,派別林立,語言繁雜,林林總總不可勝數。主流文化的C/C++、Java、
C#、VB;偏安一隅的Fortran;動態語言中的Perl、Tcl、Ruby、Forth、Python,以及本
書介紹的Lua;……,等等等等。再加上世界上那些不知道躲在哪的旮旯的奇奇怪怪的
hacker搗鼓出來的異想天開的語言,要想將各種語言囊入懷中,不異於癡人說夢。不信
可欣賞一下BrainFuck語言1的Hello World程序,語言自己依如其名。-☺-
>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.[
-]>++++++++[<++++>-]<.#>+++++++++++[<+++++>-]<.>++++++++[<+
++>-]<.+++.------.--------.[-]>++++++++[<++++>-]<+.[-]+++++
+++++.
雖然說語言的威力依使用者自己的修爲高低而定,但不一樣語言自己的設計又有不一樣。
若讓用 Java 寫寫操做系統內核、Perl 寫寫驅動程序、C/C++寫寫 web 應用,都無異於舍
近求遠,好刀只用上了刀背。
Lua 自己是以簡單優雅爲本,着眼於處理那些 C 不擅長的任務。藉助 C/C++爲其擴
展,Lua 可閃現無窮魅力。Lua 自己徹底遵循 ANSI C 而寫成,只要有 C 編譯器的地方,
Lua 即可發揮她的力量。Lua 不須要追求 Python 那樣的大而全的庫,太多的累贅,反而
會破壞她的優美。
語言的優美,來自於使用者本身的感悟。Lua 的優雅,也只有使用後纔會明白。
揚起帆,讓咱們一同踏上 Lua 的學習之旅……
1
有趣的Brain Fuck語言。http://www.muppetlabs.com/~breadbox/bf/
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
ii
本書的翻譯,是www.luachina.net中朋友們共同努力的結果。下面是參與翻譯與校對
的朋友:
-- file: 'thanks.lua'
-- desc: to print the list of the contributing guys
function list_iter (t)
local i = 0
local n = table.getn(t)
return function ()
i=i+1
if i <= n then return t[i] end
end
end
helpful_guys = {
"----參與翻譯----",
"buxiu", "鳳舞影天", "zhang3",
"morler", "lambda", "sunlight",
"\n",
"----參與校對----",
"鳳舞影天", "doyle", "flicker",
"花生魔人", "zhang3", "Kasi",
"\n"
}
for e in list_iter(helpful_guys) do
print(e)
end
www.luachina.net翻譯組
2005 年 7 月 26 日
注:本 pdf 爲翻譯稿,校對工做在進行。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
iii
目錄
版權聲明 ........................................................................................................................................ i
譯序 ................................................................................................................................................ i
目錄 .............................................................................................................................................. iii
第一篇 語言 ................................................................................................................................. 1
第 0 章 序言 ................................................................................................................................. 1
0.1 序言 .................................................................................................................................... 1
0.2 Lua的使用者 ....................................................................................................................... 2
0.3 Lua的相關資源 ................................................................................................................... 3
0.4 本書的體例 ........................................................................................................................ 3
0.5 關於本書 ............................................................................................................................ 3
0.6 感謝 .................................................................................................................................... 4
第 1 章 起點 ................................................................................................................................. 5
1.1 Chunks ................................................................................................................................. 5
1.2 全局變量 ............................................................................................................................ 7
1.3 詞法約定 ............................................................................................................................ 7
1.4 命令行方式 ........................................................................................................................ 7
第 2 章 類型和值 ......................................................................................................................... 9
2.1 Nil ........................................................................................................................................ 9
2.2 Booleans .............................................................................................................................. 9
2.3 Numbers............................................................................................................................. 10
2.4 Strings................................................................................................................................ 10
2.5 Functions ........................................................................................................................... 12
2.6 Userdata and Threads ........................................................................................................ 12
第 3 章 表達式 ........................................................................................................................... 13
3.1 算術運算符 ...................................................................................................................... 13
3.2 關係運算符 ...................................................................................................................... 13
3.3 邏輯運算符 ...................................................................................................................... 13
3.4 鏈接運算符 ...................................................................................................................... 14
3.5 優先級 .............................................................................................................................. 15
3.6 表的構造 ........................................................................................................................... 15
第 4 章 基本語法 ....................................................................................................................... 18
4.1 賦值語句 .......................................................................................................................... 18
4.2 局部變量與代碼塊(block) ......................................................................................... 19
4.3 控制結構語句 .................................................................................................................. 20
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
iv
4.4 break和return語句............................................................................................................. 23
第 5 章 函數 ............................................................................................................................... 24
5.1 返回多個結果值 .............................................................................................................. 25
5.2 可變參數 ........................................................................................................................... 27
5.3 命名參數 .......................................................................................................................... 28
第 6 章 再論函數 ....................................................................................................................... 30
6.1 閉包 .................................................................................................................................. 32
6.2 非全局函數 ...................................................................................................................... 34
6.3 正確的尾調用(Proper Tail Calls) ............................................................................... 36
第 7 章 迭代器與泛型for........................................................................................................... 40
7.1 迭代器與閉包 .................................................................................................................. 40
7.2 範性for的語義.................................................................................................................. 42
7.3 無狀態的迭代器 .............................................................................................................. 43
7.4 多狀態的迭代器 .............................................................................................................. 44
7.5 真正的迭代器 .................................................................................................................. 45
第 8 章 編譯•運行•調試 ....................................................................................................... 47
8.1 require函數........................................................................................................................ 49
8.2 C Packages......................................................................................................................... 50
8.3 錯誤 .................................................................................................................................. 51
8.4 異常和錯誤處理 .............................................................................................................. 52
8.5 錯誤信息和回跟蹤(Tracebacks) ................................................................................ 53
第 9 章 協同程序 ....................................................................................................................... 56
9.1 協同的基礎 ...................................................................................................................... 56
9.2 管道和過濾器 .................................................................................................................. 58
9.3 用做迭代器的協同 .......................................................................................................... 61
9.4 非搶佔式多線程 .............................................................................................................. 63
第 10 章 完整示例 ..................................................................................................................... 68
10.1 Lua做爲數據描述語言使用 ........................................................................................... 68
10.2 馬爾可夫鏈算法 ............................................................................................................ 71
第二篇 tables與objects............................................................................................................... 75
第 11 章 數據結構 ..................................................................................................................... 76
11.1 數組 ................................................................................................................................ 76
11.2 陣和多維數組 ................................................................................................................ 77
11.3 鏈表 ................................................................................................................................ 78
11.4 隊列和雙端隊列 ............................................................................................................ 78
11.5 集合和包 ........................................................................................................................ 80
11.6 字符串緩衝 .................................................................................................................... 80
第 12 章 數據文件與持久化 ..................................................................................................... 84
12.1 序列化 ............................................................................................................................ 86
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
v
第 13 章 Metatables and Metamethods ...................................................................................... 92
13.1 算術運算的Metamethods............................................................................................... 92
13.2 關係運算的Metamethods............................................................................................... 95
13.3 庫定義的Metamethods................................................................................................... 96
13.4 表相關的Metamethods................................................................................................... 97
第 14 章 環境 ........................................................................................................................... 103
14.1 使用動態名字訪問全局變量 ...................................................................................... 103
14.2 聲明全局變量 ............................................................................................................... 104
14.3 非全局的環境 .............................................................................................................. 106
第 15 章 Packages .................................................................................................................... 109
15.1 基本方法 ...................................................................................................................... 109
15.2 私有成員(Privacy) ...................................................................................................111
15.3 包與文件 .......................................................................................................................112
15.4 使用全局表 ...................................................................................................................113
15.5 其餘一些技巧(Other Facilities)...............................................................................115
第 16 章 面向對象程序設計 ....................................................................................................118
16.1 類 ...................................................................................................................................119
16.2 繼承 .............................................................................................................................. 121
16.3 多重繼承 ...................................................................................................................... 122
16.4 私有性(privacy) ...................................................................................................... 125
16.5 Single-Method的對象實現方法 ................................................................................... 127
第 17 章 Weak表 ...................................................................................................................... 128
17.1 記憶函數 ...................................................................................................................... 130
17.2 關聯對象屬性 .............................................................................................................. 131
17.3 重述帶有默認值的表 .................................................................................................. 132
第三篇 標準庫 ......................................................................................................................... 134
第 18 章 數學庫 ....................................................................................................................... 135
第 19 章 Table庫 ...................................................................................................................... 136
19.1 數組大小 ....................................................................................................................... 136
19.2 插入/刪除 ..................................................................................................................... 137
19.3 排序 .............................................................................................................................. 137
第 20 章 String庫 ..................................................................................................................... 140
20.1 模式匹配函數 .............................................................................................................. 141
20.2 模式 .............................................................................................................................. 143
20.3 捕獲(Captures) ........................................................................................................ 146
20.4 轉換的技巧(Tricks of the Trade) ............................................................................ 151
第 21 章 IO庫 ........................................................................................................................... 157
21.1 簡單I/O模式................................................................................................................. 157
21.2 徹底I/O 模式............................................................................................................... 160
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
vi
第 22 章 操做系統庫 ............................................................................................................... 165
22.1 Date和Time ................................................................................................................... 165
22.2 其它的系統調用 .......................................................................................................... 167
第 23 章 Debug庫..................................................................................................................... 169
23.1 自省(Introspective) ................................................................................................. 169
23.2 Hooks............................................................................................................................. 173
23.3 Profiles........................................................................................................................... 174
第四篇 C API ........................................................................................................................... 177
第 24 章 C API縱覽 ................................................................................................................. 178
24.1 第一個示例程序 .......................................................................................................... 179
24.2 堆棧 .............................................................................................................................. 181
24.3 C API的錯誤處理 ......................................................................................................... 186
第 25 章 擴展你的程序 ........................................................................................................... 188
25.1 表操做 .......................................................................................................................... 189
25.2 調用Lua函數 ................................................................................................................ 193
25.3 通用的函數調用 .......................................................................................................... 195
第 26 章 調用C函數................................................................................................................. 198
26.1 C 函數........................................................................................................................... 198
26.2 C 函數庫....................................................................................................................... 200
第 27 章 撰寫C函數的技巧..................................................................................................... 203
27.1 數組操做 ...................................................................................................................... 203
27.2 字符串處理 .................................................................................................................. 204
27.3 在C函數中保存狀態.................................................................................................... 207
第 28 章 User-Defined Types in C ........................................................................................... 212
28.1 Userdata ......................................................................................................................... 212
28.2 Metatables...................................................................................................................... 215
28.3 訪問面向對象的數據 .................................................................................................. 217
28.4 訪問數組 ...................................................................................................................... 219
28.5 Light Userdata ............................................................................................................... 220
第 29 章 資源管理 ................................................................................................................... 222
29.1 目錄迭代器 .................................................................................................................. 222
29.2 XML解析 ...................................................................................................................... 225
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
1
第一篇 語言
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
1
第 0 章 序言
本章包括做者的序言、文章的體例(convention)以及其它一些「每本書開頭都會的
內容」。
0.1 序言
目前不少程序語言都專一於幫你編寫成千上萬行的代碼,因此此類型的語言所提供
的包、命名空間、複雜的類型系統及無數的結構,有上千頁的文檔須要操做者學習。
而 Lua 並不幫你編寫大量的代碼的程序,相反的,Lua 僅讓你用少許的代碼解決關
鍵問題。爲實現這個目標,像其餘語言同樣 Lua 依賴於其可擴展性。可是與其餘語言不
同的是,不只用 Lua 編寫的軟件易於擴展,並且用其餘語言好比 C/C++編寫的軟件也很
容易使用 Lua 擴展其功能。
一開始,Lua 就被設計成很容易和傳統的 C/C++整合的語言。這種語言的二元性帶
來了極大的好處。Lua 是一個小巧而簡單的語言,由於 Lua 不致力於作 C 語言已經作得
很好的領域,好比:性能、底層操做以及與第三方軟件的接口。Lua 依賴於 C 去作完成
這些任務。Lua 所提供的機制是 C 不善於的:高級語言、動態結構、簡潔、易於測試和
調試等。正由於如此,Lua 具備良好的安全保證,自動內存管理,簡便的字符串處理功
能及其餘動態數據的改變。
Lua 不只是一種易於擴展的語言,也是一種易整合語言(glue language);Lua 支持
基於組件的,咱們能夠將一些已經存在的高級組件整合在一塊兒實現一個應用軟件。通常
狀況下,組件使用像 C/C++等靜態的語言編寫。但 Lua 是咱們整合各個組件的粘合劑。
又一般狀況下, (或對象)組件表現爲具體在程序開發過程當中不多變化的、佔用大量 CPU
時間的決定性的程序,例如窗口部件和數據結構。對那種在產品的生命週期內變化比較
多的應用方向使用 Lua 能夠更方便的適應變化。除了做爲整合語言外,Lua 自身也是一
個功能強大的語言。Lua 不只能夠整合組件,還能夠編輯組件甚至徹底使用 Lua 建立組
件。
除了 Lua 外,還有不少相似的腳本語言,例如:Perl、Tcl、Ruby、Forth、Python。
雖然其餘語言在某些方面與 Lua 有着共同的特點,但下面這些特徵是 Lua 特有的:
① 可擴展性。Lua 的擴展性很是卓越,以致於不少人把 Lua 用做搭建領域語言的
工具(注:好比遊戲腳本)。Lua 被設計爲易於擴展的,能夠經過 Lua 代碼或者 C
代碼擴展,Lua 的不少功能都是經過外部庫來擴展的。Lua 很容易與 C/C++、java、
fortran、Smalltalk、Ada,以及其餘語言接口。
② 簡單。Lua 自己簡單,小巧;內容少但功能強大,這使得 Lua 易於學習,很容
易實現一些小的應用。他的徹底發佈版(代碼、手冊以及某些平臺的二進制文件)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
僅用一張軟盤就能夠裝得下。
③ 高效率。Lua 有很高的執行效率,統計代表 Lua 是目前平均效率最高的腳本語
言。
④ 與平臺無關。Lua 幾乎能夠運行在全部咱們據說過的系統上,如 NextStep、
OS/二、PlayStation II (Sony)、Mac OS-九、OS X、BeOS、MS-DOS、IBM
mainframes、EPOC、PalmOS、MCF5206eLITE Evaluation Board、RISC
OS,及全部的 Windows 和 Unix。Lua 不是經過使用條件編譯實現平臺無關,而
是徹底使用 ANSI (ISO) C,這意味着只要你有 ANSI C 編譯器你就能夠編譯並
使用 Lua。
2
Lua 大部分強大的功能來自於他的類庫,這並不是偶然。Lua 的長處之一就是能夠通
過新類型和函數來擴展其功能。動態類型檢查最大限度容許多態出現,並自動簡化調用
內存管理的接口,由於這樣不須要關心誰來分配內存誰來釋放內存,也沒必要擔憂數據溢
出。高級函數和匿名函數都可以接受高級參數,使函數更爲通用。
Lua 自帶一個小規模的類庫。在受限系統中使用 Lua,如嵌入式系統,咱們能夠有
選擇地安裝這些類庫。若運行環境十分嚴格,咱們甚至能夠直接修改類庫源代碼,僅保
留須要的函數。記住:Lua 是很小的(即便加上所有的標準庫)而且在大部分系統下你
仍能夠不用擔憂的使用所有的功能。
0.2 Lua 的使用者
Lua 使用者分爲三大類:使用 Lua 嵌入到其餘應用中的、獨立使用 Lua 的、將 Lua
和 C 混合使用的。
第一:不少人使用 Lua 嵌入在應用程序,好比 CGILua(搭建動態網頁) LuaOrb、(訪
問 CORBA 對象。這些類型用 Lua-API 註冊新函數,建立新類型,經過配置 Lua 就能夠
改變應用宿主語言的行爲。一般,這種應用的使用者並不知道 Lua 是一種獨立的語言。
例如:CGILua 用戶通常會認爲 Lua 是一種用於 Web 的語言。
第二:做爲一種獨立運行的語言,Lua 也是頗有用的,主要用於文本處理或者只運
行一次的小程序。這種應用 Lua 主要使用它的標準庫來實現,標準庫提供模式匹配和其
它一些字串處理的功能。咱們能夠這樣認爲:Lua 是文本處理領域的嵌入式語言。
第三:還有一些使用者使用其餘語言開發,把 Lua 看成庫使用。這些人大多使用 C
語言開發,但使用 Lua 創建簡單靈活易於使用的接口。
本書面向以上三類讀者。書的第一部分闡述了語言的自己,展現語言的潛在功能。
咱們講述了不一樣的語言結構,並用一些例子展現如何解決實際問題。這部分既包括基本
的語言的控制結構,也包括高級的迭代子和協同。
第二部分重點放在 Lua 特有的數據結構——tables 上,討論了數據結構、持久性、包
及面向對象編程,這裏咱們將看到 Lua 的真正強大之處。
第三部分介紹標準庫。每一個標準庫一章:數學庫、table 庫、string 庫、I/O 庫、OS
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
庫、Debug 庫。
最後一部分介紹了 Lua 和 C 接口的 API,這部分介紹在 C 語言中開發應用而不是
Lua 中,應用對於那些打算將 Lua 嵌入到 C/C++中的讀者可能會對此部分更感興趣。
3
0.3 Lua 的相關資源
若是你真得想學一門語言,參考手冊是必備的。本書和 Lua 參考手冊互爲補充,手
冊僅僅描述語言自己,所以他既不會告訴你語言的數據結構也不會舉例說明,但手冊是
Lua 的權威性文檔,http://www.lua.org 能夠獲得手冊的內容。
-- Lua 用戶社區,提供了一些第三方包和文檔
http://lua-users.org
-- 本書的更新勘誤表,代碼和例子
http://www.inf.puc-rio.br/~roberto/book/
另外本書僅針對 Lua 5.0,若是你的版本不一樣,請查閱 Lua 手冊或者比較版本間的差
異。
0.4 本書的體例
<1> 字符串使用雙引號,好比"literal strings";單字符使用單引號,好比'a';模式串
也是用單引號,好比'[%w_]*'。
<2> 符號-->表示語句的輸出或者表達式的結果:
print(10)
13 + 3
--> 10
--> 16
<3> 符號<-->表示等價,即對於 Lua 來講,用 this 與 that 沒有區別。
this
<-->
that
0.5 關於本書
開始打算寫這本書是 1998 年冬天(南半球),那時候 Lua 版本是 3.1;2000 年 v4.0;
2003 年 v5.0。
很明顯的是,這些變化給本書帶來很大的衝擊,有些內容失去了它存在理由,好比
關於超值(upvalues)的複雜的解釋。一些章節被重寫,好比 C API,另一些章節被增
加進來,好比協同處理。
不太明顯的是,Lua 語言自己的發展對本書的完成也產生了很大的影響。一些語言
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
的變化在本書中並無被涵蓋進來,這並不是偶然的。在本書的創做過程當中,有的時候在
某個章節我會忽然感受很困惑,由於我不知道該從何開始或者怎樣去講問題闡述清楚。
當你想盡力去解釋清楚如何使用的前提是你應該以爲使用這個東西很容易,這代表 Lua
某些地方須要被改進。還有的時候,我順利的寫完某個章節,結果倒是沒有人能看得懂
我寫的或者沒有人對我在這個章節內表達的觀點達成一致。大部分狀況下,這是個人過
錯由於我是個做家,偶爾我也會所以發現語言自己的一些須要改進的缺陷(舉例來講,
從 upvalues 到 lexical scoping 的轉變是由無心義的嘗試所帶來的抱怨所引起的,在此書
的先前的草稿裏,把 upvalues 形容成是 lexical scoping 的一種)。
本書的完成必須服從語言的變化,本書在這個時候完成的緣由:
<1> Lua 5.0 是一個成熟的版本
<2> 語言變得愈來愈大,超出了最初本書的目標。此外一個緣由是我迫切的想將
Lua 介紹給你們讓更多的人瞭解 Lua。
4
0.6 感謝
在完成本書的過程當中,不少人給了我極大的幫助:
Luiz Henrique de Figueiredo 和 Waldemar Celes 給了我很大的幫助,使得本書可以更
好完成,Luiz Henrique 也幫助設計了本書的內部。
Noemi Rodriguez, André Carregal, Diego Nehab, 以及 Gavin Wraith 閱讀了本書的草
稿提出了不少有價值的建議。
Renato Cerqueira, Carlos Cassino, Tomás Guisasola, Joe Myers 和 Ed Ferguson 也提出
了不少重要的建議。
Alexandre Nakonechnyj 負責本書的封面和內部設計。
Rosane Teles 負責 CIP 數據的準備。
謝謝他們全部人。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
5
第 1 章 起點
寫一個最最簡單的程序——Hello World。
print("Hello World")
假定你把上面這句保存在 hello.lua 文件中,你在命令行只須要:
prompt> lua hello.lua
看到結果了嗎?
讓咱們來看一個稍微複雜點的例子:
-- defines a factorial function
function fact (n)
if n == 0 then
return 1
else
return n * fact(n-1)
end
end
print("enter a number:")
a = io.read("*number")
print(fact(a))
-- read a number
這個例子定義了一個函數,計算輸入參數 n 的階乘;本例要求用戶輸入一個數字 n,
而後打印 n 的階乘。
1.1 Chunks
Chunk 是一系列語句,Lua 執行的每一塊語句,好比一個文件或者交互模式下的每
一行都是一個 Chunk。
每一個語句結尾的分號(;)是可選的,但若是同一行有多個語句最好用;分開
a=1
b = a*2
-- ugly, but valid
一個 Chunk 能夠是一個語句,也能夠是一系列語句的組合,還能夠是函數,Chunk
能夠很大,在 Lua 中幾個 MByte 的 Chunk 是很常見的。
你還能夠以交互模式運行 Lua,不帶參數運行 Lua:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
Lua 5.0 Copyright © 1994-2003 Tecgraf, PUC-Rio
>
6
你鍵入的每一個命令(好比:"Hello World")在你鍵入回車以後當即被執行,鍵入文
件結束符能夠退出交互模式(Ctrl-D in Unix, Ctrl-Z in DOS/Windows),或者調用 OS 庫
的 os.exit()函數也能夠退出。
在交互模式下,Lua 一般把每個行看成一個 Chunk,但若是 Lua 一行不是一個完
整的 Chunk 時,他會等待繼續輸入直到獲得一個完整的 Chunk.在 Lua 等待續行時,顯示
不一樣的提示符(通常是>>).
能夠經過指定參數讓 Lua 執行一系列 Chunk。例如:假定一個文件 a 內有單個語句
x=1;另外一個文件 b 有語句 print(x)
prompt> lua -la -lb
命令首先在一個 Chunk 內先運行 a 而後運行 b。(注意:-l 選項會調用 require,將會
在指定的目錄下搜索文件,若是環境變量沒有設好,上面的命令可能不能正確運行。我
們將在 8.1 節詳細更詳細的討論 the require function)
-i 選項要求 Lua 運行指定 Chunk 後進入交互模式.
prompt> lua -i -la -lb
將在一個 Chunk 內先運行 a 而後運行 b,最後直接進入交互模式。
另外一個鏈接外部 Chunk 的方式是使用 dofile 函數,dofile 函數加載文件並執行它.假
設有一個文件:
-- file 'lib1.lua'
function norm (x, y)
local n2 = x^2 + y^2
return math.sqrt(n2)
end
function twice (x)
return 2*x
end
在交互模式下:
> dofile("lib1.lua")
> n = norm(3.4, 1.0)
> print(twice(n))
--> 7.0880180586677
-- load your library
-i 和 dofile 在調試或者測試 Lua 代碼時是很方便的。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
7
1.2 全局變量
全局變量不須要聲明,給一個變量賦值後即建立了這個全局變量,訪問一個沒有初
始化的全局變量也不會出錯,只不過獲得的結果是:nil.
print(b)
b = 10
print(b)
--> 10
--> nil
若是你想刪除一個全局變量,只須要將變量負值爲 nil
b = nil
print(b)
--> nil
這樣變量 b 就好像從沒被使用過同樣.換句話說, 當且僅當一個變量不等於 nil 時,
這個變量存在。
1.3 詞法約定
標示符:字母(letter)或者下劃線開頭的字母、下劃線、數字序列.最好不要使用下劃
線加大寫字母的標示符,由於 Lua 的保留字也是這樣的。Lua 中,letter 的含義是依賴於
本地環境的。
保留字:如下字符爲 Lua 的保留字,不能看成標識符。
and
end
in
repeat
while
break
false
local
return
do
for
nil
then
else
function
not
true
elseif
if
or
until
注意:Lua 是大小寫敏感的.
註釋:單行註釋:--
多行註釋:--[[
--[[
print(10)
--]]
-- no action (comment)
--]]
1.4 命令行方式
lua [options] [script [args]]
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
-e:直接將命令傳入 Lua
prompt> lua -e "print(math.sin(12))"
--> -0.53657291800043
8
-l:加載一個文件.
-i:進入交互模式.
_PROMPT 內置變量做爲交互模式的提示符
prompt> lua -i -e "_PROMPT=' lua> '"
lua>
Lua 的運行過程,在運行參數以前,Lua 會查找環境變量 LUA_INIT 的值,若是變
量存在而且值爲@filename,Lua 將加載指定文件。若是變量存在但不是以@開頭,Lua
假定 filename 爲 Lua 代碼文件而且運行他。利用這個特性,咱們能夠經過配置,靈活的
設置交互模式的環境。能夠加載包,修改提示符和路徑,定義本身的函數,修改或者重
名名函數等。
全局變量 arg 存放 Lua 的命令行參數。
prompt> lua script a b c
在運行之前,Lua 使用全部參數構造 arg 表。腳本名索引爲 0,腳本的參數從 1 開始
增長。腳本前面的參數從-1 開始減小。
prompt> lua -e "sin=math.sin" script a b
arg 表以下:
arg[-3] = "lua"
arg[-2] = "-e"
arg[-1] = "sin=math.sin"
arg[0] = "script"
arg[1] = "a"
arg[2] = "b"
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
9
第 2 章 類型和值
Lua 是動態類型語言,變量不要類型定義。 中有 8 個基本類型分別爲: boolean、Luanil、
number、string、userdata、function、thread 和 table。函數 type 能夠測試給定變量或者值
的類型。
print(type("Hello world"))
print(type(10.4*3))
print(type(print))
print(type(type))
print(type(true))
print(type(nil))
print(type(type(X)))
--> string
--> number
--> function
--> function
--> boolean
--> nil
--> string
變量沒有預約義的類型,每個變量均可能包含任一種類型的值。
print(type(a))
a = 10
print(type(a))
a = "a string!!"
print(type(a))
a = print
a(type(a))
--> string
-- yes, this is valid!
--> function
--> number
--> nil
('a' is not initialized)
注意上面最後兩行,咱們能夠使用 function 像使用其餘值同樣使用(更多的介紹參
考第六章)。通常狀況下同一變量表明不一樣類型的值會形成混亂,最好不要用,可是特殊
狀況下能夠帶來便利,好比 nil。
2.1 Nil
Lua 中特殊的類型,他只有一個值:nil;一個全局變量沒有被賦值之前默認值爲 nil;
給全局變量負 nil 能夠刪除該變量。
2.2 Booleans
兩個取值 false 和 true。但要注意 Lua 中全部的值均可以做爲條件。在控制結構的條
件中除了 false 和 nil 爲假,其餘值都爲真。因此 Lua 認爲 0 和空串都是真。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
10
2.3 Numbers
表示實數,Lua 中沒有整數。通常有個錯誤的見解 CPU 運算浮點數比整數慢。事實
不是如此,用實數代替整數不會有什麼偏差(除非數字大於 100,000,000,000,000)。Lua
的 numbers 能夠處理任何長整數不用擔憂偏差。你也能夠在編譯 Lua 的時候使用長整型
或者單精度浮點型代替 numbers,在一些平臺硬件不支持浮點數的狀況下這個特性是非
常有用的,具體的狀況請參考 Lua 發佈版所附的詳細說明。和其餘語言相似,數字常量
的小數部分和指數部分都是可選的,數字常量的例子:
4
0.4
4.57e-3
0.3e12
5e+20
2.4 Strings
指字符的序列。 是 8 位字節,lua因此字符串能夠包含任何數值字符,包括嵌入的 0。
這意味着你能夠存儲任意的二進制數據在一個字符串裏。Lua 中字符串是不能夠修改的,
你能夠建立一個新的變量存放你要的字符串,以下:
a = "one string"
b = string.gsub(a, "one", "another")
print(a)
print(b)
--> one string
--> another string
-- change string parts
string 和其餘對象同樣,Lua 自動進行內存分配和釋放,一個 string 能夠只包含一個
字母也能夠包含一本書,Lua 能夠高效的處理長字符串,1M 的 string 在 Lua 中是很常見
的。能夠使用單引號或者雙引號表示字符串
a = "a line"
b = 'another line'
爲了風格統一,最好使用一種,除非兩種引號嵌套狀況。對於字符串中含有引號的
狀況還能夠使用轉義符\來表示。Lua 中的轉義序列有:
\a bell
\b back space
\f form feed
\n newline
\r carriage return
\t horizontal tab
\v vertical tab
\\ backslash
\" double quote
\' single quote
-- 後退
-- 換頁
-- 換行
-- 回車
-- 製表
-- "\"
-- 雙引號
-- 單引號
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
\[ left square bracket
\] right square bracket
-- 左中括號
-- 右中括號
11
例子:
> print("one line\nnext line\n\"in quotes\", 'in quotes'")
one line
next line
"in quotes", 'in quotes'
> print('a backslash inside quotes: \'\\\'')
a backslash inside quotes: '\'
> print("a simpler way: '\\'")
a simpler way: '\'
還能夠在字符串中使用\ddd(ddd 爲三位十進制數字)方式表示字母。
"alo\n123\""和'\97lo\10\04923"'是相同的。
還能夠使用[[...]]表示字符串。這種形式的字符串能夠包含多行也,能夠嵌套且不會
解釋轉義序列,若是第一個字符是換行符會被自動忽略掉。這種形式的字符串用來包含
一段代碼是很是方便的。
page = [[
<HTML>
<HEAD>
<TITLE>An HTML Page</TITLE>
</HEAD>
<BODY>
Lua
[[a text between double brackets]]
</BODY>
</HTML>
]]
io.write(page)
運行時,Lua 會自動在 string 和 numbers 之間自動進行類型轉換,當一個字符串使
用算術操做符時,string 就會被轉成數字。
print("10" + 1)
print("10 + 1")
print("-5.3e - 10" * "2")
print("hello" + 1)
--> 11
--> 10 + 1
--> -1.06e-09
-- ERROR (cannot convert "hello")
反過來,當 Lua 指望一個 string 而碰到數字時,會將數字轉成 string。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print(10 .. 20)
--> 1020
12
..在 Lua 中是字符串鏈接符,當在一個數字後面寫..時,必須加上空格以防止被解釋
錯。
儘管字符串和數字能夠自動轉換,但二者是不一樣的,像 10 == "10"這樣的比較永遠
都是錯的。若是須要顯式將 string 轉成數字能夠使用函數 tonumber(),若是 string 不是正
確的數字該函數將返回 nil。
line = io.read()
n = tonumber(line)
if n == nil then
error(line .. " is not a valid number")
else
print(n*2)
end
-- read a line
-- try to convert it to a number
反之,能夠調用 tostring()將數字轉成字符串,這種轉換一直有效:
print(tostring(10) == "10")
print(10 .. "" == "10")
--> true
--> true
2.5 Functions
函數是第一類值(和其餘變量相同),意味着函數能夠存儲在變量中,能夠做爲函數
的參數,也能夠做爲函數的返回值。這個特性給了語言很大的靈活性:一個程序能夠重
新定義函數增長新的功能或者爲了不運行不可靠代碼建立安全運行環境而隱藏函數,
此外這特性在 Lua 實現面向對象中也起了重要做用(在第 16 章詳細講述)。
Lua 能夠調用 lua 或者 C 實現的函數,Lua 全部標準庫都是用 C 實現的。標準庫包
括 string 庫、table 庫、I/O 庫、OS 庫、算術庫、debug 庫。
2.6 Userdata and Threads
userdata 能夠將 C 數據存放在 Lua 變量中,userdata 在 Lua 中除了賦值和相等比較外
沒有預約義的操做。userdata 用來描述應用程序或者使用 C 實現的庫建立的新類型。例
如:用標準 I/O 庫來描述文件。下面在 C API 章節中咱們將詳細討論。
在第九章討論協同操做的時候,咱們介紹線程。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
13
第 3 章 表達式
Lua 中的表達式包括數字常量、字符串常量、變量、一元和二元運算符、函數調用。
還能夠是非傳統的函數定義和表構造。
3.1 算術運算符
二元運算符:+ - * / ^
一元運算符:-
(負值)
(加減乘除冪)
這些運算符的操做數都是實數。
3.2 關係運算符
<
>
<=
>=
==
~=
這些操做符返回結果爲 false 或者 true;==和~=比較兩個值,若是兩個值類型不一樣,
Lua 認爲二者不一樣;nil 只和本身相等。Lua 經過引用比較 tables、userdata、functions。
也就是說當且僅當二者表示同一個對象時相等。
a = {}; a.x = 1; a.y = 0
b = {}; b.x = 1; b.y = 0
c=a
a==c but a~=b
Lua 比較數字按傳統的數字大小進行,比較字符串按字母的順序進行,可是字母順
序依賴於本地環境。
當比較不一樣類型的值的時候要特別注意:
"0" == 0
2 < 15
"2" < "15"
-- false
-- true
-- false (alphabetical order!)
爲了不不一致的結果,混合比較數字和字符串,Lua 會報錯,好比:2 < "15"
3.3 邏輯運算符
and
or
not
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
邏輯運算符認爲 false 和 nil 是假(false),其餘爲真,0 也是 true.
and 和 or 的運算結果不是 true 和 false,而是和它的兩個操做數相關。
a and b
a or b
-- 若是 a 爲 false,則返回 a,不然返回 b
-- 若是 a 爲 true,則返回 a,不然返回 b
14
例如:
print(4 and 5)
print(nil and 13)
print(false and 13)
print(4 or 5)
print(false or 5)
--> 5
--> nil
--> false
--> 4
--> 5
一個很實用的技巧:若是 x 爲 false 或者 nil 則給 x 賦初始值 v
x = x or v
等價於
if not x then
x=v
end
and 的優先級比 or 高。
C 語言中的三元運算符
a?b:c
在 Lua 中能夠這樣實現:
(a and b) or c
not 的結果一直返回 false 或者 true
print(not nil)
print(not false)
print(not 0)
print(not not nil)
--> true
--> true
--> false
--> false
3.4 鏈接運算符
..
--兩個點
字符串鏈接,若是操做數爲數字,Lua 將數字轉成字符串。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print("Hello " .. "World")
print(0 .. 1)
--> Hello World
--> 01
15
3.5 優先級
從高到低的順序:
^
not
*
+
..
<
and
or
>
<=
>=
~=
==
- (unary)
/
-
除了^和..外全部的二元運算符都是左鏈接的。
a+i < b/2+1
5+x^2*8
a < y and y <= z
-x^2
x^y^z
<-->
<-->
<-->
<-->
<-->
(a+i) < ((b/2)+1)
5+((x^2)*8)
(a < y) and (y <= z)
-(x^2)
x^(y^z)
3.6 表的構造
構造器是建立和初始化表的表達式。表是 Lua 特有的功能強大的東西。最簡單的構
造函數是{},用來建立一個空表。能夠直接初始化數組:
days = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
Lua 將"Sunday"初始化 days[1](第一個元素索引爲 1),用"Monday"初始化 days[2]...
print(days[4])
--> Wednesday
構造函數能夠使用任何表達式初始化:
tab = {sin(1), sin(2), sin(3), sin(4),
sin(5),sin(6), sin(7), sin(8)}
若是想初始化一個表做爲 record 使用能夠這樣:
a = {x=0, y=0}
<-->
a = {}; a.x=0; a.y=0
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
16
無論用何種方式建立 table,咱們均可以向表中添加或者刪除任何類型的域,構造函
數僅僅影響表的初始化。
w = {x=0, y=0, label="console"}
x = {sin(0), sin(1), sin(2)}
w[1] = "another field"
x.f = w
print(w["x"])
print(w[1])
print(x.f[1])
w.x = nil
--> 0
--> another field
--> another field
-- remove field "x"
每次調用構造函數,Lua 都會建立一個新的 table,能夠使用 table 構造一個 list:
list = nil
for line in io.lines() do
list = {next=list, value=line}
end
這段代碼從標準輸入讀進每行,而後反序造成鏈表。下面的代碼打印鏈表的內容:
l = list
while l do
print(l.value)
l = l.next
end
在同一個構造函數中能夠混合列表風格和 record 風格進行初始化,如:
polyline = {color="blue", thickness=2, npoints=4,
{x=0,
y=0},
{x=-10, y=0},
{x=-10, y=1},
{x=0,
}
y=1}
這個例子也代表咱們能夠嵌套構造函數來表示複雜的數據結構.
print(polyline[2].x)
--> -10
上面兩種構造函數的初始化方式還有限制,好比你不能使用負索引初始化一個表中
元素,字符串索引也不能被恰當的表示。下面介紹一種更通常的初始化方式,咱們用
[expression]顯示的表示將被初始化的索引:
opnames = {["+"] = "add", ["-"] = "sub",
["*"] = "mul", ["/"] = "div"}
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
17
i = 20; s = "-"
a = {[i+0] = s, [i+1] = s..s, [i+2] = s..s..s}
print(opnames[s])
print(a[22])
--> sub
--> ---
list 風格初始化和 record 風格初始化是這種通常初始化的特例:
{x=0, y=0}
<-->
{["x"]=0, ["y"]=0}
<-->
{"red", "green", "blue"}
{[1]="red", [2]="green", [3]="blue"}
若是真的想要數組下標從 0 開始:
days = {[0]="Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
注意:不推薦數組下標從 0 開始,不然不少標準庫不能使用。
在構造函數的最後的","是可選的,能夠方便之後的擴展。
a = {[1]="red", [2]="green", [3]="blue",}
在構造函數中域分隔符逗號(",")能夠用分號(";")替代,一般咱們使用分號用來
分割不一樣類型的表元素。
{x=10, y=45; "one", "two", "three"}
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
18
第 4 章 基本語法
Lua 像 C 和 PASCAL 幾乎支持全部的傳統語句:賦值語句、控制結構語句、函數調
用等,同時也支持非傳統的多變量賦值、局部變量聲明。
4.1 賦值語句
賦值是改變一個變量的值和改變表域的最基本的方法。
a = "hello" .. "world"
t.n = t.n + 1
Lua 能夠對多個變量同時賦值,變量列表和值列表的各個元素用逗號分開,賦值語
句右邊的值會依次賦給左邊的變量。
a, b = 10, 2*x
<-->
a=10; b=2*x
遇到賦值語句 Lua 會先計算右邊全部的值而後再執行賦值操做,因此咱們能夠這樣
進行交換變量的值:
x, y = y, x
a[i], a[j] = a[j], a[i]
-- swap 'x' for 'y'
-- swap 'a[i]' for 'a[i]'
當變量個數和值的個數不一致時,Lua 會一直以變量個數爲基礎採起如下策略:
a. 變量個數>值的個數
b. 變量個數<值的個數
按變量個數補足 nil
多餘的值會被忽略
例如:
a, b, c = 0, 1
print(a,b,c)
a, b = a+1, b+1, b+2
print(a,b)
a, b, c = 0
print(a,b,c)
--> 0
nil
nil
--> 0
1
nil
-- value of b+2 is ignored
--> 1
2
上面最後一個例子是一個常見的錯誤狀況,注意:若是要對多個變量賦值必須依次
對每一個變量賦值。
a, b, c = 0, 0, 0
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print(a,b,c)
--> 0
0
0
19
多值賦值常常用來交換變量,或將函數調用返回給變量:
a, b = f()
f()返回兩個值,第一個賦給 a,第二個賦給 b。
4.2 局部變量與代碼塊(block)
使用 local 建立一個局部變量,與全局變量不一樣,局部變量只在被聲明的那個代碼塊
內有效。代碼塊:指一個控制結構內,一個函數體,或者一個 chunk(變量被聲明的那
個文件或者文本串)。
x = 10
local i = 1
while i<=x do
local x = i*2
print(x)
i=i+1
end
if i > 20 then
local x
x = 20
print(x + 2)
else
print(x)
end
print(x)
--> 10 (the global one)
--> 10 (the global one)
-- local to the "then" body
-- local to the while body
--> 2, 4, 6, 8, ...
-- local to the chunk
注意,若是在交互模式下上面的例子可能不能輸出指望的結果,由於第二句 local i=1
是一個完整的 chunk,在交互模式下執行完這一句後,Lua 將開始一個新的 chunk,這樣
第二句的 i 已經超出了他的有效範圍。能夠將這段代碼放在 do..end(至關於 c/c++的{})
塊中。
應該儘量的使用局部變量,有兩個好處:
1. 避免命名衝突
2. 訪問局部變量的速度比全局變量更快.
咱們給 block 劃定一個明確的界限:do..end 內的部分。當你想更好的控制局部變量
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
的做用範圍的時候這是頗有用的。
do
local a2 = 2*a
local d = sqrt(b^2 - 4*a*c)
x1 = (-b + d)/a2
x2 = (-b - d)/a2
end
print(x1, x2)
-- scope of 'a2' and 'd' ends here
20
4.3 控制結構語句
控制結構的條件表達式結果能夠是任何值,Lua 認爲 false 和 nil 爲假,其餘值爲真。
if 語句,有三種形式:
if conditions then
then-part
end;
if conditions then
then-part
else
else-part
end;
if conditions then
then-part
elseif conditions then
elseif-part
..
else
else-part
end;
--->多個 elseif
while 語句:
while condition do
statements;
end;
repeat-until 語句:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
repeat
statements;
until conditions;
21
for 語句有兩大類:
第一,數值 for 循環:
for var=exp1,exp2,exp3 do
loop-part
end
for 將用 exp3 做爲 step 從 exp1(初始值)到 exp2(終止值),執行 loop-part。其中
exp3 能夠省略,默認 step=1
有幾點須要注意:
1. 三個表達式只會被計算一次,而且是在循環開始前。
for i=1,f(x) do
print(i)
end
for i=10,1,-1 do
print(i)
end
第一個例子 f(x)只會在循環前被調用一次。
2. 控制變量 var 是局部變量自動被聲明,而且只在循環內有效.
for i=1,10 do
print(i)
end
max = i
-- probably wrong! 'i' here is global
若是須要保留控制變量的值,須要在循環中將其保存
-- find a value in a list
local found = nil
for i=1,a.n do
if a[i] == value then
found = i
break
end
end
print(found)
-- save value of 'i'
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
22
3. 循環過程當中不要改變控制變量的值,那樣作的結果是不可預知的。若是要退出循
環,使用 break 語句。
第二,範型 for 循環:
前面已經見過一個例子:
-- print all values of array 'a'
for i,v in ipairs(a) do print(v) end
範型 for 遍歷迭代子函數返回的每個值。
再看一個遍歷表 key 的例子:
-- print all keys of table 't'
for k in pairs(t) do print(k) end
範型 for 和數值 for 有兩點相同:
1. 控制變量是局部變量
2. 不要修改控制變量的值
再看一個例子,假定有一個表:
days = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
如今想把對應的名字轉換成星期幾,一個有效地解決問題的方式是構造一個反向表:
revDays = {["Sunday"] = 1, ["Monday"] = 2,
["Tuesday"] = 3, ["Wednesday"] = 4,
["Thursday"] = 5, ["Friday"] = 6,
["Saturday"] = 7}
下面就能夠很容易獲取問題的答案了:
x = "Tuesday"
print(revDays[x])
--> 3
咱們不須要手工,能夠自動構造反向表
revDays = {}
for i,v in ipairs(days) do
revDays[v] = i
end
若是你對範型 for 還有些不清楚在後面的章節咱們會繼續來學習。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
23
4.4 break 和 return 語句
break 語句用來退出當前循環(for,repeat,while)。在循環外部不能夠使用。
return 用來從函數返回結果,當一個函數天然結束結尾會有一個默認的 return。(這
種函數相似 pascal 的過程)
Lua 語法要求 break 和 return 只能出如今 block 的結尾一句(也就是說:做爲 chunk
的最後一句,或者在 end 以前,或者 else 前,或者 until 前),例如:
local i = 1
while a[i] do
if a[i] == v then break end
i=i+1
end
有時候爲了調試或者其餘目的須要在 block 的中間使用 return 或者 break,能夠顯式
的使用 do..end 來實現:
function foo ()
return
do return end
...
end
--<< SYNTAX ERROR
-- OK
-- statements not reached
-- 'return' is the last statement in the next block
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
24
第 5 章 函數
函數有兩種用途:1.完成指定的任務,這種狀況下函數做爲調用語句使用;2.計算並
返回值,這種狀況下函數做爲賦值語句的表達式使用。
語法:
function func_name (arguments-list)
statements-list;
end;
調用函數的時候,若是參數列表爲空,必須使用()代表是函數調用。
print(8*9, 9/8)
a = math.sin(3) + math.cos(10)
print(os.date())
上述規則有一個例外,當函數只有一個參數而且這個參數是字符串或者表構造的時
候,()是可選的:
print "Hello World"
dofile 'a.lua'
print [[a multi-line
message]]
f{x=10, y=20}
type{}
<-->
<-->
type({})
<-->
<-->
<-->
print("Hello World")
dofile ('a.lua')
print([[a multi-line
message]])
f({x=10, y=20})
Lua 也提供了面向對象方式調用函數的語法,好比 o:foo(x)與 o.foo(o, x)是等價的,
後面的章節會詳細介紹面向對象內容。
Lua 使用的函數能夠是 Lua 編寫也能夠是其餘語言編寫,對於 Lua 程序員來講用什
麼語言實現的函數使用起來都同樣。
Lua 函數實參和形參的匹配與賦值語句相似,多餘部分被忽略,缺乏部分用 nil 補足。
function f(a, b) return a or b end
CALL
f(3)
f(3, 4)
f(3, 4, 5)
PARAMETERS
a=3, b=nil
a=3, b=4
a=3, b=4
(5 is discarded)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
25
5.1 返回多個結果值
Lua 函數能夠返回多個結果值,好比 string.find,其返回匹配串「開始和結束的下標」
(若是不存在匹配串返回 nil)。
s, e = string.find("hello Lua users", "Lua")
print(s, e)
--> 7
9
Lua 函數中,在 return 後列出要返回的值得列表便可返回多值,如:
function maximum (a)
local mi = 1
local m = a[mi]
if val > m then
mi = i
m = val
end
end
return m, mi
end
print(maximum({8,10,23,12,5}))
--> 23
3
-- maximum index
-- maximum value
for i,val in ipairs(a) do
Lua 老是調整函數返回值的個數去適用調用環境,看成爲一個語句調用函數時,所
有返回值被忽略。假設有以下三個函數:
function foo0 () end
function foo1 () return 'a' end
function foo2 () return 'a','b' end
-- returns no results
-- returns 1 result
-- returns 2 results
第一,看成爲表達式調用函數時,有如下幾種狀況:
1. 當調用做爲表達式最後一個參數或者僅有一個參數時,根據變量個數函數儘量
多地返回多個值,不足補 nil,超出捨去。
2. 其餘狀況下,函數調用僅返回第一個值(若是沒有返回值爲 nil)
x,y = foo2()
x = foo2()
x,y,z = 10,foo2()
x,y = foo0()
x,y = foo1()
x,y,z = foo2()
-- x='a', y='b'
-- x='a', 'b' is discarded
-- x=10, y='a', z='b'
-- x=nil, y=nil
-- x='a', y=nil
-- x='a', y='b', z=nil
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
26
x,y = foo2(), 20
x,y = foo0(), 20, 30
-- x='a', y=20
-- x='nil', y=20, 30 is discarded
第二,函數調用做爲函數參數被調用時,和多值賦值是相同。
print(foo0())
print(foo1())
print(foo2())
print(foo2(), 1)
print(foo2() .. "x")
-->
--> a
--> a
--> a
--> ax
b
1
第三,函數調用在表構造函數中初始化時,和多值賦值時相同。
a = {foo0()}
a = {foo1()}
a = {foo2()}
-- a = {}
-- a = {'a'}
-- a = {'a', 'b'}
(an empty table)
a = {foo0(), foo2(), 4} -- a[1] = nil, a[2] = 'a', a[3] = 4
另外,return f()這種類型的返回 f()返回的全部值
function foo (i)
if i == 0 then return foo0()
elseif i == 1 then return foo1()
elseif i == 2 then return foo2()
end
end
print(foo(1))
print(foo(2))
print(foo(0))
print(foo(3))
--> a
--> a b
-- (no results)
-- (no results)
能夠使用圓括號強制使調用返回一個值。
print((foo0()))
print((foo1()))
print((foo2()))
--> nil
--> a
--> a
一個 return 語句若是使用圓括號將返回值括起來也將致使返回一個值。
函數多值返回的特殊函數 unpack,接受一個數組做爲輸入參數,返回數組的全部元
素。unpack 被用來實現範型調用機制,在 C 語言中能夠使用函數指針調用可變的函數,
能夠聲明參數可變的函數,但不能二者同時可變。在 Lua 中若是你想調用可變參數的可
變函數只須要這樣:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
f(unpack(a))
27
unpack 返回 a 全部的元素做爲 f()的參數
f = string.find
a = {"hello", "ll"}
print(f(unpack(a)))
--> 3 4
預約義的 unpack 函數是用 C 語言實現的,咱們也能夠用 Lua 來完成:
function unpack(t, i)
i = i or 1
if t[i] then
return t[i], unpack(t, i + 1)
end
end
5.2 可變參數
Lua 函數能夠接受可變數目的參數,和 C 語言相似在函數參數列表中使用三點(...)
表示函數有可變的參數。Lua 將函數的參數放在一個叫 arg 的表中,除了參數之外,arg
表中還有一個域 n 表示參數的個數。
例如,咱們能夠重寫 print 函數:
printResult = ""
function print(...)
for i,v in ipairs(arg) do
printResult = printResult .. tostring(v) .. "\t"
end
printResult = printResult .. "\n"
end
有時候咱們可能須要幾個固定參數加上可變參數
function g (a, b, ...) end
CALL
g(3)
g(3, 4)
g(3, 4, 5, 8)
PARAMETERS
a=3, b=nil, arg={n=0}
a=3, b=4, arg={n=0}
a=3, b=4, arg={5, 8; n=2}
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
28
如上面所示,Lua 會將前面的實參傳給函數的固定參數,後面的實參放在 arg 表中。
舉個具體的例子,若是咱們只想要 string.find 返回的第二個值:
一個典型的方法是使用虛變量(下劃線)
local _, x = string.find(s, p)
-- now use `x'
...
還能夠利用可變參數聲明一個 select 函數:
function select (n, ...)
return arg[n]
end
print(string.find("hello hello", " hel"))
--> 6 9
print(select(1, string.find("hello hello", " hel"))) --> 6
print(select(2, string.find("hello hello", " hel"))) --> 9
有時候須要將函數的可變參數傳遞給另外的函數調用,能夠使用前面咱們說過的
unpack(arg)返回 arg 表全部的可變參數,Lua 提供了一個文本格式化的函數 string.format
(相似 C 語言的 sprintf 函數):
function fwrite(fmt, ...)
return io.write(string.format(fmt, unpack(arg)))
end
這個例子將文本格式化操做和寫操做組合爲一個函數。
5.3 命名參數
Lua 的函數參數是和位置相關的,調用時實參會按順序依次傳給形參。有時候用名
字指定參數是頗有用的,好比 rename 函數用來給一個文件重命名,有時候咱們咱們記不
清命名先後兩個參數的順序了:
-- invalid code
rename(old="temp.lua", new="temp1.lua")
上面這段代碼是無效的,Lua 能夠經過將全部的參數放在一個表中,把表做爲函數
的惟一參數來實現上面這段僞代碼的功能。由於 Lua 語法支持函數調用時實參能夠是表
的構造。
rename{old="temp.lua", new="temp1.lua"}
根據這個想法咱們重定義了 rename:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
function rename (arg)
return os.rename(arg.old, arg.new)
end
29
當函數的參數不少的時候,這種函數參數的傳遞方式很方便的。例如 GUI 庫中建立
窗體的函數有不少參數而且大部分參數是可選的,能夠用下面這種方式:
w = Window {
x=0, y=0, width=300, height=200,
title = "Lua", background="blue",
border = true
}
function Window (options)
-- check mandatory options
if type(options.title) ~= "string" then
error("no title")
elseif type(options.width) ~= "number" then
error("no width")
elseif type(options.height) ~= "number" then
error("no height")
end
-- everything else is optional
_Window(options.title,
options.x or 0,
options.y or 0,
-- default value
-- default value
options.width, options.height,
options.background or "white", -- default
options.border
)
end
-- default is false (nil)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
30
第 6 章 再論函數
Lua 中的函數是帶有詞法定界(lexical scoping)的第一類值(first-class values)。
第一類值指:在 Lua 中函數和其餘值(數值、字符串)同樣,函數能夠被存放在變
量中,也能夠存放在表中,能夠做爲函數的參數,還能夠做爲函數的返回值。
詞法定界指:被嵌套的函數能夠訪問他外部函數中的變量。這一特性給 Lua 提供了
強大的編程能力。
Lua 中關於函數稍微難以理解的是函數也能夠沒有名字,匿名的。當咱們提到函數
名(好比 print),其實是說一個指向函數的變量,像持有其餘類型值的變量同樣:
a = {p = print}
a.p("Hello World")
a.p(print(1))
sin = a.p
sin(10, 20)
--> Hello World
print = math.sin -- `print' now refers to the sine function
--> 0.841470
-- `sin' now refers to the print function
--> 10
20
既然函數是值,那麼表達式也能夠建立函數了,Lua 中咱們常常這樣寫:
function foo (x) return 2*x end
這其實是利用 Lua 提供的「語法上的甜頭」(syntactic sugar)的結果,下面是原
本的函數:
foo = function (x) return 2*x end
函數定義其實是一個賦值語句,將類型爲 function 的變量賦給一個變量。咱們使
用 function (x) ... end 來定義一個函數和使用{}建立一個表同樣。
table 標準庫提供一個排序函數,接受一個表做爲輸入參數而且排序表中的元素。這
個函數必須可以對不一樣類型的值(字符串或者數值)按升序或者降序進行排序。Lua 不
是儘量多地提供參數來知足這些狀況的須要,而是接受一個排序函數做爲參數(相似
C++的函數對象) 排序函數接受兩個排序元素做爲輸入參數,,而且返回二者的大小關係,
例如:
network = {
{name = "grauna",
{name = "arraial",
{name = "lua",
{name = "derain",
}
IP = "210.26.30.34"},
IP = "210.26.30.23"},
IP = "210.26.23.12"},
IP = "210.26.23.20"},
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
若是咱們想經過表的 name 域排序:
table.sort(network, function (a,b)
return (a.name > b.name)
end)
31
以其餘函數做爲參數的函數在 Lua 中被稱做高級函數,高級函數在 Lua 中並無特
權,只是 Lua 把函數看成第一類函數處理的一個簡單的結果。
下面給出一個繪圖函數的例子:
function eraseTerminal()
io.write("\27[2J")
end
-- writes an `*' at column `x' , row `y'
function mark (x,y)
io.write(string.format("\27[%d;%dH*", y, x))
end
-- Terminal size
TermSize = {w = 80, h = 24}
-- plot a function
-- (assume that domain and image are in the range [-1,1])
function plot (f)
eraseTerminal()
for i=1,TermSize.w do
local x = (i/TermSize.w)*2 - 1
local y = (f(x) + 1)/2 * TermSize.h
mark(i, y)
end
io.read() -- wait before spoiling the screen
end
要想讓這個例子正確的運行,你必須調整你的終端類型和代碼中的控制符一致:
plot(function (x) return math.sin(x*2*math.pi) end)
將在屏幕上輸出一個正弦曲線。
將第一類值函數應用在表中是 Lua 實現面向對象和包機制的關鍵,這部份內容在後
面章節介紹。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
32
6.1 閉包
當一個函數內部嵌套另外一個函數定義時,內部的函數體能夠訪問外部的函數的局部
變量,這種特徵咱們稱做詞法定界。雖然這看起來很清楚,事實並不是如此,詞法定界加
上第一類函數在編程語言裏是一個功能強大的概念,不多語言提供這種支持。
下面看一個簡單的例子,假定有一個學生姓名的列表和一個學生名和成績對應的表;
如今想根據學生的成績從高到低對學生進行排序,能夠這樣作:
names = {"Peter", "Paul", "Mary"}
grades = {Mary = 10, Paul = 7, Peter = 8}
table.sort(names, function (n1, n2)
return grades[n1] > grades[n2]
end)
-- compare the grades
假定建立一個函數實現此功能:
function sortbygrade (names, grades)
table.sort(names, function (n1, n2)
return grades[n1] > grades[n2]
end)
end
-- compare the grades
例子中包含在 sortbygrade 函數內部的 sort 中的匿名函數能夠訪問 sortbygrade 的參數
grades,在匿名函數內部 grades 不是全局變量也不是局部變量,咱們稱做外部的局部變
量(external local variable)或者 upvalue。(upvalue 意思有些誤導,然而在 Lua 中他的存
在有歷史的根源,還有他比起 external local variable 簡短)。
看下面的代碼:
function newCounter()
local i = 0
return function()
i=i+1
return i
end
end
c1 = newCounter()
print(c1()) --> 1
print(c1()) --> 2
-- anonymous function
匿名函數使用 upvalue i 保存他的計數,當咱們調用匿名函數的時候 i 已經超出了做
用範圍,由於建立 i 的函數 newCounter 已經返回了。然而 Lua 用閉包的思想正確處理了
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
33
這種狀況。簡單的說閉包是一個函數加上它能夠正確訪問的 upvalues。若是咱們再次調
用 newCounter,將建立一個新的局部變量 i,所以咱們獲得了一個做用在新的變量 i 上的
新閉包。
c2 = newCounter()
print(c2()) --> 1
print(c1()) --> 3
print(c2()) --> 2
c一、c2 是創建在同一個函數上,但做用在同一個局部變量的不一樣實例上的兩個不一樣
的閉包。
技術上來說,閉包指值而不是指函數,函數僅僅是閉包的一個原型聲明;儘管如此,
在不會致使混淆的狀況下咱們繼續使用術語函數代指閉包。
閉包在上下文環境中提供頗有用的功能,如前面咱們見到的能夠做爲高級函數(sort)
的參數;做爲函數嵌套的函數(newCounter)。這一機制使得咱們能夠在 Lua 的函數世界
裏組合出奇幻的編程技術。閉包也可用在回調函數中,好比在 GUI 環境中你須要建立一
系列 button,但用戶按下 button 時回調函數被調用,可能不一樣的按鈕被按下時須要處理
的任務有點區別。具體來說,一個十進制計算器須要 10 個類似的按鈕,每一個按鈕對應一
個數字,能夠使用下面的函數建立他們:
function digitButton (digit)
return Button{ label = digit,
action = function ()
add_to_display(digit)
end
}
end
這個例子中咱們假定 Button 是一個用來建立新按鈕的工具, label 是按鈕的標籤,
action 是按鈕被按下時調用的回調函數。(其實是一個閉包,由於他訪問 upvalue digit)。
digitButton 完成任務返回後,局部變量 digit 超出範圍,回調函數仍然能夠被調用而且可
以訪問局部變量 digit。
閉包在徹底不一樣的上下文中也是頗有用途的。由於函數被存儲在普通的變量內咱們
能夠很方便的重定義或者預約義函數。一般當你須要原始函數有一個新的實現時能夠重
定義函數。例如你能夠重定義 sin 使其接受一個度數而不是弧度做爲參數:
oldSin = math.sin
math.sin = function (x)
return oldSin(x*math.pi/180)
end
更清楚的方式:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
do
local oldSin = math.sin
local k = math.pi/180
math.sin = function (x)
return oldSin(x*k)
end
end
34
這樣咱們把原始版本放在一個局部變量內,訪問 sin 的惟一方式是經過新版本的函
數。
利用一樣的特徵咱們能夠建立一個安全的環境(也稱做沙箱, java 裏的沙箱同樣)和,
當咱們運行一段不信任的代碼(好比咱們運行網絡服務器上獲取的代碼)時安全的環境
是須要的,好比咱們能夠使用閉包重定義 io 庫的 open 函數來限制程序打開的文件。
do
local oldOpen = io.open
io.open = function (filename, mode)
if access_OK(filename, mode) then
return oldOpen(filename, mode)
else
return nil, "access denied"
end
end
end
6.2 非全局函數
Lua 中函數能夠做爲全局變量也能夠做爲局部變量,咱們已經看到一些例子:函數
做爲 table 的域(大部分 Lua 標準庫使用這種機制來實現的好比 io.read、math.sin)。這種
狀況下,必須注意函數和表語法:
1. 表和函數放在一塊兒
Lib = {}
Lib.foo = function (x,y) return x + y end
Lib.goo = function (x,y) return x - y end
2. 使用表構造函數
Lib = {
foo = function (x,y) return x + y end,
goo = function (x,y) return x - y end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
}
35
3. Lua 提供另外一種語法方式
Lib = {}
function Lib.foo (x,y)
return x + y
end
function Lib.goo (x,y)
return x - y
end
當咱們將函數保存在一個局部變量內時,咱們獲得一個局部函數,也就是說局部函
數像局部變量同樣在必定範圍內有效。這種定義在包中是很是有用的:由於 Lua 把 chunk
看成函數處理,在 chunk 內能夠聲明局部函數(僅僅在 chunk 內可見),詞法定界保證了
包內的其餘函數能夠調用此函數。下面是聲明局部函數的兩種方式:
1. 方式一
local f = function (...)
...
end
local g = function (...)
...
f()
...
end
-- external local `f' is visible here
2. 方式二
local function f (...)
...
end
有一點須要注意的是在聲明遞歸局部函數的方式:
local fact = function (n)
if n == 0 then
return 1
else
return n*fact(n-1)
end
end
-- buggy
上面這種方式致使 Lua 編譯時遇到 fact(n-1)並不知道他是局部函數 fact, 會去查Lua
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
找是否有這樣的全局函數 fact。爲了解決這個問題咱們必須在定義函數之前先聲明:
local fact
fact = function (n)
if n == 0 then
return 1
else
return n*fact(n-1)
end
end
36
這樣在 fact 內部 fact(n-1)調用是一個局部函數調用,運行時 fact 就能夠獲取正確的
值了。
可是 Lua 擴展了他的語法使得能夠在直接遞歸函數定義時使用兩種方式均可以。
在定義非直接遞歸局部函數時要先聲明而後定義才能夠:
local f, g
function g ()
... f() ...
end
function f ()
... g() ...
end
-- `forward' declarations
6.3 正確的尾調用(Proper Tail Calls)
Lua 中函數的另外一個有趣的特徵是能夠正確的處理尾調用(proper tail recursion,一
些書使用術語「尾遞歸」,雖然並未涉及到遞歸的概念)。
尾調用是一種相似在函數結尾的 goto 調用,當函數最後一個動做是調用另一個函
數時,咱們稱這種調用尾調用。例如:
function f(x)
return g(x)
end
g 的調用是尾調用。
例子中 f 調用 g 後不會再作任何事情,這種狀況下當被調用函數 g 結束時程序不需
要返回到調用者 f;因此尾調用以後程序不須要在棧中保留關於調用者的任何信息。一
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
37
些編譯器好比 Lua 解釋器利用這種特性在處理尾調用時不使用額外的棧,咱們稱這種語
言支持正確的尾調用。
因爲尾調用不須要使用棧空間,那麼尾調用遞歸的層次能夠無限制的。例以下面調
用不論 n 爲什麼值不會致使棧溢出。
function foo (n)
if n > 0 then return foo(n - 1) end
end
須要注意的是:必須明確什麼是尾調用。
一些調用者函數調用其餘函數後也沒有作其餘的事情但不屬於尾調用。好比:
function f (x)
g(x)
return
end
上面這個例子中 f 在調用 g 後,不得不丟棄 g 地返回值,因此不是尾調用,一樣的
下面幾個例子也不時尾調用:
return g(x) + 1
return x or g(x)
return (g(x))
-- must do the addition
-- must adjust to 1 result
-- must adjust to 1 result
Lua 中相似 return g(...)這種格式的調用是尾調用。可是 g 和 g 的參數均可以是複雜
表達式,由於 Lua 會在調用以前計算表達式的值。例以下面的調用是尾調用:
return x[i].foo(x[j] + a*b, i + j)
能夠將尾調用理解成一種 goto,在狀態機的編程領域尾調用是很是有用的。狀態機
的應用要求函數記住每個狀態,改變狀態只須要 goto(or call)一個特定的函數。咱們考
慮一個迷宮遊戲做爲例子:迷宮有不少個房間,每一個房間有東西南北四個門,每一步輸
入一個移動的方向,若是該方向存在即到達該方向對應的房間,不然程序打印警告信息。
目標是:從開始的房間到達目的房間。
這個迷宮遊戲是典型的狀態機,每一個當前的房間是一個狀態。咱們能夠對每一個房間
寫一個函數實現這個迷宮遊戲,咱們使用尾調用從一個房間移動到另一個房間。一個
四個房間的迷宮代碼以下:
function room1 ()
local move = io.read()
if move == "south" then
return room3()
elseif move == "east" then
return room2()
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
else
print("invalid move")
return room1()
end
end
function room2 ()
local move = io.read()
if move == "south" then
return room4()
elseif move == "west" then
return room1()
else
print("invalid move")
return room2()
end
end
function room3 ()
local move = io.read()
if move == "north" then
return room1()
elseif move == "east" then
return room4()
else
print("invalid move")
return room3()
end
end
function room4 ()
print("congratilations!")
end
-- stay in the same room
38
咱們能夠調用 room1()開始這個遊戲。
若是沒有正確的尾調用,每次移動都要建立一個棧,屢次移動後可能致使棧溢出。
但正確的尾調用能夠無限制的尾調用,由於每次尾調用只是一個 goto 到另一個函數並
不是傳統的函數調用。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
39
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
40
第 7 章 迭代器與泛型 for
在這一章咱們討論爲範性 for 寫迭代器,咱們從一個簡單的迭代器開始,而後咱們
學習如何經過利用範性 for 的強大之處寫出更高效的迭代器。
7.1 迭代器與閉包
迭代器是一種支持指針類型的結構,它能夠遍歷集合的每個元素。在 Lua 中咱們
經常使用函數來描述迭代器,每次調用該函數就返回集合的下一個元素。
迭代器須要保留上一次成功調用的狀態和下一次成功調用的狀態,也就是他知道來
自於哪裏和將要前往哪裏。閉包提供的機制能夠很容易實現這個任務。記住:閉包是一
個內部函數,它能夠訪問一個或者多個外部函數的外部局部變量。每次閉包的成功調用
後這些外部局部變量都保存他們的值(狀態) 固然若是要建立一個閉包必需要建立其外。
部局部變量。因此一個典型的閉包的結構包含兩個函數:一個是閉包本身;另外一個是工
廠(建立閉包的函數)。
舉一個簡單的例子,咱們爲一個 list 寫一個簡單的迭代器,與 ipairs()不一樣的是咱們
實現的這個迭代器返回元素的值而不是索引下標:
function list_iter (t)
local i = 0
local n = table.getn(t)
return function ()
i=i+1
if i <= n then return t[i] end
end
end
這個例子中 list_iter 是一個工廠,每次調用他都會建立一個新的閉包(迭代器自己)。
閉包保存內部局部變量(t,i,n),所以每次調用他返回 list 中的下一個元素值,當 list 中沒
有值時,返回 nil.咱們能夠在 while 語句中使用這個迭代器:
t = {10, 20, 30}
iter = list_iter(t)
while true do
local element = iter()
print(element)
-- calls the iterator
if element == nil then break end
-- creates the iterator
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
41
咱們設計的這個迭代器也很容易用於範性 for 語句
t = {10, 20, 30}
for element in list_iter(t) do
print(element)
end
範性 for 爲迭代循環處理全部的薄記(bookkeeping):首先調用迭代工廠;內部保留
迭代函數,所以咱們不須要 iter 變量;而後在每個新的迭代處調用迭代器函數;當迭
代器返回 nil 時循環結束(後面咱們將看到範性 for 能勝任更多的任務)。
下面看一個稍微高級一點的例子:咱們寫一個迭代器遍歷一個文件內的全部匹配的
單詞。爲了實現目的,咱們須要保留兩個值:當前行和在當前行的偏移量,咱們使用兩
個外部局部變量 line、pos 保存這兩個值。
function allwords()
local line = io.read()
local pos = 1
return function ()
while line do
if s then
pos = e + 1
else
line = io.read() -- word not found; try next line
pos = 1
end
end
return nil
end
end
-- no more lines: end of traversal
-- restart from first position
-- current line
-- current position in the line
-- iterator function
-- repeat while there are lines
-- found a word?
-- next position is after this word
local s, e = string.find(line, "%w+", pos)
return string.sub(line, s, e) -- return the word
迭代函數的主體部分調用了 string.find 函數,string.find 在當前行從當前位置開始查
找匹配的單詞,例子中匹配的單詞使用模式'%w+'描述的;若是查找到一個單詞,迭代函
數更新當前位置 pos 爲單詞後的第一個位置,而且返回這個單詞(string.sub 函數從 line
中提取兩個位置參數之間的子串)。不然迭代函數讀取新的一行並從新搜索。若是沒有
line 可讀返回 nil 結束。
儘管迭代函數有些複雜,但使用起來是很直觀的:
for word in allwords() do
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print(word)
end
42
一般狀況下,迭代函數都難寫易用。這不是一個大問題:通常 Lua 編程不須要本身
定義迭代函數,而是使用語言提供的,除非確實須要本身定義。
7.2 範性 for 的語義
前面咱們看到的迭代器有一個缺點:每次調用都須要建立一個閉包,大多數狀況下
這種作法都沒什麼問題,例如在 allwords 迭代器中建立一個閉包的代價比起讀整個文件
來講微不足道,然而在有些狀況下建立閉包的代價是不能忍受的。在這些狀況下咱們可
以使用範性 for 自己來保存迭代的狀態。
前面咱們看到在循環過程當中範性 for 在本身內部保存迭代函數,實際上它保存三個
值:迭代函數,狀態常量和控制變量.下面詳細說明。
範性 for 的文法以下:
for <var-list> in <exp-list> do
<body>
end
<var-list>是一個或多個以逗號分割的變量名列表,<exp-list>是一個或多個以逗號分
割的表達式列表,一般狀況下 exp-list 只有一個值:迭代工廠的調用。
for k, v in pairs(t) do
print(k, v)
end
變量列表 k,v;表達式列表 pair(t),在不少狀況下變量列表也只有一個變量,好比:
for line in io.lines() do
io.write(line, '\n')
end
咱們稱變量列表中第一個變量爲控制變量,其值爲 nil 時循環結束。
下面咱們看看範性 for 的執行過程:
首先,初始化,計算 in 後面表達式的值,表達式應該返回範性 for 須要的三個值:
迭代函數,狀態常量和控制變量;與多值賦值同樣,若是表達式返回的結果個數不足三
個會自動用 nil 補足,多出部分會被忽略。
第二,將狀態常量和控制變量做爲參數調用迭代函數(注意:對於 for 結構來講,
狀態常量沒有用處,僅僅在初始化時獲取他的值並傳遞給迭代函數)。
第三,將迭代函數返回的值賦給變量列表。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
第四,若是返回的第一個值爲 nil 循環結束,不然執行循環體。
第五,回到第二步再次調用迭代函數。
更精確的來講:
for var_1, ..., var_n in explist do block end
43
等價於
do
local _f, _s, _var = explist
while true do
local var_1, ... , var_n = _f(_s, _var)
_var = var_1
if _var == nil then break end
block
end
end
若是咱們的迭代函數是 f,狀態常量是 s,控制變量的初始值是 a0,那麼控制變量將
循環:a1=f(s,a0)、a2=f(s,a1)、……,直到 ai=nil。
7.3 無狀態的迭代器
無狀態的迭代器是指不保留任何狀態的迭代器,所以在循環中咱們能夠利用無狀態
迭代器避免建立閉包花費額外的代價。
每一次迭代,迭代函數都是用兩個變量(狀態常量和控制變量)的值做爲參數被調
用,一個無狀態的迭代器只利用這兩個值能夠獲取下一個元素。這種無狀態迭代器的典
型的簡單的例子是 ipairs,他遍歷數組的每個元素。
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end
迭代的狀態包括被遍歷的表(循環過程當中不會改變的狀態常量)和當前的索引下標
(控制變量),ipairs 和迭代函數都很簡單,咱們在 Lua 中能夠這樣實現:
function iter (a, i)
i=i+1
local v = a[i]
if v then
return i, v
end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
44
function ipairs (a)
return iter, a, 0
end
當 Lua 調用 ipairs(a)開始循環時,他獲取三個值:迭代函數 iter,狀態常量 a 和控制變
量初始值 0;而後 Lua 調用 iter(a,0)返回 1,a[1](除非 a[1]=nil);第二次迭代調用 iter(a,1)
返回 2,a[2]……直到第一個非 nil 元素。
Lua 庫中實現的 pairs 是一個用 next 實現的原始方法:
function pairs (t)
return next, t, nil
end
還能夠不使用 ipairs 直接使用 next
for k, v in next, t do
...
end
記住:exp-list 返回結果會被調整爲三個,因此 Lua 獲取 next、t、nil;確切地說當
他調用 pairs 時獲取。
7.4 多狀態的迭代器
不少狀況下,迭代器須要保存多個狀態信息而不是簡單的狀態常量和控制變量,最
簡單的方法是使用閉包,還有一種方法就是將全部的狀態信息封裝到 table 內,將 table
做爲迭代器的狀態常量,由於這種狀況下能夠將全部的信息存放在 table 內,因此迭代函
數一般不須要第二個參數。
下面咱們重寫 allwords 迭代器,這一次咱們不是使用閉包而是使用帶有兩個域
(line,pos)的 table。
開始迭代的函數是很簡單的,他必須返回迭代函數和初始狀態:
local iterator
function allwords()
local state = {line = io.read(), pos = 1}
return iterator, state
end
-- to be defined later
真正的處理工做是在迭代函數內完成:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
function iterator (state)
while state.line do
-- repeat while there are lines
45
-- search for next word
local s, e = string.find(state.line, "%w+", state.pos)
if s then
-- found a word?
-- update next position (after this word)
state.pos = e + 1
return string.sub(state.line, s, e)
else
-- word not found
-- try next line...
-- ... from first position
state.line = io.read()
state.pos = 1
end
end
return nil
end
-- no more lines: end loop
咱們應該儘量的寫無狀態的迭代器,由於這樣循環的時候由 for 來保存狀態,不
須要建立對象花費的代價小;若是不能用無狀態的迭代器實現,應儘量使用閉包;盡
可能不要使用 table 這種方式,由於建立閉包的代價要比建立 table 小,另外 Lua 處理閉
包要比處理 table 速度快些。後面咱們還將看到另外一種使用協同來建立迭代器的方式,這
種方式功能更強但更復雜。
7.5 真正的迭代器
迭代器的名字有一些誤導,由於它並無迭代,完成迭代功能的是 for 語句,也許
更好的叫法應該是'生成器';可是在其餘語言好比 java、C++迭代器的說法已經很廣泛了,
咱們也將沿用這種術語。
有一種方式建立一個在內部完成迭代的迭代器。這樣當咱們使用迭代器的時候就不
須要使用循環了;咱們僅僅使用每一次迭代須要處理的任務做爲參數調用迭代器便可,
具體地說,迭代器接受一個函數做爲參數,而且這個函數在迭代器內部被調用。
做爲一個具體的例子,咱們使用上述方式重寫 allwords 迭代器:
function allwords (f)
-- repeat for each line in the file
for l in io.lines() do
-- repeat for each word in the line
for w in string.gfind(l, "%w+") do
-- call the function
f(w)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
end
end
46
若是咱們想要打印出單詞,只須要
allwords(print)
更通常的作法是咱們使用匿名函數做爲做爲參數,下面的例子打印出單詞'hello'出現
的次數:
local count = 0
allwords(function (w)
if w == "hello" then count = count + 1 end
end)
print(count)
用 for 結構完成一樣的任務:
local count = 0
for w in allwords() do
if w == "hello" then count = count + 1 end
end
print(count)
真正的迭代器風格的寫法在 Lua 老版本中很流行,那時尚未 for 循環。
兩種風格的寫法相差不大,但也有區別:一方面,第二種風格更容易書寫和理解;
另外一方面,for 結構更靈活,能夠使用 break 和 continue 語句;在真正的迭代器風格寫法
中 return 語句只是從匿名函數中返回而不是退出循環。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
47
第 8 章 編譯•運行•調試
雖然咱們把 Lua 看成解釋型語言,可是 Lua 會首先把代碼預編譯成中間碼而後再執
行(不少解釋型語言都是這麼作的) 在解釋型語言中存在編譯階段聽起來不合適,。然而,
解釋型語言的特徵不在於他們是否被編譯,而是編譯器是語言運行時的一部分,因此,
執行編譯產生的中間碼速度會更快。咱們能夠說函數 dofile 的存在就是說明能夠將 Lua
做爲一種解釋型語言被調用。
前面咱們介紹過 dofile,把它看成 Lua 運行代碼的 chunk 的一種原始的操做。dofile
其實是一個輔助的函數。真正完成功能的函數是 loadfile;與 dofile 不一樣的是 loadfile
編譯代碼成中間碼而且返回編譯後的 chunk 做爲一個函數,而不執行代碼;另外 loadfile
不會拋出錯誤信息而是返回錯誤代。.咱們能夠這樣定義 dofile:
function dofile (filename)
local f = assert(loadfile(filename))
return f()
end
若是 loadfile 失敗 assert 會拋出錯誤。
完成簡單的功能 dofile 比較方便,他讀入文件編譯而且執行。然而 loadfile 更加靈活。
在發生錯誤的狀況下,loadfile 返回 nil 和錯誤信息,這樣咱們就能夠自定義錯誤處理。
另外,若是咱們運行一個文件屢次的話,loadfile 只須要編譯一次,但可屢次運行。dofile
卻每次都要編譯。
loadstring 與 loadfile 類似,只不過它不是從文件裏讀入 chunk,而是從一個串中讀入。
例如:
f = loadstring("i = i + 1")
f 將是一個函數,調用時執行 i=i+1。
i=0
f(); print(i)
f(); print(i)
--> 1
--> 2
loadstring 函數功能強大,但使用時需多加當心。確認沒有其它簡單的解決問題的方
法再使用。
Lua 把每個 chunk 都做爲一個匿名函數處理。例如:chunk "a = 1",loadstring 返
回與其等價的 function () a = 1 end
與其餘函數同樣,chunks 能夠定義局部變量也能夠返回值:
f = loadstring("local a = 10; return a + 20")
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print(f())
--> 30
48
loadfile 和 loadstring 都不會拋出錯誤,若是發生錯誤他們將返回 nil 加上錯誤信息:
print(loadstring("i i"))
--> nil
[string "i i"]:1: '=' expected near 'i'
另外,loadfile 和 loadstring 都不會有邊界效應產生,他們僅僅編譯 chunk 成爲本身
內部實現的一個匿名函數。一般對他們的誤解是他們定義了函數。Lua 中的函數定義是
發生在運行時的賦值而不是發生在編譯時。假如咱們有一個文件 foo.lua:
-- file `foo.lua'
function foo (x)
print(x)
end
當咱們執行命令 f = loadfile("foo.lua")後,foo 被編譯了但尚未被定義,若是要定
義他必須運行 chunk:
f()
foo("ok")
-- defines `foo'
--> ok
若是你想快捷的調用 dostring(好比加載並運行),能夠這樣
loadstring(s)()
調用 loadstring 返回的結果,然而若是加載的內容存在語法錯誤的話,loadstring 返
回 nil 和錯誤信息(attempt to call a nil value)爲了返回更清楚的錯誤信息能夠使用 assert:;
assert(loadstring(s))()
一般使用 loadstring 加載一個字串沒什麼意義,例如:
f = loadstring("i = i + 1")
大概與 f = function () i = i + 1 end 等價,可是第二段代碼速度更快由於它只須要編譯
一次,第一段代碼每次調用 loadstring 都會從新編譯,還有一個重要區別:loadstring 編
譯的時候不關心詞法範圍:
local i = 0
f = loadstring("i = i + 1")
g = function () i = i + 1 end
這個例子中,和想象的同樣 g 使用局部變量 i,然而 f 使用全局變量 i;loadstring 總
是在全局環境中編譯他的串。
loadstring 一般用於運行程序外部的代碼,好比運行用戶自定義的代碼。注意:
loadstring 指望一個 chunk,即語句。若是想要加載表達式,須要在表達式前加 return,
那樣將返回表達式的值。看例子:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print "enter your expression:"
local l = io.read()
local func = assert(loadstring("return " .. l))
print("the value of your expression is " .. func())
49
loadstring 返回的函數和普通函數同樣,能夠屢次被調用:
print "enter function to be plotted (with variable `x'):"
local l = io.read()
local f = assert(loadstring("return " .. l))
for i=1,20 do
x=i
end
-- global `x' (to be visible from the chunk)
print(string.rep("*", f()))
8.1 require 函數
Lua 提供高級的 require 函數來加載運行庫。粗略的說 require 和 dofile 完成一樣的功
能但有兩點不一樣:
1. require 會搜索目錄加載文件
2. require 會判斷是否文件已經加載避免重複加載同一文件。因爲上述特徵,require
在 Lua 中是加載庫的更好的函數。
require 使用的路徑和普通咱們看到的路徑還有些區別,咱們通常見到的路徑都是一
個目錄列表。require 的路徑是一個模式列表,每個模式指明一種由虛文件名(require
的參數)轉成實文件名的方法。更明確地說,每個模式是一個包含可選的問號的文件
名。匹配的時候 Lua 會首先將問號用虛文件名替換,而後看是否有這樣的文件存在。如
果不存在繼續用一樣的方法用第二個模式匹配。例如,路徑以下:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
調用 require "lili"時會試着打開這些文件:
lili
lili.lua
c:\windows\lili
/usr/local/lua/lili/lili.lua
require 關注的問題只有分號(模式之間的分隔符)和問號,其餘的信息(目錄分隔
符,文件擴展名)在路徑中定義。
爲了肯定路徑,Lua 首先檢查全局變量 LUA_PATH 是否爲一個字符串,若是是則認
爲這個串就是路徑;不然 require 檢查環境變量 LUA_PATH 的值,若是兩個都失敗 require
使用固定的路徑(典型的"?;?.lua")
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
50
require 的另外一個功能是避免重複加載同一個文件兩次。Lua 保留一張全部已經加載
的文件的列表(使用 table 保存)。若是一個加載的文件在表中存在 require 簡單的返回;
表中保留加載的文件的虛名,而不是實文件名。因此若是你使用不一樣的虛文件名 require
同一個文件兩次,將會加載兩次該文件。好比 require "foo"和 require "foo.lua",路徑爲
"?;?.lua"將會加載 foo.lua 兩次。咱們也能夠經過全局變量_LOADED 訪問文件名列表,
這樣咱們就能夠判斷文件是否被加載過;一樣咱們也能夠使用一點小技巧讓 require 加載
一個文件兩次。好比,require "foo"以後_LOADED["foo"]將不爲 nil,咱們能夠將其賦值
爲 nil,require "foo.lua"將會再次加載該文件。
一個路徑中的模式也能夠不包含問號而只是一個固定的路徑,好比:
?;?.lua;/usr/local/default.lua
這種狀況下,require 沒有匹配的時候就會使用這個固定的文件(固然這個固定的路
徑必須放在模式列表的最後纔有意義)。在 require 運行一個 chunk 之前,它定義了一個
全局變量_REQUIREDNAME 用來保存被 required 的虛文件的文件名。咱們能夠經過使
用 這 個 技 巧 擴 展 require 的 功 能 。 舉 個 極 端 的 例 子 , 我 們 可 以 把 路 徑 設 爲
"/usr/local/lua/newrequire.lua",這樣之後每次調用 require 都會運行 newrequire.lua,這種
狀況下能夠經過使用_REQUIREDNAME 的值去實際加載 required 的文件。
8.2 C Packages
Lua 和 C 是很容易結合的,使用 C 爲 Lua 寫包。與 Lua 中寫包不一樣,C 包在使用以
前必須首先加載並鏈接,在大多數系統中最容易的實現方式是經過動態鏈接庫機制,然
而動態鏈接庫不是 ANSI C 的一部分,也就是說在標準 C 中實現動態鏈接是很困難的。
一般 Lua 不包含任何不能用標準 C 實現的機制,動態鏈接庫是一個特例。咱們能夠
將動態鏈接庫機制視爲其餘機制之母:一旦咱們擁有了動態鏈接機制,咱們就能夠動態
的加載 Lua 中不存在的機制。因此,在這種特殊狀況下,Lua 打破了他平臺兼容的原則
而經過條件編譯的方式爲一些平臺實現了動態鏈接機制。標準的 Lua 爲 windows、Linux、
FreeBSD、Solaris 和其餘一些 Unix 平臺實現了這種機制,擴展其它平臺支持這種機制也
是不難的。在 Lua 提示符下運行 print(loadlib())看返回的結果,若是顯示 bad arguments
則說明你的發佈版支持動態鏈接機制,不然說明動態鏈接機制不支持或者沒有安裝。
Lua 在一個叫 loadlib 的函數內提供了全部的動態鏈接的功能。這個函數有兩個參數:
庫的絕對路徑和初始化函數。因此典型的調用的例子以下:
local path = "/usr/local/lua/lib/libluasocket.so"
local f = loadlib(path, "luaopen_socket")
loadlib 函數加載指定的庫而且鏈接到 Lua,然而它並不打開庫(也就是說沒有調用
初始化函數),反之他返回初始化函數做爲 Lua 的一個函數,這樣咱們就能夠直接在 Lua
中調用他。若是加載動態庫或者查找初始化函數時出錯,loadlib 將返回 nil 和錯誤信息。
咱們能夠修改前面一段代碼,使其檢測錯誤而後調用初始化函數:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
local path = "/usr/local/lua/lib/libluasocket.so"
-- or path = "C:\\windows\\luasocket.dll"
local f = assert(loadlib(path, "luaopen_socket"))
f() -- actually open the library
51
通常狀況下咱們指望二進制的發佈庫包含一個與前面代碼段類似的 stub 文件,安裝
二進制庫的時候能夠隨便放在某個目錄,只須要修改 stub 文件對應二進制庫的實際路徑
便可。將 stub 文件所在的目錄加入到 LUA_PATH,這樣設定後就能夠使用 require 函數
加載 C 庫了。
8.3 錯誤
Errare humanum est(拉丁諺語:犯錯是人的本性)。因此咱們要儘量的防止錯誤
的發生,Lua 常常做爲擴展語言嵌入在別的應用中,因此不能當錯誤發生時簡單的崩潰
或者退出。相反,當錯誤發生時 Lua 結束當前的 chunk 並返回到應用中。
當 Lua 遇到不指望的狀況時就會拋出錯誤,好比:兩個非數字進行相加;調用一個
非函數的變量;訪問表中不存在的值等(能夠經過 metatables 修改這種行爲,後面介紹)。
你也能夠經過調用 error 函數顯示的拋出錯誤,error 的參數是要拋出的錯誤信息。
print "enter a number:"
n = io.read("*number")
if not n then error("invalid input") end
Lua 提供了專門的內置函數 assert 來完成上面相似的功能:
print "enter a number:"
n = assert(io.read("*number"), "invalid input")
assert 首先檢查第一個參數是否返回錯誤,若是不返回錯誤 assert 簡單的返回,不然
assert 以第二個參數拋出錯誤信息。第二個參數是可選的。注意 assert 是普通的函數,他
會首先計算兩個參數而後再調用函數,因此如下代碼:
n = io.read()
assert(tonumber(n), "invalid input: " .. n .. " is not a number")
將會老是進行鏈接操做,使用顯示的 test 能夠避免這種狀況。
當函數遇到異常有兩個基本的動做:返回錯誤代碼或者拋出錯誤。這兩種方式選擇
哪種沒有固定的規則,但有通常的原則:容易避免的異常應該拋出錯誤不然返回錯誤
代碼。
例如咱們考慮 sin 函數,若是以一個 table 做爲參數,假定咱們返回錯誤代碼,咱們
須要檢查錯誤的發生,代碼可能以下:
local res = math.sin(x)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
if not res then
...
-- error
52
然而咱們能夠在調用函數之前很容易的判斷是否有異常:
if not tonumber(x) then
...
-- error: x is not a number
然而一般狀況下咱們既不是檢查參數也不是檢查返回結果,由於參數錯誤可能意味
着咱們的程序某個地方存在問題,這種狀況下,處理異常最簡單最實際的方式是拋出錯
誤而且終止代碼的運行。
再來看一個例子 io.open 函數用來打開一個文件,若是文件不存在結果會怎麼樣呢?
不少系統中,經過試着去打開文件來判斷是否文件存在。因此若是 io.open 不能打開文件
(因爲文件不存在或者沒有權限),函數返回 nil 和錯誤信息。以這種方式咱們能夠經過
與用戶交互(好比:是否要打開另外一個文件)合理的處理問題:
local file, msg
repeat
print "enter a file name:"
local name = io.read()
if not name then return end
file, msg = io.open(name, "r")
if not file then print(msg) end
until file
-- no input
若是你想偷懶不想處理這些狀況,又想代碼安全的運行,能夠簡單的使用 assert:
file = assert(io.open(name, "r"))
Lua 中有一個習慣:若是 io.open 失敗,assert 將拋出錯誤。
file = assert(io.open("no-file", "r"))
--> stdin:1: no-file: No such file or directory
注意:io.open 返回的第二個結果(錯誤信息)做爲 assert 的第二個參數。
8.4 異常和錯誤處理
不少應用中,不須要在 Lua 進行錯誤處理,通常有應用來完成。一般應用要求 Lua
運行一段 chunk,若是發生異常,應用根據 Lua 返回的錯誤代碼進行處理。在控制檯模
式下的 Lua 解釋器若是遇到異常,打印出錯誤而後繼續顯示提示符等待下一個命令。
若是在 Lua 中須要處理錯誤,須要使用 pcall 函數封裝你的代碼。
假定你想運行一段 Lua 代碼,這段代碼運行過程當中能夠捕捉全部的異常和錯誤。
第一步:將這段代碼封裝在一個函數內
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
function foo ()
...
if unexpected_condition then error() end
...
print(a[i])
...
end
-- potential error: `a' may not be a table
53
第二步:使用 pcall 調用這個函數
if pcall(foo) then
-- no errors while running `foo'
...
else
-- `foo' raised an error: take appropriate actions
...
end
固然也能夠用匿名函數的方式調用 pcall:
if pcall(function () ... end) then ...
else ...
pcall 在保護模式下調用他的第一個參數並運行,所以能夠捕獲全部的異常和錯誤。
若是沒有異常和錯誤,pcall 返回 true 和調用返回的任何值;不然返回 nil 加錯誤信息。
錯誤信息不必定非要是一個字符串(下面的例子是一個 table),傳遞給 error 的任何
信息都會被 pcall 返回:
local status, err = pcall(function () error({code=121}) end)
print(err.code) --> 121
這種機制提供了咱們在 Lua 中處理異常和錯誤的所須要的所有內容。咱們經過 error
拋出異常,而後經過 pcall 捕獲他。
8.5 錯誤信息和回跟蹤(Tracebacks)
雖然你能夠使用任何類型的值做爲錯誤信息,一般狀況下,咱們使用字符串來描述
遇到的錯誤信息。若是遇到內部錯誤(好比對一個非 table 的值使用索引下表訪問)Lua
將本身產生錯誤信息,不然 Lua 使用傳遞給 error 函數的參數做爲錯誤信息。無論在什麼
狀況下,Lua 都儘量清楚的描述發生的錯誤。
local status, err = pcall(function () a = 'a'+1 end)
print(err)
--> stdin:1: attempt to perform arithmetic on a string value
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
54
local status, err = pcall(function () error("my error") end)
print(err)
--> stdin:1: my error
例子中錯誤信息給出了文件名(stdin)加上行號。
函數 error 還能夠有第二個參數,表示錯誤的運行級別。有了這個參數你就沒法抵賴
錯誤是別人的了,好比,加入你寫了一個函數用來檢查 error 是否被正確的調用:
function foo (str)
if type(str) ~= "string" then
error("string expected")
end
...
end
可能有人這樣調用這個函數:
foo({x=1})
Lua 會指出發生錯誤的是 foo 而不是 error,實際的錯誤是調用 error 時產生的,爲了
糾正這個問題修改前面的代碼讓 error 報告錯誤發生在第二級(你本身的函數是第一級)
以下:
function foo (str)
if type(str) ~= "string" then
error("string expected", 2)
end
...
end
當錯誤發生的時候,咱們經常須要更多的錯誤發生相關的信息,而不僅僅是錯誤發
生的位置。至少指望有一個完整的顯示致使錯誤發生的調用棧的 tracebacks,當 pcall 返
回錯誤信息的時候他已經釋放了保存錯誤發生狀況的棧的信息。所以,若是咱們想獲得
tracebacks 咱們必須在 pcall 返回之前獲取。Lua 提供了 xpcall 來實現這個功能,xpcall
接受兩個參數:調用函數和錯誤處理函數。當錯誤發生時。Lua 會在棧釋放之前調用錯
誤處理函數,所以能夠使用 debug 庫收集錯誤相關的信息。有兩個經常使用的 debug 處理函
數:debug。debug 和 debug.traceback,前者給出 Lua 的提示符,你能夠本身動手察看錯誤
發生時的狀況;後者經過 traceback 建立更多的錯誤信息,後者是控制檯解釋器用來構建
錯誤信息的函數。你能夠在任什麼時候候調用 debug.traceback 獲取當前運行的 traceback 信息:
print(debug.traceback())
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
55
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
56
第 9 章 協同程序
協同程序(coroutine)與多線程狀況下的線程比較相似:有本身的堆棧,本身的局
部變量,有本身的指令指針,可是和其餘協同程序共享全局變量等不少信息。線程和協
同程序的主要不一樣在於:在多處理器狀況下,從概念上來說多線程程序同時運行多個線
程;而協同程序是經過協做來完成,在任一指定時刻只有一個協同程序在運行,而且這
個正在運行的協同程序只有在明確的被要求掛起的時候纔會被掛起。
協同是很是強大的功能,可是用起來也很複雜。若是你第一次閱讀本章時不理解本
章中的例子請不要擔憂,你能夠繼續閱讀本書的其餘部分而後再回過頭來閱讀本章。
9.1 協同的基礎
Lua 經過 table 提供了全部的協同函數,create 函數建立一個新的協同程序,create
只有一個參數:協同程序將要運行的代碼封裝而成的函數,返回值爲 thread 類型的值表
示建立了一個新的協同程序。一般狀況下,create 的參數是一個匿名函數:
co = coroutine.create(function ()
print("hi")
end)
print(co)
--> thread: 0x8071d98
協同有三個狀態:掛起態、運行態、中止態。當咱們建立一個協同程序時他開始的
狀態爲掛起態,也就是說咱們建立協同程序的時候不會自動運行,能夠使用 status 函數
檢查協同的狀態:
print(coroutine.status(co))
--> suspended
函數 coroutine.resume 能夠使程序由掛起狀態變爲運行態:
coroutine.resume(co)
--> hi
這個例子中,協同體僅僅打印出"hi"以後便進入終止狀態:
print(coroutine.status(co))
--> dead
當目前爲止,協同看起來只是一種複雜的調用函數的方式,真正的強大之處體如今
yield 函數,它能夠將正在運行的代碼掛起,看一個例子:
co = coroutine.create(function ()
for i=1,10 do
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print("co", i)
coroutine.yield()
end
end)
57
如今從新執行這個協同程序,程序將在第一個 yield 處被掛起:
coroutine.resume(co)
print(coroutine.status(co))
--> co
1
--> suspended
從協同的觀點看:使用函數 yield 能夠使程序掛起,當咱們激活被掛起的程序時,yield
返回並繼續程序的執行直到再次遇到 yield 或者程序結束。
coroutine.resume(co)
coroutine.resume(co)
...
coroutine.resume(co)
coroutine.resume(co)
--> co
10
-- prints nothing
--> co
--> co
2
3
上面最後一次調用的時候,協同體已經結束,所以協同程序處於終止狀態。若是我
們仍然企圖激活他,resume 將返回 false 和錯誤信息。
print(coroutine.resume(co))
--> false
cannot resume dead coroutine
注意:resume 運行在保護模式下,所以若是協同內部存在錯誤 Lua 並不會拋出錯誤而
是將錯誤返回給 resume 函數。
Lua 中一對 resume-yield 能夠相互交換數據。
下面第一個例子 resume,沒有相應的 yield,resume 把額外的參數傳遞給協同的主
程序。
co = coroutine.create(function (a,b,c)
print("co", a,b,c)
end)
coroutine.resume(co, 1, 2, 3)
--> co 1 2 3
第二個例子,resume 返回除了 true 之外的其餘部分將做爲參數傳遞給相應的 yield
co = coroutine.create(function (a,b)
coroutine.yield(a + b, a - b)
end)
print(coroutine.resume(co, 20, 10))
--> true 30 10
對稱性,yield 返回的額外的參數也將會傳遞給 resume。
co = coroutine.create (function ()
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print("co", coroutine.yield())
end)
coroutine.resume(co)
coroutine.resume(co, 4, 5)
--> co 4 5
58
最後一個例子,當協同代碼結束時主函數返回的值都會傳給相應的 resume:
co = coroutine.create(function ()
return 6, 7
end)
print(coroutine.resume(co))
--> true 6 7
咱們不多在同一個協同程序中使用這幾種特性,但每一種都有其用處。
如今已經瞭解了一些協同的內容,在咱們繼續學習之前,先要澄清兩個概念:Lua
提供的這種協同咱們稱爲不對稱的協同,就是說掛起一個正在執行的協同的函數與使一
個被掛起的協同再次執行的函數是不一樣的,有些語言提供對稱的協同,這種狀況下,由
執行到掛起之間狀態轉換的函數是相同的。
有人稱不對稱的協同爲半協同,另外一些人使用一樣的術語表示真正的協同,嚴格意
義上的協同不論在什麼地方只要它不是在其餘的輔助代碼內部的時候均可以而且只能使
執行掛起,不論何時在其控制棧內都不會有不可決定的調用。(However, other people
use the same term semi-coroutine to denote a restricted implementation of coroutines, where a
coroutine can only suspend its execution when it is not inside any auxiliary function, that is,
when it has no pending calls in its control stack.)。只有半協同程序的主體中才能夠 yield,
python 中的產生器(generator)就是這種類型的半協同的例子。
與對稱的協同和不對稱協同的區別不一樣的是,協同與產生器的區別更大。產生器相
對比較簡單,他不能完成真正的協同所能完成的一些任務。咱們熟練使用不對稱的協同
以後,能夠利用不對稱的協同實現比較優越的對稱協同。
9.2 管道和過濾器
協同最有表明性的做用是用來描述生產者-消費者問題。咱們假定有一個函數在不斷
的生產值(好比從文件中讀取),另外一個函數不斷的消費這些值(好比寫到另外一文件中),
這兩個函數以下:
function producer ()
while true do
local x = io.read()
send(x)
end
end
-- produce new value
-- send to consumer
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
59
function consumer ()
while true do
local x = receive()
io.write(x, "\n")
end
end
-- receive from producer
-- consume new value
(例子中生產者和消費者都在不停的循環,修改一下使得沒有數據的時候他們停下
來並不困難),問題在於如何使得 receive 和 send 協同工做。只是一個典型的誰擁有住循
環的狀況,生產者和消費者都處在活動狀態,都有本身的主循環,都認爲另外一方是可調
用的服務。對於這種特殊的狀況,能夠改變一個函數的結構解除循環,使其做爲被動的
接受。然而這種改變在某些特定的實際狀況下可能並不簡單。
協同爲解決這種問題提供了理想的方法,由於調用者與被調用者之間的 resume-yield
關係會不斷顛倒。當一個協同調用 yield 時並不會進入一個新的函數,取而代之的是返回
一個未決的 resume 的調用。類似的,調用 resume 時也不會開始一個新的函數而是返回
yield 的調用。這種性質正是咱們所須要的,與使得 send-receive 協同工做的方式是一致
的.receive 喚醒生產者生產新值,send 把產生的值送給消費者消費。
function receive ()
local status, value = coroutine.resume(producer)
return value
end
function send (x)
coroutine.yield(x)
end
producer = coroutine.create( function ()
while true do
local x = io.read()
send(x)
end
end)
-- produce new value
這種設計下,開始時調用消費者,當消費者須要值時他喚起生產者生產值,生產者
生產值後中止直到消費者再次請求。咱們稱這種設計爲消費者驅動的設計。
咱們能夠使用過濾器擴展這個涉及,過濾器指在生產者與消費者之間,能夠對數據
進行某些轉換處理。過濾器在同一時間既是生產者又是消費者,他請求生產者生產值並
且轉換格式後傳給消費者,咱們修改上面的代碼加入過濾器(每一行前面加上行號)。完
整的代碼以下:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
function receive (prod)
local status, value = coroutine.resume(prod)
return value
end
function send (x)
coroutine.yield(x)
end
function producer ()
return coroutine.create(function ()
while true do
local x = io.read()
send(x)
end
end)
end
function filter (prod)
return coroutine.create(function ()
local line = 1
while true do
local x = receive(prod) -- get new value
x = string.format("%5d %s", line, x)
send(x)
end
end)
end
function consumer (prod)
while true do
local x = receive(prod) -- get new value
io.write(x, "\n")
end
end
-- consume new value
-- send it to consumer
line = line + 1
-- produce new value
60
能夠調用:
p = producer()
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
f = filter(p)
consumer(f)
61
或者:
consumer(filter(producer()))
看完上面這個例子你可能很天然的想到 UNIX 的管道,協同是一種非搶佔式的多線
程。管道的方式下,每個任務在獨立的進程中運行,而協同方式下,每一個任務運行在
獨立的協同代碼中。管道在讀(consumer)與寫(producer)之間提供了一個緩衝,所以
二者相關的的速度沒有什麼限制,在上下文管道中這是很是重要的,由於在進程間的切
換代價是很高的。協同模式下,任務間的切換代價較小,與函數調用至關,所以讀寫可
以很好的協同處理。
9.3 用做迭代器的協同
咱們能夠將循環的迭代器看做生產者-消費者模式的特殊的例子。迭代函數產生值給
循環體消費。因此能夠使用協同來實現迭代器。協同的一個關鍵特徵是它能夠不斷顛倒
調用者與被調用者之間的關係,這樣咱們毫無顧慮的使用它實現一個迭代器,而不用保
存迭代函數返回的狀態。
咱們來完成一個打印一個數組元素的全部的排列來闡明這種應用。直接寫這樣一個
迭代函數來完成這個任務並不容易,可是寫一個生成全部排列的遞歸函數並不難。思路
是這樣的:將數組中的每個元素放到最後,依次遞歸生成全部剩餘元素的排列。代碼
以下:
function permgen (a, n)
if n == 0 then
printResult(a)
else
for i=1,n do
-- put i-th element as the last one
a[n], a[i] = a[i], a[n]
-- generate all permutations of the other elements
permgen(a, n - 1)
-- restore i-th element
a[n], a[i] = a[i], a[n]
end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
end
function printResult (a)
for i,v in ipairs(a) do
io.write(v, " ")
end
io.write("\n")
end
permgen ({1,2,3,4}, 4)
62
有了上面的生成器後,下面咱們將這個例子修改一下使其轉換成一個迭代函數:
1. 第一步 printResult 改成 yield
function permgen (a, n)
if n == 0 then
coroutine.yield(a)
else
...
2. 第二步,咱們定義一個迭代工廠,修改生成器在生成器內建立迭代函數,並使生
成器運行在一個協同程序內。迭代函數負責請求協同產生下一個可能的排列。
function perm (a)
local n = table.getn(a)
local co = coroutine.create(function () permgen(a, n) end)
return function ()
return res
end
end
-- iterator
local code, res = coroutine.resume(co)
這樣咱們就能夠使用 for 循環來打印出一個數組的全部排列狀況了:
for p in perm{"a", "b", "c"} do
printResult(p)
end
--> b c a
--> c b a
--> c a b
--> a c b
--> b a c
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
--> a b c
63
perm 函數使用了 Lua 中經常使用的模式:將一個對協同的 resume 的調用封裝在一個函
數 內 部 , 這 種 方 式 在 Lua 非 常 常 見 , 所 以 Lua 專 門 爲 此 專 門 提 供 了 一 個 函 數
coroutine.wrap。與 create 相同的是,wrap 建立一個協同程序;不一樣的是 wrap 不返回協
同自己,而是返回一個函數,當這個函數被調用時將 resume 協同。wrap 中 resume 協同
的時候不會返回錯誤代碼做爲第一個返回結果,一旦有錯誤發生,將拋出錯誤。咱們可
以使用 wrap 重寫 perm:
function perm (a)
local n = table.getn(a)
return coroutine.wrap(function () permgen(a, n) end)
end
通常狀況下,coroutine.wrap 比 coroutine.create 使用起來簡單直觀,前者更確切的提
供了咱們所須要的:一個能夠 resume 協同的函數,然而缺乏靈活性,沒有辦法知道 wrap
所建立的協同的狀態,也沒有辦法檢查錯誤的發生。
9.4 非搶佔式多線程
如前面所見,Lua 中的協同是一協做的多線程,每個協同等同於一個線程,
yield-resume 能夠實如今線程中切換。然而與真正的多線程不一樣的是,協同是非搶佔式的。
當一個協同正在運行時,不能在外部終止他。只能經過顯示的調用 yield 掛起他的執行。
對於某些應用來講這個不存在問題,但有些應用對此是不能忍受的。不存在搶佔式調用
的程序是容易編寫的。不須要考慮同步帶來的 bugs,由於程序中的全部線程間的同步都
是顯示的。你僅僅須要在協同代碼超出臨界區時調用 yield 便可。
對非搶佔式多線程來講,無論何時只要有一個線程調用一個阻塞操做(blocking
operation),整個程序在阻塞操做完成以前都將中止。對大部分應用程序而言,只是沒法
忍受的,這使得不少程序員離協同而去。下面咱們將看到這個問題能夠被有趣的解決。
看一個多線程的例子:咱們想經過 http 協議從遠程主機上下在一些文件。咱們使用
Diego Nehab 開發的 LuaSocket 庫來完成。咱們先看下在一個文件的實現,大概步驟是打
開一個到遠程主機的鏈接,發送下載文件的請求,開始下載文件,下載完畢後關閉鏈接。
第一,加載 LuaSocket 庫
require "luasocket"
第二,定義遠程主機和須要下載的文件名
host = "www.w3.org"
file = "/TR/REC-html32.html"
第三,打開一個 TCP 鏈接到遠程主機的 80 端口(http 服務的標準端口)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
c = assert(socket.connect(host, 80))
64
上面這句返回一個鏈接對象,咱們能夠使用這個鏈接對象請求發送文件
c:send("GET " .. file .. " HTTP/1.0\r\n\r\n")
receive 函數返回他送接收到的數據加上一個表示操做狀態的字符串。當主機斷開連
接時,咱們退出循環。
第四,關閉鏈接
c:close()
如今咱們知道了如何下載一個文件,下面咱們來看看如何下載多個文件。一種方法
是咱們在一個時刻只下載一個文件,這種順序下載的方式必須等前一個文件下載完成後
一個文件才能開始下載。其實是,當咱們發送一個請求以後有不少時間是在等待數據
的到達,也就是說大部分時間浪費在調用 receive 上。若是同時能夠下載多個文件,效率
將會有很大提升。當一個鏈接沒有數據到達時,能夠從另外一個鏈接讀取數據。很顯然,
協同爲這種同時下載提供了很方便的支持,咱們爲每個下載任務建立一個線程,當一
個線程沒有數據到達時,他將控制權交給一個分配器,由分配器喚起另外的線程讀取數
據。
使用協同機制重寫上面的代碼,在一個函數內:
function download (host, file)
local c = assert(socket.connect(host, 80))
local count = 0
while true do
local s, status = receive©
count = count + string.len(s)
if status == "closed" then break end
end
c:close()
print(file, count)
end
-- counts number of bytes read
c:send("GET " .. file .. " HTTP/1.0\r\n\r\n")
因爲咱們不關心文件的內容,上面的代碼只是計算文件的大小而不是將文件內容輸
出。(當有多個線程下載多個文件時,輸出會混雜在一塊兒),在新的函數代碼中,咱們使
用 receive 從遠程鏈接接收數據,在順序接收數據的方式下代碼以下:
function receive (connection)
return connection:receive(2^10)
end
在同步接受數據的方式下,函數接收數據時不能被阻塞,而是在沒有數據可取時
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
yield,代碼以下:
function receive (connection)
connection:timeout(0)
-- do not block
local s, status = connection:receive(2^10)
if status == "timeout" then
coroutine.yield(connection)
end
return s, status
end
65
調用函數 timeout(0)使得對鏈接的任何操做都不會阻塞。當操做返回的狀態爲
timeout 時意味着操做未完成就返回了。在這種狀況下,線程 yield。非 false 的數值做爲
yield 的參數告訴分配器線程仍在執行它的任務。(後面咱們將看到分配器須要 timeout
鏈接的狀況),注意:即便在 timeout 模式下,鏈接依然返回他接受到直到 timeout 爲止,
所以 receive 會一直返回 s 給她的調用者。
下面的函數保證每個下載運行在本身獨立的線程內:
threads = {}
-- list of all live threads
function get (host, file)
-- create coroutine
local co = coroutine.create(function ()
download(host, file)
end)
-- insert it in the list
table.insert(threads, co)
end
代碼中 table 中爲分配器保存了全部活動的線程。
分配器代碼是很簡單的,它是一個循環,逐個調用每個線程。而且從線程列表中
移除已經完成任務的線程。當沒有線程能夠運行時退出循環。
function dispatcher ()
while true do
local n = table.getn(threads)
if n == 0 then break end
for i=1,n do
local status, res = coroutine.resume(threads[i])
if not res then
break
end
-- thread finished its task?
table.remove(threads, i)
-- no more threads to run
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
end
end
66
最後,在主程序中建立須要的線程調用分配器,例如:從 W3C 站點上下載 4 個文
件:
host = "www.w3c.org"
get(host, "/TR/html401/html40.txt")
get(host, "/TR/2002/REC-xhtml1-20020801/xhtml1.pdf")
get(host, "/TR/REC-html32.html")
get(host,
"/TR/2000/REC-DOM-Level-2-Core-20001113/DOM2-Core.txt")
dispatcher()
-- main loop
使用協同方式下,個人機器花了 6s 下載完這幾個文件;順序方式下用了 15s,大概
2 倍的時間。
儘管效率提升了,但距離理想的實現還相差甚遠,當至少有一個線程有數據可讀取
的時候,這段代碼能夠很好的運行。不然,分配器將進入忙等待狀態,從一個線程到另
一個線程不停的循環判斷是否有數據可獲取。結果協同實現的代碼比順序讀取將花費 30
倍的 CPU 時間。
爲了不這種狀況出現,咱們能夠使用 LuaSocket 庫中的 select 函數。當程序在一
組 socket 中不斷的循環等待狀態改變時,它能夠使程序被阻塞。咱們只須要修改分配器,
使用 select 函數修改後的代碼以下:
function dispatcher ()
while true do
local n = table.getn(threads)
if n == 0 then break end
local connections = {}
for i=1,n do
local status, res = coroutine.resume(threads[i])
if not res then
break
else
end
end
-- timeout
table.insert(connections, res)
-- thread finished its task?
table.remove(threads, i)
-- no more threads to run
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
if table.getn(connections) == n then
socket.select(connections)
end
end
end
67
在內層的循環分配器收集鏈接表中 timeout 地鏈接,注意:receive 將鏈接傳遞給 yield,
所以 resume 返回他們。當全部的鏈接都 timeout 分配器調用 select 等待任一鏈接狀態的
改變。最終的實現效率和上一個協同實現的方式至關,另外,他不會發生忙等待,比起
順序實現的方式消耗 CPU 的時間僅僅多一點點。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
68
第 10 章 完整示例
咱們看兩個完整的例子來闡明 Lua 語言的使用。第一個例子來自於 Lua 網站,他展
示了 Lua 做爲數據描述語言的使用。第二個例子講解了馬爾可夫鏈算法的實現,這個算
法在 Kernighan & Pike 著做的 Practice of Programming 書中也有描述。這兩個完整的例子
以後,Lua 語言方面的介紹便到此結束。後面將繼續介紹 table 和麪向對象的內容以及標
準庫、C-API 等。
10.1 Lua 做爲數據描述語言使用
Lua 網站保留一個包含世界各地使用 Lua 建立的工程的例子的數據庫。在數據庫中
咱們用一個構造器以自動歸檔的方式表示每個工程入口點,代碼以下:
entry{
title = "Tecgraf",
org = "Computer Graphics Technology Group, PUC-Rio",
url = "http://www.tecgraf.puc-rio.br/",
contact = "Waldemar Celes",
description = [[
TeCGraf is the result of a partnership between PUC-Rio,
the Pontifical Catholic University of Rio de Janeiro,
and <A HREF="http://www.petrobras.com.br/">PETROBRAS</A>,
the Brazilian Oil Company.
TeCGraf is Lua's birthplace,
and the language has been used there since 1993.
Currently, more than thirty programmers in TeCGraf use
Lua regularly; they have written more than two hundred
thousand lines of code, distributed among dozens of
final products.]]
}
有趣的是,工程入口的列表是存放在一個 Lua 文件中的,每一個工程入口以 table 的形
式做爲參數去調用 entry 函數。咱們的目的是寫一個程序將這些數據以 html 格式展現出
來。因爲工程太多,咱們首先列出工程的標題,而後顯示每一個工程的明細。結果以下:
<HTML>
<HEAD><TITLE>Projects using Lua</TITLE></HEAD>
<BODY BGCOLOR="#FFFFFF">
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
Here are brief descriptions of some projects around the
world that use <A HREF="home.html">Lua</A>.
69
<UL>
<LI><A HREF="#1">TeCGraf</A>
<LI> ...
</UL>
<H3>
<A NAME="1"
HREF="http://www.tecgraf.puc-rio.br/">TeCGraf</A>
<SMALL><EM>Computer Graphics Technology Group,
PUC-Rio</EM></SMALL>
</H3>
TeCGraf is the result of a partnership between
...
distributed among dozens of final products.<P>
Contact: Waldemar Celes
<A NAME="2"></A><HR>
...
</BODY></HTML>
爲了讀取數據,咱們須要作的是正確的定義函數 entry,而後使用 dofile 直接運行數
據文件便可(db.lua)。注意,咱們須要遍歷入口列表兩次,第一次爲了獲取標題,第二
次爲了獲取每一個工程的表述。一種方法是:使用相同的 entry 函數運行數據文件一次將
全部的入口放在一個數組內;另外一種方法:使用不一樣的 entry 函數運行數據文件兩次。
由於 Lua 編譯文件是很快的,這裏咱們選用第二種方法。
首先,咱們定義一個輔助函數用來格式化文本的輸出(參見 5.2 函數部份內容)
function fwrite (fmt, ...)
return io.write(string.format(fmt, unpack(arg)))
end
第二,咱們定義一個 BEGIN 函數用來寫 html 頁面的頭部
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
function BEGIN()
io.write([[
<HTML>
<HEAD><TITLE>Projects using Lua</TITLE></HEAD>
<BODY BGCOLOR="#FFFFFF">
Here are brief descriptions of some projects around the
world that use <A HREF="home.html">Lua</A>.
]])
end
70
第三,定義 entry 函數
a. 第一個 entry 函數,將每一個工程一列表方式寫出,entry 的參數 o 是描述工程的 table。
function entry0 (o)
N=N + 1
local title = o.title or '(no title)'
fwrite('<LI><A HREF="#%d">%s</A>\n', N, title)
end
若是 o.title 爲 nil 代表 table 中的域 title 沒有提供,咱們用固定的"no title"替換。
b. 第二個 entry 函數,寫出工程全部的相關信息,稍微有些複雜,由於全部項都是
可選的。
function entry1 (o)
N=N + 1
local title = o.title or o.org or 'org'
fwrite('<HR>\n<H3>\n')
local href = ''
if o.url then
href = string.format(' HREF="%s"', o.url)
end
fwrite('<A NAME="%d"%s>%s</A>\n', N, href, title)
if o.title and o.org then
fwrite('\n<SMALL><EM>%s</EM></SMALL>', o.org)
end
fwrite('\n</H3>\n')
if o.description then
fwrite('%s', string.gsub(o.description,
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
'\n\n\n*', '<P>\n'))
fwrite('<P>\n')
end
if o.email then
fwrite('Contact: <A HREF="mailto:%s">%s</A>\n',
o.email, o.contact or o.email)
elseif o.contact then
fwrite('Contact: %s\n', o.contact)
end
end
71
因爲 html 中使用雙引號,爲了不衝突咱們這裏使用單引號表示串。
第四,定義 END 函數,寫 html 的尾部
function END()
fwrite('</BODY></HTML>\n')
end
在主程序中,咱們首先使用第一個 entry 運行數據文件輸出工程名稱的列表,而後
再以第二個 entry 運行數據文件輸出工程相關信息。
BEGIN()
N=0
entry = entry0
fwrite('<UL>\n')
dofile('db.lua')
fwrite('</UL>\n')
N=0
entry = entry1
dofile('db.lua')
END()
10.2 馬爾可夫鏈算法
咱們第二個例子是馬爾可夫鏈算法的實現,咱們的程序之前 n(n=2)個單詞串爲基礎
隨機產生一個文本串。
程序的第一部分讀出原文,而且對沒兩個單詞的前綴創建一個表,這個表給出了具
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
72
有那些前綴的單詞的一個順序。建表完成後,這個程序利用這張表生成一個隨機的文本。
在此文本中,每一個單詞都跟隨着它的的前兩個單詞,這兩個單詞在文本中有相同的機率。
這樣,咱們就產生了一個很是隨機,但並不徹底隨機的文本。例如,當應用這個程序的
輸出結果會出現「構造器也能夠經過表構造器,那麼一下幾行的插入語對於整個文件來
說,不是來存儲每一個功能的內容,而是來展現它的結構。」若是你想在隊列裏找到最大元
素並返回最大值,接着顯示提示和運行代碼。下面的單詞是保留單詞,不能用在度和弧
度之間轉換。
咱們編寫一個函數用來將兩個單詞中間加上空個鏈接起來:
function prefix (w1, w2)
return w1 .. ' ' .. w2
end
咱們用 NOWORD(即\n)表示文件的結尾而且初始化前綴單詞,例如,下面的文
本:
the more we try the more we do
初始化構造的表爲:
{
["\n \n"]
["\n the"]
["the more"]
["more we"]
["we try"]
["try the"]
["we do"]
}
= {"the"},
= {"more"},
= {"we", "we"},
= {"try", "do"},
= {"the"},
= {"more"},
= {"\n"},
咱們使用全局變量 statetab 來保存這個表,下面咱們完成一個插入函數用來在這個
statetab 中插入新的單詞。
function insert (index, value)
if not statetab[index] then
statetab[index] = {value}
else
table.insert(statetab[index], value)
end
end
這個函數中首先檢查指定的前綴是否存在,若是不存在則建立一個新的並賦上新值。
若是已經存在則調用 table.insert 將新值插入到列表尾部。
咱們使用兩個變量 w1 和 w2 來保存最後讀入的兩個單詞的值,對於每個前綴,我
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
們保存緊跟其後的單詞的列表。例如上面例子中初始化構造的表。
73
初始化表以後,下面來看看如何生成一個 MAXGEN(=1000)個單詞的文本。首先,
從新初始化 w1 和 w2,而後對於每個前綴,在其 next 單詞的列表中隨機選擇一個,打
印此單詞並更新 w1 和 w2,完整的代碼以下:
-- Markov Chain Program in Lua
function allwords ()
local line = io.read() -- current line
local pos = 1 -- current position in the line
return function () -- iterator function
while line do -- repeat while there are lines
local s, e = string.find(line, "%w+", pos)
if s then -- found a word?
pos = e + 1 -- update next position
return string.sub(line, s, e) -- return the word
else
line = io.read() -- word not found; try next line
pos = 1 -- restart from first position
end
end
return nil -- no more lines: end of traversal
end
end
function prefix (w1, w2)
return w1 .. ' ' .. w2
end
local statetab
function insert (index, value)
if not statetab[index] then
statetab[index] = {n=0}
end
table.insert(statetab[index], value)
end
local N = 2
local MAXGEN = 10000
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
local NOWORD = "\n"
74
-- build table
statetab = {}
local w1, w2 = NOWORD, NOWORD
for w in allwords() do
insert(prefix(w1, w2), w)
w1 = w2; w2 = w;
end
insert(prefix(w1, w2), NOWORD)
-- generate text
w1 = NOWORD; w2 = NOWORD -- reinitialize
for i=1,MAXGEN do
local list = statetab[prefix(w1, w2)]
-- choose a random item from list
local r = math.random(table.getn(list))
local nextword = list[r]
if nextword == NOWORD then return end
io.write(nextword, " ")
w1 = w2; w2 = nextword
end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
75
第二篇 tables 與 objects
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
76
第 11 章 數據結構
table 是 Lua 中惟一的數據結構,其餘語言所提供的其餘數據結構好比:arrays、
records、lists、queues、sets 等,Lua 都是經過 table 來實現,而且在 lua 中 table 很好的實
現了這些數據結構。
在傳統的 C 語言或者 Pascal 語言中咱們常用 arrays 和 lists(record+pointer)來
實現大部分的數據結構,在 Lua 中不只能夠用 table 完成一樣的功能,並且 table 的功能
更增強大。經過使用 table 不少算法的實現都簡化了,好比你在 lua 中不多須要本身去實
現一個搜索算法,由於 table 自己就提供了這樣的功能。
咱們須要花一些時間去學習如何有效的使用 table,下面咱們經過一些例子來看看如
果經過 table 來實現一些經常使用的數據結構。首先,咱們從 arrays 和 lists 開始,不只由於它
是其餘數據結構的基礎,並且是咱們所熟悉的。在第一部分語言的介紹中,咱們已經接
觸到了一些相關的內容,在這一章咱們將再來完整的學習他。
11.1 數組
在 lua 中經過整數下標訪問表中的元素便可簡單的實現數組。而且數組沒必要事先指
定大小,大小能夠隨須要動態的增加。
一般咱們初始化數組的時候就間接的定義了數組的大小,好比下面的代碼:
a = {}
-- new array
for i=1, 1000 do
a[i] = 0
end
經過初始化,數組 a 的大小已經肯定爲 1000,企圖訪問 1-1000 之外的下標對應的
值將返回 nil。你能夠根據須要定義數組的下標從 0,1 或者任意其餘的數值開始,好比:
-- creates an array with indices from -5 to 5
a = {}
for i=-5, 5 do
a[i] = 0
end
然而在 Lua 中習慣上數組的下表從 1 開始,Lua 的標準庫與此習慣保持一致,所以
若是你的數組下標也是從 1 開始你就能夠直接使用標準庫的函數,不然就沒法直接使用。
咱們能夠用構造器在建立數組的同時並初始化數組:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
squares = {1, 4, 9, 16, 25, 36, 49, 64, 81}
77
這樣的語句中數組的大小能夠任意的大,甚至幾百萬。
11.2 陣和多維數組
Lua 中主要有兩種表示矩陣的方法,第一種是用數組的數組表示。也就是說一個表
的元素是另外一個表。例如,能夠使用下面代碼建立一個 n 行 m 列的矩陣:
mt = {}
for i=1,N do
mt[i] = {}
for j=1,M do
mt[i][j] = 0
end
end
-- create a new row
-- create the matrix
因爲 Lua 中 table 是個對象,因此對於每一行咱們必須顯式的建立一個 table,這看
起來比起 c 或者 pascal 顯得冗餘,另外一方面它也提供了更多的靈活性,例如能夠修改前
面的例子來建立一個三角矩陣:
for j=1,M do
改爲
for j=1,i do
這樣實現的三角矩陣比起整個矩陣,僅僅使用一半的內存空間。
第二中表示矩陣的方法是將行和列組合起來,若是索引下標都是整數,經過第一個
索引乘於一個常量(列)再加上第二個索引,看下面的例子實現建立 n 行 m 列的矩陣:
mt = {}
for i=1,N do
for j=1,M do
mt[i*M + j] = 0
end
end
-- create the matrix
若是索引都是字符串的話,能夠用一個單字符將兩個字符串索引鏈接起來構成一個
單一的索引下標,例如一個矩陣 m,索引下標爲 s 和 t,假定 s 和 t 都不包含冒號,代碼
爲:m[s..':'..t],若是 s 或者 t 包含冒號將致使混淆,好比("a:", "b") 和("a", ":b"),當對這
種狀況有疑問的時候能夠使用控制字符來鏈接兩個索引字符串,好比'\0'。
實際應用中經常使用稀疏矩陣,稀疏矩陣指矩陣的大部分元素都爲空或者 0 的矩陣。
例如,咱們經過圖的鄰接矩陣來存儲圖,也就是說:當 m,n 兩個節點有鏈接時,矩陣的
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
78
m,n 值爲對應的 x,不然爲 nil。若是一個圖有 10000 個節點,平均每一個節點大約有 5 條
邊,爲了存儲這個圖須要一個行列分別爲 10000 的矩陣,總計 10000*10000 個元素,實
際上大約只有 50000 個元素非空(每行有五列非空,與每一個節點有五條邊對應)。不少數
據結構的書上討論採用何種方式才能節省空間,可是在 Lua 中你不須要這些技術,由於
用 table 實現的數據自己天生的就具備稀疏的特性。若是用咱們上面說的第一種多維數組
來表示,須要 10000 個 table,每一個 table 大約須要五個元素(table);若是用第二種表示
方法來表示,只須要一張大約 50000 個元素的表,無論用那種方式,你只須要存儲那些
非 nil 的元素。
11.3 鏈表
Lua 中用 tables 很容易實現鏈表,每個節點是一個 table,指針是這個表的一個域,
而且指向另外一個節點(table)。例如,要實現一個只有兩個域:值和指針的基本鏈表,代
碼以下:
根節點:
list = nil
在鏈表開頭插入一個值爲 v 的節點:
list = {next = list, value = v}
要遍歷這個鏈表只須要:
local l = list
while l do
print(l.value)
l = l.next
end
其餘類型的鏈表,像雙向鏈表和循環鏈表相似的也是很容易實現的。而後在 Lua 中
在不多狀況下才須要這些數據結構,由於一般狀況下有更簡單的方式來替換鏈表。好比,
咱們能夠用一個很是大的數組來表示棧,其中一個域 n 指向棧頂。
11.4 隊列和雙端隊列
雖然能夠使用 Lua 的 table 庫提供的 insert 和 remove 操做來實現隊列,但這種方式
實現的隊列針對大數據量時效率過低,有效的方式是使用兩個索引下標,一個表示第一
個元素,另外一個表示最後一個元素。
function ListNew ()
return {first = 0, last = -1}
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
79
爲了不污染全局命名空間,咱們重寫上面的代碼,將其放在一個名爲 list 的 table
中:
List = {}
function List.new ()
return {first = 0, last = -1}
end
下面,咱們能夠在常量時間內,完成在隊列的兩端進行插入和刪除操做了。
function List.pushleft (list, value)
local first = list.first - 1
list.first = first
list[first] = value
end
function List.pushright (list, value)
local last = list.last + 1
list.last = last
list[last] = value
end
function List.popleft (list)
local first = list.first
if first > list.last then error("list is empty") end
local value = list[first]
list[first] = nil
return value
end
function List.popright (list)
local last = list.last
if list.first > last then error("list is empty") end
local value = list[last]
list[last] = nil
list.last = last - 1
return value
end
-- to allow garbage collection
-- to allow garbage collection
list.first = first + 1
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
80
對嚴格意義上的隊列來說,咱們只能調用 pushright 和 popleft,這樣以來,first 和 last
的索引值都隨之增長,幸運的是咱們使用的是 Lua 的 table 實現的,你能夠訪問數組的元
素,經過使用下標從 1 到 20,也能夠 16,777,216 到 16,777,236。另外,Lua 使用雙精度
表示數字,假定你每秒鐘執行 100 萬次插入操做,在數值溢出之前你的程序能夠運行 200
年。
11.5 集合和包
假定你想列出在一段源代碼中出現的全部標示符,某種程度上,你須要過濾掉那些
語言自己的保留字。一些 C 程序員喜歡用一個字符串數組來表示,將全部的保留字放在
數組中,對每個標示符到這個數組中查找看是否爲保留字,有時候爲了提升查詢效率,
對數組存儲的時候使用二分查找或者 hash 算法。
Lua 中表示這個集合有一個簡單有效的方法,將全部集合中的元素做爲下標存放在
一個 table 裏,下面不須要查找 table,只須要測試看對於給定的元素,表的對應下標的
元素值是否爲 nil。好比:
reserved = {
["while"] = true,
}
for w in allwords() do
if reserved[w] then
-- `w' is a reserved word
...
["end"] = true,
["function"] = true, ["local"] = true,
還能夠使用輔助函數更加清晰的構造集合:
function Set (list)
local set = {}
for _, l in ipairs(list) do set[l] = true end
return set
end
reserved = Set{"while", "end", "function", "local", }
11.6 字符串緩衝
假定你要拼接不少個小的字符串爲一個大的字符串,好比,從一個文件中逐行讀入
字符串。你可能寫出下面這樣的代碼:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
-- WARNING: bad code ahead!!
local buff = ""
for line in io.lines() do
buff = buff .. line .. "\n"
end
81
儘管這段代碼看上去很正常,但在 Lua 中他的效率極低,在處理大文件的時候,你
會明顯看到很慢,例如,須要花大概 1 分鐘讀取 350KB 的文件。(這就是爲何 Lua 專
門提供了 io.read(*all)選項,她讀取一樣的文件只須要 0.02s)
爲何這樣呢?Lua 使用真正的垃圾收集算法,但他發現程序使用太多的內存他就
會遍歷他全部的數據結構去釋放垃圾數據,通常狀況下,這個算法有很好的性能(Lua
的快並不是偶然的),可是上面那段代碼 loop 使得算法的效率極其低下。
爲了理解現象的本質,假定咱們身在 loop 中間,buff 已是一個 50KB 的字符串,
每一行的大小爲 20bytes, Lua 執行 buff..line.."\n"時,當她建立了一個新的字符串大小爲
50,020 bytes,而且從 buff 中將 50KB 的字符串拷貝到新串中。也就是說,對於每一行,
都要移動 50KB 的內存,而且愈來愈多。讀取 100 行的時候(僅僅 2KB),Lua 已經移動
了 5MB 的內存,使狀況變遭的是下面的賦值語句:
buff = buff .. line .. "\n"
老的字符串變成了垃圾數據,兩輪循環以後,將有兩個老串包含超過 100KB 的垃圾
數據。這個時候 Lua 會作出正確的決定,進行他的垃圾收集並釋放 100KB 的內存。問題
在於每兩次循環 Lua 就要進行一次垃圾收集,讀取整個文件須要進行 200 次垃圾收集。
而且它的內存使用是整個文件大小的三倍。
這個問題並非 Lua 特有的:其它的採用垃圾收集算法的而且字符串不可變的語言
也都存在這個問題。Java 是最著名的例子,Java 專門提供 StringBuffer 來改善這種狀況。
在繼續進行以前,咱們應該作個註釋的是,在通常狀況下,這個問題並不存在。對
於小字符串,上面的那個循環沒有任何問題。爲了讀取整個文件咱們能夠使用
io.read(*all),能夠很快的將這個文件讀入內存。可是在某些時候,沒有解決問題的簡單
的辦法,因此下面咱們將介紹更加高效的算法來解決這個問題。
咱們最初的算法經過將循環每一行的字符串鏈接到老串上來解決問題,新的算法避
免如此:它鏈接兩個小串成爲一個稍微大的串,而後鏈接稍微大的串成更大的串。。算。
法的核心是:用一個棧,在棧的底部用來保存已經生成的大的字符串,而小的串從棧定
入棧。棧的狀態變化和經典的漢諾塔問題相似:位於棧下面的串確定比上面的長,只要
一個較長的串入棧後比它下面的串長,就將兩個串合併成一個新的更大的串,新生成的
串繼續與相鄰的串比較若是長於底部的將繼續進行合併,循環進行到沒有串能夠合併或
者到達棧底。
function newStack ()
return {""}
-- starts with an empty string
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
function addString (stack, s)
table.insert(stack, s)
-- push 's' into the the stack
for i=table.getn(stack)-1, 1, -1 do
if string.len(stack[i]) > string.len(stack[i+1]) then
break
end
stack[i] = stack[i] .. table.remove(stack)
end
end
82
要想獲取最終的字符串,咱們只須要從上向下一次合併全部的字符串便可。
table.concat 函數能夠將一個列表的全部串合併。
使用這個新的數據結構,咱們重寫咱們的代碼:
local s = newStack()
for line in io.lines() do
addString(s, line .. "\n")
end
s = toString(s)
最終的程序讀取 350 KB 的文件只須要 0.5s,固然調用 io.read("*all")仍然是最快的
只須要 0.02s。
實際上,咱們調用 io.read("*all")的時候,io.read 就是使用咱們上面的數據結構,只
不過是用 C 實現的, Lua 標準庫中,在有些其餘函數也是用 C 實現的,好比 table.concat,
使用 table.concat 咱們能夠很容易的將一個 table 的中的字符串鏈接起來,由於它使用 C
實現的,因此即便字符串很大它處理起來速度仍是很快的。
Concat 接受第二個可選的參數,表明插入的字符串之間的分隔符。經過使用這個參
數,咱們不須要在每一行以後插入一個新行:
local t = {}
for line in io.lines() do
table.insert(t, line)
end
s = table.concat(t, "\n") .. "\n"
io.lines 迭代子返回不帶換行符的一行,concat 在字符串之間插入分隔符,可是最後
一字符串以後不會插入分隔符,所以咱們須要在最後加上一個分隔符。最後一個鏈接操
做複製了整個字符串,這個時候整個字符串多是很大的。咱們能夠使用一點小技巧,
插入一個空串:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
table.insert(t, "")
s = table.concat(t, "\n")
83
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
84
第 12 章 數據文件與持久化
當咱們處理數據文件的,通常來講,寫文件比讀取文件內容來的容易。由於咱們可
以很好的控制文件的寫操做,而從文件讀取數據經常碰到不可預知的狀況。一個健壯的
程序不只應該能夠讀取存有正確格式的數據還應該可以處理壞文件(譯者注:對數據內
容和格式進行校驗,對異常狀況可以作出恰當處理)。正由於如此,實現一個健壯的讀取
數據文件的程序是很困難的。
正如咱們在 Section 10.1(譯者:第 10 章 Complete Examples)中看到的例子,文件
格式能夠經過使用 Lua 中的 table 構造器來描述。咱們只須要在寫數據的稍微作一些作一
點額外的工做,讀取數據將變得容易不少。方法是:將咱們的數據文件內容做爲 Lua 代
碼寫到 Lua 程序中去。經過使用 table 構造器,這些存放在 Lua 代碼中的數據能夠像其
他普通的文件同樣看起來引人注目。
爲了更清楚地描述問題,下面咱們看看例子.若是咱們的數據是預先肯定的格式,比
如 CSV(逗號分割值),咱們幾乎沒得選擇。(在第 20 章,咱們介紹如何在 Lua 中處理
CSV 文件)。可是若是咱們打算建立一個文件爲了未來使用,除了 CSV,咱們能夠使用
Lua 構造器來咱們表述咱們數據,這種狀況下,咱們將每個數據記錄描述爲一個 Lua
構造器。將下面的代碼
Donald E. Knuth,Literate Programming,CSLI,1992
Jon Bentley,More Programming Pearls,Addison-Wesley,1990
寫成
Entry{"Donald E. Knuth",
"Literate Programming",
"CSLI",
1992}
Entry{"Jon Bentley",
"More Programming Pearls",
"Addison-Wesley",
1990}
記住 Entry{...}與 Entry({...})等價,他是一個以表做爲惟一參數的函數調用。因此,
前面那段數據在 Lua 程序中表示如上。若是要讀取這個段數據,咱們只須要運行咱們的
Lua 代碼。例以下面這段代碼計算數據文件中記錄數:
local count = 0
function Entry (b) count = count + 1 end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
dofile("data")
print("number of entries: " .. count)
85
下面這段程序收集一個做者名列表中的名字是否在數據文件中出現,若是在文件中
出現則打印出來。(做者名字是 Entry 的第一個域;因此,若是 b 是一個 entry 的值,b[1]
則表明做者名)
local authors = {}
dofile("data")
for name in pairs(authors) do print(name) end
-- a set to collect authors
function Entry (b) authors[b[1]] = true end
注意,在這些程序段中使用事件驅動的方法:Entry 函數做爲回調函數,dofile 處理
數據文件中的每一記錄都回調用它。當數據文件的大小不是太大的狀況下,咱們能夠使
用 name-value 對來描述數據:
Entry{
author = "Donald E. Knuth",
title = "Literate Programming",
publisher = "CSLI",
year = 1992
}
Entry{
author = "Jon Bentley",
title = "More Programming Pearls",
publisher = "Addison-Wesley",
year = 1990
}
(若是這種格式讓你想起 BibTeX,這並不奇怪。Lua 中構造器正是根據來自 BibTeX
的靈感實現的)這種格式咱們稱之爲自描述數據格式,由於每個數據段都根據他的意
思簡短的描述爲一種數據格式。相對 CSV 和其餘緊縮格式,自描述數據格式更容易閱讀
和理解,當須要修改的時候能夠容易的手工編輯,並且不須要改動數據文件。例如,如
果咱們想增長一個域,只須要對讀取程序稍做修改便可,當指定的域不存在時,也能夠
賦予默認值。使用 name-value 對描述的狀況下,上面收集做者名的代碼能夠改寫爲:
local authors = {} -- a set to collect authors
function Entry (b) authors[b.author] = true end
dofile("data")
for name in pairs(authors) do print(name) end
如今,記錄域的順序可有可無了,甚至某些記錄即便不存在 author 這個域,咱們也
只須要稍微改動一下代碼便可:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
function Entry (b)
if b.author then authors[b.author] = true end
end
86
Lua 不只運行速度快,編譯速度也快。例如,上面這段蒐集做者名的代碼處理一個
2MB 的數據文件時間不會超過 1 秒。另外,這不是偶然的,數據描述是 Lua 的主要應用
之一,從 Lua 發明以來,咱們花了不少心血使他可以更快的編譯和運行大的 chunks。
12.1 序列化
咱們常常須要序列化一些數據,爲了將數據轉換爲字節流或者字符流,這樣咱們就
能夠保存到文件或者經過網絡發送出去。咱們能夠在 Lua 代碼中描述序列化的數據,在
這種方式下,咱們運行讀取程序便可從代碼中構造出保存的值。
一般,咱們使用這樣的方式 varname = <exp>來保存一個全局變量的值。varname 部
分比較容易理解,下面咱們來看看如何寫一個產生值的代碼。對於一個數值來講:
function serialize (o)
if type(o) == "number" then
io.write(o)
else ...
end
對於字符串值而言,原始的寫法應該是:
if type(o) == "string" then
io.write("'", o, "'")
然而,若是字符串包含特殊字符(好比引號或者換行符),產生的代碼將不是有效的
Lua 程序。這時候你可能用下面方法解決特殊字符的問題:
if type(o) == "string" then
io.write("[[", o, "]]")
千萬不要這樣作!雙引號是針對手寫的字符串的而不是針對自動產生的字符串。如
果有人惡意的引導你的程序去使用" ]]..os.execute('rm *')..[[ "這樣的方式去保存某些東西
(好比它可能提供字符串做爲地址)你最終的 chunk 將是這個樣子:
varname = [[ ]]..os.execute('rm *')..[[ ]]
若是你 load 這個數據,運行結果可想而知的。爲了以安全的方式引用任意的字符串,
string 標準庫提供了格式化函數專門提供"%q"選項。它能夠使用雙引號表示字符串而且
能夠正確的處理包含引號和換行等特殊字符的字符串。這樣一來,咱們的序列化函數可
以寫爲:
function serialize (o)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
if type(o) == "number" then
io.write(o)
elseif type(o) == "string" then
io.write(string.format("%q", o))
else ...
end
87
12.1.1 保存不帶循環的 table
咱們下一個艱鉅的任務是保存表。根據表的結構不一樣,採起的方法也有不少。沒有
一種單一的算法對全部狀況都能很好地解決問題。簡單的表不只須要簡單的算法並且結
果文件也須要看起來也更美觀。
咱們第一次嘗試以下:
function serialize (o)
if type(o) == "number" then
io.write(o)
elseif type(o) == "string" then
io.write(string.format("%q", o))
elseif type(o) == "table" then
io.write("{\n")
for k,v in pairs(o) do
io.write(" ", k, " = ")
serialize(v)
io.write(",\n")
end
io.write("}\n")
else
error("cannot serialize a " .. type(o))
end
end
儘管他很簡單,但他的確很好的解決了問題。只要表結構是一個樹型結構(也就是
說,沒有共享的子表而且沒有循環),他甚至能夠處理嵌套表(表中表)。對於所進不整
齊的表咱們能夠少做改進使結果更美觀,這能夠做爲一個練習嘗試一下。(提示:增長一
個參數表示縮進的字符串,來進行序列化) 前面的函數假定表中出現的全部關鍵字都是。
合法的標示符。若是表中有不符合 Lua 語法的數字關鍵字或者字符串關鍵字,上面的代
碼將碰到麻煩。一個簡單的解決這個難題的方法是將:
io.write(" ", k, " = ")
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
改成
io.write(" [")
serialize(k)
io.write(") = ")
88
這樣一來,咱們改善了咱們的函數的健壯性,比較一下兩次的結果:
-- result of serialize{a=12, b='Lua', key='another "one"'}
-- 第一個版本
{
a = 12,
b = "Lua",
key = "another \"one\"",
}
-- 第二個版本
{
["a"] = 12,
["b"] = "Lua",
["key"] = "another \"one\"",
}
咱們能夠經過測試每一種狀況,看是否須要方括號,另外,咱們將這個問題留做一
個練習給你們。
12.1.2 保存帶有循環的 table
針對普通拓撲概念上的帶有循環表和共享子表的 table,咱們須要另一種不一樣的方
法來處理。構造器不能很好地解決這種狀況,咱們不使用。爲了表示循環咱們須要將表
名記錄下來,下面咱們的函數有兩個參數:table 和對應的名字。另外,咱們還必須記錄
已經保存過的 table 以防止因爲循環而被重複保存。咱們使用一個額外的 table 來記錄保
存過的表的軌跡,這個表的下表索引爲 table,而值爲對應的表名。
咱們作一個限制:要保存的 table 只有一個字符串或者數字關鍵字。下面的這個函數
序列化基本類型並返回結果。
function basicSerialize (o)
if type(o) == "number" then
return tostring(o)
else
end
-- assume it is a string
return string.format("%q", o)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
89
關鍵內容在接下來的這個函數,saved 這個參數是上面提到的記錄已經保存的表的
蹤影的 table。
function save (name, value, saved)
saved = saved or {}
io.write(name, " = ")
if type(value) == "number" or type(value) == "string" then
io.write(basicSerialize(value), "\n")
elseif type(value) == "table" then
if saved[value] then
-- value already saved?
-- use its previous name
io.write(saved[value], "\n")
else
saved[value] = name
io.write("{}\n")
-- save name for next time
-- create a new table
-- save its fields
basicSerialize(k))
save(fieldname, v, saved)
end
end
else
error("cannot save a " .. type(value))
end
end
-- initial value
for k,v in pairs(value) do
local fieldname = string.format("%s[%s]", name,
舉個例子:
咱們將要保存的 table 爲:
a = {x=1, y=2; {3,4,5}}
a[2] = a
a.z = a[1]
-- cycle
-- shared sub-table
調用 save('a', a)以後結果爲:
a = {}
a[1] = {}
a[1][1] = 3
a[1][2] = 4
a[1][3] = 5
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
a[2] = a
a["y"] = 2
a["x"] = 1
a["z"] = a[1]
90
(實際的順序可能有所變化,它依賴於 table 遍歷的順序,不過,這個算法保證了一
個新的定義中須要的前面的節點都已經被定義過)
若是咱們想保存帶有共享部分的表,咱們能夠使用一樣 table 的 saved 參數調用 save
函數,例如咱們建立下面兩個表:
a = {{"one", "two"}, 3}
b = {k = a[1]}
保存它們:
save('a', a)
save('b', b)
結果將分別包含相同部分:
a = {}
a[1] = {}
a[1][1] = "one"
a[1][2] = "two"
a[2] = 3
b = {}
b["k"] = {}
b["k"][1] = "one"
b["k"][2] = "two"
然而若是咱們使用同一個 saved 表來調用 save 函數:
local t = {}
save('a', a, t)
save('b', b, t)
結果將共享相同部分:
a = {}
a[1] = {}
a[1][1] = "one"
a[1][2] = "two"
a[2] = 3
b = {}
b["k"] = a[1]
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
91
上面這種方法是 Lua 中經常使用的方法,固然也有其餘一些方法能夠解決問題。好比,
咱們能夠不使用全局變量名來保存(chunk 構造一個 local 值而後返回他);經過構造一
張表,每張表名與其對應的函數對應起來等。Lua 給予你權力,由你決定如何實現。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
92
第 13 章 Metatables and
Metamethods
Lua 中的 table 因爲定義的行爲,咱們能夠對 key-value 對執行加操做,訪問 key 對
應的 value,遍歷全部的 key-value。可是咱們不能夠對兩個 table 執行加操做,也不能夠
比較兩個表的大小。
Metatables 容許咱們改變 table 的行爲,例如,使用 Metatables 咱們能夠定義 Lua 如
何計算兩個 table 的相加操做 a+b。當 Lua 試圖對兩個表進行相加時,他會檢查兩個表是
否有一個表有 Metatable,而且檢查 Metatable 是否有__add 域。若是找到則調用這個__add
函數(所謂的 Metamethod)去計算結果。
Lua 中的每個表都有其 Metatable。(後面咱們將看到 userdata 也有 Metatable) Lua,
默認建立一個不帶 metatable 的新表
t = {}
print(getmetatable(t))
--> nil
能夠使用 setmetatable 函數設置或者改變一個表的 metatable
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)
任何一個表均可以是其餘一個表的 metatable,一組相關的表能夠共享一個 metatable
(描述他們共同的行爲)。一個表也能夠是自身的 metatable(描述其私有行爲)。
13.1 算術運算的 Metamethods
這一部分咱們經過一個簡單的例子介紹如何使用 metamethods。假定咱們使用 table
來描述結合,使用函數來描述集合的並操做,交集操做,like 操做。咱們在一個表內定
義這些函數,而後使用構造函數建立一個集合:
Set = {}
function Set.new (t)
local set = {}
for _, l in ipairs(t) do set[l] = true end
return set
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
function Set.union (a,b)
local res = Set.new{}
for k in pairs(a) do res[k] = true end
for k in pairs(b) do res[k] = true end
return res
end
function Set.intersection (a,b)
local res = Set.new{}
for k in pairs(a) do
res[k] = b[k]
end
return res
end
93
爲了幫助理解程序運行結果,咱們也定義了打印函數輸出結果:
function Set.tostring (set)
local s = "{"
local sep = ""
for e in pairs(set) do
s = s .. sep .. e
sep = ", "
end
return s .. "}"
end
function Set.print (s)
print(Set.tostring(s))
end
如今咱們想加號運算符(+)執行兩個集合的並操做,咱們將全部集合共享一個
metatable,而且爲這個 metatable 添加如何處理相加操做。
第一步,咱們定義一個普通的表,用來做爲 metatable。爲避免污染命名空間,咱們
將其放在 set 內部。
Set.mt = {}
-- metatable for sets
第二步,修改 set.new 函數,增長一行,建立表的時候同時指定對應的 metatable。
function Set.new (t)
-- 2nd version
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
local set = {}
setmetatable(set, Set.mt)
for _, l in ipairs(t) do set[l] = true end
return set
end
94
這樣一來,set.new 建立的全部的集合都有相同的 metatable 了:
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
print(getmetatable(s1))
print(getmetatable(s2))
--> table: 00672B60
--> table: 00672B60
第三步,給 metatable 增長__add 函數。
Set.mt.__add = Set.union
當 Lua 試圖對兩個集合相加時,將調用這個函數,以兩個相加的表做爲參數。
經過 metamethod,咱們能夠對兩個集合進行相加:
s3 = s1 + s2
Set.print(s3)
--> {1, 10, 20, 30, 50}
一樣的咱們能夠使用相乘運算符來定義集合的交集操做
Set.mt.__mul = Set.intersection
Set.print((s1 + s2)*s1)
--> {10, 20, 30, 50}
對於每個算術運算符,metatable 都有對應的域名與其對應,除了__add,__mul 外,
還有__sub(減),__div(除),__unm(負),__pow(冪),咱們也能夠定義__concat 定義鏈接行爲。
當咱們對兩個表進行加沒有問題,但若是兩個操做數有不一樣的 metatable 例如:
s = Set.new{1,2,3}
s=s+8
Lua 選擇 metamethod 的原則:若是第一個參數存在帶有__add 域的 metatable,Lua
使用它做爲 metamethod,和第二個參數無關;
不然第二個參數存在帶有__add 域的 metatable, 使用它做爲 metamethod 不然報Lua
錯。
Lua 不關心這種混合類型的,若是咱們運行上面的 s=s+8 的例子在 Set.union 發生錯
誤:
bad argument #1 to `pairs' (table expected, got number)
若是咱們想獲得更加清楚地錯誤信息,咱們須要本身顯式的檢查操做數的類型:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
function Set.union (a,b)
if getmetatable(a) ~= Set.mt or
getmetatable(b) ~= Set.mt then
error("attempt to `add' a set with a non-set value", 2)
end
... -- same as before
95
13.2 關係運算的 Metamethods
Metatables 也容許咱們使用 metamethods:__eq(等於),__lt(小於),和__le(小於
等於)給關係運算符賦予特殊的含義。對剩下的三個關係運算符沒有專門的 metamethod,
由於 Lua 將 a ~= b 轉換爲 not (a == b);a > b 轉換爲 b < a;a >= b 轉換爲 b <= a。
(直到 Lua 4.0 爲止,全部的比較運算符被轉換成一個,a <= b 轉爲 not (b < a)。然
而這種轉換並不一致正確。當咱們遇到偏序(partial order)狀況,也就是說,並非所
有的元素均可以正確的被排序狀況。例如,在大多數機器上浮點數不能被排序,由於他
的值不是一個數字(Not a Number 即 NaN)。根據 IEEE 754 的標準,NaN 表示一個未定
義的值,好比 0/0 的結果。該標準指出任何涉及到 NaN 比較的結果都應爲 false。也就是
說,NaN <= x 老是 false,x < NaN 也老是 false。這樣一來,在這種狀況下 a <= b 轉換
爲 not (b < a)就再也不正確了。)
在咱們關於基和操做的例子中,有相似的問題存在。<=表明集合的包含:a <= b 表
示集合 a 是集合 b 的子集。這種意義下,可能 a <= b 和 b < a 都是 false;所以,咱們需
要將__le 和__lt 的實現分開:
Set.mt.__le = function (a,b)
for k in pairs(a) do
if not b[k] then return false end
end
return true
end
Set.mt.__lt = function (a,b)
return a <= b and not (b <= a)
end
-- set containment
最後,咱們經過集合的包含來定義集合相等:
Set.mt.__eq = function (a,b)
return a <= b and b <= a
end
有了上面的定義以後,如今咱們就能夠來比較集合了:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2)
print(s1 < s2)
print(s1 >= s1)
print(s1 > s1)
print(s1 == s2 * s1)
--> true
--> true
--> true
--> false
--> true
96
與算術運算的 metamethods 不一樣,關係元算的 metamethods 不支持混合類型運算。
對於混合類型比較運算的處理方法和 Lua 的公共行爲相似。若是你試圖比較一個字符串
和一個數字,Lua 將拋出錯誤.類似的,若是你試圖比較兩個帶有不一樣 metamethods 的對
象,Lua 也將拋出錯誤。
但相等比較歷來不會拋出錯誤,若是兩個對象有不一樣的 metamethod,比較的結果爲
false,甚至可能不會調用 metamethod。這也是模仿了 Lua 的公共的行爲,由於 Lua 老是
認爲字符串和數字是不等的,而不去判斷它們的值。僅當兩個有共同的 metamethod 的對
象進行相等比較的時候,Lua 纔會調用對應的 metamethod。
13.3 庫定義的 Metamethods
在一些庫中,在本身的 metatables 中定義本身的域是很廣泛的狀況。到目前爲止,
咱們看到的全部 metamethods 都是 Lua 核心部分的。有虛擬機負責處理運算符涉及到的
metatables 和爲運算符定義操做的 metamethods。可是,metatable 是一個普通的表,任何
人均可以使用。
tostring 是一個典型的例子。如前面咱們所見,tostring 以簡單的格式表示出 table:
print({})
--> table: 0x8062ac0
(注意:print 函數老是調用 tostring 來格式化它的輸出)。然而當格式化一個對象的
時候,tostring 會首先檢查對象是否存在一個帶有__tostring 域的 metatable。若是存在則
以對象做爲參數調用對應的函數來完成格式化,返回的結果即爲 tostring 的結果。
在咱們集合的例子中咱們已經定義了一個函數來將集合轉換成字符串打印出來。因
此,咱們只須要將集合的 metatable 的__tostring 域調用咱們定義的打印函數:
Set.mt.__tostring = Set.tostring
這樣,無論何時咱們調用 print 打印一個集合,print 都會自動調用 tostring,而
tostring 則會調用 Set.tostring:
s1 = Set.new{10, 4, 5}
print(s1)
--> {4, 5, 10}
setmetatable/getmetatable 函 數 也 會 使 用 metafield , 在 這 種 情 況 下 , 可 以 保 護
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
97
metatables。假定你想保護你的集合使其使用者既看不到也不能修改 metatables。若是你
對 metatable 設置了__metatable 的值,getmetatable 將返回這個域的值,而調用 setmetatable
將會出錯:
Set.mt.__metatable = "not your business"
s1 = Set.new{}
print(getmetatable(s1))
setmetatable(s1, {})
stdin:1: cannot change protected metatable
--> not your business
13.4 表相關的 Metamethods
關於算術運算和關係元算的 metamethods 都定義了錯誤狀態的行爲,他們並不改變
語言自己的行爲。針對在兩種正常狀態:表的不存在的域的查詢和修改,Lua 也提供了
改變 tables 的行爲的方法。
13.4.1 The __index Metamethod
前面說過,當咱們訪問一個表的不存在的域,返回結果爲 nil,這是正確的,但並不
一致正確。實際上,這種訪問觸發 lua 解釋器去查找__index metamethod:若是不存在,
返回結果爲 nil;若是存在則由__index metamethod 返回結果。
這個例子的原型是一種繼承。假設咱們想建立一些表來描述窗口。每個表必須描
述窗口的一些參數,好比:位置,大小,顏色風格等等。全部的這些參數都有默認的值,
當咱們想要建立窗口的時候只須要給出非默認值的參數便可建立咱們須要的窗口。第一
種方法是,實現一個表的構造器,對這個表內的每個缺乏域都填上默認值。第二種方
法是,建立一個新的窗口去繼承一個原型窗口的缺乏域。首先,咱們實現一個原型和一
個構造函數,他們共享一個 metatable:
-- create a namespace
Window = {}
-- create the prototype with default values
Window.prototype = {x=0, y=0, width=100, height=100, }
-- create a metatable
Window.mt = {}
-- declare the constructor function
function Window.new (o)
setmetatable(o, Window.mt)
return o
end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
如今咱們定義__index metamethod:
Window.mt.__index = function (table, key)
return Window.prototype[key]
end
98
這樣一來,咱們建立一個新的窗口,而後訪問他缺乏的域結果以下:
w = Window.new{x=10, y=20}
print(w.width)
--> 100
當 Lua 發現 w 不存在域 width 時,可是有一個 metatable 帶有__index 域,Lua 使用
w(the table)和 width(缺乏的值)來調用__index metamethod,metamethod 則經過訪問
原型表(prototype)獲取缺乏的域的結果。
__index metamethod 在繼承中的使用很是常見,因此 Lua 提供了一個更簡潔的使用
方式。__index metamethod 不須要非是一個函數,他也能夠是一個表。但它是一個函數
的時候,Lua 將 table 和缺乏的域做爲參數調用這個函數;當他是一個表的時候,Lua 將
在這個表中看是否有缺乏的域。因此,上面的那個例子能夠使用第二種方式簡單的改寫
爲:
Window.mt.__index = Window.prototype
如今,當 Lua 查找 metatable 的__index 域時,他發現 window.prototype 的值,它是
一個表,因此 Lua 將訪問這個表來獲取缺乏的值,也就是說它至關於執行:
Window.prototype["width"]
將一個表做爲__index metamethod 使用,提供了一種廉價而簡單的實現單繼承的方
法。一個函數的代價雖然稍微高點,但提供了更多的靈活性:咱們能夠實現多繼承,隱
藏,和其餘一些變異的機制。咱們將在第 16 章詳細的討論繼承的方式。
當咱們想不經過調用__index metamethod 來訪問一個表,咱們能夠使用 rawget 函數。
Rawget(t,i)的調用以 raw access 方式訪問表。這種訪問方式不會使你的代碼變快(the
overhead of a function call kills any gain you could have),但有些時候咱們須要他,在後面
咱們將會看到。
13.4.2 The __newindex Metamethod
__newindex metamethod 用來對錶更新,__index 則用來對錶訪問。當你給表的一個
缺乏的域賦值,解釋器就會查找__newindex metamethod:若是存在則調用這個函數而不
進行賦值操做。像__index 同樣,若是 metamethod 是一個表,解釋器對指定的那個表,
而不是原始的表進行賦值操做。另外,有一個 raw 函數能夠繞過 metamethod:調用
rawset(t,k,v)不掉用任何 metamethod 對錶 t 的 k 域賦值爲 v。__index 和__newindex
metamethods 的混合使用提供了強大的結構:從只讀表到面向對象編程的帶有繼承默認
值的表。在這一張的剩餘部分咱們看一些這些應用的例子,面向對象的編程在另外的章
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
節介紹。
99
13.4.3 有默認值的表
在一個普通的表中任何域的默認值都是 nil。很容易經過 metatables 來改變默認值:
function setDefault (t, d)
local mt = {__index = function () return d end}
setmetatable(t, mt)
end
tab = {x=10, y=20}
print(tab.x, tab.z)
setDefault(tab, 0)
print(tab.x, tab.z)
--> 10
0
--> 10
nil
如今,無論何時咱們訪問表的缺乏的域,他的__index metamethod 被調用並返
回 0。setDefault 函數爲每個須要默認值的表建立了一個新的 metatable。在有不少的表
須要默認值的狀況下,這可能使得花費的代價變大。然而 metatable 有一個默認值 d 和它
自己關聯,因此函數不能爲全部表使用單一的一個 metatable。爲了不帶有不一樣默認值
的全部的表使用單一的 metatable,咱們將每一個表的默認值,使用一個惟一的域存儲在表
自己裏面。若是咱們不擔憂命名的混亂,我可以使用像"___"做爲咱們的惟一的域:
local mt = {__index = function (t) return t.___ end}
function setDefault (t, d)
t.___ = d
setmetatable(t, mt)
end
若是咱們擔憂命名混亂,也很容易保證這個特殊的鍵值惟一性。咱們要作的只是創
建一個新表用做鍵值:
local key = {}
-- unique key
local mt = {__index = function (t) return t[key] end}
function setDefault (t, d)
t[key] = d
setmetatable(t, mt)
end
另一種解決表和默認值關聯的方法是使用一個分開的表來處理,在這個特殊的表
中索引是表,對應的值爲默認值。然而這種方法的正確實現咱們須要一種特殊的表:weak
table,到目前爲止咱們尚未介紹這部份內容,將在第 17 章討論。
爲了帶有不一樣默認值的表能夠重用相同的原表,還有一種解決方法是使用 memoize
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
metatables,然而這種方法也須要 weak tables,因此咱們再次不得不等到第 17 章。
100
13.4.4 監控表
__index 和__newindex 都是隻有當表中訪問的域不存在時候才起做用。捕獲對一個
表的全部訪問狀況的惟一方法就是保持表爲空。所以,若是咱們想監控一個表的全部訪
問狀況,咱們應該爲真實的表建立一個代理。這個代理是一個空表,而且帶有__index
和__newindex metamethods,由這兩個方法負責跟蹤表的全部訪問狀況並將其指向原始的
表。假定,t 是咱們想要跟蹤的原始表,咱們能夠:
t = {}
-- original table (created somewhere)
-- keep a private access to original table
local _t = t
-- create proxy
t = {}
-- create metatable
local mt = {
__index = function (t,k)
print("*access to element " .. tostring(k))
return _t[k]
end,
__newindex = function (t,k,v)
print("*update of element " .. tostring(k) ..
" to " .. tostring(v))
_t[k] = v
end
}
setmetatable(t, mt)
-- update original table
-- access the original table
這段代碼將跟蹤全部對 t 的訪問狀況:
> t[2] = 'hello'
*update of element 2 to hello
> print(t[2])
*access to element 2
hello
(注意:不幸的是,這個設計不容許咱們遍歷表。Pairs 函數將對 proxy 進行操做,
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
101
而不是原始的表。)若是咱們想監控多張表,咱們不須要爲每一張表都創建一個不一樣的
metatable。咱們只要將每個 proxy 和他原始的表關聯,全部的 proxy 共享一個公用的
metatable 便可。將表和對應的 proxy 關聯的一個簡單的方法是將原始的表做爲 proxy 的
域,只要咱們保證這個域不用做其餘用途。一個簡單的保證它不被做他用的方法是建立
一個私有的沒有他人能夠訪問的 key。將上面的思想彙總,最終的結果以下:
-- create private index
local index = {}
-- create metatable
local mt = {
__index = function (t,k)
print("*access to element " .. tostring(k))
return t[index][k]
end
__newindex = function (t,k,v)
print("*update of element " .. tostring(k) .. " to "
.. tostring(v))
t[index][k] = v
end
}
function track (t)
local proxy = {}
proxy[index] = t
setmetatable(proxy, mt)
return proxy
end
-- update original table
-- access the original table
如今,無論何時咱們想監控表 t,咱們要作得只是 t=track(t)。
13.4.5 只讀表
採用代理的思想很容易實現一個只讀表。咱們須要作得只是當咱們監控到企圖修改
表時候拋出錯誤。經過__index metamethod,咱們能夠不使用函數而是用原始表自己來使
用表,由於咱們不須要監控查尋。這是比較簡單而且高效的重定向全部查詢到原始表的
方法。可是,這種用法要求每個只讀代理有一個單獨的新的 metatable,使用__index
指向原始表:
function readOnly (t)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
local proxy = {}
local mt = {
__index = t,
__newindex = function (t,k,v)
error("attempt to update a read-only table", 2)
end
}
setmetatable(proxy, mt)
return proxy
end
-- create metatable
102
(記住:error 的第二個參數 2,將錯誤信息返回給企圖執行 update 的地方)做爲一
個簡單的例子,咱們對工做日創建一個只讀表:
days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
print(days[1])
days[2] = "Noday"
stdin:1: attempt to update a read-only table
--> Sunday
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
103
第 14 章 環境
Lua 用一個名爲 environment 普通的表來保存全部的全局變量。(更精確的說,Lua
在一系列的 environment 中保存他的「global」變量,可是咱們有時候能夠忽略這種多樣
性)這種結果的優勢之一是他簡化了 Lua 的內部實現,由於對於全部的全局變量沒有必
要非要有不一樣的數據結構。另外一個(主要的)優勢是咱們能夠像其餘表同樣操做這個保存
全局變量的表。爲了簡化操做,Lua 將環境自己存儲在一個全局變量_G 中,(_G._G 等
於_G)。例如,下面代碼打印在當前環境中全部的全局變量的名字:
for n in pairs(_G) do print(n) end
這一章咱們將討論一些如何操縱環境的有用的技術。
14.1 使用動態名字訪問全局變量
一般,賦值操做對於訪問和修改全局變量已經足夠。然而,咱們常常須要一些原編
程(meta-programming)的方式,好比當咱們須要操縱一個名字被存儲在另外一個變量中
的全局變量,或者須要在運行時才能知道的全局變量。爲了獲取這種全局變量的值,有
的程序員可能寫出下面相似的代碼:
loadstring("value = " .. varname)()
or
value = loadstring("return " .. varname)()
若是 varname 是 x,上面鏈接操做的結果爲:"return x"(第一種形式爲 "value = x"),
當運行時纔會產生最終的結果。然而這段代碼涉及到一個新的 chunk 的建立和編譯以及
其餘不少額外的問題。你能夠換種方式更高效更簡潔的完成一樣的功能,代碼以下:
value = _G[varname]
由於環境是一個普通的表,因此你能夠使用你須要獲取的變量(變量名)索引表即
可。
也能夠用類似的方式對一個全局變量賦值:_G[varname] = value。當心:一些程序
員對這些函數很興奮,而且可能寫出這樣的代碼:_G["a"] = _G["var1"],這只是 a = var1
的複雜的寫法而已。
對前面的問題歸納一下,表域能夠是型如"io.read" or "a.b.c.d"的動態名字。咱們用循
環解決這個問題,從_G 開始,一個域一個域的遍歷:
function getfield (f)
local v = _G
-- start with the table of globals
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
for w in string.gfind(f, "[%w_]+") do
v = v[w]
end
return v
end
104
咱們使用 string 庫的 gfind 函數來迭代 f 中的全部單詞(單詞指一個或多個子母下劃
線的序列)。相對應的,設置一個域的函數稍微複雜些。賦值如:
a.b.c.d.e = v
實際等價於:
local temp = a.b.c.d
temp.e = v
也就是說,咱們必須記住最後一個名字,必須獨立的處理最後一個域。新的 setfield
函數當其中的域(譯者注:中間的域確定是表)不存在的時候還須要建立中間表。
function setfield (f, v)
local t = _G
-- start with the table of globals
for w, d in string.gfind(f, "([%w_]+)(.?)") do
if d == "." then -- not last field?
t[w] = t[w] or {}
t = t[w]
else
t[w] = v
end
end
end
-- create table if absent
-- get the table
-- last field
-- do the assignment
這個新的模式匹配以變量 w 加上一個可選的點(保存在變量 d 中)的域。若是一個
域名後面不容許跟上點,代表它是最後一個名字。咱們將在第 20 章討論模式匹配問題)(。
使用上面的函數
setfield("t.x.y", 10)
建立一個全局變量表 t,另外一個表 t.x,而且對 t.x.y 賦值爲 10:
print(t.x.y)
print(getfield("t.x.y"))
--> 10
--> 10
14.2 聲明全局變量
全局變量不須要聲明,雖然這對一些小程序來講很方便,但程序很大時,一個簡單
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
105
的拼寫錯誤可能引發 bug 而且很難發現。然而,若是咱們喜歡,咱們能夠改變這種行爲。
由於 Lua 全部的全局變量都保存在一個普通的表中,咱們能夠使用 metatables 來改變訪
問全局變量的行爲。
第一個方法以下:
setmetatable(_G, {
__newindex = function (_, n)
error("attempt to write to undeclared variable "..n, 2)
end,
__index = function (_, n)
error("attempt to read undeclared variable "..n, 2)
end,
})
這樣一來,任何企圖訪問一個不存在的全局變量的操做都會引發錯誤:
>a=1
stdin:1: attempt to write to undeclared variable a
可是咱們如何聲明一個新的變量呢?使用 rawset,能夠繞過 metamethod:
function declare (name, initval)
rawset(_G, name, initval or false)
end
or 帶有 false 是爲了保證新的全局變量不會爲 nil。注意:你應該在安裝訪問控制
之前(before installing the access control)定義這個函數,不然將獲得錯誤信息:畢竟你
是在企圖建立一個新的全局聲明。只要剛纔那個函數在正確的地方,你就能夠控制你的
全局變量了:
>a=1
stdin:1: attempt to write to undeclared variable a
> declare "a"
>a=1
-- OK
可是如今,爲了測試一個變量是否存在,咱們不能簡單的比較他是否爲 nil。若是他
是 nil 訪問將拋出錯誤。因此,咱們使用 rawget 繞過 metamethod:
if rawget(_G, var) == nil then
-- 'var' is undeclared
...
end
改變控制容許全局變量能夠爲 nil 也不難,全部咱們須要的是建立一個輔助表用來
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
106
保存全部已經聲明的變量的名字。無論何時 metamethod 被調用的時候,他會檢查這
張輔助表看變量是否已經存在。代碼以下:
local declaredNames = {}
function declare (name, initval)
rawset(_G, name, initval)
declaredNames[name] = true
end
setmetatable(_G, {
__newindex = function (t, n, v)
if not declaredNames[n] then
error("attempt to write to undeclared var. "..n, 2)
else
rawset(t, n, v)
end
end,
__index = function (_, n)
if not declaredNames[n] then
error("attempt to read undeclared var. "..n, 2)
else
return nil
end
end,
})
-- do the actual set
兩種實現方式,代價都很小能夠忽略不計的。第一種解決方法:metamethods 在平
常操做中不會被調用。第二種解決方法:他們可能被調用,不過當且僅當訪問一個值爲
nil 的變量時。
14.3 非全局的環境
全局環境的一個問題是,任何修改都會影響你的程序的全部部分。例如,當你安裝
一個 metatable 去控制全局訪問時,你的整個程序都必須遵循同一個指導方針。若是你想
使用標準庫,標準庫中可能使用到沒有聲明的全局變量,你將碰到壞運。
Lua 5.0 容許每一個函數能夠有本身的環境來改善這個問題,聽起來這很奇怪;畢竟,
全局變量表的目的就是爲了全局性使用。然而在 Section 15.4 咱們將看到這個機制帶來很
多有趣的結構,全局的值依然是隨處能夠獲取的。
能夠使用 setfenv 函數來改變一個函數的環境。Setfenv 接受函數和新的環境做爲參
數。除了使用函數自己,還能夠指定一個數字表示棧頂的活動函數。數字 1 表明當前函
數,數字 2 表明調用當前函數的函數(這對寫一個輔助函數來改變他們調用者的環境是
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
很方便的)依此類推。下面這段代碼是企圖應用 setfenv 失敗的例子:
a=1
-- create a global variable
107
-- change current environment to a new empty table
setfenv(1, {})
print(a)
致使:
stdin:5: attempt to call global `print' (a nil value)
(你必須在單獨的 chunk 內運行這段代碼,若是你在交互模式逐行運行他,每一行
都是一個不一樣的函數,調用 setfenv 只會影響他本身的那一行。)一旦你改變了你的環境,
全部全局訪問都使用這個新的表,若是她爲空,你就丟失全部你的全局變量,甚至_G,
因此,你應該首先使用一些有用的值封裝(populate)她,好比老的環境:
a=1
-- create a global variable
-- change current environment
setfenv(1, {_G = _G})
_G.print(a)
_G.print(_G.a)
--> nil
--> 1
如今,當你訪問"global" _G,他的值爲舊的環境,其中你能夠使用 print 函數。
你也能夠使用繼承封裝(populate)你的新的環境:
a=1
local newgt = {}
setfenv(1, newgt)
print(a)
-- create new environment
-- set it
--> 1
setmetatable(newgt, {__index = _G})
在這段代碼新的環境從舊的環境中繼承了 print 和 a;然而,任何賦值操做都對新表
進行,不用擔憂誤操做修改了全局變量表。另外,你仍然能夠經過_G 修改全局變量:
-- continuing previous code
a = 10
print(a)
print(_G.a)
_G.a = 20
print(_G.a)
--> 20
--> 10
--> 1
當你建立一個新的函數時,他從建立他的函數繼承了環境變量。因此,若是一個
chunk 改變了他本身的環境,這個 chunk 全部在改變以後定義的函數都共享相同的環境,
都會受到影響。這對建立命名空間是很是有用的機制,咱們下一章將會看到。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
108
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
109
第 15 章 Packages
不少語言專門提供了某種機制組織全局變量的命名,好比 Modula 的 modules,Java
和 Perl 的 packages,C++的 namespaces。每一種機制對在 package 中聲明的元素的可見
性以及其餘一些細節的使用都有不一樣的規則。可是他們都提供了一種避免不一樣庫中命名
衝突的問題的機制。每個程序庫建立本身的命名空間,在這個命名空間中定義的名字
和其餘命名空間中定義的名字互不干涉。
Lua 並無提供明確的機制來實現 packages。然而,咱們經過語言提供的基本的機
制很容易實現他。主要的思想是:像標準庫同樣,使用表來描述 package。
使用表實現 packages 的明顯的好處是:咱們能夠像其餘表同樣使用 packages,而且
能夠使用語言提供的全部的功能,帶來不少便利。大多數語言中,packages 不是第一類
值(first-class values)(也就是說,他們不能存儲在變量裏,不能做爲函數參數。。。)所以,
這些語言須要特殊的方法和技巧才能實現相似的功能。
Lua 中,雖然咱們一直都用表來實現 pachages,但也有其餘不一樣的方法能夠實現
package,在這一章,咱們將介紹這些方法。
15.1 基本方法
第一包的簡單的方法是對包內的每個對象前都加包名做爲前綴。例如,假定咱們
正在寫一個操做複數的庫:咱們使用表來表示複數,表有兩個域 r(實數部分)和 i(虛
數部分)。咱們在另外一張表中聲明咱們全部的操做來實現一個包:
complex = {}
function complex.new (r, i) return {r=r, i=i} end
-- defines a constant `i'
complex.i = complex.new(0, 1)
function complex.add (c1, c2)
return complex.new(c1.r + c2.r, c1.i + c2.i)
end
function complex.sub (c1, c2)
return complex.new(c1.r - c2.r, c1.i - c2.i)
end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
function complex.mul (c1, c2)
return complex.new(c1.r*c2.r - c1.i*c2.i,
c1.r*c2.i + c1.i*c2.r)
end
function complex.inv (c)
local n = c.r^2 + c.i^2
return complex.new(c.r/n, -c.i/n)
end
return complex
110
這個庫定義了一個全局名:coplex。其餘的定義都是放在這個表內。
有了上面的定義,咱們就能夠使用符合規範的任何複數操做了,如:
c = complex.add(complex.i, complex.new(10, 20))
這種使用表來實現的包和真正的包的功能並不徹底相同。首先,咱們對每個函數
定義都必須顯示的在前面加上包的名稱。第二,同一包內的函數相互調用必須在被調用
函數前指定包名。咱們能夠使用固定的局部變量名,來改善這個問題,而後,將這個局
部變量賦值給最終的包。依據這個原則,咱們重寫上面的代碼:
local P = {}
complex = P
P.i = {r=0, i=1}
function P.new (r, i) return {r=r, i=i} end
function P.add (c1, c2)
return P.new(c1.r + c2.r, c1.i + c2.i)
end
...
-- package name
當在同一個包內的一個函數調用另外一個函數的時候(或者她調用自身) 他仍然須要,
加上前綴名。至少,它再也不依賴於固定的包名。另外,只有一個地方須要包名。可能你
注意到包中最後一個語句:
return complex
這個 return 語句並不是必需的,由於 package 已經賦值給全局變量 complex 了。可是,
咱們認爲 package 打開的時候返回自己是一個很好的習慣。額外的返回語句並不會花費
什麼代價,而且提供了另外一種操做 package 的可選方式。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
111
15.2 私有成員(Privacy)
有時候,一個 package 公開他的全部內容,也就是說,任何 package 的客戶端均可
以訪問他。然而,一個 package 擁有本身的私有部分(也就是隻有 package 自己才能訪
問)也是頗有用的。在 Lua 中一個傳統的方法是將私有部分定義爲局部變量來實現。例
如,咱們修改上面的例子增長私有函數來檢查一個值是否爲有效的複數:
local P = {}
complex = P
local function checkComplex (c)
if not ((type(c) == "table") and
tonumber(c.r) and tonumber(c.i)) then
error("bad complex number", 3)
end
end
function P.add (c1, c2)
checkComplex(c1);
checkComplex(c2);
return P.new(c1.r + c2.r, c1.i + c2.i)
end
...
return P
這種方式各有什麼優勢和缺點呢?package 中全部的名字都在一個獨立的命名空間
中。Package 中的每個實體(entity)都清楚地標記爲公有仍是私有。另外,咱們實現
一個真正的隱私(privacy):私有實體在 package 外部是不可訪問的。缺點是訪問同一個
package 內的其餘公有的實體寫法冗餘,必須加上前綴 P.。還有一個大的問題是,當咱們
修改函數的狀態(公有變成私有或者私有變成公有)咱們必須修改函數得調用方式。
有一個有趣的方法能夠馬上解決這兩個問題。咱們能夠將 package 內的全部函數都
聲明爲局部的,最後將他們放在最終的表中。按照這種方法,上面的 complex package
修改以下:
local function checkComplex (c)
if not ((type(c) == "table")
and tonumber(c.r) and tonumber(c.i)) then
error("bad complex number", 3)
end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
local function new (r, i) return {r=r, i=i} end
local function add (c1, c2)
checkComplex(c1);
checkComplex(c2);
return new(c1.r + c2.r, c1.i + c2.i)
end
...
complex = {
new = new,
add = add,
sub = sub,
mul = mul,
div = div,
}
112
如今咱們再也不須要調用函數的時候在前面加上前綴,公有的和私有的函數調用方法
相同。在 package 的結尾處,有一個簡單的列表列出全部公有的函數。可能大多數人覺
得這個列表放在 package 的開始處更天然,但咱們不能這樣作,由於咱們必須首先定義
局部函數。
15.3 包與文件
咱們常常寫一個 package 而後將全部的代碼放到一個單獨的文件中。而後咱們只需
要執行這個文件即加載 package。例如,若是咱們將上面咱們的複數的 package 代碼放到
一個文件 complex.lua 中,命令「require complex」將打開這個 package。記住 require 命
令不會將相同的 package 加載屢次。
須要注意的問題是,搞清楚保存 package 的文件名和 package 名的關係。固然,將
他們聯繫起來是一個好的想法,由於 require 命令使用文件而不是 packages。一種解決方
法是在 package 的後面加上後綴(好比.lua)來命名文件。Lua 並不須要固定的擴展名,
而是由你的路徑設置決定。例如,若是你的路徑包含:"/usr/local/lualibs/?.lua",那麼復
數 package 可能保存在一個 complex.lua 文件中。
有些人喜歡先命名文件後命名 package。也就是說,若是你重命名文件,package 也
會被重命名。這個解決方法提供了很大的靈活性。例如,若是你有兩個有相同名稱的
package , 你 不 需 要 修 改 任 何 一 個 , 只 需 要 重 命 名 一 下 文 件 。 在 Lua 中 我 們 使 用
_REQUIREDNAME 變量來重命名。記住,當 require 加載一個文件的時候,它定義了一
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
個變量來表示虛擬的文件名。所以,在你的 package 中能夠這樣寫:
local P = {}
complex = P
else
_G[_REQUIREDNAME] = P
end
-- package
113
if _REQUIREDNAME == nil then
代 碼 中 的 if 測 試 使 得 我 們 可 以 不 需 要 require 就 可 以 使 用 package 。 如 果
_REQUIREDNAME 沒有定義,咱們用固定的名字表示 package(例子中 complex) 另外,。
package 使用虛擬文件名註冊他本身。若是以使用者將庫放到文件 cpx.lua 中而且運行
require cpx,那麼 package 將自己加載到表 cpx 中。若是其餘的使用者將庫更名爲
cpx_v1.lua 而且運行 require cpx_v1,那麼 package 將自動將自己加載到表 cpx_v1 當中。
15.4 使用全局表
上面這些建立 package 的方法的缺點是:他們要求程序員注意不少東西,好比,在
聲明的時候也很容易忘掉 local 關鍵字。全局變量表的 Metamethods 提供了一些有趣的技
術,也能夠用來實現 package。這些技術中共同之處在於:package 使用獨佔的環境。這
很容易實現:若是咱們改變了 package 主 chunk 的環境,那麼由 package 建立的全部函數
都共享這個新的環境。
最簡單的技術實現。一旦 package 有一個獨佔的環境,不只全部她的函數共享環境,
並且它的全部全局變量也共享這個環境。因此,咱們能夠將全部的公有函數聲明爲全局
變量,而後他們會自動做爲獨立的表(表指 package 的名字)存在,全部 package 必須
要作的是將這個表註冊爲 package 的名字。下面這段代碼闡述了複數庫使用這種技術的
結果:
local P = {}
complex = P
setfenv(1, P)
如今,當咱們聲明函數 add,她會自動變成 complex.add:
function add (c1, c2)
return new(c1.r + c2.r, c1.i + c2.i)
end
另外,咱們能夠在這個 package 中不須要前綴調用其餘的函數。例如,add 函數調用
new 函數,環境會自動轉換爲 complex.new。這種方法提供了對 package 很好的支持:程
序員幾乎不須要作什麼額外的工做,調用同一個 package 內的函數不須要前綴,調用公
有和私有函數也沒什麼區別。若是程序員忘記了 local 關鍵字,也不會污染全局命名空間,
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
114
只不過使得私有函數變成公有函數而已。另外,咱們能夠將這種技術和前一節咱們使用
的 package 名的方法組合起來:
local P = {}
complex = P
else
_G[_REQUIREDNAME] = P
end
setfenv(1, P)
-- package
if _REQUIREDNAME == nil then
這樣就不能訪問其餘的 packages 了。一旦咱們將一個空表 P 做爲咱們的環境,咱們
就失去了訪問全部之前的全局變量。下面有好幾種方法能夠解決這個問題,但都各有利
弊。
最簡單的解決方法是使用繼承,像前面咱們看到的同樣:
local P = {}
setfenv(1, P)
-- package
setmetatable(P, {__index = _G})
(你必須在調用 setfenv 以前調用 setmetatable,你能說出緣由麼?)使用這種結構,
package 就能夠直接訪問全部的全局標示符,但必須爲每個訪問付出一小點代價。理
論上來說,這種解決方法帶來一個有趣的結果:你的 package 如今包含了全部的全局變
量。例如,使用你的 package 人也能夠調用標準庫的 sin 函數:complex.math.sin(x)。(Perl's
package 系統也有這種特性)
另一種快速的訪問其餘 packages 的方法是聲明一個局部變量來保存老的環境:
local P = {}
pack = P
local _G = _G
setfenv(1, P)
如今,你必須對外部的訪問加上前綴_G.,可是訪問速度更快,由於這不涉及到
metamethod。與繼承不一樣的是這種方法,使得你能夠訪問老的環境;這種方法的好與壞
是有爭議的,可是有時候你可能須要這種靈活性。
一個更加正規的方法是:只把你須要的函數或者 packages 聲明爲 local:
local P = {}
pack = P
-- Import Section:
-- declare everything this package needs from outside
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
local sqrt = math.sqrt
local io = io
-- no more external access after this point
setfenv(1, P)
115
這一技術要求稍多,但他使你的 package 的獨立性比較好。他的速度也比前面那幾
種方法快。
15.5 其餘一些技巧(Other Facilities)
正如前面我所說的,用表來實現 packages 過程當中能夠使用 Lua 的全部強大的功能。
這裏面有無限的可能性。在這裏,我只給出一些建議。
咱們不須要將 package 的全部公有成員的定義放在一塊兒,例如,咱們能夠在一個獨
立分開的 chunk 中給咱們的複數 package 增長一個新的函數:
function complex.div (c1, c2)
return complex.mul(c1, complex.inv(c2))
end
(可是注意:私有成員必須限制在一個文件以內,我認爲這是一件好事)反過來,
咱們能夠在同一個文件以內定義多個 packages,咱們須要作的只是將每個 package 放
在一個 do 代碼塊內,這樣 local 變量才能被限制在那個代碼塊中。
在 package 外部,若是咱們須要常用某個函數,咱們能夠給他們定義一個局部
變量名:
local add, i = complex.add, complex.i
c1 = add(complex.new(10, 20), i)
若是咱們不想一遍又一遍的重寫 package 名,咱們用一個短的局部變量表示 package:
local C = complex
c1 = C.add(C.new(10, 20), C.i)
寫一個函數拆開 package 也是很容易的,將 package 中全部的名字放到全局命名空
間便可:
function openpackage (ns)
for n,v in pairs(ns) do _G[n] = v end
end
openpackage(complex)
c1 = mul(new(10, 20), i)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
116
若是你擔憂打開 package 的時候會有命名衝突,能夠在賦值之前檢查一下名字是否
存在:
function openpackage (ns)
for n,v in pairs(ns) do
if _G[n] ~= nil then
error("name clash: " .. n .. " is already defined")
end
_G[n] = v
end
end
因爲 packages 自己也是表,咱們甚至能夠在 packages 中嵌套 packages;也就是說我
們在一個 package 內還能夠建立 package,而後不多有必要這麼作。
另外一個有趣之處是自動加載:函數只有被實際使用的時候纔會自動加載。當咱們加
載一個自動加載的 package,會自動建立一個新的空表來表示 package 而且設置表的
__index metamethod 來完成自動加載。當咱們調用任何一個沒有被加載的函數的時候,
__index metamethod 將被觸發去加載着個函數。當調用發現函數已經被加載,__index 將
不會被觸發。
下面有一個簡單的實現自動加載的方法。每個函數定義在一個輔助文件中。(也可
能一個文件內有多個函數)這些文件中的每個都以標準的方式定義函數,例如:
function pack1.foo ()
...
end
function pack1.goo ()
...
end
然而,文件並不會建立 package,由於當函數被加載的時候 package 已經存在了。
在主 package 中咱們定義一個輔助表來記錄函數存放的位置:
local location = {
foo = "/usr/local/lua/lib/pack1_1.lua",
goo = "/usr/local/lua/lib/pack1_1.lua",
foo1 = "/usr/local/lua/lib/pack1_2.lua",
goo1 = "/usr/local/lua/lib/pack1_3.lua",
}
下面咱們建立 package 而且定義她的 metamethod:
pack1 = {}
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
117
setmetatable(pack1, {__index = function (t, funcname)
local file = location[funcname]
if not file then
error("package pack1 does not define " .. funcname)
end
assert(loadfile(file))()
return t[funcname]
end})
return pack1
-- load and run definition
-- return the function
加載這個 package 以後,第一次程序執行 pack1.foo()將觸發__index metamethod,接
着發現函數有一個相應的文件,並加載這個文件。微妙之處在於:加載了文件,同時返
回函數做爲訪問的結果。
由於整個系統(譯者:這裏可能指複數吧?)都使用 Lua 寫的,因此很容易改變系
統的行爲。例如,函數能夠是用 C 寫的,在 metamethod 中用 loadlib 加載他。或者咱們
咱們能夠在全局表中設定一個 metamethod 來自動加載整個 packages.這裏有無限的可能
等着你去發掘。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
118
第 16 章 面向對象程序設計
Lua 中的表不只在某種意義上是一種對象。像對象同樣,表也有狀態(成員變量);
也有與對象的值獨立的本性,特別是擁有兩個不一樣值的對象(table)表明兩個不一樣的對
象;一個對象在不一樣的時候也能夠有不一樣的值,但他始終是一個對象;與對象相似,表
的生命週期與其由什麼建立、在哪建立沒有關係。對象有他們的成員函數,表也有:
Account = {balance = 0}
function Account.withdraw (v)
Account.balance = Account.balance - v
end
這個定義建立了一個新的函數,而且保存在 Account 對象的 withdraw 域內,下面我
們能夠這樣調用:
Account.withdraw(100.00)
這種函數就是咱們所謂的方法,然而,在一個函數內部使用全局變量名 Account 是
一個很差的習慣。首先,這個函數只能在這個特殊的對象(譯者:指 Account)中使用;
第二,即便對這個特殊的對象而言,這個函數也只有在對象被存儲在特殊的變量(譯者:
指 Account)中才能夠使用。若是咱們改變了這個對象的名字,函數 withdraw 將不能工
做:
a = Account; Account = nil
a.withdraw(100.00)
-- ERROR!
這種行爲違背了前面的對象應該有獨立的生命週期的原則。
一個靈活的方法是:定義方法的時候帶上一個額外的參數,來表示方法做用的對象。
這個參數常常爲 self 或者 this:
function Account.withdraw (self, v)
self.balance = self.balance - v
end
如今,當咱們調用這個方法的時候不須要指定他操做的對象了:
a1 = Account; Account = nil
...
a1.withdraw(a1, 100.00)
-- OK
使用 self 參數定義函數後,咱們能夠將這個函數用於多個對象上:
a2 = {balance=0, withdraw = Account.withdraw}
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
...
a2.withdraw(a2, 260.00)
119
self 參數的使用是不少面嚮對象語言的要點。大多數 OO 語言將這種機制隱藏起來,
這樣程序員沒必要聲明這個參數(雖然仍然能夠在方法內使用這個參數)。Lua 也提供了通
過使用冒號操做符來隱藏這個參數的聲明。咱們能夠重寫上面的代碼:
function Account:withdraw (v)
self.balance = self.balance - v
end
調用方法以下:
a:withdraw(100.00)
冒號的效果至關於在函數定義和函數調用的時候,增長一個額外的隱藏參數。這種
方式只是提供了一種方便的語法,實際上並無什麼新的內容。咱們能夠使用 dot 語法
定義函數而用冒號語法調用函數,反之亦然,只要咱們正確的處理好額外的參數:
Account = {
balance=0,
withdraw = function (self, v)
self.balance = self.balance - v
end
}
function Account:deposit (v)
self.balance = self.balance + v
end
Account.deposit(Account, 200.00)
Account:withdraw(100.00)
如今咱們的對象擁有一個標示符,一個狀態和操做這個狀態的方法。但他們依然缺
少一個 class 系統,繼承和隱藏。先解決第一個問題:咱們如何才能建立擁有類似行爲的
多個對象呢?明確地說,咱們怎樣才能建立多個 accounts?(譯者:針對上面的對象
Account 而言)
16.1 類
一些面向對象的語言中提供了類的概念,做爲建立對象的模板。在這些語言裏,對
象是類的實例。Lua 不存在類的概念,每一個對象定義他本身的行爲並擁有本身的形狀
(shape)。然而,依據基於原型(prototype)的語言好比 Self 和 NewtonScript,在 Lua
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
120
中仿效類的概念並不難。在這些語言中,對象沒有類。相反,每一個對象都有一個 prototype
(原型),當調用不屬於對象的某些操做時,會最早會到 prototype 中查找這些操做。在
這類語言中實現類(class)的機制,咱們建立一個對象,做爲其它對象的原型便可(原
型對象爲類,其它對象爲類的 instance)。類與 prototype 的工做機制相同,都是定義了特
定對象的行爲。
在 Lua 中,使用前面章節咱們介紹過的繼承的思想,很容易實現 prototypes.更明確
的來講,若是咱們有兩個對象 a 和 b,咱們想讓 b 做爲 a 的 prototype 只須要:
setmetatable(a, {__index = b})
這樣,對象 a 調用任何不存在的成員都會到對象 b 中查找。術語上,能夠將 b 看做
類,a 看做對象。回到前面銀行帳號的例子上。爲了使得新建立的對象擁有和 Account
類似的行爲,咱們使用__index metamethod,使新的對象繼承 Account。注意一個小的優
化:咱們不須要建立一個額外的表做爲 account 對象的 metatable;咱們能夠用 Account
表自己做爲 metatable:
function Account:new (o)
o = o or {}
-- create object if user does not provide one
setmetatable(o, self)
self.__index = self
return o
end
(當咱們調用 Account:new 時,self 等於 Account;所以咱們能夠直接使用 Account
取代 self。然而,使用 self 在咱們下一節介紹類繼承時更合適)。有了這段代碼以後,當
咱們建立一個新的帳號而且掉用一個方法的時候,有什麼發生呢?
a = Account:new{balance = 0}
a:deposit(100.00)
當咱們建立這個新的帳號 a 的時候,a 將 Account 做爲他的 metatable(調用
Account:new 時,self 即 Account)。當咱們調用 a:deposit(100.00),咱們實際上調用的是
a.deposit(a,100.00)(冒號僅僅是語法上的便利)。然而,Lua 在表 a 中找不到 deposit,因
此他回到 metatable 的__index 對應的表中查找,狀況大體以下:
getmetatable(a).__index.deposit(a, 100.00)
a 的 metatable 是 Account,Account.__index 也是 Account(由於 new 函數中 self.__index
= self)。因此咱們能夠重寫上面的代碼爲:
Account.deposit(a, 100.00)
也就是說,Lua 傳遞 a 做爲 self 參數調用原始的 deposit 函數。因此,新的帳號對象
從 Account 繼承了 deposit 方法。使用一樣的機制,能夠從 Account 繼承全部的域。繼承
機制不只對方法有效,對錶中全部的域都有效。因此,一個類不只提供方法,也提供了
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
121
他的實例的成員的默認值。記住:在咱們第一個 Account 定義中,咱們提供了成員 balance
默認值爲 0,因此,若是咱們建立一個新的帳號而沒有提供 balance 的初始值,他將繼承
默認值:
b = Account:new()
print(b.balance)
--> 0
當咱們調用 b 的 deposit 方法時,實際等價於:
b.balance = b.balance + v
(由於 self 就是 b)。表達式 b.balance 等於 0 而且初始的存款(b.balance)被賦予
b.balance。下一次咱們訪問這個值的時候,不會在涉及到 index metamethod,由於 b 已經
存在他本身的 balance 域。
16.2 繼承
一般面嚮對象語言中,繼承使得類能夠訪問其餘類的方法,這在 Lua 中也很容易現
實:
假定咱們有一個基類 Account:
Account = {balance = 0}
function Account:new (o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
function Account:deposit (v)
self.balance = self.balance + v
end
function Account:withdraw (v)
if v > self.balance then error"insufficient funds" end
self.balance = self.balance - v
end
咱們打算從基類派生出一個子類 SpecialAccount,這個子類容許客戶取款超過它的
存款餘額限制,咱們從一個空類開始,從基類繼承全部操做:
SpecialAccount = Account:new()
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
122
到如今爲止,SpecialAccount 僅僅是 Account 的一個實例。如今奇妙的事情發生了:
s = SpecialAccount:new{limit=1000.00}
SpecialAccount 從 Account 繼承了 new 方法,當 new 執行的時候,self 參數指向
SpecialAccount。因此,s 的 metatable 是 SpecialAccount,__index 也是 SpecialAccount。
這樣,s 繼承了 SpecialAccount,後者繼承了 Account。當咱們執行:
s:deposit(100.00)
Lua 在 s 中找不到 deposit 域,他會到 SpecialAccount 中查找,在 SpecialAccount 中
找不到,會到 Account 中查找。使得 SpecialAccount 特殊之處在於,它能夠重定義從父
類中繼承來的方法:
function SpecialAccount:withdraw (v)
if v - self.balance >= self:getLimit() then
error"insufficient funds"
end
self.balance = self.balance - v
end
function SpecialAccount:getLimit ()
return self.limit or 0
end
如今,當咱們調用方法 s:withdraw(200.00),Lua 不會到 Account 中查找,由於它第
一次救在 SpecialAccount 中發現了新的 withdraw 方法,因爲 s.limit 等於 1000.00(記住
咱們建立 s 的時候初始化了這個值)程序執行了取款操做,s 的 balance 變成了負值。
在 Lua 中面向對象有趣的一個方面是你不須要建立一個新類去指定一個新的行爲。
若是僅僅一個對象須要特殊的行爲,你能夠直接在對象中實現,例如,若是帳號 s 表示
一些特殊的客戶:取款限制是他的存款的 10%,你只須要修改這個單獨的帳號:
function s:getLimit ()
return self.balance * 0.10
end
這樣聲明以後,調用 s:withdraw(200.00)將運行 SpecialAccount 的 withdraw 方法,但
是當方法調用 self:getLimit 時,最後的定義被觸發。
16.3 多重繼承
因爲 Lua 中的對象不是元生(primitive)的,因此在 Lua 中有不少方法能夠實現面向對
象的程序設計。咱們前面所見到的使用 index metamethod 的方法多是簡潔、性能、靈
活各方面綜合最好的。然而,針對一些特殊狀況也有更適合的實現方式。下面咱們在 Lua
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
中多重繼承的實現。
123
實現的關鍵在於:將函數用做__index。記住,當一個表的 metatable 存在一個__index
函數時,若是 Lua 調用一個原始表中不存在的函數, 將調用這個__index 指定的函數。Lua
這樣能夠用__index 實如今多個父類中查找子類不存在的域。
多重繼承意味着一個類擁有多個父類,因此,咱們不能用建立一個類的方法去建立
子類。取而代之的是,咱們定義一個特殊的函數 createClass 來完成這個功能,將被建立
的新類的父類做爲這個函數的參數。這個函數建立一個表來表示新類,而且將它的
metatable 設定爲一個能夠實現多繼承的__index metamethod。儘管是多重繼承,每個
實例依然屬於一個在其中能找獲得它須要的方法的單獨的類。因此,這種類和父類之間
的關係與傳統的類與實例的關係是有區別的。特別是,一個類不能同時是其實例的
metatable 又是本身的 metatable。在下面的實現中,咱們將一個類做爲他的實例的
metatable,建立另外一個表做爲類的 metatable:
-- look up for `k' in list of tables 'plist'
local function search (k, plist)
for i=1, table.getn(plist) do
local v = plist[i][k]
if v then return v end
end
end
function createClass (...)
local c = {}
-- new class
-- try 'i'-th superclass
-- class will search for each method in the list of its
-- parents (`arg' is the list of parents)
setmetatable(c, {__index = function (t, k)
return search(k, arg)
end})
-- prepare `c' to be the metatable of its instances
c.__index = c
-- define a new constructor for this new class
function c:new (o)
o = o or {}
setmetatable(o, c)
return o
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
-- return new class
return c
end
124
讓咱們用一個小例子闡明一下 createClass 的使用,假定咱們前面的類 Account 和另
一個類 Named,Named 只有兩個方法 setname and getname:
Named = {}
function Named:getname ()
return self.name
end
function Named:setname (n)
self.name = n
end
爲了建立一個繼承於這兩個類的新類,咱們調用 createClass:
NamedAccount = createClass(Account, Named)
爲了建立和使用實例,咱們像一般同樣:
account = NamedAccount:new{name = "Paul"}
print(account:getname())
--> Paul
如今咱們看看上面最後一句發生了什麼,Lua 在 account 中找不到 getname,所以他
查找 account 的 metatable 的__index,即 NamedAccount。可是,NamedAccount 也沒有
getname,所以 Lua 查找 NamedAccount 的 metatable 的__index,由於這個域包含一個函
數,Lua 調用這個函數並首先到 Account 中查找 getname,沒有找到,而後到 Named 中
查找,找到並返回最終的結果。固然,因爲搜索的複雜性,多重繼承的效率比起單繼承
要低。一個簡單的改善性能的方法是將繼承方法拷貝到子類。使用這種技術,index 方法
以下:
...
setmetatable(c, {__index = function (t, k)
local v = search(k, arg)
t[k] = v
return v
end})
...
-- save for next access
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
125
應用這個技巧,訪問繼承的方法和訪問局部方法同樣快(特別是第一次訪問)。缺點
是系統運行以後,很難改變方法的定義,由於這種改變不能影響繼承鏈的下端。
16.4 私有性(privacy)
不少人認爲私有性是面嚮對象語言的應有的一部分。每一個對象的狀態應該是這個對
象本身的事情。在一些面向對象的語言中,好比 C++和 Java 你能夠控制對象成員變量或
者成員方法是否私有。其餘一些語言好比 Smalltalk 中,全部的成員變量都是私有,全部
的成員方法都是公有的。第一個面嚮對象語言 Simula 不提供任何保護成員機制。
如前面咱們所看到的 Lua 中的主要對象設計不提供私有性訪問機制。部分緣由由於
這是咱們使用通用數據結構 tables 來表示對象的結果。可是這也反映了後來的 Lua 的設
計思想。Lua 沒有打算被用來進行大型的程序設計,相反,Lua 目標定於小型到中型的
程序設計,一般是做爲大型系統的一部分。典型的,被一個或者不多幾個程序員開發,
甚至被非程序員使用。因此,Lua 避免太冗餘和太多的人爲限制。若是你不想訪問一個
對象內的一些東西就不要訪問(If you do not want to access something inside an object, just
do not do it.)。
然而,Lua 的另外一個目標是靈活性,提供程序員元機制(meta-mechanisms),經過
他你能夠實現不少不一樣的機制。雖然 Lua 中基本的面向對象設計並不提供私有性訪問的
機制,咱們能夠用不一樣的方式來實現他。雖然這種實現並不經常使用,但知道他也是有益的,
不只由於它展現了 Lua 的一些有趣的角落,也由於它多是某些問題的很好地解決方案。
設計的基本思想是,每一個對象用兩個表來表示:一個描述狀態;另外一個描述操做(或者
叫接口)。對象自己經過第二個表來訪問,也就是說,經過接口來訪問對象。爲了不未
受權的訪問,表示狀態的表中不涉及到操做;表示操做的表也不涉及到狀態,取而代之
的是,狀態被保存在方法的閉包內。例如,用這種設計表述咱們的銀行帳號,咱們使用
下面的函數工廠建立新的對象:
function newAccount (initialBalance)
local self = {balance = initialBalance}
local withdraw = function (v)
self.balance = self.balance - v
end
local deposit = function (v)
self.balance = self.balance + v
end
local getBalance = function () return self.balance end
return {
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
withdraw = withdraw,
deposit = deposit,
getBalance = getBalance
}
end
126
首先,函數建立一個表用來描述對象的內部狀態,並保存在局部變量 self 內。而後,
函數爲對象的每個方法建立閉包(也就是說,嵌套的函數實例)。最後,函數建立並返
回外部對象,外部對象中將局部方法名指向最終要實現的方法。這兒的關鍵點在於:這
些方法沒有使用額外的參數 self,代替的是直接訪問 self。由於沒有這個額外的參數,我
們不能使用冒號語法來訪問這些對象。函數只能像其餘函數同樣調用:
acc1 = newAccount(100.00)
acc1.withdraw(40.00)
print(acc1.getBalance())
--> 60
這種設計實現了任何存儲在 self 表中的部分都是私有的,newAccount 返回以後,沒
有什麼方法能夠直接訪問對象,咱們只能經過 newAccount 中定義的函數來訪問他。雖
然咱們的例子中僅僅將一個變量放到私有表中,可是咱們能夠將對象的任何的部分放到
私有表中。咱們也能夠定義私有方法,他們看起來象公有的,但咱們並不將其放到接口
中。例如,咱們的帳號能夠給某些用戶取款享有額外的 10%的存款上限,可是咱們不想
用戶直接訪問這種計算的詳細信息,咱們實現以下:
function newAccount (initialBalance)
local self = {
balance = initialBalance,
LIM = 10000.00,
}
local extra = function ()
if self.balance > self.LIM then
return self.balance*0.10
else
return 0
end
end
local getBalance = function ()
return self.balance + self.extra()
end
...
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
這樣,對於用戶而言就沒有辦法直接訪問 extra 函數了。
127
16.5 Single-Method 的對象實現方法
前面的 OO 程序設計的方法有一種特殊狀況:對象只有一個單一的方法。這種狀況
下,咱們不須要建立一個接口表,取而代之的是,咱們將這個單一的方法做爲對象返回。
這聽起來有些難以想象,若是須要能夠複習一下 7.1 節,那裏咱們介紹瞭如何構造迭代
子函數來保存閉包的狀態。其實,一個保存狀態的迭代子函數就是一個 single-method 對
象。
關於 single-method 的對象一個有趣的狀況是:當這個 single-method 實際是一個基於
重要的參數而執行不一樣的任務的分派(dispatch)方法時。針對這種對象:
function newObject (value)
return function (action, v)
if action == "get" then return value
elseif action == "set" then value = v
else error("invalid action")
end
end
end
使用起來很簡單:
d = newObject(0)
print(d("get"))
d("set", 10)
print(d("get"))
--> 10
--> 0
這種非傳統的對象實現是很是有效的,語法 d("set",10)雖然很罕見,但也只不過比
傳統的 d:set(10)長兩個字符而已。每個對象是用一個單獨的閉包,代價比起表來小的
多。這種方式沒有繼承但有私有性:訪問對象狀態的惟一方式是經過它的內部方法。
Tcl/Tk 的窗口部件(widgets)使用了類似的方法,在 Tk 中一個窗口部件的名字表
示一個在窗口部件上執行各類可能操做的函數(a widget command)。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
128
第 17 章 Weak 表
Lua 自動進行內存的管理。程序只能建立對象(表,函數等),而沒有執行刪除對象
的函數。經過使用垃圾收集技術,Lua 會自動刪除那些失效的對象。這能夠使你從內存
管理的負擔中解脫出來。更重要的,可讓你從那些由此引起的大部分 BUG 中解脫出
來,好比指針掛起(dangling pointers)和內存溢出。
和其餘的不一樣,Lua 的垃圾收集器不存在循環的問題。在使用循環性的數據結構的
時候,你無須加入特殊的操做;他們會像其餘數據同樣被收集。固然,有些時候即便更
智能化的收集器也須要你的幫助。沒有任何的垃圾收集器可讓你忽略掉內存管理的所
有問題。
垃圾收集器只能在確認對象失效以後纔會進行收集;它是不會知道你對垃圾的定義
的。一個典型的例子就是堆棧:有一個數組和指向棧頂的索引構成。你知道這個數組中
有效的只是在頂端的那一部分,但 Lua 不那麼認爲。若是你經過簡單的出棧操做提取一
個數組元素,那麼數組對象的其餘部分對 Lua 來講仍然是有效的。一樣的,任何在全局
變量中聲明的對象,都不是 Lua 認爲的垃圾,即便你的程序中根本沒有用到他們。這兩
種狀況下,你應當本身處理它(你的程序),爲這種對象賦 nil 值,防止他們鎖住其餘的
空閒對象。
然而,簡單的清理你的聲明並不老是足夠的。有些語句須要你和收集器進行額外的
合做。一個典型的例子發生在當你想在你的程序中對活動的對象(好比文件)進行收集
的時候。那看起來是個簡單的任務:你須要作的是在收集器中插入每個新的對象。然
而,一旦對象被插入了收集器,它就不會再被收集!即便沒有其餘的指針指向它,收集
器也不會作什麼的。Lua 會認爲這個引用是爲了阻止對象被回收的,除非你告訴 Lua 怎
麼作。
Weak 表是一種用來告訴 Lua 一個引用不該該防止對象被回收的機制。一個 weak 引
用是指一個不被 Lua 認爲是垃圾的對象的引用。若是一個對象全部的引用指向都是
weak,對象將被收集,而那些 weak 引用將會被刪除。Lua 經過 weak tables 來實現 weak
引用:一個 weak tables 是指全部引用都是 weak 的 table。這意味着,若是一個對象只存
在於 weak tables 中,Lua 將會最終將它收集。
表有 keys 和 values,而這二者均可能包含任何類型的對象。在通常狀況下,垃圾收
集器並不會收集做爲 keys 和 values 屬性的對象。也就是說,keys 和 values 都屬於強引
用,他們能夠防止他們指向的對象被回收。在一個 weak tables 中,keys 和 vaules 也可能
是 weak 的。那意味着這裏存在三種類型的 weak tables:weak keys 組成的 tables;weak
values 組成的 tables;以及純 weak tables 類型,他們的 keys 和 values 都是 weak 的。與
table 自己的類型無關,當一個 keys 或者 vaule 被收集時,整個的入口(entry)都將從這
個 table 中消失。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
129
表的 weak 性由他的 metatable 的__mode 域來指定的。在這個域存在的時候,必須是
個字符串:若是這個字符串包含小寫字母‘k’,這個 table 中的 keys 就是 weak 的;若是
這個字符串包含小寫字母‘v’,這個 table 中的 vaules 就是 weak 的。下面是一個例子,
雖然是人造的,可是能夠闡明 weak tables 的基本應用:
a = {}
b = {}
setmetatable(a, b)
b.__mode = \"k\"
key = {}
a[key] = 1
key = {}
a[key] = 2
collectgarbage()
-- forces a garbage collection cycle
-- creates second key
-- now 'a' has weak keys
-- creates first key
for k, v in pairs(a) do print(v) end
--> 2
在這個例子中,第二個賦值語句 key={}覆蓋了第一個 key 的值。當垃圾收集器工做
時,在其餘地方沒有指向第一個 key 的引用,因此它被收集了,所以相對應的 table 中的
入口也同時被移除了。但是,第二個 key,仍然是佔用活動的變量 key,因此它不會被收
集。
要注意,只有對象才能夠從一個 weak table 中被收集。好比數字和布爾值類型的值,
都是不會被收集的。例如,若是咱們在 table 中插入了一個數值型的 key(在前面那個例
子中) 它將永遠不會被收集器從 table 中移除。,固然,若是對應於這個數值型 key 的 vaule
被收集,那麼它的整個入口將會從 weak table 中被移除。
關於字符串的一些細微差異:從上面的實現來看,儘管字符串是能夠被收集的,他
們仍然跟其餘可收集對象有所區別。 其餘對象,好比 tables 和函數,他們都是顯示的被
建立。好比,無論何時當 Lua 遇到{}時,它創建了一個新的 table。任什麼時候候這個
function()。。。end 創建了一個新的函數(其實是一個閉包)。然而,Lua 見到「a」..
「b」的時候會建立一個新的字符串?若是系統中已經有一個字符串「ab」的話怎麼辦?
Lua 會從新創建一個新的?編譯器能夠在程序運行以前建立字符串麼?這可有可無:這
些是實現的細節。所以,從程序員的角度來看,字符串是值而不是對象。因此,就像數
值或布爾值,一個字符串不會從 weak tables 中被移除(除非它所關聯的 vaule 被收集)。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
130
17.1 記憶函數
一個至關廣泛的編程技術是用空間來換取時間。你能夠經過記憶函數結果來進行優
化,當你用一樣的參數再次調用函數時,它能夠自動返回記憶的結果。
想像一下一個通用的服務器,接收包含 Lua 代碼的字符串請求。每當它收到一個請
求,它調用 loadstring 加載字符串,而後調用函數進行處理。然而,loadstring 是一個「巨
大」的函數,一些命令在服務器中會頻繁地使用。不須要反覆調用 loadstring 和後面接着
的 closeconnection(),服務器能夠經過使用一個輔助 table 來記憶 loadstring 的結果。在
調用 loadstring 以前,服務器會在這個 table 中尋找這個字符串是否已經有了翻譯好的結
果。若是沒有找到,那麼(並且只是這個狀況)服務器會調用 loadstring 並把此次的結果
存入輔助 table。咱們能夠將這個操做包裝爲一個函數:
local results = {}
function mem_loadstring (s)
if results[s] then
return results[s]
else
local res = loadstring(s)
results[s] = res
return res
end
end
-- compute new result
-- save for later reuse
-- result available?
-- reuse it
這個方案的存儲消耗多是巨大的。儘管如此,它仍然可能會致使意料以外的數據
冗餘。儘管一些命令一遍遍的重複執行,但有些命令可能只運行一次。漸漸地,這個 table
積累了服務器全部命令被調用處理後的結果;遲早有一天,它會擠爆服務器的內存。一
個 weak table 提供了對於這個問題的簡單解決方案。若是這個結果表中有 weak 值,每次
的垃圾收集循環都會移除當前時間內全部未被使用的結果(一般是差很少所有):
local results = {}
setmetatable(results, {__mode = \"v\"})
function mem_loadstring (s)
...
-- as before
-- make values weak
事實上,由於 table 的索引下標常常是字符串式的,若是願意,咱們能夠將 table 全
部置 weak:
setmetatable(results, {__mode = \"kv\"})
最終結果是徹底同樣的。
記憶技術在保持一些類型對象的惟一性上一樣有用。例如,假如一個系統將經過
tables 表達顏色,經過有必定組合方式的紅色,綠色,藍色。一個天然顏色調色器經過
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
每一次新的請求產生新的顏色:
function createRGB (r, g, b)
return {red = r, green = g, blue = b}
end
131
使用記憶技術,咱們能夠將一樣的顏色結果存儲在同一個 table 中。爲了創建每一種
顏色惟一的 key,咱們簡單的使用一個分隔符鏈接顏色索引下標:
local results = {}
setmetatable(results, {__mode = \"v\"})
function createRGB (r, g, b)
local key = r .. \"-\" .. g .. \"-\" .. b
if results[key] then return results[key]
else
local newcolor = {red = r, green = g, blue = b}
results[key] = newcolor
return newcolor
end
end
-- make values weak
一個有趣的後果就是,用戶能夠使用這個原始的等號運算符比對操做來辨別顏色,
由於兩個同時存在的顏色經過同一個的 table 來表達。要注意,一樣的顏色可能在不一樣的
時間經過不一樣的 tales 來表達,由於垃圾收集器一次次的在清理結果 table。然而,只要
給定的顏色正在被使用,它就不會從結果中被移除。因此,任什麼時候候一個顏色在同其餘
顏色進行比較的時候存活的夠久,它的結果鏡像也一樣存活。
17.2 關聯對象屬性
weak tables 的另外一個重要的應用就是和對象的屬性關聯。在一個對象上加入更多的
屬性是無時無刻都會發生的: 函數名稱,tables 的缺省值,數組的大小,等等。
當對象是表的時候,咱們能夠使用一個合適的惟一 key 來將屬性保存在表中。就像
咱們在前面說的那樣,一個很簡單而且能夠防止錯誤的方法是創建一個新的對象(典型
的好比 table)而後把它當成 key 使用。然而,若是對象不是 table,它就不能本身保存自
身的屬性。即便是 tables,有些時候咱們可能也不想把屬性保存在原來的對象中去。例
如,咱們可能但願將屬性做爲私有的,或者咱們不想在訪問 table 中元素的時候受到這個
額外的屬性的干擾。在上述這些狀況下,咱們須要一個替代的方法來將屬性和對象聯繫
起來。固然,一個外部的 table 提供了一種理想化的方式來聯繫屬性和對象(tables 有時
被稱做聯合數組並不偶然)。咱們把這個對象看成 key 來使用,他們的屬性做爲 vaule。
一個外部的 table 能夠保存任何類型對象的屬性(就像 Lua 容許咱們將任何對象看做
key)。此外,保存在一個外部 table 的屬性不會妨礙到其餘的對象,而且能夠像這個 table
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
自己同樣私有化。
132
然而,這個看起來完美的解決方案有一個巨大的缺點:一旦咱們在一個 table 中將一
個對象使用爲 key,咱們就將這個對象鎖定爲永久存在。Lua 不能收集一個正在被看成
key 使用的對象。若是咱們使用一個普通的 table 來關聯函數和名字,那麼全部的這些函
數將永遠不會被收集。正如你所想的那樣,咱們能夠經過使用 weak table 來解決這個問
題。這一次,咱們須要 weak keys。一旦沒有其餘地方的引用,weak keys 並不會阻止任
何的 key 被收集。從另外一方面說,這個 table 不會存在 weak vaules;不然,活動對象的
屬性就可能被收集了。
Lua 自己使用這種技術來保存數組的大小。像咱們下面即將看到的那樣,table 庫提
供了一個函數來設定數組的大小,另外一個函數來讀取數組的大小。當你設定了一個數組
的大小,Lua 將這個尺寸保存在一個私有的 weak table,索引就是數組自己,而 value 就
是它的尺寸。
17.3 重述帶有默認值的表
在章節 13.4.3,咱們討論了怎樣使用非 nil 的默認值來實現表。咱們提到一種特殊的
技術並註釋說另外兩種技術須要使用 weak tables,因此咱們推遲在這裏介紹他們。如今,
介紹她們的時候了。就像咱們說的那樣,這兩種默認值的技術實際上來源於咱們前面提
到的兩種通用的技術的特殊應用:對象屬性和記憶。
在第一種解決方案中,咱們使用 weak table 來將默認 vaules 和每個 table 相聯繫:
local defaults = {}
setmetatable(defaults, {__mode = \"k\"})
local mt = {__index = function (t) return defaults[t] end}
function setDefault (t, d)
defaults[t] = d
setmetatable(t, mt)
end
若是默認值沒有 weak 的 keys,它就會將全部的帶有默認值的 tables 設定爲永久存
在。在第二種方法中,咱們使用不一樣的 metatables 來保存不一樣的默認值,但當咱們重複
使用一個默認值的時候,重用同一個相同的 metatable。這是一個典型的記憶技術的應用:
local metas = {}
setmetatable(metas, {__mode = \"v\"})
function setDefault (t, d)
local mt = metas[d]
if mt == nil then
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
mt = {__index = function () return d end}
metas[d] = mt
end
setmetatable(t, mt)
end
-- memoize
133
這種狀況下,咱們使用 weak vaules,容許將不會被使用的 metatables 能夠被回收。
把這兩種方法放在一塊兒,哪一個更好?一般,取決於具體狀況。它們都有類似的複雜
性和類似的性能。第一種方法須要在每一個默認值的 tables 中添加一些文字(一個默認的
入口) 第二種方法須要在每一個不一樣的默認值加入一些文字。(一個新的表,一個新的閉包,
metas 中新增入口)。因此,若是你的程序有數千個 tables,而這些表只有不多數帶有不
同默認值的,第二種方法顯然更優秀。另外一方面,若是隻有不多的 tabels 能夠共享相同
的默認 vaules,那麼你仍是用第一種方法吧。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
134
第三篇 標準庫
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
135
第 18 章 數學庫
在這一章中(下面關於標準庫的幾章中一樣)個人主要目的不是對每個函數給出
完整地說明,而是告訴你標準庫可以提供什麼功能。爲了可以清楚地說明問題,我可能
會忽略一些小的選項或者行爲。主要的思想是激發你的好奇心,這些好奇之處可能在參
考手冊中找到答案。
數學庫由算術函數的標準集合組成,好比三角函數庫(sin, cos, tan, asin, acos, etc.),
冪指函數(exp, log, log10),舍入函數(floor, ceil)、max、min,加上一個變量 pi。數學
庫也定義了一個冪操做符(^)。
全部的三角函數都在弧度單位下工做。(Lua4.0 之前在度數下工做。 你能夠使用 deg)
和 rad 函數在度和弧度之間轉換。若是你想在 degree 狀況下使用三角函數,你能夠重定
義三角函數:
local sin, asin, ... = math.sin, math.asin, ...
local deg, rad = math.deg, math.rad
math.sin = function (x) return sin(rad(x)) end
math.asin = function (x) return deg(asin(x)) end
...
math.random 用來產生僞隨機數,有三種調用方式:
第一:不帶參數,將產生 [0,1)範圍內的隨機數.
第二:帶一個參數 n,將產生 1 <= x <= n 範圍內的隨機數 x.
第三:帶兩個參數 a 和 b,將產生 a <= x <= b 範圍內的隨機數 x.
你能夠使用 randomseed 設置隨機數發生器的種子,只能接受一個數字參數。一般在
程序開始時,使用固定的種子初始化隨機數發生器,意味着每次運行程序,將產生相同
的隨機數序列。爲了調試方便,這頗有好處,可是在遊戲中就意味着每次運行都擁有相
同的關卡。解決這個問題的一個一般的技巧是使用當前系統時間做爲種子:
math.randomseed(os.time())
(os.time 函數返回一個表示當前系統時間的數字,一般是自新紀元以來的一個整
數。)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
136
第 19 章 Table 庫
table 庫由一些操做 table 的輔助函數組成。他的主要做用之一是對 Lua 中 array 的大
小給出一個合理的解釋。另外還提供了一些從 list 中插入刪除元素的函數,以及對 array
元素排序函數。
19.1 數組大小
Lua 中咱們常常假定 array 在最後一個非 nil 元素處結束。這個傳統的約定有一個弊
端:咱們的 array 中不能擁有 nil 元素。對大部分應用來講這個限制不是什麼問題,好比
當全部的 array 有固定的類型的時候。但有些時候咱們的 array 須要擁有 nil 元素,這種
狀況下,咱們須要一種方法來明確的代表 array 的大小。
Table 庫定義了兩個函數操縱 array 的大小:getn,返回 array 的大小;setn,設置 array
的大小。如前面咱們所見到的,這兩個方法和 table 的一個屬性相關:要麼咱們在 table
的一個域中保存這個屬性,要麼咱們使用一個獨立的(weak)table 來關聯 table 和這個
屬性。兩種方法各有利弊,因此 table 庫使用了這兩個方法。
一般,調用 table.setn(t, n)使得 t 和 n 在內部(weak)table 關聯,調用 table.getn(t)
將獲得內部 table 中和 t 關聯的那個值。然而,若是表 t 有一個帶有數字值 n 的域,setn
將修改這個值,而 getn 返回這個值。Getn 函數還有一個選擇:若是他不能使用上述方法
返回 array 的大小,就會使用原始的方法:遍歷 array 直到找到第一個 nil 元素。所以,
你能夠在 array 中一直使用 table.getn(t)得到正確的結果。看例子:
print(table.getn{10,2,4})
print(table.getn{10,2,nil})
print(table.getn{10,2,nil; n=3})
print(table.getn{n=1000})
a = {}
print(table.getn(a))
table.setn(a, 10000)
print(table.getn(a))
a = {n=10}
print(table.getn(a))
table.setn(a, 10000)
print(table.getn(a))
--> 10000
--> 10
--> 10000
--> 0
--> 3
--> 2
--> 3
--> 1000
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
137
默認的,setn 和 getn 使用內部表存儲表的大小。這是最乾淨的選擇,由於它不會使
用額外的元素污染 array。然而,使用 n 域的方法也有一些優勢。在帶有可變參數的函數
種,Lua 內核使用這種方法設置 arg 數組的大小,由於內核不依賴於庫,他不能使用 setn。
另一個好處在於:咱們能夠在 array 建立的時候直接初始化他的大小,如咱們在上面
例子中看到的。
使用 setn 和 getn 操縱 array 的大小是個好的習慣,即便你知道大小在域 n 中。table
庫中的全部函數(sort、concat、insert 等等)都遵循這個習慣。實際上,提供 setn 用來
改變域 n 的值可能只是爲了與舊的 lua 版本兼容,這個特性可能在未來的版本中改變,
爲了安全起見,不要假定依賴於這個特性。請一直使用 getn 獲取數組大小,使用 setn 設
置數組大小。
19.2 插入/刪除
table 庫提供了從一個 list 的任意位置插入和刪除元素的函數。table.insert 函數在 array
指定位置插入一個元素,並將後面全部其餘的元素後移。另外,insert 改變 array 的大小
(using setn)。例如,若是 a 是一個數組{10,20,30},調用 table.insert(a,1,15)後,a 變爲
{15,10,20,30}。常用的一個特殊狀況是,咱們不帶位置參數調用 insert,將會在 array
最後位置插入元素(因此不須要元素移動)。下面的代碼逐行獨入程序,並將全部行保存
在一個 array 內:
a = {}
for line in io.lines() do
table.insert(a, line)
end
print(table.getn(a))
--> (number of lines read)
table.remove 函數刪除數組中指定位置的元素,並返回這個元素,全部後面的元素前
移,而且數組的大小改變。不帶位置參數調用的時候,他刪除 array 的最後一個元素。
使用這兩個函數,很容易實現棧、隊列和雙端隊列。咱們能夠初始化結構爲 a={}。
一個 push 操做等價於 table.insert(a,x);一個 pop 操做等價於 table.remove(a)。要在結構的
另外一端結尾插入元素咱們使用 table.insert(a,1,x);刪除元素用 table.remove(a,1)。最後兩
個操做不是特別有效的,由於他們必須來回移動元素。然而,由於 table 庫這些函數使用
C 實現,對於小的數組(幾百個元素)來講效率都不會有什麼問題。
19.3 排序
另外一個有用的函數是 table.sort。他有兩個參數:存放元素的 array 和排序函數。排序
函數有兩個參數而且若是在 array 中排序後第一個參數在第二個參數前面,排序函數必
須返回 true。若是未提供排序函數,sort 使用默認的小於操做符進行比較。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
138
一個常見的錯誤是企圖對錶的下標域進行排序。在一個表中,全部下標組成一個集
合,可是無序的。若是你想對他們排序,必須將他們複製到一個 array 而後對這個 array
排序。咱們看個例子,假定上面的讀取源文件並建立了一個表,這個表給出了源文件中
每個函數被定義的地方的行號:
lines = {
luaH_set = 10,
luaH_get = 24,
luaH_present = 48,
}
如今你想以字母順序打印出這些函數名,若是你使用 pairs 遍歷這個表,函數名出現
的順序將是隨機的。然而,你不能直接排序他們,由於這些名字是表的 key。當你將這
些函數名放到一個數組內,就能夠對這個數組進行排序。首先,必須建立一個數組來保
存這些函數名,而後排序他們,最後打印出結果:
a = {}
for n in pairs(lines) do table.insert(a, n) end
table.sort(a)
for i,n in ipairs(a) do print(n) end
注意,對於 Lua 來講,數組也是無序的。可是咱們知道怎樣去計數,所以只要咱們
使用排序好的下標訪問數組就能夠獲得排好序的函數名。這就是爲何咱們一直使用
ipairs 而不是 pairs 遍歷數組的緣由。前者使用 key 的順序 一、二、……,後者表的天然存
儲順序。
有一個更好的解決方法,咱們能夠寫一個迭代子來根據 key 值遍歷這個表。一個可
選的參數 f 能夠指定排序的方式。首先,將排序的 keys 放到數組內,而後遍歷這個數組,
每一步從原始表中返回 key 和 value:
function pairsByKeys (t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = 0
local iter = function ()
i=i+1
if a[i] == nil then return nil
else return a[i], t[a[i]]
end
end
return iter
end
-- iterator variable
-- iterator function
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
有了這個函數,很容易以字母順序打印這些函數名,循環:
for name, line in pairsByKeys(lines) do
print(name, line)
end
139
打印結果:
luaH_get
luaH_present
luaH_set
24
48
10
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
140
第 20 章 String 庫
Lua 解釋器對字符串的支持頗有限。一個程序能夠建立字符串並鏈接字符串,但不
能截取子串,檢查字符串的大小,檢測字符串的內容。在 Lua 中操縱字符串的功能基本
來自於 string 庫。
String 庫中的一些函數是很是簡單的:string.len(s)返回字符串 s 的長度;string.rep(s,
n)返回重複 n 次字符串 s 的串;你使用 string.rep("a", 2^20)能夠建立一個 1M bytes 的字符
串(好比,爲了測試須要);string.lower(s)將 s 中的大寫字母轉換成小寫(string.upper
將小寫轉換成大寫)。若是你想不關心大小寫對一個數組進行排序的話,你能夠這樣:
table.sort(a, function (a, b)
return string.lower(a) < string.lower(b)
end)
string.upper 和 string.lower 都依賴於本地環境變量。因此,若是你在 European Latin-1
環境下,表達式:
string.upper("a??o")
--> "A??O".
調用 string.sub(s,i,j)函數截取字符串 s 的從第 i 個字符到第 j 個字符之間的串。Lua
中,字符串的第一個字符索引從 1 開始。你也能夠使用負索引,負索引從字符串的結尾
向前計數:-1 指向最後一個字符,-2 指向倒數第二個,以此類推。因此, string.sub(s, 1,
j)返回字符串 s 的長度爲 j 的前綴;string.sub(s, j, -1)返回從第 j 個字符開始的後綴。若是
不提供第 3 個參數,默認爲-1,所以咱們將最後一個調用寫爲 string.sub(s, j);string.sub(s,
2, -2)返回去除第一個和最後一個字符後的子串。
s = "[in brackets]"
print(string.sub(s, 2, -2))
--> in brackets
記住:Lua 中的字符串是恆定不變的。String.sub 函數以及 Lua 中其餘的字符串操做
函數都不會改變字符串的值,而是返回一個新的字符串。一個常見的錯誤是:
string.sub(s, 2, -2)
認爲上面的這個函數會改變字符串 s 的值。若是你想修改一個字符串變量的值,你
必須將變量賦給一個新的字符串:
s = string.sub(s, 2, -2)
string.char 函數和 string.byte 函數用來將字符在字符和數字之間轉換。string.char 獲
取 0 個或多個整數,將每個數字轉換成字符,而後返回一個全部這些字符鏈接起來的
字符串。string.byte(s, i)將字符串 s 的第 i 個字符的轉換成整數;第二個參數是可選的,
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
缺省狀況下 i=1。下面的例子中,咱們假定字符用 ASCII 表示:
print(string.char(97))
i = 99; print(string.char(i, i+1, i+2))
print(string.byte("abc"))
print(string.byte("abc", 2))
print(string.byte("abc", -1))
--> a
--> cde
--> 97
--> 98
--> 99
141
上面最後一行,咱們使用負數索引訪問字符串的最後一個字符。
函數 string.format 在用來對字符串進行格式化的時候,特別是字符串輸出,是功能
強大的工具。這個函數有兩個參數,使用和 C 語言的 printf 函數幾乎如出一轍,你徹底
能夠照 C 語言的 printf 來使用這個函數。第一個參數爲格式化串:由指示符和控制格式
的字符組成。指示符後的控制格式的字符能夠爲:十進制'd';十六進制'x';八進制'o';
浮點數'f';字符串's'。在指示符'%'和控制格式字符之間還能夠有其餘的選項:用來控制
更詳細的格式,好比一個浮點數的小數的位數:
print(string.format("pi = %.4f", PI))
--> pi = 3.1416
d = 5; m = 11; y = 1990
print(string.format("%02d/%02d/%04d", d, m, y))
--> 05/11/1990
tag, title = "h1", "a title"
print(string.format("<%s>%s</%s>", tag, title, tag))
--> <h1>a title</h1>
第一個例子,%.4f 表明小數點後面有 4 位小數的浮點數。第二個例子%02d 表明以
固定的兩位顯示十進制數,不足的前面補 0。而%2d 前面沒有指定 0,不足兩位時會以
空白補足。對於格式串部分指示符得詳細描述清參考 lua 手冊,或者參考 C 手冊,由於
Lua 調用標準 C 的 printf 函數來實現最終的功能。
20.1 模式匹配函數
在 string 庫中功能最強大的函數是:string.find(字符串查找),string.gsub(全局字
符串替換),and string.gfind(全局字符串查找)。這些函數都是基於模式匹配的。
與其餘腳本語言不一樣的是,Lua並不使用POSIX規範的正則表達式2(也寫做regexp)
來進行模式匹配。主要的緣由出於程序大小方面的考慮:實現一個典型的符合POSIX標
準的regexp大概須要 4000 行代碼,這比整個Lua標準庫加在一塊兒都大。權衡之下,Lua
中的模式匹配的實現只用了 500 行代碼,固然這意味着不可能實現POSIX所規範的全部
更能。然而,Lua中的模式匹配功能是很強大的,而且包含了一些使用標準POSIX模式匹
2
譯註:POSIX是unix的工業標準,regexp最初來源於unix,POSIX對regexp也做了規範。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
配不容易實現的功能。
142
string.find 的基本應用就是用來在目標串(subject string)內搜索匹配指定的模式的
串。函數若是找到匹配的串返回他的位置,不然返回 nil.最簡單的模式就是一個單詞,
僅僅匹配單詞自己。好比,模式'hello'僅僅匹配目標串中的"hello"。當查找到模式的時候,
函數返回兩個值:匹配串開始索引和結束索引。
s = "hello world"
i, j = string.find(s, "hello")
print(i, j)
print(string.sub(s, i, j))
print(string.find(s, "world"))
i, j = string.find(s, "l")
print(i, j)
print(string.find(s, "lll"))
--> 3
--> nil
3
--> 1
--> 7
5
11
--> hello
例子中,匹配成功的時候,string.sub 利用 string.find 返回的值截取匹配的子串。(對
簡單模式而言,匹配的就是其自己)
string.find 函數第三個參數是可選的:標示目標串中搜索的起始位置。當咱們想查找
目標串中全部匹配的子串的時候,這個選項很是有用。咱們能夠不斷的循環搜索,每一
次從前一次匹配的結束位置開始。下面看一個例子,下面的代碼用一個字符串中全部的
新行構造一個表:
local t = {}
local i = 0
while true do
i = string.find(s, "\n", i+1)
if i == nil then break end
table.insert(t, i)
end
-- find 'next' newline
-- table to store the indices
後面咱們還會看到能夠使用 string.gfind 迭代子來簡化上面這個循環。
string.gsub 函數有三個參數:目標串,模式串,替換串。他基本做用是用來查找匹
配模式的串,並將使用替換串其替換掉:
s = string.gsub("Lua is cute", "cute", "great")
print(s)
print(s)
print(s)
--> Lua is great
--> axx xii
--> Lua is great
s = string.gsub("all lii", "l", "x")
s = string.gsub("Lua is great", "perl", "tcl")
第四個參數是可選的,用來限制替換的範圍:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
s = string.gsub("all lii", "l", "x", 1)
print(s)
print(s)
--> axl lii
--> axx lii
s = string.gsub("all lii", "l", "x", 2)
143
string.gsub 的第二個返回值表示他進行替換操做的次數。例如,下面代碼涌來計算
一個字符串中空格出現的次數:
_, count = string.gsub(str, " ", " ")
(注意,_ 只是一個啞元變量)
20.2 模式
你還能夠在模式串中使用字符類。字符類指能夠匹配一個特定字符集合內任何字符
的 模 式 項 。 比 如 , 字 符 類 %d 匹 配 任 意 數 字 。 所 以 你 可 以 使 用 模 式 串
'%d%d/%d%d/%d%d%d%d'搜索 dd/mm/yyyy 格式的日期:
s = "Deadline is 30/05/1999, firm"
date = "%d%d/%d%d/%d%d%d%d"
print(string.sub(s, string.find(s, date)))
--> 30/05/1999
下面的表列出了 Lua 支持的全部字符類:
.
%a
%c
%d
%l
%p
%s
%u
%w
%x
%z
任意字符
字母
控制字符
數字
小寫字母
標點字符
空白符
大寫字母
字母和數字
十六進制數字
表明 0 的字符
上面字符類的大寫形式表示小寫所表明的集合的補集。例如,'%A'非字母的字符:
print(string.gsub("hello, up-down!", "%A", "."))
--> hello..up.down. 4
(數字 4 不是字符串結果的一部分,他是 gsub 返回的第二個結果,表明發生替換的
次數。下面其餘的關於打印 gsub 結果的例子中將會忽略這個數值。)在模式匹配中有一
些特殊字符,他們有特殊的意義,Lua 中的特殊字符以下:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
().%+-*?[^$
144
'%' 用做特殊字符的轉義字符,所以 '%.' 匹配點;'%%' 匹配字符 '%'。轉義字符 '%'
不只能夠用來轉義特殊字符,還能夠用於全部的非字母的字符。當對一個字符有疑問的
時候,爲安全起見請使用轉義字符轉義他。
對 Lua 而言,模式串就是普通的字符串。他們和其餘的字符串沒有區別,也不會受
到特殊對待。只有他們被用做模式串用於函數的時候,'%' 才做爲轉義字符。因此,如
果你須要在一個模式串內放置引號的話,你必須使用在其餘的字符串中放置引號的方法
來處理,使用 '\' 轉義引號,'\' 是 Lua 的轉義符。你能夠使用方括號將字符類或者字符
括起來建立本身的字符類(譯者:Lua 稱之爲 char-set,就是指傳統正則表達式概念中的
括號表達式) 好比,。'[%w_]' 將匹配字母數字和下劃線,'[01]' 匹配二進制數字,'[%[%]]'
匹配一對方括號。下面的例子統計文本中元音字母出現的次數:
_, nvow = string.gsub(text, "[AEIOUaeiou]", "")
在 char-set 中能夠使用範圍表示字符的集合,第一個字符和最後一個字符之間用連
字符鏈接表示這兩個字符之間範圍內的字符集合。大部分的經常使用字符範圍都已經預約義
好了,因此通常你不須要本身定義字符的集合。好比,'%d' 表示 '[0-9]';'%x' 表示
'[0-9a-fA-F]' 。 然 而 , 如 果 你 想 查 找 八 進 制 數 , 你 可 能 更 喜 歡 使 用 '[0-7]' 而 不 是
'[01234567]'。你能夠在字符集(char-set)的開始處使用 '^' 表示其補集:'[^0-7]' 匹配任何
不是八進制數字的字符;'[^\n]' 匹配任何非換行符戶的字符。記住,能夠使用大寫的字
符類表示其補集:'%S' 比 '[^%s]' 要簡短些。
Lua 的字符類依賴於本地環境,因此 '[a-z]' 可能與 '%l' 表示的字符集不一樣。在一
般狀況下,後者包括 'ç' 和 'ã',而前者沒有。應該儘量的使用後者來表示字母,除非
出於某些特殊考慮,由於後者更簡單、方便、更高效。
能夠使用修飾符來修飾模式加強模式的表達能力,Lua 中的模式修飾符有四個:
+
*
-
?
匹配前一字符 1 次或屢次
匹配前一字符 0 次或屢次
匹配前一字符 0 次或屢次
匹配前一字符 0 次或 1 次
'+',匹配一個或多個字符,老是進行最長的匹配。好比,模式串 '%a+' 匹配一個或
多個字母或者一個單詞:
print(string.gsub("one, and two; and three", "%a+", "word"))
--> word, word word; word word
'%d+' 匹配一個或多個數字(整數):
i, j = string.find("the number 1298 is even", "%d+")
print(i,j)
--> 12
15
'*' 與 '+' 相似,可是他匹配一個字符 0 次或屢次出現.一個典型的應用是匹配空白。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
145
好比,爲了匹配一對圓括號()或者括號之間的空白,能夠使用 '%(%s*%)'。 '%s*' 用來(
匹配 0 個或多個空白。因爲圓括號在模式中有特殊的含義,因此咱們必須使用 '%' 轉義
他。)再看一個例子,'[_%a][_%w]*' 匹配 Lua 程序中的標示符:字母或者下劃線開頭的
字母下劃線數字序列。
'-' 與 '*' 同樣,都匹配一個字符的 0 次或屢次出現,可是他進行的是最短匹配。某
些時候這兩個用起來沒有區別,但有些時候結果將大相徑庭。好比,若是你使用模式
'[_%a][_%w]-' 來查找標示符,你將只能找到第一個字母,由於 '[_%w]-' 永遠匹配空。
另外一方面,假定你想查找 C 程序中的註釋,不少人可能使用 '/%*.*%*/'(也就是說 "/*"
後面跟着任意多個字符,而後跟着 "*/" )。然而,因爲 '.*' 進行的是最長匹配,這個模
式將匹配程序中第一個 "/*" 和最後一個 "*/" 之間全部部分:
test = "int x; /* x */ int y; /* y */"
print(string.gsub(test, "/%*.*%*/", "<COMMENT>"))
--> int x; <COMMENT>
然而模式 '.-' 進行的是最短匹配,她會匹配 "/*" 開始到第一個 "*/" 以前的部分:
test = "int x; /* x */ int y; /* y */"
print(string.gsub(test, "/%*.-%*/", "<COMMENT>"))
--> int x; <COMMENT> int y; <COMMENT>
'?' 匹配一個字符 0 次或 1 次。舉個例子,假定咱們想在一段文本內查找一個整數,
整數可能帶有正負號。模式 '[+-]?%d+' 符合咱們的要求,它能夠匹配像 "-12"、"23" 和
"+1009" 等數字。'[+-]' 是一個匹配 '+' 或者 '-' 的字符類;接下來的 '?' 意思是匹配前
面的字符類 0 次或者 1 次。
與其餘系統的模式不一樣的是,Lua 中的修飾符不能用字符類;不能將模式分組而後
使用修飾符做用這個分組。好比,沒有一個模式能夠匹配一個可選的單詞(除非這個單
詞只有一個字母)。下面我將看到,一般你能夠使用一些高級技術繞開這個限制。
以 '^' 開頭的模式只匹配目標串的開始部分,類似的,以 '$' 結尾的模式只匹配目
標串的結尾部分。這不只能夠用來限制你要查找的模式,還能夠定位(anchor)模式。
好比:
if string.find(s, "^%d") then ...
檢查字符串 s 是否以數字開頭,而
if string.find(s, "^[+-]?%d+$") then ...
檢查字符串 s 是不是一個整數。
'%b' 用來匹配對稱的字符。常寫爲 '%bxy' ,x 和 y 是任意兩個不一樣的字符;x 做爲
匹配的開始,y 做爲匹配的結束。好比,'%b()' 匹配以 '(' 開始,以 ')' 結束的字符串:
print(string.gsub("a (enclosed (in) parentheses) line",
"%b()", ""))
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
--> a line
146
經常使用的這種模式有:'%b()' ,'%b[]','%b%{%}' 和 '%b<>'。你也能夠使用任何字符
做爲分隔符。
20.3 捕獲(Captures)
Capture3是這樣一種機制:能夠使用模式串的一部分匹配目標串的一部分。將你想捕
獲的模式用圓括號括起來,就指定了一個capture。
在 string.find 使用 captures 的時候,函數會返回捕獲的值做爲額外的結果。這常被用
來將一個目標串拆分紅多個:
pair = "name = Anna"
_, _, key, value = string.find(pair, "(%a+)%s*=%s*(%a+)")
print(key, value)
--> name
Anna
'%a+' 表示菲空的字母序列;'%s*' 表示 0 個或多個空白。在上面的例子中,整個模
式表明:一個字母序列,後面是任意多個空白,而後是 '=' 再後面是任意多個空白,然
後是一個字母序列。兩個字母序列都是使用圓括號括起來的子模式,當他們被匹配的時
候,他們就會被捕獲。當匹配發生的時候,find 函數老是先返回匹配串的索引下標(上
面例子中咱們存儲啞元變量 _ 中),而後返回子模式匹配的捕獲部分。下面的例子狀況
相似:
date = "17/7/1990"
_, _, d, m, y = string.find(date, "(%d+)/(%d+)/(%d+)")
print(d, m, y)
--> 17 7 1990
咱們能夠在模式中使用向前引用, (d 表明 1-9 的數字)'%d'表示第 d 個捕獲的拷貝。
看個例子,假定你想查找一個字符串中單引號或者雙引號引發來的子串,你可能使用模
式 '["'].-["']',可是這個模式對處理相似字符串 "it's all right" 會出問題。爲了解決這個問
題,能夠使用向前引用,使用捕獲的第一個引號來表示第二個引號:
s = [[then he said: "it's all right"!]]
a, b, c, quotedPart = string.find(s, "(["'])(.-)%1")
print(quotedPart)
print(c)
--> it's all right
--> "
第一個捕獲是引號字符自己,第二個捕獲是引號中間的內容('.-' 匹配引號中間的子
串)。
捕獲值的第三個應用是用在函數 gsub 中。與其餘模式同樣,gsub 的替換串能夠包
譯註:下面譯爲捕獲或者capture,模式中捕獲的概念指,使用臨時變量來保存匹配的子模式,經常使用於
向前引用。
3
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
147
含 '%d',當替換髮生時他被轉換爲對應的捕獲值。(順便說一下,因爲存在這些狀況,
替換串中的字符 '%' 必須用 "%%" 表示)。下面例子中,對一個字符串中的每個字母
進行復制,並用連字符將複製的字母和原字母鏈接起來:
print(string.gsub("hello Lua!", "(%a)", "%1-%1"))
--> h-he-el-ll-lo-o L-Lu-ua-a!
下面代碼互換相鄰的字符:
print(string.gsub("hello Lua", "(.)(.)", "%2%1"))
--> ehll ouLa
讓咱們看一個更有用的例子,寫一個格式轉換器:從命令行獲取 LaTeX 風格的字符
串,形如:
\command{some text}
將它們轉換爲 XML 風格的字符串:
<command>some text</command>
對於這種狀況,下面的代碼能夠實現這個功能:
s = string.gsub(s, "\\(%a+){(.-)}", "<%1>%2</%1>")
好比,若是字符串 s 爲:
the \quote{task} is to \em{change} that.
調用 gsub 以後,轉換爲:
the <quote>task</quote> is to change that.
另外一個有用的例子是去除字符串首尾的空格:
function trim (s)
return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end
注意模式串的用法,兩個定位符('^' 和 '$')保證咱們獲取的是整個字符串。由於,
兩個 '%s*' 匹配首尾的全部空格,'.-' 匹配剩餘部分。還有一點須要注意的是 gsub 返回
兩個值,咱們使用額外的圓括號丟棄多餘的結果(替換髮生的次數)。
最後一個捕獲值應用之處多是功能最強大的。咱們能夠使用一個函數做爲
string.gsub 的第三個參數調用 gsub。在這種狀況下,string.gsub 每次發現一個匹配的時候
就會調用給定的做爲參數的函數,捕獲值能夠做爲被調用的這個函數的參數,而這個函
數的返回值做爲 gsub 的替換串。先看一個簡單的例子,下面的代碼將一個字符串中全局
變量$varname 出現的地方替換爲變量 varname 的值:
function expand (s)
s = string.gsub(s, "$(%w+)", function (n)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
return _G[n]
end)
return s
end
name = "Lua"; status = "great"
print(expand("$name is $status, isn't it?"))
--> Lua is great, isn't it?
148
若是你不能肯定給定的變量是否爲 string 類型,能夠使用 tostring 進行轉換:
function expand (s)
return (string.gsub(s, "$(%w+)", function (n)
return tostring(_G[n])
end))
end
print(expand("print = $print; a = $a"))
--> print = function: 0x8050ce0; a = nil
下面是一個稍微複雜點的例子,使用 loadstring 來計算一段文本內$後面跟着一對方
括號內表達式的值:
s = "sin(3) = $[math.sin(3)]; 2^5 = $[2^5]"
print((string.gsub(s, "$(%b[])", function (x)
x = "return " .. string.sub(x, 2, -2)
local f = loadstring(x)
return f()
end)))
--> sin(3) = 0.1411200080598672; 2^5 = 32
第一次匹配是 "$[math.sin(3)]",對應的捕獲爲 "$[math.sin(3)]",調用 string.sub 去
掉首尾的方括號,因此被加載執行的字符串是 "return math.sin(3)","$[2^5]" 的匹配狀況
相似。
咱們經常須要使用 string.gsub 遍歷字符串,而對返回結果不感興趣。好比,咱們收
集一個字符串中全部的單詞,而後插入到一個表中:
words = {}
string.gsub(s, "(%a+)", function (w)
table.insert(words, w)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end)
149
若是字符串 s 爲 "hello hi, again!",上面代碼的結果將是:
{"hello", "hi", "again"}
使用 string.gfind 函數能夠簡化上面的代碼:
words = {}
for w in string.gfind(s, "(%a)") do
table.insert(words, w)
end
gfind 函數比較適合用於範性 for 循環。他能夠遍歷一個字符串內全部匹配模式的子
串。咱們能夠進一步的簡化上面的代碼,調用 gfind 函數的時候,若是不顯示的指定捕
獲,函數將捕獲整個匹配模式。因此,上面代碼能夠簡化爲:
words = {}
for w in string.gfind(s, "%a") do
table.insert(words, w)
end
下面的例子咱們使用 URL 編碼,URL 編碼是 HTTP 協議來用發送 URL 中的參數進
行的編碼。這種編碼將一些特殊字符(好比 '='、'&'、'+')轉換爲 "%XX" 形式的編碼,
其中 XX 是字符的 16 進製表示,而後將空白轉換成 '+'。好比,將字符串 "a+b = c" 編
碼爲 "a%2Bb+%3D+c"。最後,將參數名和參數值之間加一個 '=';在 name=value 對之
間加一個 "&"。好比字符串:
name = "al"; query = "a+b = c"; q="yes or no"
被編碼爲:
name=al&query=a%2Bb+%3D+c&q=yes+or+no
如今,假如咱們想講這 URL 解碼並把每一個值存儲到表中,下標爲對應的名字。下面
的函數實現瞭解碼功能:
function unescape (s)
s = string.gsub(s, "+", " ")
s = string.gsub(s, "%%(%x%x)", function (h)
return string.char(tonumber(h, 16))
end)
return s
end
第一個語句將 '+' 轉換成空白,第二個 gsub 匹配全部的 '%' 後跟兩個數字的 16 進
制數,而後調用一個匿名函數,匿名函數將 16 進制數轉換成一個數字(tonumber 在 16
進制狀況下使用的)而後再轉化爲對應的字符。好比:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print(unescape("a%2Bb+%3D+c"))
--> a+b = c
150
對於 name=value 對,咱們使用 gfind 解碼,由於 names 和 values 都不能包含 '&' 和 '='
咱們能夠用模式 '[^&=]+' 匹配他們:
cgi = {}
function decode (s)
for name, value in string.gfind(s, "([^&=]+)=([^&=]+)") do
name = unescape(name)
value = unescape(value)
cgi[name] = value
end
end
調用 gfind 函數匹配全部的 name=value 對,對於每個 name=value 對,迭代子將其
相對應的捕獲的值返回給變量 name 和 value。循環體內調用 unescape 函數解碼 name 和
value 部分,並將其存儲到 cgi 表中。
與解碼對應的編碼也很容易實現。首先,咱們寫一個 escape 函數,這個函數將全部
的特殊字符轉換成 '%' 後跟字符對應的 ASCII 碼轉換成兩位的 16 進制數字(不足兩位,
前面補 0),而後將空白轉換爲 '+':
function escape (s)
s = string.gsub(s, "([&=+%c])", function (c)
return string.format("%%%02X", string.byte(c))
end)
s = string.gsub(s, " ", "+")
return s
end
編碼函數遍歷要被編碼的表,構造最終的結果串:
function encode (t)
local s = ""
for k,v in pairs(t) do
s = s .. "&" .. escape(k) .. "=" .. escape(v)
end
return string.sub(s, 2)
end
t = {name = "al", query = "a+b = c", q="yes or no"}
print(encode(t)) --> q=yes+or+no&query=a%2Bb+%3D+c&name=al
-- remove first `&'
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
151
20.4 轉換的技巧(Tricks of the Trade)
模式匹配對於字符串操縱來講是強大的工具,你可能只須要簡單的調用 string.gsub
和 find 就能夠完成複雜的操做,然而,由於它功能強大你必須謹慎的使用它,不然會帶
來意想不到的結果。
對正常的解析器而言,模式匹配不是一個替代品。對於一個 quick-and-dirty 程序,
你能夠在源代碼上進行一些有用的操做,但很難完成一個高質量的產品。前面提到的匹
配 C 程序中註釋的模式是個很好的例子:'/%*.-%*/'。若是你的程序有一個字符串包含了
"/*",最終你將獲得錯誤的結果:
test = [[char s[] = "a /* here"; /* a tricky string */]]
print(string.gsub(test, "/%*.-%*/", "<COMMENT>"))
--> char s[] = "a <COMMENT>
雖然這樣內容的字符串很罕見,若是是你本身使用的話上面的模式可能還湊活。但
你不能將一個帶有這種毛病的程序做爲產品出售。
通常狀況下, 中的模式匹配效率是不錯的:Lua一個奔騰 333MHz 機器在一個有 200K
字符的文本內匹配全部的單詞(30K 的單詞)只須要 1/10 秒。可是你不能掉以輕心,應該
一直對不一樣的狀況特殊對待,儘量的更明確的模式描述。一個限制寬鬆的模式比限制
嚴格的模式可能慢不少。一個極端的例子是模式 '(.-)%$' 用來獲取一個字符串內$符號以
前全部的字符,若是目標串中存在$符號,沒有什麼問題;可是若是目標串中不存在$符
號。上面的算法會首先從目標串的第一個字符開始進行匹配,遍歷整個字符串以後沒有
找到$符號,而後從目標串的第二個字符開始進行匹配,……這將花費原來平方次冪的時
間,致使在一個奔騰 333MHz 的機器中須要 3 個多小時來處理一個 200K 的文本串。可
以使用下面這個模式避免上面的問題 '^(.-)%$'。定位符^告訴算法若是在第一個位置沒有
沒找到匹配的子串就中止查找。使用這個定位符以後,一樣的環境也只須要不到 1/10 秒
的時間。
也須要當心空模式:匹配空串的模式。好比,若是你打算用模式 '%a*' 匹配名字,
你會發現處處都是名字:
i, j = string.find(";$% **#$hello13", "%a*")
print(i,j)
--> 1 0
這個例子中調用 string.find 正確的在目標串的開始處匹配了空字符。永遠不要寫一
個以 '-' 開頭或者結尾的模式,由於它將匹配空串。這個修飾符得周圍老是須要一些東
西來定位他的擴展。類似的,一個包含 '.*' 的模式是一個須要注意的,由於這個結構可
能會比你預算的擴展的要多。
有時候,使用 Lua 自己構造模式是頗有用的。看一個例子,咱們查找一個文本中行
字符大於 70 個的行,也就是匹配一個非換行符以前有 70 個字符的行。咱們使用字符類
'[^\n]'表示非換行符的字符。因此,咱們能夠使用這樣一個模式來知足咱們的須要:重複
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
152
匹配單個字符的模式 70 次,後面跟着一個匹配一個字符 0 次或屢次的模式。咱們不手工
來寫這個最終的模式,而使用函數 string.rep:
pattern = string.rep("[^\n]", 70) .. "[^\n]*"
另外一個例子,假如你想進行一個大小寫無關的查找。方法之一是將任何一個字符 x
變爲字符類 '[xX]'。咱們也能夠使用一個函數進行自動轉換:
function nocase (s)
s = string.gsub(s, "%a", function (c)
return string.format("[%s%s]", string.lower(c),
string.upper(c))
end)
return s
end
print(nocase("Hi there!"))
--> [hH][iI] [tT][hH][eE][rR][eE]!
有時候你可能想要將字符串 s1 轉化爲 s2,而不關心其中的特殊字符。若是字符串
s1 和 s2 都是字符串序列,你能夠給其中的特殊字符加上轉義字符來實現。可是若是這
些字符串是變量呢,你能夠使用 gsub 來完成這種轉義:
s1 = string.gsub(s1, "(%W)", "%%%1")
s2 = string.gsub(s2, "%%", "%%%%")
在查找串中,咱們轉義了全部的非字母的字符。在替換串中,咱們只轉義了 '%' 。
另外一個對模式匹配而言有用的技術是在進行真正處理以前,對目標串先進行預處理。一
個預處理的簡單例子是,將一段文本內的雙引號內的字符串轉換爲大寫,可是要注意雙
引號之間能夠包含轉義的引號("""):
這是一個典型的字符串例子:
"This is "great"!".
咱們處理這種狀況的方法是,預處理文本把有問題的字符序列轉換成其餘的格式。
好比,咱們能夠將 """ 編碼爲 "\1",可是若是原始的文本中包含 "\1",咱們又陷入麻煩
之中。一個避免這個問題的簡單的方法是將全部 "\x" 類型的編碼爲 "\ddd",其中 ddd
是字符 x 的十進制表示:
function code (s)
return (string.gsub(s, "\\(.)", function (x)
return string.format("\\%03d", string.byte(x))
end))
end
注意,原始串中的 "\ddd" 也會被編碼,解碼是很容易的:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
function decode (s)
return (string.gsub(s, "\\(%d%d%d)", function (d)
return "\" .. string.char(d)
end))
end
153
若是被編碼的串不包含任何轉義符,咱們能夠簡單的使用 ' ".-" ' 來查找雙引號字符
串:
s = [[follows a typical string: "This is "great"!".]]
s = code(s)
s = string.gsub(s, '(".-")', string.upper)
s = decode(s)
print(s)
--> follows a typical string: "THIS IS "GREAT"!".
更緊縮的形式:
print(decode(string.gsub(code(s), '(".-")', string.upper)))
咱們回到前面的一個例子,轉換\command{string}這種格式的命令爲 XML 風格:
<command>string</command>
可是這一次咱們原始的格式中能夠包含反斜槓做爲轉義符,這樣就能夠使用"\"、"\{"
和 "\}",分別表示 '\'、'{' 和 '}'。爲了不命令和轉義的字符混合在一塊兒,咱們應該首
先將原始串中的這些特殊序列從新編碼,然而,與上面的一個例子不一樣的是,咱們不能
轉義全部的 \x,由於這樣會將咱們的命令(\command)也轉換掉。這裏,咱們僅當 x
不是字符的時候纔對 \x 進行編碼:
function code (s)
return (string.gsub(s, '\\(%A)', function (x)
return string.format("\\%03d", string.byte(x))
end))
end
解碼部分和上面那個例子相似,可是在最終的字符串中不包含反斜槓,因此咱們可
直接調用 string.char:
function decode (s)
return (string.gsub(s, '\\(%d%d%d)', string.char))
end
s = [[a \emph{command} is written as \\command\{text\}.]]
s = code(s)
s = string.gsub(s, "\\(%a+){(.-)}", "<%1>%2</%1>")
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
154
print(decode(s))
--> a <emph>command</emph> is written as \command{text}.
咱們最後一個例子是處理 CSV(逗號分割)的文件,不少程序都使用這種格式的文
本,好比 Microsoft Excel。CSV 文件十多條記錄的列表,每一條記錄一行,一行內值與
值之間逗號分割,若是一個值內也包含逗號這個值必須用雙引號引發來,若是值內還包
含雙引號,需使用雙引號轉義雙引號(就是兩個雙引號表示一個) 看例子,,下面的數組:
{'a b', 'a,b', 'a,"b"c', 'hello "world"!', }
能夠看做爲:
a b,"a,b"," a,""b""c", hello "world"!,
將一個字符串數組轉換爲 CSV 格式的文件是很是容易的。咱們要作的只是使用逗號
將全部的字符串鏈接起來:
function toCSV (t)
local s = ""
for _,p in pairs(t) do
s = s .. "," .. escapeCSV(p)
end
return string.sub(s, 2)
end
-- remove first comma
若是一個字符串包含逗號活着引號在裏面,咱們須要使用引號將這個字符串引發來,
並轉義原始的引號:
function escapeCSV (s)
if string.find(s, '[,"]') then
s = '"' .. string.gsub(s, '"', '""') .. '"'
end
return s
end
將 CSV 文件內容存放到一個數組中稍微有點難度,由於咱們必須區分出位於引號中
間的逗號和分割域的逗號。咱們能夠設法轉義位於引號中間的逗號,然而並非全部的
引號都是做爲引號存在,只有在逗號以後的引號纔是一對引號的開始的那一個。只有不
在引號中間的逗號纔是真正的逗號。這裏面有太多的細節須要注意,好比,兩個引號可
能表示單個引號,可能表示兩個引號,還有可能表示空:
"hello""hello", "",""
這個例子中,第一個域是字符串 "hello"hello",第二個域是字符串 " """(也就是一個
空白加兩個引號),最後一個域是一個空串。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
155
咱們能夠屢次調用 gsub 來處理這些狀況,可是對於這個任務使用傳統的循環(在每
個域上循環)來處理更有效。循環體的主要任務是查找下一個逗號;並將域的內容存放
到一個表中。對於每個域,咱們循環查找封閉的引號。循環內使用模式 ' "("?) ' 來查
找一個域的封閉的引號:若是一個引號後跟着一個引號,第二個引號將被捕獲並賦給一
個變量 c,意味着這仍然不是一個封閉的引號
function fromCSV (s)
s = s .. ','
local t = {}
repeat
-- next field is quoted? (start with `"'?)
if string.find(s, '^"', fieldstart) then
local a, c
local i = fieldstart
repeat
-- find closing quote
a, i, c = string.find(s, '"("?)', i+1)
until c ~= '"'
-- quote not followed by quote?
if not i then error('unmatched "') end
local f = string.sub(s, fieldstart+1, i-1)
table.insert(t, (string.gsub(f, '""', '"')))
fieldstart = string.find(s, ',', i) + 1
else
-- unquoted; find next comma
local nexti = string.find(s, ',', fieldstart)
table.insert(t, string.sub(s, fieldstart,
nexti-1))
fieldstart = nexti + 1
end
until fieldstart > string.len(s)
return t
end
t = fromCSV('"hello "" hello", "",""')
for i, s in ipairs(t) do print(i, s) end
--> 1
--> 2
--> 3
hello " hello
""
-- ending comma
-- table to collect fields
local fieldstart = 1
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
156
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
157
第 21 章 IO 庫
I/O 庫爲文件操做提供兩種模式。簡單模式(simple model)擁有一個當前輸入文件
和一個當前輸出文件,而且提供針對這些文件相關的操做。徹底模式(complete model)
使用外部的文件句柄來實現。它以一種面對對象的形式,將全部的文件操做定義爲文件
句柄的方法。簡單模式在作一些簡單的文件操做時較爲合適。在本書的前面部分咱們一
直都在使用它。可是在進行一些高級的文件操做的時候,簡單模式就顯得力不從心。例
如同時讀取多個文件這樣的操做,使用徹底模式則較爲合適。I/O 庫的全部函數都放在
表(table)io 中。
21.1 簡單 I/O 模式
簡單模式的全部操做都是在兩個當前文件之上。I/O 庫將當前輸入文件做爲標準輸
入(stdin),將當前輸出文件做爲標準輸出(stdout)。這樣當咱們執行 io.read,就是在標
準輸入中讀取一行。咱們能夠使用 io.input 和 io.output 函數來改變當前文件。例如
io.input(filename)就是打開給定文件(以讀模式),並將其設置爲當前輸入文件。接下來
全部的輸入都來自於該文,直到再次使用 io.input。io.output 函數。相似於 io.input。一旦
產生錯誤兩個函數都會產生錯誤。若是你想直接控制錯誤必須使用徹底模式中 io.read 函
數。寫操做較讀操做簡單,咱們先從寫操做入手。下面這個例子裏函數 io.write 獲取任
意數目的字符串參數,接着將它們寫到當前的輸出文件。一般數字轉換爲字符串是按照
一般的規則,若是要控制這一轉換,能夠使用 string 庫中的 format 函數:
> io.write("sin (3) = ", math.sin(3), "\n")
--> sin (3) = 0.1411200080598672
> io.write(string.format("sin (3) = %.4f\n", math.sin(3)))
--> sin (3) = 0.1411
在編寫代碼時應當避免像 io.write(a..b..c);這樣的書寫,這同 io.write(a,b,c)的效果是
同樣的。可是後者由於避免了串聯操做,而消耗較少的資源。原則上當你進行粗略(quick
and dirty)編程,或者進行排錯時常使用 print 函數。當須要徹底控制輸出時使用 write。
> print("hello", "Lua"); print("Hi")
--> hello
--> Hi
> io.write("hello", "Lua"); io.write("Hi", "\n")
--> helloLuaHi
Lua
Write 函數與 print 函數不一樣在於,write 不附加任何額外的字符到輸出中去,例如制
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
158
表符,換行符等等。還有 write 函數是使用當前輸出文件,而 print 始終使用標準輸出。
另外 print 函數會自動調用參數的 tostring 方法,因此能夠顯示出表(tables) (functions)函數
和 nil。
read 函數從當前輸入文件讀取串,由它的參數控制讀取的內容:
"*all"
"*line"
"*number"
num
讀取整個文件
讀取下一行
從串中轉換出一個數值
讀取 num 個字符到串
io.read("*all")函數從當前位置讀取整個輸入文件。若是當前位置在文件末尾,或者
文件爲空,函數將返回空串。因爲 Lua 對長串類型值的有效管理,在 Lua 中使用過濾器
的簡單方法就是讀取整個文件到串中去,處理完以後(例如使用函數 gsub),接着寫到
輸出中去:
t = io.read("*all")
t = string.gsub(t, ...)
io.write(t)
-- read the whole file
-- do the job
-- write the file
如下代碼是一個完整的處理字符串的例子。文件的內容要使用 MIME(多用途的網
際郵件擴充協議)中的 quoted-printable 碼進行編碼。以這種形式編碼,非 ASCII 字符將
被編碼爲「=XX」,其中 XX 是該字符值的十六進制表示,爲表示一致性「=」字符一樣
要求被改寫。在 gsub 函數中的「模式」參數的做用就是獲得全部值在 128 到 255 之間的
字符,給它們加上等號標誌。
t = io.read("*all")
t = string.gsub(t, "([\128-\255=])", function (c)
return string.format("=%02X", string.byte(c))
end)
io.write(t)
該程序在奔騰 333MHz 環境下轉換 200k 字符須要 0.2 秒。
io.read("*line")函數返回當前輸入文件的下一行(不包含最後的換行符)。當到達文
件末尾,返回值爲 nil(表示沒有下一行可返回)。該讀取方式是 read 函數的默認方式,
因此能夠簡寫爲 io.read()。一般使用這種方式讀取文件是因爲對文件的操做是天然逐行
進行的,不然更傾向於使用*all 一次讀取整個文件,或者稍後見到的逐塊的讀取文件。
下面的程序演示了應如何使用該模式讀取文件。此程序複製當前輸入文件到輸出文件,
並記錄行數。
local count = 1
while true do
local line = io.read()
if line == nil then break end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
io.write(string.format("%6d ", count), line, "\n")
count = count + 1
end
159
然而爲了在整個文件中逐行迭代。咱們最好使用 io.lines 迭代器。例如對文件的行進
行排序的程序以下:
local lines = {}
-- read the lines in table 'lines'
for line in io.lines() do
table.insert(lines, line)
end
-- sort
table.sort(lines)
-- write all the lines
for i, l in ipairs(lines) do io.write(l, "\n") end
在奔騰 333MHz 上該程序處理處理 4.5MB 大小,32K 行的文件耗時 1.8 秒,比使用
高度優化的 C 語言系統排序程序快 0.6 秒。io.read("*number")函數從當前輸入文件中讀
取出一個數值。只有在該參數下 read 函數才返回數值,而不是字符串。當須要從一個文
件中讀取大量數字時,數字間的字符串爲空白能夠顯著的提升執行性能。*number 選項
會跳過兩個可被識別數字之間的任意空格。這些可識別的字符串能夠是-三、+5.二、1000,
和 -3.4e-23。若是在當前位置找不到一個數字(因爲格式不對,或者是到了文件的結尾),
則返回 nil 能夠對每一個參數設置選項,函數將返回各自的結果。假若有一個文件每行包
含三個數字:
6.0
4.3
...
-3.23
234
15e12
1000001
如今要打印出每行最大的一個數,就能夠使用一次 read 函數調用來讀取出每行的全
部三個數字:
while true do
local n1, n2, n3 = io.read("*number", "*number", "*number")
if not n1 then break end
print(math.max(n1, n2, n3))
end
在任何狀況下,都應該考慮選擇使用 io.read 函數的 " *.all " 選項讀取整個文件,然
後使用 gfind 函數來分解:
local pat = "(%S+)%s+(%S+)%s+(%S+)%s+"
for n1, n2, n3 in string.gfind(io.read("*all"), pat) do
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print(math.max(n1, n2, n3))
end
160
除了基本讀取方式外,還能夠將數值 n 做爲 read 函數的參數。在這樣的狀況下 read
函數將嘗試從輸入文件中讀取 n 個字符。若是沒法讀取到任何字符(已經到了文件末尾),
函數返回 nil。不然返回一個最多包含 n 個字符的串。如下是關於該 read 函數參數的一
個進行高效文件複製的例子程序(固然是指在 Lua 中)
local size = 2^13
while true do
local block = io.read(size)
if not block then break end
io.write(block)
end
-- good buffer size (8K)
特別的,io.read(0)函數的能夠用來測試是否到達了文件末尾。若是不是返回一個空
串,若是已經是文件末尾返回 nil。
21.2 徹底 I/O 模式
爲了對輸入輸出的更全面的控制,能夠使用徹底模式。徹底模式的核心在於文件句
柄(file handle)。該結構相似於 C 語言中的文件流(FILE*),其呈現了一個打開的文件
以及當前存取位置。打開一個文件的函數是 io.open。它模仿 C 語言中的 fopen 函數,同
樣須要打開文件的文件名參數,打開模式的字符串參數。模式字符串能夠是 "r"(讀模
式),"w"(寫模式,對數據進行覆蓋),或者是 "a"(附加模式)。而且字符 "b" 可附加
在後面表示以二進制形式打開文件。正常狀況下 open 函數返回一個文件的句柄。若是發
生錯誤,則返回 nil,以及一個錯誤信息和錯誤代碼。
print(io.open("non-existent file", "r"))
--> nil
No such file or directory
2
print(io.open("/etc/passwd", "w"))
--> nil
Permission denied
13
錯誤代碼的定義由系統決定。
如下是一段典型的檢查錯誤的代碼:
local f = assert(io.open(filename, mode))
若是 open 函數失敗,錯誤信息做爲 assert 的參數,由 assert 顯示出信息。文件打開
後就能夠用 read 和 write 方法對他們進行讀寫操做。它們和 io 表的 read/write 函數相似,
可是調用方法上不一樣,必須使用冒號字符,做爲文件句柄的方法來調用。例如打開一個
文件並所有讀取。能夠使用以下代碼。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
local f = assert(io.open(filename, "r"))
local t = f:read("*all")
f:close()
161
同 C 語言中的流(stream)設定相似, 庫提供三種預約義的句柄:I/Oio.stdin、io.stdout
和 io.stderr。所以能夠用以下代碼直接發送信息到錯誤流(error stream)。
io.stderr:write(message)
咱們還能夠將徹底模式和簡單模式混合使用。使用沒有任何參數的 io.input()函數得
到當前的輸入文件句柄;使用帶有參數的 io.input(handle)函數設置當前的輸入文件爲
handle 句柄表明的輸入文件。(一樣的用法對於 io.output 函數也適用)例如要實現暫時的
改變當前輸入文件,能夠使用以下代碼:
local temp = io.input()
io.input("newinput")
...
io.input():close()
io.input(temp)
-- save current file
-- open a new current file
-- do something with new input
-- close current file
-- restore previous current file
21.2.1 I/O 優化的一個小技巧
因爲一般 Lua 中讀取整個文件要比一行一行的讀取一個文件快的多。儘管咱們有時
候針對較大的文件(幾十,幾百兆),不可能把一次把它們讀取出來。要處理這樣的文件
咱們仍然能夠一段一段(例如 8kb 一段)的讀取它們。同時爲了不切割文件中的行,
還要在每段後加上一行:
local lines, rest = f:read(BUFSIZE, "*line")
以上代碼中的 rest 就保存了任何可能被段劃分切斷的行。而後再將段(chunk)和行
接起來。這樣每一個段就是以一個完整的行結尾的了。如下代碼就較爲典型的使用了這一
技巧。該段程序實現對輸入文件的字符,單詞,行數的計數。
local BUFSIZE = 2^13
local f = io.input(arg[1])
local cc, lc, wc = 0, 0, 0
while true do
local lines, rest = f:read(BUFSIZE, "*line")
if not lines then break end
if rest then lines = lines .. rest .. '\n' end
cc = cc + string.len(lines)
-- count words in the chunk
-- 8K
-- open input file
-- char, line, and word counts
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
local _,t = string.gsub(lines, "%S+", "")
wc = wc + t
-- count newlines in the chunk
_,t = string.gsub(lines, "\n", "\n")
lc = lc + t
end
print(lc, wc, cc)
162
21.2.2 二進制文件
默認的簡單模式老是以文本模式打開。在 Unix 中二進制文件和文本文件並無區
別,可是在如 Windows 這樣的系統中,二進制文件必須以顯式的標記來打開文件。控制
這樣的二進制文件,你必須將「b」標記添加在 io.open 函數的格式字符串參數中。在 Lua
中二進制文件的控制和文本相似。一個串能夠包含任何字節值,庫中幾乎全部的函數都
能夠用來處理任意字節值。(你甚至能夠對二進制的「串」進行模式比較,只要串中不存
在 0 值。若是想要進行 0 值字節的匹配,你能夠使用%z 代替)這樣使用*all 模式就是讀
取整個文件的值,使用數字 n 就是讀取 n 個字節的值。如下是一個將文本文件從 DOS
模式轉換到 Unix 模式的簡單程序。(這樣轉換過程就是將「回車換行字符」替換成「換
行字符」)由於是以二進制形式(原稿是 Text Mode!。!??)打開這些文件的,這裏無
法使用標準輸入輸入文件(stdin/stdout)。因此使用程序中提供的參數來獲得輸入、輸出
文件名。
local inp = assert(io.open(arg[1], "rb"))
local out = assert(io.open(arg[2], "wb"))
local data = inp:read("*all")
data = string.gsub(data, "\r\n", "\n")
out:write(data)
assert(out:close())
能夠使用以下的命令行來調用該程序。
> lua prog.lua file.dos file.unix
第二個例子程序:打印在二進制文件中找到的全部特定字符串。該程序定義了一種
最少擁有六個「有效字符」,以零字節值結尾的特定串。(本程序中「有效字符」定義爲
文本數字、標點符號和空格符,由變量 validchars 定義。)在程序中咱們使用鏈接和
string.rep 函數建立 validchars,以%z 結尾來匹配串的零結尾。
local f = assert(io.open(arg[1], "rb"))
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
local data = f:read("*all")
local validchars = "[%w%p%s]"
local pattern = string.rep(validchars, 6) .. "+%z"
for w in string.gfind(data, pattern) do
print(w)
end
163
最後一個例子:該程序對二進制文件進行一次值分析4(Dump)。程序的第一個參數
是輸入文件名,輸出爲標準輸出。其按照 10 字節爲一段讀取文件,將每一段各字節的十
六進製表示顯示出來。接着再以文本的形式寫出該段,並將控制字符轉換爲點號。
local f = assert(io.open(arg[1], "rb"))
local block = 10
while true do
local bytes = f:read(block)
if not bytes then break end
for b in string.gfind(bytes, ".") do
io.write(string.format("%02X ", string.byte(b)))
end
io.write(string.rep("
end
", block - string.len(bytes) + 1))
io.write(string.gsub(bytes, "%c", "."), "\n")
若是以 vip 來命名該程序腳本文件。能夠使用以下命令來執行該程序處理其自身:
prompt> lua vip vip
在 Unix 系統中它將會會產生一個以下的輸出樣式:
6C 6F 63 61 6C 20 66 20 3D 20
61 73 73 65 72 74 28 69 6F 2E
6F 70 65 6E 28 61 72 67 5B 31
5D 2C 20 22 72 62 22 29 29 0A
...
22 25 63 22 2C 20 22 2E 22 29
2C 20 22 5C 6E 22 29 0A 65 6E
64 0A
"%c", ".")
, "\n").en
d.
local f =
assert(io.
open(arg[1
], "rb")).
21.3 關於文件的其它操做
函數 tmpfile 函數用來返回零時文件的句柄,而且其打開模式爲 read/write 模式。該
4
譯註:獲得相似於十六進制編輯器的一個界面顯示
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
164
零時文件在程序執行完後會自動進行清除。函數 flush 用來應用針對文件的全部修改。同
write 函數同樣,該函數的調用既能夠按函數調用的方法使用 io.flush()來應用當前輸出文
件;也能夠按文件句柄方法的樣式 f:flush()來應用文件 f。函數 seek 用來獲得和設置一個
文件的當前存取位置。它的通常形式爲 filehandle:seek(whence,offset)。Whence 參數是一
個表示偏移方式的字符串。它能夠是 "set",偏移值是從文件頭開始;"cur",偏移值從
當前位置開始;"end",偏移值從文件尾往前計數。offset 即爲偏移的數值,由 whence 的
值和 offset 相結合獲得新的文件讀取位置。該位置是實際從文件開頭計數的字節數。
whence 的默認值爲 "cur",offset 的默認值爲 0。這樣調用 file:seek()獲得的返回值就是文
件當前的存取位置,且保持不變。file:seek("set")就是將文件的存取位置重設到文件開頭。
(返回值固然就是 0)。而 file:seek("end")就是將位置設爲文件尾,同時就能夠獲得文件
的大小。以下的代碼實現了獲得文件的大小而不改變存取位置。
function fsize (file)
local current = file:seek()
local size = file:seek("end")
file:seek("set", current)
return size
end
-- get current position
-- get file size
-- restore position
以上的幾個函數在出錯時都將返回一個包含了錯誤信息的 nil 值。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
165
第 22 章 操做系統庫
操做系統庫包含了文件管理,系統時鐘等等與操做系統相關信息。這些函數定義在
表(table)os 中。定義該庫時考慮到 Lua 的可移植性,由於 Lua 是以 ANSI C 寫成的,
因此只能使用 ANSI 定義的一些標準函數。許多的系統屬性並不包含在 ANSI 定義中,
例如目錄管理,套接字等等。因此在系統庫裏並無提供這些功能。另外有一些沒有包
含在主體發行版中的 Lua 庫提供了操做系統擴展屬性的訪問。例如 posix 庫,提供了對
POSIX 1 標準的徹底支持;在好比 luasocket 庫,提供了網絡支持。
在文件管理方面操做系統庫就提供了 os.rename 函數(修改文件名)和 os.remove 函
數(刪除文件)。
22.1 Date 和 Time
time 和 date 兩個函數在 Lua 中實現全部的時鐘查詢功能。函數 time 在沒有參數時
返回當前時鐘的數值。(在許多系統中該數值是當前距離某個特定時間的秒數。)當爲函
數調用附加一個特殊的時間表時,該函數就是返回距該表描述的時間的數值。這樣的時
間表有以下的區間:
year
month
day
hour
min
sec
isdst
a full year
01-12
01-31
01-31
00-59
00-59
a boolean, true if daylight saving
前三項是必需的,若是未定義後幾項,默認時間爲正午(12:00:00)。若是是在里約
熱內盧(格林威治向西三個時區)的一臺 Unix 計算機上(相對時間爲 1970 年 1 月 1 日,
00:00:00)執行以下代碼,其結果將以下。
-- obs: 10800 = 3*60*60 (3 hours)
print(os.time{year=1970, month=1, day=1, hour=0})
--> 10800
print(os.time{year=1970, month=1, day=1, hour=0, sec=1})
--> 10801
print(os.time{year=1970, month=1, day=1})
--> 54000
(obs: 54000 = 10800 + 12*60*60)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
166
函數 data,無論它的名字是什麼,實際上是 time 函數的一種「反函數」。它將一個表
示日期和時間的數值,轉換成更高級的表現形式。其第一個參數是一個格式化字符串,
描述了要返回的時間形式。第二個參數就是時間的數字表示,默認爲當前的時間。使用
格式字符 "*t",建立一個時間表。例以下面這段代碼:
temp = os.date("*t", 906000490)
則會產生表
{year = 1998, month = 9, day = 16, yday = 259, wday = 4,
hour = 23, min = 48, sec = 10, isdst = false}
不難發現該表中除了使用到了在上述時間表中的區域之外,這個表還提供了星期
(wday,星期天爲 1)和一年中的第幾天(yday,一月一日爲 1)除了使用 "*t" 格式字
符串外,若是使用帶標記(見下表)的特殊字符串,os.data 函數會將相應的標記位以時
間信息進行填充,獲得一個包含時間的字符串。(這些特殊標記都是以 "%" 和一個字母
的形式出現)以下:
print(os.date("today is %A, in %B"))
--> today is Tuesday, in May
print(os.date("%x", 906000490))
--> 09/16/1998
這些時間輸出的字符串表示是通過本地化的。因此若是是在巴西(葡萄牙語系),
"%B" 獲得的就是 "setembro"(譯者按:大概是葡萄牙語九月?),"%x" 獲得的就是
"16/09/98"(月日次序不一樣)。標記的意義和顯示實例總結以下表。實例的時間是在 1998
年九月 16 日,星期三,23:48:10。返回值爲數字形式的還列出了它們的範圍。(都是按
照英語系的顯示描述的,也比較簡單,就不煩了)
%a
%A
%b
%B
%c
%d
%H
%I
%M
%m
%p
%S
%w
%x
%X
abbreviated weekday name (e.g., Wed)
full weekday name (e.g., Wednesday)
abbreviated month name (e.g., Sep)
full month name (e.g., September)
date and time (e.g., 09/16/98 23:48:10)
day of the month (16) [01-31]
hour, using a 24-hour clock (23) [00-23]
hour, using a 12-hour clock (11) [01-12]
minute (48) [00-59]
month (09) [01-12]
either "am" or "pm" (pm)
second (10) [00-61]
weekday (3) [0-6 = Sunday-Saturday]
date (e.g., 09/16/98)
time (e.g., 23:48:10)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
%Y
%y
%%
full year (1998)
two-digit year (98) [00-99]
the character '%'
167
事實上若是不使用任何參數就調用 date,就是以%c 的形式輸出。這樣就是獲得通過
格式化的完整時間信息。還要注意%x、%X 和%c 由所在地區和計算機系統的改變會發
生變化。若是該字符串要肯定下來(例如肯定爲 mm/dd/yyyy),能夠使用明確的字符串
格式方式(例如"%m/%d/%Y")。
函數 os.clock 返回執行該程序 CPU 花去的時鐘秒數。該函數經常使用來測試一段代碼。
local x = os.clock()
local s = 0
for i=1,100000 do s = s + i end
print(string.format("elapsed time: %.2f\n", os.clock() - x))
22.2 其它的系統調用
函數 os.exit 終止一個程序的執行。函數 os.getenv 獲得「環境變量」的值。以「變量
名」做爲參數,返回該變量值的字符串:
print(os.getenv("HOME"))
--> /home/lua
若是沒有該環境變量則返回 nil。函數 os.execute 執行一個系統命令(和 C 中的 system
函 數 等 價 ) 該 函 數 獲 取 一 個 命 令 字 符 串 , 返 回 一 個 錯 誤 代 碼 。 例 如 在 Unix 和。
DOS-Windows 系統裏均可以執行以下代碼建立一個新目錄:
function createDir (dirname)
os.execute("mkdir " .. dirname)
end
os.execute 函數較爲強大,同時也更加倚賴於計算機系統。函數 os.setlocale 設定 Lua
程序所使用的區域(locale)。區域定義的變化對於文化和語言是至關敏感的。setlocale
有兩個字符串參數:區域名和特性(category,用來表示區域的各項特性)。在區域中包
含六項特性:「collate」(排序)控制字符的排列順序;"ctype" controls the types of individual
characters (e.g., what is a letter) and the conversion between lower and upper cases;
"monetary"(貨幣)對 Lua 程序沒有影響;"numeric"(數字)控制數字的格式;"time"
控制時間的格式(也就是 os.date 函數);和「all」包含以上因此特性。函數默認的特性
就是「all」 因此若是你只包含地域名就調用函數 setlocale 那麼全部的特性都會被改變爲,
新的區域特性。若是運行成功函數返回地域名,不然返回 nil(一般由於系統不支持給定
的區域)。
print(os.setlocale("ISO-8859-1", "collate")) --> ISO-8859-1
關於「numeric」特性有一點難處理的地方。儘管葡萄牙語和其它的一些拉丁文語言
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
168
使用逗號代替點號來表示十進制數,可是區域設置並不會改變Lua劃分數字的方式。(除
了其它一些緣由以外,因爲print(3,4)還有其它的函數意義。)所以設置以後獲得的系
5統也許既不能識別帶逗號的數值,又不能理解帶點號的數值 :
-- 設置區域爲葡萄牙語系巴西
print(os.setlocale('pt_BR'))
print(3,4)
print(3.4)
--> 3
4
--> pt_BR
--> stdin:1: malformed number near `3.4'
The category "numeric" is a little tricky. Although Portuguese and other Latin languages
use a comma instead of a point to represent decimal numbers, the locale does not change the
way that Lua parses numbers (among other reasons because expressions like print(3,4) already
have a meaning in Lua). Therefore, you may end with a system that cannot recognize numbers
with commas, but cannot understand numbers with points either:
-- set locale for Portuguese-Brazil
print(os.setlocale('pt_BR'))
print(3,4)
print(3.4)
--> 3
4
--> pt_BR
--> stdin:1: malformed number near '3.4'
5
譯者按:好像是巴西人的煩惱,不甚解。附原文。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
169
第 23 章 Debug 庫
debug 庫並不給你一個可用的 Lua 調試器,而是給你提供一些爲 Lua 寫一個調試器
的方便。出於性能方面的考慮,關於這方面官方的接口是經過 C API 實現的。Lua 中的
debug 庫就是一種在 Lua 代碼中直接訪問這些 C 函數的方法。Debug 庫在一個 debug 表
內聲明瞭他全部的函數。
與其餘的標準庫不一樣的是,你應該儘量少的是有 debug 庫。首先,debug 庫中的
一些函數性能比較低;第二,它破壞了語言的一些真理(sacred truths),好比你不能在定
義一個局部變量的函數外部,訪問這個變量。一般,在你的最終產品中,你不想打開這
個 debug 庫,或者你可能想刪除這個庫:
debug = nil
debug 庫由兩種函數組成:自省(introspective)函數和 hooks。自省函數使得咱們能夠
檢查運行程序的某些方面,好比活動函數棧、當前執行代碼的行號、本地變量的名和值。
Hooks 能夠跟蹤程序的執行狀況。
Debug 庫中的一個重要的思想是棧級別(stack level)。一個棧級別就是一個指向在當
前時刻正在活動的特殊函數的數字,也就是說,這個函數正在被調用但尚未返回。調
用 debug 庫的函數級別爲 1,調用他(他指調用 debug 庫的函數)的函數級別爲 2,以此類
推。
23.1 自省(Introspective)
在 debug 庫中主要的自省函數是 debug.getinfo。他的第一個參數能夠是一個函數或
者棧級別。對於函數 foo 調用 debug.getinfo(foo),將返回關於這個函數信息的一個表。
這個表有下列一些域:
source,標明函數被定義的地方。若是函數在一個字符串內被定義(經過
loadstring),source 就是那個字符串。若是函數在一個文件中定義,source 是@
加上文件名。
short_src,source 的簡短版本(最多 60 個字符),記錄一些有用的錯誤信息。
linedefined,source 中函數被定義之處的行號。
what,標明函數類型。若是 foo 是一個普通得 Lua 函數,結果爲 "Lua";若是
是一個 C 函數,結果爲 "C";若是是一個 Lua 的主 chunk,結果爲 "main"。
name,函數的合理名稱。
namewhat,上一個字段表明的含義。這個字段的取值可能爲:W"global"、"local"、
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
170
"method"、"field",或者 ""(空字符串)。空字符串意味着 Lua 沒有找到這個函
數名。
nups,函數的 upvalues 的個數。
func,函數自己;詳細狀況看後面。
當 foo 是一個 C 函數的時候,Lua 沒法知道不少相關的信息,因此對這種函數,只
有 what、name、namewhat 這幾個域的值可用。
以數字 n 調用 debug.getinfo(n)時,返回在 n 級棧的活動函數的信息數據。好比,如
果 n=1,返回的是正在進行調用的那個函數的信息。(n=0 表示 C 函數 getinfo 自己)如
果 n 比棧中活動函數的個數大的話,debug.getinfo 返回 nil。當你使用數字 n 調用
debug.getinfo 查詢活動函數的信息的時候,返回的結果 table 中有一個額外的域:
currentline,即在那個時刻函數所在的行號。另外,func 表示指定 n 級的活動函數。
字段名的寫法有些技巧。記住:由於在 Lua 中函數是第一類值,因此一個函數可能
有多個函數名。查找指定值的函數的時候,Lua 會首先在全局變量中查找,若是沒找到
纔會到調用這個函數的代碼中看它是如何被調用的。後面這種狀況只有在咱們使用數字
調用 getinfo 的時候纔會起做用,也就是這個時候咱們可以獲取調用相關的詳細信息。
函數 getinfo 的效率並不高。 以不消弱程序執行的方式保存 debug 信息Lua(Lua keeps
debug information in a form that does not impair program execution),效率被放在第二位。
爲了獲取比較好地執行性能,getinfo 可選的第二個參數能夠用來指定選取哪些信息。指
定了這個參數以後,程序不會浪費時間去收集那些用戶不關心的信息。這個參數的格式
是一個字符串,每個字母表明一種類型的信息,可用的字母的含義以下:
'n'
'f'
'S'
'l'
'u'
selects fields name and namewhat
selects field func
selects fields source, short_src, what, and linedefined
selects field currentline
selects field nup
下面的函數闡明瞭 debug.getinfo 的使用,函數打印一個活動棧的原始跟蹤信息
(traceback):
function traceback ()
local level = 1
while true do
local info = debug.getinfo(level, "Sl")
if not info then break end
if info.what == "C" then
else
-- a Lua function
info.short_src, info.currentline))
-- is a C function?
print(level, "C function")
print(string.format("[%s]:%d",
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
end
level = level + 1
end
end
171
不難改進這個函數,使得 getinfo 獲取更多的數據,實際上 debug 庫提供了一個改善
的版本 debug.traceback,與咱們上面的函數不一樣的是,debug.traceback 並不打印結果,
而是返回一個字符串。
23.1.1 訪問局部變量
調用 debug 庫的 getlocal 函數能夠訪問任何活動狀態的局部變量。這個函數由兩個
參數:將要查詢的函數的棧級別和變量的索引。函數有兩個返回值:變量名和變量當前
值。若是指定的變量的索引大於活動變量個數,getlocal 返回 nil。若是指定的棧級別無
效,函數會拋出錯誤。(你能夠使用 debug.getinfo 檢查棧級別的有效性)
Lua 對函數中所出現的全部局部變量依次計數,只有在當前函數的範圍內是有效的
局部變量纔會被計數。好比,下面的代碼
function foo (a,b)
local x
do local c = a - b end
local a = 1
while true do
local name, value = debug.getlocal(1, a)
if not name then break end
print(name, value)
a=a+1
end
end
foo(10, 20)
結果爲:
a
b
x
a
10
20
nil
4
索引爲 1 的變量是 a,2 是 b,3 是 x,4 是另外一個 a。在 getlocal 被調用的那一點,c
已經超出了範圍,name 和 value 都不在範圍內。(記住:局部變量僅僅在他們被初始化
以後纔可見)也能夠使用 debug.setlocal 修改一個局部變量的值,他的前兩個參數是棧級
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
172
別和變量索引,第三個參數是變量的新值。這個函數返回一個變量名或者 nil(若是變量
索引超出範圍)
23.1.2 訪問 Upvalues
咱們也能夠經過 debug 庫的 getupvalue 函數訪問 Lua 函數的 upvalues。和局部變量
不一樣的是,即便函數不在活動狀態他依然有 upvalues(這也就是閉包的意義所在) 因此,。
getupvalue 的第一個參數不是棧級別而是一個函數(精確的說應該是一個閉包),第二個
參數是 upvalue 的索引。Lua 按照 upvalue 在一個函數中被引用(refer)的順序依次編號,
由於一個函數不能有兩個相同名字的 upvalues,因此這個順序和 upvalue 並沒什麼關聯
(relevant)。
能夠使用函數 ebug.setupvalue 修改 upvalues。也許你已經猜到,他有三個參數:一
個閉包,一個 upvalues 索引和一個新的 upvalue 值。 setlocal 相似,和這個函數返回 upvalue
的名字,或者 nil(若是 upvalue 索引超出索引範圍)。
下面的代碼顯示了,在給定變量名的狀況下,如何訪問一個正在調用的函數的任意
的給定變量的值:
function getvarvalue (name)
local value, found
-- try local variables
local i = 1
while true do
local n, v = debug.getlocal(2, i)
if not n then break end
if n == name then
value = v
found = true
end
i=i+1
end
if found then return value end
-- try upvalues
local func = debug.getinfo(2).func
i=1
while true do
local n, v = debug.getupvalue(func, i)
if not n then break end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
if n == name then return v end
i=i+1
end
-- not found; get global
return getfenv(func)[name]
end
173
首先,咱們嘗試這個變量是否爲局部變量:若是對於給定名字的變量有多個變量,
咱們必須訪問具備最高索引的那一個,因此咱們老是須要遍歷整個循環。若是在局部變
量中找不到指定名字的變量,咱們嘗試這個變量是否爲 upvalues:首先,咱們使用
debug.getinfo(2).func 獲取調用的函數,而後遍歷這個函數的 upvalues,最後若是咱們找
到給定名字的變量,咱們在全局變量中查找。注意調用 debug.getlocal 和 debug.getinfo
的參數 2(用來訪問正在調用的函數)的用法。
23.2 Hooks
debug 庫的 hook 是這樣一種機制:註冊一個函數,用來在程序運行中某一事件到達
時被調用。有四種能夠觸發一個 hook 的事件: Lua 調用一個函數的時候 call 事件發生;當
每次函數返回的時候,return 事件發生;Lua 開始執行代碼的新行時候,line 事件發生;
運行指定數目的指令以後,count 事件發生。Lua 使用單個參數調用 hooks,參數爲一個
描述產生調用的事件:"call"、"return"、"line" 或 "count"。另外,對於 line 事件,還可
以傳遞第二個參數:新行號。咱們在一個 hook 內老是能夠使用 debug.getinfo 獲取更多
的信息。
使用帶有兩個或者三個參數的 debug.sethook 函數來註冊一個 hook:第一個參數是
hook 函數;第二個參數是一個描述咱們打算監控的事件的字符串;可選的第三個參數是
一個數字,描述咱們打算獲取 count 事件的頻率。爲了監控 call、return 和 line 事件,可
以將他們的第一個字母('c'、'r' 或 'l')組合成一個 mask 字符串便可。要想關掉 hooks,
只須要不帶參數地調用 sethook 便可。
下面的簡單代碼,是一個安裝原始的跟蹤器:打印解釋器執行的每個新行的行號:
debug.sethook(print, "l")
上面這一行代碼,簡單的將 print 函數做爲 hook 函數,並指示 Lua 當 line 事件發生
時調用 print 函數。能夠使用 getinfo 將當前正在執行的文件名信息加上去,使得跟蹤器
稍微精緻點的:
function trace (event, line)
local s = debug.getinfo(2).short_src
print(s .. ":" .. line)
end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
174
debug.sethook(trace, "l")
23.3 Profiles
儘管 debug 庫名字上看來是一個調式庫,除了用於調式之外,還能夠用於完成其餘
任務。這種常見的任務就是 profiling。對於一個實時的 profile 來講(For a profile with
timing),最好使用 C 接口來完成:對於每個 hook 過多的 Lua 調用代價太大而且一般
會致使測量的結果不許確。然而,對於計數的 profiles 而言,Lua 代碼能夠很好的勝任。
下面這部分咱們將實現一個簡單的 profiler:列出在程序運行過程當中,每個函數被調用
的次數。
咱們程序的主要數據結構是兩張表,一張關聯函數和他們調用次數的表,一張關聯
函數和函數名的表。這兩個表的索引下標是函數自己。
local Counters = {}
local Names = {}
在 profiling 以後,咱們能夠訪問函數名數據,可是記住:在函數在活動狀態的狀況
下,能夠獲得比較好的結果,由於那時候 Lua 會察看正在運行的函數的代碼來查找指定
的函數名。
如今咱們定義 hook 函數,他的任務就是獲取正在執行的函數並將對應的計數器加 1;
同時這個 hook 函數也收集函數名信息:
local function hook ()
local f = debug.getinfo(2, "f").func
if Counters[f] == nil then
Counters[f] = 1
Names[f] = debug.getinfo(2, "Sn")
else
end
end
-- only increment the counter
Counters[f] = Counters[f] + 1
-- first time `f' is called?
下一步就是使用這個 hook 運行程序,咱們假設程序的主 chunk 在一個文件內,而且
用戶將這個文件名做爲 profiler 的參數:
prompt> lua profiler main-prog
這種狀況下,咱們的文件名保存在 arg[1],打開 hook 並運行文件:
local f = assert(loadfile(arg[1]))
debug.sethook(hook, "c")
-- turn on the hook
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
f()
-- run the main program
-- turn off the hook
175
debug.sethook()
最後一步是顯示結果,下一個函數爲一個函數產生名稱,由於在 Lua 中的函數名不
肯定,因此咱們對每個函數加上他的位置信息,型如 file:line 。若是一個函數沒有名
字,那麼咱們只用它的位置表示。若是一個函數是 C 函數,咱們只是用它的名字表示(他
沒有位置信息)。
function getname (func)
local n = Names[func]
if n.what == "C" then
return n.name
end
local loc = string.format("[%s]:%s",
n.short_src, n.linedefined)
if n.namewhat ~= "" then
return string.format("%s (%s)", loc, n.name)
else
return string.format("%s", loc)
end
end
最後,咱們打印每個函數和他的計數器:
for func, count in pairs(Counters) do
print(getname(func), count)
end
若是咱們將咱們的 profiler 應用到 Section 10.2 的馬爾科夫鏈的例子上,咱們獲得如
下結果:
[markov.lua]:4 884723
write
read
sub
10000
1
31103
884722
1
894723
884723
[markov.lua]:0 (f)
[markov.lua]:1 (allwords)
[markov.lua]:20 (prefix)
find
915824
[markov.lua]:26 (insert)
random 10000
sethook 1
insert 884723
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
176
那意味着第四行的匿名函數(在 allwords 內定義的迭代函數)被調用 884,723 次,
write(io.write)被調用 10,000 次。
你能夠對這個 profiler 進行一些改進,好比對輸出排序、打印出比較好的函數名、改
善輸出格式。不過,這個基本的 profiler 已經頗有用,而且能夠做爲不少高級工具的基礎。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
177
第四篇 C API
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
178
第 24 章 C API 縱覽
Lua 是一個嵌入式的語言,意味着 Lua 不只能夠是一個獨立運行的程序包也能夠是
一個用來嵌入其餘應用的程序庫。你可能以爲奇怪:若是 Lua 不僅是獨立的程序,爲什
麼到目前爲止貫穿整本書咱們都是在使用 Lua 獨立程序呢?這個問題的答案在於 Lua 解
釋器(可執行的 lua)。Lua 解釋器是一個使用 Lua 標準庫實現的獨立的解釋器,她是一
個很小的應用(總共不超過 500 行的代碼)。解釋器負責程序和使用者的接口:從使用者
那裏獲取文件或者字符串,並傳給 Lua 標準庫,Lua 標準庫負責最終的代碼運行。
Lua 能夠做爲程序庫用來擴展應用的功能,也就是 Lua 能夠做爲擴展性語言的緣由
所在。同時,Lua 程序中能夠註冊有其餘語言實現的函數,這些函數可能由 C 語言(或其
他語言)實現,能夠增長一些不容易由 Lua 實現的功能。這使得 Lua 是可擴展的。與上面
兩種觀點(Lua 做爲擴展性語言和可擴展的語言)對應的 C 和 Lua 中間有兩種交互方式。
第一種,C 做爲應用程序語言,Lua 做爲一個庫使用;第二種,反過來,Lua 做爲程序
語言,C 做爲庫使用。這兩種方式,C 語言都使用相同的 API 與 Lua 通訊,所以 C 和
Lua 交互這部分稱爲 C API。
C API 是一個 C 代碼與 Lua 進行交互的函數集。他有如下部分組成:讀寫 Lua 全局
變量的函數,調用 Lua 函數的函數,運行 Lua 代碼片段的函數,註冊 C 函數而後能夠在
Lua 中被調用的函數,等等。(本書中,術語函數實際上指函數或者宏,API 有些函數爲
了方便以宏的方式實現)
C API 遵循 C 語言的語法形式,這 Lua 有所不一樣。當使用 C 進行程序設計的時候,
咱們必須注意,類型檢查,錯誤處理,內存分配都不少問題。API 中的大部分函數並不
檢查他們參數的正確性;你須要在調用函數以前負責確保參數是有效的。若是你傳遞了
錯誤的參數,可能獲得 \"segmentation fault\" 這樣或者相似的錯誤信息,而沒有很明確
的錯誤信息能夠得到。另外,API 重點放在了靈活性和簡潔性方面,有時候以犧牲方便
實用爲代價的。通常的任務可能須要涉及不少個 API 調用,這可能使人煩惱,可是他給
你提供了對細節的所有控制的能力,好比錯誤處理,緩衝大小,和相似的問題。如本章
的標題所示,這一章的目標是對當你從 C 調用 Lua 時將涉及到哪些內容的預覽。若是不
能理解某些細節不要着急,後面咱們會一一詳細介紹。不過,在 Lua 參考手冊中有對指
定函數的詳細描述。另外,在 Lua 發佈版中你能夠看到 API 的應用的例子,Lua 獨立的
解釋器(lua.c)提供了應用代碼的例子,而標準庫(lmathlib.c、lstrlib.c 等等)提供了程
序庫代碼的例子。
從如今開始,你戴上了 C 程序員的帽子,當咱們談到「你/大家」,咱們意思是指當
你使用 C 編程的時候。在 C 和 Lua 之間通訊關鍵內容在於一個虛擬的棧。幾乎全部的
API 調用都是對棧上的值進行操做,全部 C 與 Lua 之間的數據交換也都經過這個棧來完
成。另外,你也能夠使用棧來保存臨時變量。棧的使用解決了 C 和 Lua 之間兩個不協調
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
179
的問題:第一,Lua 會自動進行垃圾收集,而 C 要求顯示的分配存儲單元,二者引發的
矛盾。第二,Lua 中的動態類型和 C 中的靜態類型不一致引發的混亂。咱們將在 24.2 節
詳細地介紹棧的相關內容。
24.1 第一個示例程序
經過一個簡單的應用程序讓咱們開始這個預覽:一個獨立的 Lua 解釋器的實現。我
們寫一個簡單的解釋器,代碼以下:
#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main (void)
{
char buff[256];
int error;
lua_State *L = lua_open();
luaopen_base(L);
luaopen_table(L);
luaopen_io(L);
luaopen_string(L);
luaopen_math(L);
/* opens Lua */
/* opens the basic library */
/* opens the table library */
/* opens the I/O library */
/* opens the string lib. */
/* opens the math lib. */
while (fgets(buff, sizeof(buff), stdin) != NULL) {
error = luaL_loadbuffer(L, buff, strlen(buff),
"line") || lua_pcall(L, 0, 0, 0);
if (error) {
fprintf(stderr, "%s", lua_tostring(L, -1));
lua_pop(L, 1);/* pop error message from the stack */
}
}
lua_close(L);
return 0;
}
頭文件 lua.h 定義了 Lua 提供的基礎函數。其中包括建立一個新的 Lua 環境的函數
(如 lua_open),調用 Lua 函數(如 lua_pcall)的函數,讀取/寫入 Lua 環境的全局變量
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
180
的函數,註冊能夠被 Lua 代碼調用的新函數的函數,等等。全部在 lua.h 中被定義的都
有一個 lua_前綴。
頭文件 lauxlib.h 定義了輔助庫(auxlib)提供的函數。一樣,全部在其中定義的函
數等都以 luaL_打頭(例如,luaL_loadbuffer)。輔助庫利用 lua.h 中提供的基礎函數提供
了更高層次上的抽象;全部 Lua 標準庫都使用了 auxlib。基礎 API 致力於 economy and
orthogonality,相反 auxlib 致力於實現通常任務的實用性。固然,基於你的程序的須要而
建立其它的抽象也是很是容易的。須要銘記在心的是,auxlib 沒有存取 Lua 內部的權限。
它完成它全部的工做都是經過正式的基本 API。
Lua 庫沒有定義任何全局變量。它全部的狀態保存在動態結構 lua_State 中,並且指
向這個結構的指針做爲全部 Lua 函數的一個參數。這樣的實現方式使得 Lua 可以重入
(reentrant)且爲在多線程中的使用做好準備。
函數 lua_open 建立一個新環境(或 state)。lua_open 建立一個新的環境時,這個環
境並不包括預約義的函數,甚至是 print。爲了保持 Lua 的苗條,全部的標準庫以單獨的
包提供,因此若是你不須要就不會強求你使用它們。頭文件 lualib.h 定義了打開這些庫
的函數。例如,調用 luaopen_io,以建立 io table 並註冊 I/O 函數(io.read,io.write 等等)
到 Lua 環境中。
建立一個 state 並將標準庫載入以後,就能夠着手解釋用戶的輸入了。對於用戶輸入
的每一行,C 程序首先調用 luaL_loadbuffer 編譯這些 Lua 代碼。若是沒有錯誤,這個調
用返回零並把編譯以後的 chunk 壓入棧。(記住,咱們將在下一節中討論魔法般的棧)之
後,C 程序調用 lua_pcall,它將會把 chunk 從棧中彈出並在保護模式下運行它。和
luaL_laodbuffer 同樣,lua_pcall 在沒有錯誤的狀況下返回零。在有錯誤的狀況下,這兩
個函數都將一條錯誤消息壓入棧;咱們能夠用 lua_tostring 來獲得這條信息、輸出它,用
lua_pop 將它從棧中刪除。
注意,在有錯誤發生的狀況下,這個程序簡單的輸出錯誤信息到標準錯誤流。在 C
中,實際的錯誤處理多是很是複雜的並且如何處理依賴於應用程序自己。Lua 核心決
不會直接輸出任何東西到任務輸出流上;它經過返回錯誤代碼和錯誤信息來發出錯誤信
號。每個應用程序均可以用最適合它們本身的方式來處理這些錯誤。爲了討論的簡單,
如今咱們假想一個簡單的錯誤處理方式,就象下面代碼同樣,它只是輸出一條錯誤信息、
關閉 Lua state、退出整個應用程序。
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
void error (lua_State *L, const char *fmt, ...) {
va_list argp;
va_start(argp, fmt);
vfprintf(stderr, argp);
va_end(argp);
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
lua_close(L);
exit(EXIT_FAILURE);
}
181
稍候咱們再詳細的討論關於在應用代碼中如何處理錯誤.由於你能夠將 Lua 和 C/C++
代碼一塊兒編譯,lua.h 並不包含這些典型的在其餘 C 庫中出現的整合代碼:
#ifdef __cplusplus
extern "C" {
#endif
...
#ifdef __cplusplus
}
#endif
所以,若是你用 C 方式來編譯它,但用在 C++中,那麼你須要象下面這樣來包含 lua.h
頭文件。
extern "C" {
#include <lua.h>
}
一個經常使用的技巧是創建一個包含上面代碼的 lua.hpp 頭文件,並將這個新的頭文件包
含進你的 C++程序。
24.2 堆棧
當在 Lua 和 C 之間交換數據時咱們面臨着兩個問題:動態與靜態類型系統的不匹配
和自動與手動內存管理的不一致。
在 Lua 中,咱們寫下 a[k]=v 時,k 和 v 能夠有幾種不一樣的類型(因爲 metatables 的
存在,a 也可能有不一樣的類型)。若是咱們想在 C 中提供相似的操做,不管怎樣,操做表
的函數(settable)一定有一個固定的類型。咱們將須要幾十個不一樣的函數來完成這一個的
操做(三個參數的類型的每一種組合都須要一個函數)。
咱們能夠在 C 中聲明一些 union 類型來解決這個問題,咱們稱之爲 lua_Value,它能
夠描述全部類型的 Lua 值。而後,咱們就能夠這樣聲明 settable
void lua_settable (lua_Value a, lua_Value k, lua_Value v);
這個解決方案有兩個缺點。第一,要將如此複雜的類型映射到其它語言可能很困難;
Lua 不只被設計爲與 C/C++易於交互,Java,Fortran 以及相似的語言也同樣。第二,Lua
負責垃圾回收:若是咱們將 Lua 值保存在 C 變量中,Lua 引擎沒有辦法瞭解這種用法;
它可能錯誤地認爲某個值爲垃圾並收集他。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
182
所以,Lua API 沒有定義任何相似 lua_Value 的類型。替代的方案,它用一個抽象的
棧在 Lua 與 C 之間交換值。棧中的每一條記錄均可以保存任何 Lua 值。不管你什麼時候想要
從 Lua 請求一個值(好比一個全局變量的值),調用 Lua,被請求的值將會被壓入棧。無
論你什麼時候想要傳遞一個值給 Lua,首先將這個值壓入棧,而後調用 Lua(這個值將被彈
出) 咱們仍然須要一個不一樣的函數將每種 C 類型壓入棧和一個不一樣函數從棧上取值。(譯
注:只是取出不是彈出),可是咱們避免了組合式的爆炸(combinatorial explosion)。另
外,由於棧是由 Lua 來管理的,垃圾回收器知道那個值正在被 C 使用。 幾乎全部的 API
函數都用到了棧。正如咱們在第一個例子中所看到的,luaL_loadbuffer 把它的結果留在
了棧上(被編譯的 chunk 或一條錯誤信息);lua_pcall 從棧上獲取要被調用的函數並把任
何臨時的錯誤信息放在這裏。
Lua 以一個嚴格的 LIFO 規則(後進先出;也就是說,始終存取棧頂)來操做棧。
當你調用 Lua 時,它只會改變棧頂部分。你的C代碼卻有更多的自由;更明確的來說,
你能夠查詢棧上的任何元素,甚至是在任何一個位置插入和刪除元素。
24.2.1 壓入元素
API 有一系列壓棧的函數,它將每種能夠用 C 來描述的 Lua 類型壓棧:空值(nil)
用 lua_pushnil,數值型(double)用 lua_pushnumber,布爾型(在 C 中用整數表示)用
lua_pushboolean,任意的字符串(char*類型,容許包含'\0'字符)用 lua_pushlstring,C
語言風格(以'\0'結束)的字符串(const char*)用 lua_pushstring:
void lua_pushnil (lua_State *L);
void lua_pushboolean (lua_State *L, int bool);
void lua_pushnumber (lua_State *L, double n);
void lua_pushlstring (lua_State *L, const char *s,
size_t length);
void lua_pushstring (lua_State *L, const char *s);
一樣也有將 C 函數和 userdata 值壓入棧的函數,稍後會討論到它們。
Lua 中的字符串不是以零爲結束符的;它們依賴於一個明確的長度,所以能夠包含
任意的二進制數據。將字符串壓入串的正式函數是 lua_pushlstring,它要求一個明確的長
度做爲參數。對於以零結束的字符串,你能夠用 lua_pushstring(它用 strlen 來計算字符
串長度)。Lua 歷來不保持一個指向外部字符串(或任何其它對象,除了 C 函數——它總
是靜態指針)的指針。對於它保持的全部字符串,Lua 要麼作一分內部的拷貝要麼從新
利用已經存在的字符串。所以,一旦這些函數返回以後你能夠自由的修改或是釋放你的
緩衝區。
不管你什麼時候壓入一個元素到棧上,你有責任確保在棧上有空間來作這件事情。記住,
你如今是 C 程序員;Lua 不會寵着你。當 Lua 在起始以及在 Lua 調用 C 的時候,棧上至
少有 20 個空閒的記錄(lua.h 中的 LUA_MINSTACK 宏定義了這個常量) 對於多數普通。
的用法棧是足夠的,因此一般咱們沒必要去考慮它。不管如何,有些任務或許須要更多的
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
183
棧空間(如,調用一個不定參數數目的函數)。在這種狀況下,或許你須要調用下面這個
函數:
int lua_checkstack (lua_State *L, int sz);
它檢測棧上是否有足夠你須要的空間(稍後會有關於它更多的信息)。
24.2.2 查詢元素
API 用索引來訪問棧中的元素。在棧中的第一個元素(也就是第一個被壓入棧的)
有索引 1,下一個有索引 2,以此類推。咱們也能夠用棧頂做爲參照來存取元素,利用負
索引。在這種狀況下,-1 指出棧頂元素(也就是最後被壓入的),-2 指出它的前一個元
素,以此類推。例如,調用 lua_tostring(L, -1)以字符串的形式返回棧頂的值。咱們下面
將看到,在某些場合使用正索引訪問棧比較方便,另一些狀況下,使用負索引訪問棧
更方便。
API 提供了一套 lua_is*函數來檢查一個元素是不是一個指定的類型,*能夠是任何
Lua 類型。所以有 lua_isnumber,lua_isstring,lua_istable 以及相似的函數。全部這些函數都
有一樣的原型:
int lua_is... (lua_State *L, int index);
lua_isnumber 和 lua_isstring 函數不檢查這個值是不是指定的類型,而是看它是否能
被轉換成指定的那種類型。例如,任何數字類型都知足 lua_isstring。
還有一個 lua_type 函數,它返回棧中元素的類型。(lua_is*中的有些函數其實是用
了這個函數定義的宏)在 lua.h 頭文件中,每種類型都被定義爲一個常量:LUA_TNIL、
LUA_TBOOLEAN 、 LUA_TNUMBER 、 LUA_TSTRING 、 LUA_TTABLE 、
LUA_TFUNCTION、LUA_TUSERDATA 以及 LUA_TTHREAD。這個函數主要被用在與
一個 switch 語句聯合使用。當咱們須要真正的檢查字符串和數字類型時它也是有用的
爲了從棧中得到值,這裏有 lua_to*函數:
int
double
const char *
size_t
lua_toboolean (lua_State *L, int index);
lua_tonumber (lua_State *L, int index);
lua_tostring (lua_State *L, int index);
lua_strlen (lua_State *L, int index);
即便給定的元素的類型不正確,調用上面這些函數也沒有什麼問題。在這種狀況下,
lua_toboolean、lua_tonumber 和 lua_strlen 返回 0,其餘函數返回 NULL。因爲 ANSI C 沒
有提供有效的能夠用來判斷錯誤發生數字值,因此返回的 0 是沒有什麼用處的。對於其
他函數而言,咱們通常不須要使用對應的 lua_is*函數:咱們只須要調用 lua_is*,測試返
回結果是否爲 NULL 便可。
Lua_tostring 函數返回一個指向字符串的內部拷貝的指針。你不能修改它(使你想起
那裏有一個 const)。只要這個指針對應的值還在棧內,Lua 會保證這個指針一直有效。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
184
當一個 C 函數返回後,Lua 會清理他的棧,因此,有一個原則:永遠不要將指向 Lua 字
符串的指針保存到訪問他們的外部函數中。
Lua_string 返回的字符串結尾總會有一個字符結束標誌 0,可是字符串中間也可能包
含 0,lua_strlen 返回字符串的實際長度。特殊狀況下,假定棧頂的值是一個字符串,下
面的斷言(assert)老是有效的:
const char *s = lua_tostring(L, -1);
size_t l = lua_strlen(L, -1);
assert(s[l] == '\0');
assert(strlen(s) <= l);
/* any Lua string */
/* its length */
24.2.3 其餘堆棧操做
除開上面所說起的 C 與堆棧交換值的函數外,API 也提供了下列函數來完成一般的
堆棧維護工做:
int lua_gettop (lua_State *L);
void lua_settop (lua_State *L, int index);
void lua_pushvalue (lua_State *L, int index);
void lua_remove (lua_State *L, int index);
void lua_insert (lua_State *L, int index);
void lua_replace (lua_State *L, int index);
函數 lua_gettop 返回堆棧中的元素個數,它也是棧頂元素的索引。注意一個負數索
引-x 對應於正數索引 gettop-x+1。lua_settop 設置棧頂(也就是堆棧中的元素個數)爲一
個指定的值。若是開始的棧頂高於新的棧頂,頂部的值被丟棄。不然,爲了獲得指定的
大小這個函數壓入相應個數的空值(nil)到棧上。特別的,lua_settop(L,0)清空堆棧。你
也能夠用負數索引做爲調用 lua_settop 的參數;那將會設置棧頂到指定的索引。利用這
種技巧,API 提供了下面這個宏,它從堆棧中彈出 n 個元素:
#define lua_pop(L,n) lua_settop(L, -(n)-1)
函數 lua_pushvalue 壓入堆棧上指定索引的一個摶貝到棧頂;lua_remove 移除指定索
引位置的元素,並將其上面全部的元素下移來填補這個位置的空白;lua_insert 移動棧頂
元素到指定索引的位置,並將這個索引位置上面的元素所有上移至棧頂被移動留下的空
隔;最後,lua_replace 從棧頂彈出元素值並將其設置到指定索引位置,沒有任何移動操
做。注意到下面的操做對堆棧沒有任何影響:
lua_settop(L, -1);
lua_insert(L, -1);
/* set top to its current value */
/* move top element to the top */
爲了說明這些函數的用法,這裏有一個有用的幫助函數,它 dump 整個堆棧的內容:
static void stackDump (lua_State *L) {
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
int i;
int top = lua_gettop(L);
for (i = 1; i <= top; i++) { /* repeat for each level */
int t = lua_type(L, i);
switch (t) {
case LUA_TSTRING: /* strings */
printf("`%s'", lua_tostring(L, i));
break;
case LUA_TBOOLEAN:
break;
case LUA_TNUMBER: /* numbers */
printf("%g", lua_tonumber(L, i));
break;
default: /* other values */
printf("%s", lua_typename(L, t));
break;
}
printf(" "); /* put a separator */
}
printf("\n");
}
/* end the listing */
/* booleans */
185
printf(lua_toboolean(L, i) ? "true" : "false");
這個函數從棧底到棧頂遍歷了整個堆棧,依照每一個元素本身的類型打印出其值。它
用引號輸出字符串;以%g 的格式輸出數字;對於其它值(table,函數,等等)它僅僅
輸出它們的類型(lua_typename 轉換一個類型碼到類型名)。
下面的函數利用 stackDump 更進一步的說明了 API 堆棧的操做。
#include <stdio.h>
#include <lua.h>
static void stackDump (lua_State *L) {
...
}
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
int main (void) {
lua_State *L = lua_open();
lua_pushboolean(L, 1); lua_pushnumber(L, 10);
lua_pushnil(L); lua_pushstring(L, "hello");
stackDump(L);
/* true 10 nil `hello' */
lua_pushvalue(L, -4); stackDump(L);
/* true 10 nil `hello' true */
lua_replace(L, 3); stackDump(L);
/* true 10 true `hello' */
lua_settop(L, 6); stackDump(L);
/* true 10 true `hello' nil nil */
lua_remove(L, -3); stackDump(L);
/* true 10 true nil nil */
lua_settop(L, -5); stackDump(L);
/* true */
lua_close(L);
return 0;
}
186
24.3 C API 的錯誤處理
不象 C++或者 JAVA 同樣,C 語言沒有提供一種異常處理機制。爲了改善這個難處,
Lua 利用 C 的 setjmp 技巧構造了一個相似異常處理的機制。(若是你用 C++來編譯 Lua,
那麼修改代碼以使用真正的異常並不困難。)
Lua 中的全部結構都是動態的:它們按需增加,最終當可能時又會縮減。意味着內
存分配失敗的可能性在 Lua 中是廣泛的。幾乎任意操做都會面對這種意外。Lua 的 API
中用異常發出這些錯誤而不是爲每步操做產生錯誤碼。這意味着全部的 API 函數可能拋
出一個錯誤(也就是調用 longjmp)來代替返回。
當咱們寫一個庫代碼時(也就是被 Lua 調用的 C 函數)長跳轉(long jump)的用處
幾乎和一個真正的異常處理同樣的方便,由於 Lua 抓取了任務偶然的錯誤。當咱們寫應
用程序代碼時(也就是調用 Lua 的 C 代碼),不管如何,咱們必須提供一種方法來抓取
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
這些錯誤。
187
24.3.1 應用程序中的錯誤處理
典型的狀況是應用的代碼運行在非保護模式下。因爲應用的代碼不是被 Lua 調用的,
Lua 根據上下文狀況來捕捉錯誤的發生(也就是說,Lua 不能調用 setjmp)。在這些狀況
下,當 Lua 遇到像 "not enough memory" 的錯誤,他不知道如何處理。他只能調用一個
panic 函數退出應用。(你能夠使用 lua_atpanic 函數設置你本身的 panic 函數)
不是全部的 API 函數都會拋出異常,lua_open、lua_close、lua_pcall 和 lua_load 都是
安全的,另外,大多數其餘函數只能在內存分配失敗的狀況下拋出異常:好比,
luaL_loadfile 若是沒有足夠內存來拷貝指定的文件將會失敗。有些程序當碰到內存不足
時,他們可能須要忽略異常不作任何處理。對這些程序而言,若是 Lua 致使內存不足,
panic 是沒有問題的。
若是你不想你的應用退出,即便在內存分配失敗的狀況下,你必須在保護模式下運
行你的代碼。大部分或者全部你的 Lua 代碼經過調用 lua_pcall 來運行,因此,它運行在
保護模式下。即便在內存分配失敗的狀況下,lua_pcall 也返回一個錯誤代碼,使得 lua
解釋器處於和諧的(consistent)狀態。若是你也想保護全部你的與 Lua 交互的 C 代碼,
你能夠使用 lua_cpcall。(請看參考手冊,有對這個函數更深的描述,在 Lua 的發佈版的
lua.c 文件中有它應用的例子)
24.3.2 類庫中的錯誤處理
Lua 是安全的語言,也就是說,無論你些什麼樣的代碼,也無論代碼如何錯誤,你
均可以根據 Lua 自己知道程序的行爲。另外,錯誤也會根據 Lua 被發現和解釋。你能夠
與 C 比較一下,C 語言中不少錯誤的程序的行爲只能依據硬件或者由程序計數器給出的
錯誤出現的位置被解釋。
不論何時你向 Lua 中添加一個新的 C 函數,你均可能打破原來的安全性。好比,
一個相似 poke 的函數,在任意的內存地址存聽任意的字節,可能使得內存癱瘓。你必須
想法設法保證你的插件(add-ons)對於 Lua 來說是安全的,而且提升比較好的錯誤處理。
正如咱們前面所討論的,每個 C 程序都有他本身的錯勿處理方式,當你打算爲
Lua 寫一個庫函數的時候,這裏有一些標準的處理錯誤的方法能夠參考。不論何時,
C 函數發現錯誤只要簡單的調用 lua_error(或者 luaL_error,後者更好,由於她調用了前
者並格式化了錯誤信息)。Lua_error 函數會清理全部在 Lua 中須要被清理的,而後和錯
誤信息一塊兒回到最初的執行 lua_pcall 的地方。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
188
第 25 章 擴展你的程序
做爲配置語言是 LUA 的一個重要應用。在這個章節裏,咱們舉例說明如何用 LUA 設
置一個程序。讓咱們用一個簡單的例子開始而後展開到更復雜的應用中。
首先,讓咱們想象一下一個簡單的配置情節:你的 C 程序(程序名爲 PP)有一個
窗口界面而且可讓用戶指定窗口的初始大小。顯然,相似這樣簡單的應用,有多種解
決方法比使用 LUA 更簡單,好比環境變量或者存有變量值的文件。但,即便是用一個
簡單的文本文件,你也不知道如何去解析。因此,最後決定採用一個 LUA 配置文件(這
就是 LUA 程序中的純文本文件)在這種簡單的文本形式中一般包含相似以下的信息行:。
-- configuration file for program `pp'
-- define window size
width = 200
height = 300
如今,你得調用 LUA API 函數去解析這個文件,取得 width 和 height 這兩個全局變
量的值。下面這個取值函數就起這樣的做用:
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
void load (char *filename, int *width, int *height) {
lua_State *L = lua_open();
luaopen_base(L);
luaopen_io(L);
luaopen_string(L);
luaopen_math(L);
if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0))
error(L, "cannot run configuration file: %s",
lua_tostring(L, -1));
lua_getglobal(L, "width");
lua_getglobal(L, "height");
if (!lua_isnumber(L, -2))
error(L, "`width' should be a number\n");
if (!lua_isnumber(L, -1))
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
error(L, "`height' should be a number\n");
*width = (int)lua_tonumber(L, -2);
*height = (int)lua_tonumber(L, -1);
lua_close(L);
}
189
首先,程序打開 LUA 包並加載了標準函數庫(雖然這是可選的,但一般包含這些
庫是比較好的編程思想) 而後程序使用 luaL_loadfile 方法根據參數 filename 加載此文件。
中的信息塊並調用 lua_pcall 函數運行,這些函數運行時若發生錯誤(例如配置文件中有
語法錯誤) 將返回非零的錯誤代碼並將此錯誤信息壓入棧中。,一般,咱們用帶參數 index
值爲-1 的 lua_tostring 函數取得棧頂元素(error 函數咱們已經在 24.1 章節中定義)。
解析完取得的信息塊後,程序會取得全局變量值。爲此,程序調用了兩次
lua_getglobal 函數,其中一參數爲變量名稱。每調用一次就把相應的變量值壓入棧頂,
因此變量 width 的 index 值是-2 而變量 height 的 index 值是-1(在棧頂)(由於先前的棧。
是空的,須要從棧底從新索引,1 表示第一個元素 2 表示第二個元素。因爲從棧頂索引,
無論棧是否爲空,你的代碼也能運行)。接着,程序用 lua_isnumber 函數判斷每一個值是否
爲數字。lua_tonumber 函數將獲得的數值轉換成 double 類型並用(int)強制轉換成整型。
最後,關閉數據流並返回值。
Lua 是否值得一用?正如我前面提到的,在這個簡單的例子中,相比較於 lua 用一個
只包含有兩個數字的文件會更簡單。即便如此,使用 lua 也帶來了一些優點。首先,它
爲你處理全部的語法細節(包括錯誤);你的配置文件甚至能夠包含註釋!其次,用能夠
用 lua 作更多複雜的配置。例如,腳本能夠向用戶提示相關信息,或者也能夠查詢環境
變量以選擇合適的大小:
-- configuration file for program 'pp'
if getenv("DISPLAY") == ":0.0" then
width = 300; height = 300
else
width = 200; height = 200
end
在這樣簡單的配置情節中,很難預料用戶想要什麼;不過只要腳本定義了這兩個變
量,你的 C 程序無需改變就可運行。
最後一個使用 lua 的理由:在你的程序中很容易的加入新的配置單元。方便的屬性
添加使程序更具備擴展性。
25.1 表操做
如今,咱們打算使用 Lua 做爲配置文件,配置窗口的背景顏色。咱們假定最終的顏
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
190
色有三個數字(RGB)描述,每個數字表明顏色的一部分。一般,在 C 語言中,這些
數字使用[0,255]範圍內的整數表示,因爲在 Lua 中全部數字都是實數,咱們能夠使用更
天然的範圍[0,1]來表示。
一個粗糙的解決方法是,對每個顏色組件使用一個全局變量表示,讓用戶來配置
這些變量:
-- configuration file for program 'pp'
width = 200
height = 300
background_red = 0.30
background_green = 0.10
background_blue = 0
這個方法有兩個缺點:第一,太冗餘(爲了表示窗口的背景,窗口的前景,菜單的
背景等,一個實際的應用程序可能須要幾十個不一樣的顏色);第二,沒有辦法預約義共同
部分的顏色,好比,假如咱們事先定義了 WHITE,用戶能夠簡單的寫 background = WHITE
來表示全部的背景色爲白色。爲了不這些缺點,咱們使用一個 table 來表示顏色:
background = {r=0.30, g=0.10, b=0}
表的使用給腳本的結構帶來不少靈活性,如今對於用戶(或者應用程序)很容易預
定義一些顏色,以便未來在配置中使用:
BLUE = {r=0, g=0, b=1}
...
background = BLUE
爲了在 C 中獲取這些值,咱們這樣作:
lua_getglobal(L, "background");
if (!lua_istable(L, -1))
error(L, "`background' is not a valid color table");
red = getfield("r");
green = getfield("g");
blue = getfield("b");
通常來講,咱們首先獲取全局變量 backgroud 的值,並保證它是一個 table。而後,
咱們使用 getfield 函數獲取每個顏色組件。這個函數不是 API 的一部分,咱們須要自
己定義他:
#define MAX_COLOR
255
/* assume that table is on the stack top */
int getfield (const char *key) {
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
int result;
lua_pushstring(L, key);
lua_gettable(L, -2); /* get background[key] */
if (!lua_isnumber(L, -1))
error(L, "invalid component in background color");
result = (int)lua_tonumber(L, -1) * MAX_COLOR;
lua_pop(L, 1); /* remove number */
return result;
}
191
這裏咱們再次面對多態的問題:可能存在不少個 getfield 的版本,key 的類型,value
的類型,錯誤處理等都不盡相同。Lua API 只提供了一個 lua_gettable 函數,他接受 table
在棧中的位置爲參數,將對應 key 值出棧,返回與 key 對應的 value。咱們上面的 getfield
函數假定 table 在棧頂,所以,lua_pushstring 將 key 入棧以後,table 在-2 的位置。返回
以前,getfield 會將棧恢復到調用前的狀態。
咱們對上面的例子稍做延伸,加入顏色名。用戶仍然能夠使用顏色 table,可是也可
覺得共同部分的顏色預約義名字,爲了實現這個功能,咱們在 C 代碼中須要一個顏色
table:
struct ColorTable {
char *name;
unsigned char red, green, blue;
} colortable[] = {
{"WHITE", MAX_COLOR, MAX_COLOR, MAX_COLOR},
{"RED",
{"BLUE",
...
{NULL,
};
0,
0,
0}
/* sentinel */
MAX_COLOR, 0,
0,
0,
0,
0},
MAX_COLOR},
0},
{"GREEN", 0,
{"BLACK", 0,
MAX_COLOR, 0},
咱們的這個實現會使用顏色名建立一個全局變量,而後使用顏色 table 初始化這些全
局變量。結果和用戶在腳本中使用下面這幾行代碼是同樣的:
WHITE
RED
...
= {r=1, g=1, b=1}
= {r=1, g=0, b=0}
腳本中用戶定義的顏色和應用中(C 代碼)定義的顏色不一樣之處在於:應用在腳本
以前運行。
爲了能夠設置 table 域的值,咱們定義個輔助函數 setfield;這個函數將 field 的索引
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
和 field 的值入棧,而後調用 lua_settable:
/* assume that table is at the top */
void setfield (const char *index, int value) {
lua_pushstring(L, index);
lua_pushnumber(L, (double)value/MAX_COLOR);
lua_settable(L, -3);
}
192
與其餘的 API 函數同樣,lua_settable 在不一樣的參數類型狀況下均可以使用,他從棧
中獲取全部的參數。lua_settable 以 table 在棧中的索引做爲參數,並將棧中的 key 和 value
出棧,用這兩個值修改 table。Setfield 函數假定調用以前 table 是在棧頂位置(索引爲-1)。
將 index 和 value 入棧以後,table 索引變爲-3。
Setcolor 函數定義一個單一的顏色,首先建立一個 table,而後設置對應的域,而後
將這個 table 賦值給對應的全局變量:
void setcolor (struct ColorTable *ct) {
lua_newtable(L);
setfield("r", ct->red);
setfield("g", ct->green);
setfield("b", ct->blue);
lua_setglobal(ct->name);
}
/* creates a table */
/* table.r = ct->r */
/* table.g = ct->g */
/* table.b = ct->b */
/* 'name' = table */
lua_newtable 函數建立一個新的空 table 而後將其入棧,調用 setfield 設置 table 的域,
最後 lua_setglobal 將 table 出棧並將其賦給一個全局變量名。
有了前面這些函數,下面的循環註冊全部的顏色到應用程序中的全局變量:
int i = 0;
while (colortable[i].name != NULL)
setcolor(&colortable[i++]);
記住:應用程序必須在運行用戶腳本以前,執行這個循環。
對於上面的命名顏色的實現有另一個可選的方法。用一個字符串來表示顏色名,
而不是上面使用全局變量表示,好比用戶能夠這樣設置 background = "BLUE"。因此,
background 能夠是 table 也能夠是 string。對於這種實現,應用程序在運行用戶腳本以前
不須要作任何特殊處理。可是須要額外的工做來獲取顏色。當他獲得變量 background 的
值以後,必須判斷這個值的類型,是 table 仍是 string:
lua_getglobal(L, "background");
if (lua_isstring(L, -1)) {
const char *name = lua_tostring(L, -1);
int i = 0;
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
while (colortable[i].name != NULL &&
strcmp(colorname, colortable[i].name) != 0)
i++;
if (colortable[i].name == NULL) /* string not found? */
error(L, "invalid color name (%s)", colorname);
else { /* use colortable[i] */
red = colortable[i].red;
green = colortable[i].green;
blue = colortable[i].blue;
}
} else if (lua_istable(L, -1)) {
red = getfield("r");
green = getfield("g");
blue = getfield("b");
} else
error(L, "invalid value for `background'");
193
哪一個是最好的選擇呢?在 C 程序中,使用字符串表示不是一個好的習慣,由於編譯
器不會對字符串進行錯誤檢查。然而在 Lua 中,全局變量不須要聲明,所以當用戶將顏
色名字拼寫錯誤的時候, 不會發出任何錯誤信息。Lua好比,用戶將 WHITE 誤寫成 WITE,
background 變量將爲 nil(WITE 的值沒有初始化),而後應用程序就認爲 background 的值
爲 nil。沒有其餘關於這個錯誤的信息能夠得到。另外一方面,使用字符串表示,background
的值也多是拼寫錯了的字符串。所以,應用程序能夠在發生錯誤的時候,定製輸出的
錯誤信息。應用能夠不區分大小寫比較字符串,所以,用戶能夠寫"white","WHITE",
甚至"White"。可是,若是用戶腳本很小,而且顏色種類比較多,註冊成百上千個顏色(需
要建立成百上千個 table 和全局變量),最終用戶可能只是用其中幾個,這會讓人以爲很
怪異。在使用字符串表示的時候,應避免這種狀況出現。
25.2 調用 Lua 函數
Lua 做爲配置文件的一個最大的長處在於它能夠定義個被應用調用的函數。好比,
你能夠寫一個應用程序來繪製一個函數的圖像,使用 Lua 來定義這個函數。
使用 API 調用函數的方法是很簡單的:首先,將被調用的函數入棧;第二,依次將
全部參數入棧;第三,使用 lua_pcall 調用函數;最後,從棧中獲取函數執行返回的結果。
看一個例子,假定咱們的配置文件有下面這個函數:
function f (x, y)
return (x^2 * math.sin(y))/(1 - x)
end
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
194
而且咱們想在 C 中對於給定的 x,y 計算 z=f(x,y)的值。假如你已經打開了 lua 庫而且
運行了配置文件,你能夠將這個調用封裝成下面的 C 函數:
/* call a function `f' defined in Lua */
double f (double x, double y) {
double z;
/* push functions and arguments */
lua_getglobal(L, "f");
lua_pushnumber(L, x);
lua_pushnumber(L, y);
/* function to be called */
/* push 1st argument */
/* push 2nd argument */
/* do the call (2 arguments, 1 result) */
if (lua_pcall(L, 2, 1, 0) != 0)
error(L, "error running function `f': %s",
lua_tostring(L, -1));
/* retrieve result */
if (!lua_isnumber(L, -1))
error(L, "function `f' must return a number");
z = lua_tonumber(L, -1);
lua_pop(L, 1); /* pop returned value */
return z;
}
能夠調用 lua_pcall 時指定參數的個數和返回結果的個數。第四個參數能夠指定一個
錯誤處理函數,咱們下面再討論它。和 Lua 中賦值操做同樣,lua_pcall 會根據你的要求
調整返回結果的個數,多餘的丟棄,少的用 nil 補足。在將結果入棧以前,lua_pcall 會將
棧內的函數和參數移除。若是函數返回多個結果,第一個結果被第一個入棧,所以若是
有 n 個返回結果,第一個返回結果在棧中的位置爲-n,最後一個返回結果在棧中的位置
爲-1。
若是 lua_pcall 運行時出現錯誤,lua_pcall 會返回一個非 0 的結果。另外,他將錯誤
信息入棧(仍然會先將函數和參數從棧中移除)。在將錯誤信息入棧以前,若是指定了錯
誤處理函數,lua_pcall 毀掉用錯誤處理函數。使用 lua_pcall 的最後一個參數來指定錯誤
處理函數,0 表明沒有錯誤處理函數,也就是說最終的錯誤信息就是原始的錯誤信息。
不然,那個參數應該是一個錯誤函數被加載的時候在棧中的索引,注意,在這種狀況下,
錯誤處理函數必需要在被調用函數和其參數入棧以前入棧。
對於通常錯誤,lua_pcall 返回錯誤代碼 LUA_ERRRUN。有兩種特殊狀況,會返回
特殊的錯誤代碼,由於他們歷來不會調用錯誤處理函數。第一種狀況是,內存分配錯誤,
對於這種錯誤,lua_pcall 老是返回 LUA_ERRMEM。第二種狀況是,當 Lua 正在運行錯
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
195
誤處理函數時發生錯誤,這種狀況下,再次調用錯誤處理函數沒有意義,因此 lua_pcall
當即返回錯誤代碼 LUA_ERRERR。
25.3 通用的函數調用
看一個稍微高級的例子,咱們使用 C 的 vararg 來封裝對 Lua 函數的調用。咱們的封
裝後的函數(call_va)接受被調用的函數明做爲第一個參數,第二參數是一個描述參數
和結果類型的字符串,最後是一個保存返回結果的變量指針的列表。使用這個函數,我
們能夠將前面的例子改寫爲:
call_va("f", "dd>d", x, y, &z);
字符串 "dd>d" 表示函數有兩個 double 類型的參數,一個 double 類型的返回結果。
咱們使用字母 'd' 表示 double;'i' 表示 integer,'s' 表示 strings;'>' 做爲參數和結果的
分隔符。若是函數沒有返回結果,'>' 是可選的。
#include <stdarg.h>
void call_va (const char *func, const char *sig, ...) {
va_list vl;
int narg, nres;
/* number of arguments and results */
va_start(vl, sig);
lua_getglobal(L, func); /* get function */
/* push arguments */
narg = 0;
while (*sig) {
/* push arguments */
switch (*sig++) {
case 'd': /* double argument */
lua_pushnumber(L, va_arg(vl, double));
break;
case 'i': /* int argument */
lua_pushnumber(L, va_arg(vl, int));
break;
case 's': /* string argument */
lua_pushstring(L, va_arg(vl, char *));
break;
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
196
case '>':
goto endwhile;
default:
error(L, "invalid option (%c)", *(sig - 1));
}
narg++;
luaL_checkstack(L, 1, "too many arguments");
} endwhile:
/* do the call */
nres = strlen(sig);
/* number of expected results */
if (lua_pcall(L, narg, nres, 0) != 0) /* do the call */
error(L, "error running function `%s': %s",
func, lua_tostring(L, -1));
/* retrieve results */
nres = -nres;
while (*sig) {
/* stack index of first result */
/* get results */
switch (*sig++) {
case 'd': /* double result */
if (!lua_isnumber(L, nres))
error(L, "wrong result type");
*va_arg(vl, double *) = lua_tonumber(L, nres);
break;
case 'i': /* int result */
if (!lua_isnumber(L, nres))
error(L, "wrong result type");
*va_arg(vl, int *) = (int)lua_tonumber(L, nres);
break;
case 's': /* string result */
if (!lua_isstring(L, nres))
error(L, "wrong result type");
*va_arg(vl, const char **) = lua_tostring(L, nres);
break;
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
197
default:
error(L, "invalid option (%c)", *(sig - 1));
}
nres++;
}
va_end(vl);
}
儘管這段代碼具備通常性,這個函數和前面咱們的例子有相同的步驟:將函數入棧,
參數入棧,調用函數,獲取返回結果。大部分代碼都很直觀,但也有一點技巧。首先,
不須要檢查 func 是不是一個函數,lua_pcall 能夠捕捉這個錯誤。第二,能夠接受任意多
個參數,因此必須檢查棧的空間。第三,由於函數可能返回字符串,call_va 不能從棧中
彈出結果,在調用者獲取臨時字符串的結果以後(拷貝到其餘的變量中),由調用者負責
彈出結果。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
198
第 26 章 調用 C 函數
擴展 Lua 的基本方法之一就是爲應用程序註冊新的 C 函數到 Lua 中去。
當咱們提到 Lua 能夠調用 C 函數,不是指 Lua 能夠調用任何類型的 C 函數(有一些
包可讓 Lua 調用任意的 C 函數,但缺少便捷和健壯性)。正如咱們前面所看到的,當
C 調用 Lua 函數的時候,必須遵循一些簡單的協議來傳遞參數和獲取返回結果。類似的,
從 Lua 中調用 C 函數,也必須遵循一些協議來傳遞參數和得到返回結果。另外,從 Lua
調用 C 函數咱們必須註冊函數,也就是說,咱們必須把 C 函數的地址以一個適當的方式
傳遞給 Lua 解釋器。
當 Lua 調用 C 函數的時候,使用和 C 調用 Lua 相同類型的棧來交互。C 函數從棧中
獲取她的參數,調用結束後將返回結果放到棧中。爲了區分返回結果和棧中的其餘的值,
每一個 C 函數還會返回結果的個數(the function returns (in C) the number of results it is
leaving on the stack.)。這兒有一個重要的概念:用來交互的棧不是全局變量,每個函
數都有他本身的私有棧。當 Lua 調用 C 函數的時候,第一個參數老是在這個私有棧的
index=1 的位置。甚至當一個 C 函數調用 Lua 代碼(Lua 代碼調用同一個 C 函數或者其
他的 C 函數),每個 C 函數都有本身的獨立的私有棧,而且第一個參數在 index=1 的
位置。
26.1 C 函數
先看一個簡單的例子,如何實現一個簡單的函數返回給定數值的 sin 值(更專業的
實現應該檢查他的參數是否爲一個數字):
static int l_sin (lua_State *L) {
double d = lua_tonumber(L, 1); /* get argument */
lua_pushnumber(L, sin(d));
return 1;
}
/* push result */
/* number of results */
任何在 Lua 中註冊的函數必須有一樣的原型,這個原型聲明定義就是 lua.h 中的
lua_CFunction:
typedef int (*lua_CFunction) (lua_State *L);
從 C 的角度來看,一個 C 函數接受單一的參數 Lua state,返回一個表示返回值個數
的數字。因此,函數在將返回值入棧以前不須要清理棧,函數返回以後,Lua 自動的清
除棧中返回結果下面的全部內容。
我 們 要 想 在 Lua 使 用 這 個 函 數 , 還 必 須 首 先 注 冊 這 個 函 數 。 我 們 使 用
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
199
lua_pushcfunction 來完成這個任務:他獲取指向 C 函數的指針,並在 Lua 中建立一個
function 類型的值來表示這個函數。一個 quick-and-dirty 的解決方案是將這段代碼直接放
到 lua.c 文件中,並在調用 lua_open 後面適當的位置加上下面兩行:
lua_pushcfunction(l, l_sin);
lua_setglobal(l, "mysin");
第一行將類型爲 function 的值入棧,第二行將 function 賦值給全局變量 mysin。這樣
修改以後,從新編譯 Lua,你就能夠在你的 Lua 程序中使用新的 mysin 函數了。在下面
一節,咱們將討論以比較好的方法將新的 C 函數添加到 Lua 中去。
對於稍微專業點的 sin 函數,咱們必須檢查 sin 的參數的類型。有一個輔助庫中的
luaL_checknumber 函數能夠檢查給定的參數是否爲數字:當有錯誤發生的時候,將拋出
一個錯誤信息;不然返回做爲參數的那個數字。將上面咱們的函數稍做修改:
static int l_sin (lua_State *L) {
double d = luaL_checknumber(L, 1);
lua_pushnumber(L, sin(d));
return 1; /* number of results */
}
根據上面的定義,若是你調用 mysin('a'),會獲得以下信息:
bad argument #1 to 'mysin' (number expected, got string)
注意看看 luaL_checknumber 是如何自動使用:參數 number(1),函數名("mysin"),
指望的參數類型("number"),實際的參數類型("string")來拼接最終的錯誤信息的。
下面看一個稍微複雜的例子:寫一個返回給定目錄內容的函數。Lua 的標準庫並沒
有提供這個函數,由於 ANSI C 沒有能夠實現這個功能的函數。在這兒,咱們假定咱們
的系統符合 POSIX 標準。咱們的 dir 函數接受一個表明目錄路徑的字符串做爲參數,以
數組的形式返回目錄的內容。好比,調用 dir("/home/lua")可能返回{".", "..", "src", "bin",
"lib"}。當有錯誤發生的時候,函數返回 nil 加上一個描述錯誤信息的字符串。
#include <dirent.h>
#include <errno.h>
static int l_dir (lua_State *L) {
DIR *dir;
struct dirent *entry;
int i;
const char *path = luaL_checkstring(L, 1);
/* open directory */
dir = opendir(path);
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
if (dir == NULL) {
lua_pushnil(L);
/* error opening the directory? */
/* return nil and ... */
200
lua_pushstring(L, strerror(errno)); /* error message */
return 2; /* number of results */
}
/* create result table */
lua_newtable(L);
i = 1;
while ((entry = readdir(dir)) != NULL) {
lua_pushnumber(L, i++);
lua_pushstring(L, entry->d_name);
lua_settable(L, -3);
}
closedir(dir);
return 1;
}
/* table is already on top */
/* push key */
/* push value */
輔助庫的 luaL_checkstring 函數用來檢測參數是否爲字符串,與 luaL_checknumber
相似。(在極端狀況下,上面的 l_dir 的實現可能會致使小的內存泄漏。調用的三個 Lua
函數 lua_newtable、lua_pushstring 和 lua_settable 可能因爲沒有足夠的內存而失敗。其中
任何一個調用失敗都會拋出錯誤而且終止 l_dir,這種狀況下,不會調用 closedir。正如
前面咱們所討論過的,對於大多數程序來講這不算個問題:若是程序致使內存不足,最
好的處理方式是當即終止程序。另外, 29 章咱們將看到另一種解決方案能夠避免這在
個問題的發生)
26.2 C 函數庫
一個 Lua 庫其實是一個定義了一系列 Lua 函數的 chunk,並將這些函數保存在適
當的地方,一般做爲 table 的域來保存。Lua 的 C 庫就是這樣實現的。除了定義 C 函數
以外,還必須定義一個特殊的用來和 Lua 庫的主 chunk 通訊的特殊函數。一旦調用,這
個函數就會註冊庫中全部的 C 函數,並將他們保存到適當的位置。像一個 Lua 主 chunk
同樣,她也會初始化其餘一些在庫中須要初始化的東西。
Lua 經過這個註冊過程,就能夠看到庫中的 C 函數。一旦一個 C 函數被註冊以後並
保存到 Lua 中,在 Lua 程序中就能夠直接引用他的地址(當咱們註冊這個函數的時候傳
遞給 Lua 的地址)來訪問這個函數了。換句話說,一旦 C 函數被註冊以後,Lua 調用這
個函數並不依賴於函數名,包的位置,或者調用函數的可見的規則。一般 C 庫都有一個
外部(public/extern)的用來打開庫的函數。其餘的函數可能都是私有的,在 C 中被聲明
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
爲 static。
201
當你打算使用 C 函數來擴展 Lua 的時候,即便你僅僅只想註冊一個 C 函數,將你的
C 代碼設計爲一個庫是個比較好的思想:不久的未來你就會發現你須要其餘的函數。一
般狀況下,輔助庫對這種實現提供了幫助。luaL_openlib 函數接受一個 C 函數的列表和
他們對應的函數名,而且做爲一個庫在一個 table 中註冊全部這些函數。看一個例子,假
定咱們想用一個咱們前面提過的 l_dir 函數建立一個庫。首先,咱們必須定義庫函數:
static int l_dir (lua_State *L) {
...
}
/* as before */
第二步,咱們聲明一個數組,保存全部的函數和他們對應的名字。這個數組的元素
類型爲 luaL_reg:是一個帶有兩個域的結構體,一個字符串和一個函數指針。
static const struct luaL_reg mylib [] = {
{"dir", l_dir},
{NULL, NULL}
};
/* sentinel */
在咱們的例子中,只有一個函數 l_dir 須要聲明。注意數組中最後一對必須是{NULL,
NULL},用來表示結束。第三步,咱們使用 luaL_openlib 聲明主函數:
int luaopen_mylib (lua_State *L) {
luaL_openlib(L, "mylib", mylib, 0);
return 1;
}
luaL_openlib 的第二個參數是庫的名稱。這個函數按照指定的名字建立(或者 reuse)
一個表,並使用數組 mylib 中的 name-function 對填充這個表。luaL_openlib 還容許咱們
爲庫中全部的函數註冊公共的 upvalues。例子中不須要使用 upvalues,因此最後一個參
數爲 0。luaL_openlib 返回的時候,將保存庫的表放到棧內。luaL_openlib 函數返回 1,
返回這個值給 Lua。(The luaopen_mylib function returns 1 to return this value to Lua)(和
Lua 庫同樣,這個返回值是可選的,由於庫自己已經賦給了一個全局變量。另外,像在
Lua 標準庫中的同樣,這個返回不會有額外的花費,在有時候多是有用的。)
完成庫的代碼編寫以後,咱們必須將它連接到 Lua 解釋器。最經常使用的方式使用動態
鏈接庫,若是你的 Lua 解釋器支持這個特性的話(咱們在 8.2 節已經討論過了動態鏈接
庫) 在這種狀況下,。你必須用你的代碼建立動態鏈接庫(windows 下.dll 文件,linux 下.so
文件)。到這一步,你就能夠在 Lua 中直接使用 loadlib 加載你剛纔定義的函數庫了,下
面這個調用:
mylib = loadlib("fullname-of-your-library", "luaopen_mylib")
將 luaopen_mylib 函數轉換成 Lua 中的一個 C 函數,並將這個函數賦值給 mylib(那
就是爲何 luaopen_mylib 必須和其餘的 C 函數有相同的原型的緣由所在)。而後,調用
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
mylib(),將運行 luaopen_mylib 打開你定義的函數庫。
202
若是你的解釋器不支持動態連接庫,你必須將你的新的函數庫從新編譯到你的 Lua
中去。除了這之外,還不要一些方式告訴獨立運行的 Lua 解釋器,當他打開一個新的狀
態的時候必須打開這個新定義的函數庫。宏定義能夠很容易實現這個功能。第一,你必
須使用下面的內容建立一個頭文件(咱們能夠稱之爲 mylib.h):
int luaopen_mylib (lua_State *L);
#define LUA_EXTRALIBS { "mylib", luaopen_mylib },
第一行聲明瞭打開庫的函數。第二行定義了一個宏 LUA_EXTRALIBS 做爲函數數
組的新的入口,當解釋器建立新的狀態的時候會調用這個宏。(這個函數數組的類型爲
struct luaL_reg[],所以咱們須要將名字也放進去)
爲了在解釋器中包含這個頭文件,你能夠在你的編譯選項中定義一個宏
LUA_USERCONFIG。對於命令行的編譯器,你只需添加一個下面這樣的選項便可:
-DLUA_USERCONFIG=\"mylib.h\"
(反斜線防止雙引號被 shell 解釋,當咱們在 C 中指定一個頭文件時,這些引號是
必需的。)在一個整合的開發環境中,你必須在工程設置中添加相似的東西。而後當你重
新編譯 lua.c 的時候,它包含 mylib.h,而且所以在函數庫的列表中能夠用新定義的
LUA_EXTRALIBS 來打開函數庫。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
203
第 27 章 撰寫 C 函數的技巧
官方的 API 和輔助函數庫 都提供了一些幫助程序員如何寫好 C 函數的機制。在這
一章咱們將討論數組操縱、string 處理、在 C 中存儲 Lua 值等一些特殊的機制。
27.1 數組操做
Lua 中數組實際上就是以特殊方式使用的 table 的別名。咱們能夠使用任何操縱 table
的函數來對數組操做, lua_settable 和 lua_gettable。即然而, Lua 常規簡潔思想與(economy
and simplicity)相反的是,API 爲數組操做提供了一些特殊的函數。這樣作的緣由出於
性能的考慮:由於咱們常常在一個算法(好比排序)的循環的內層訪問數組,因此這種
內層操做的性能的提升會對總體的性能的改善有很大的影響。
API 提供了下面兩個數組操做函數:
void lua_rawgeti (lua_State *L, int index, int key);
void lua_rawseti (lua_State *L, int index, int key);
關於的 lua_rawgeti 和 lua_rawseti 的描述有些令人糊塗,由於它涉及到兩個索引:
index 指向 table 在棧中的位置;key 指向元素在 table 中的位置。當 t 使用負索引的時候
(otherwise,you must compensate for the new item in the stack),調用 lua_rawgeti(L,t,key)
等價於:
lua_pushnumber(L, key);
lua_rawget(L, t);
調用 lua_rawseti(L, t, key)(也要求 t 使用負索引)等價於:
lua_pushnumber(L, key);
lua_insert(L, -2);
lua_rawset(L, t);
/* put 'key' below previous value */
注意這兩個寒暑都是用 raw 操做,他們的速度較快,總之,用做數組的 table 不多使
用 metamethods。
下面看如何使用這些函數的具體的例子,咱們將前面的 l_dir 函數的循環體:
lua_pushnumber(L, i++);
lua_pushstring(L, entry->d_name);
lua_settable(L, -3);
/* key */
/* value */
改寫爲:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
lua_pushstring(L, entry->d_name);
lua_rawseti(L, -2, i++);
/* value */
/* set table at key 'i' */
204
下面是一個更完整的例子,下面的代碼實現了 map 函數:以數組的每個元素爲參
數調用一個指定的函數,並將數組的該元素替換爲調用函數返回的結果。
int l_map (lua_State *L) {
int i, n;
/* 1st argument must be a table (t) */
luaL_checktype(L, 1, LUA_TTABLE);
/* 2nd argument must be a function (f) */
luaL_checktype(L, 2, LUA_TFUNCTION);
n = luaL_getn(L, 1); /* get size of table */
for (i=1; i<=n; i++) {
lua_pushvalue(L, 2);
lua_rawgeti(L, 1, i);
lua_call(L, 1, 1);
lua_rawseti(L, 1, i);
}
return 0; /* no results */
}
/* push f */
/* push t[i] */
/* call f(t[i]) */
/* t[i] = result */
這裏面引入了三個新的函數。luaL_checktype(在 lauxlib.h 中定義)用來檢查給定的
參數有指定的類型;不然拋出錯誤。luaL_getn 函數棧中指定位置的數組的大小(table.getn
是調用 luaL_getn 來完成工做的)。lua_call 的運行是無保護的,他與 lua_pcall 類似,但
是在錯誤發生的時候她拋出錯誤而不是返回錯誤代碼。當你在應用程序中寫主流程的代
碼時,不該該使用 lua_call,由於你應該捕捉任何可能發生的錯誤。當你寫一個函數的代
碼時,使用 lua_call 是比較好的想法,若是有錯誤發生,把錯誤留給關心她的人去處理。
27.2 字符串處理
當 C 函數接受一個來自 lua 的字符串做爲參數時,有兩個規則必須遵照:當字符串
正在被訪問的時候不要將其出棧;永遠不要修改字符串。
當 C 函數須要建立一個字符串返回給 lua 的時候,狀況變得更加複雜。這樣須要由
C 代碼來負責緩衝區的分配和釋放,負責處理緩衝溢出等狀況。然而,Lua API 提供了
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
一些函數來幫助咱們處理這些問題。
205
標準 API 提供了對兩種基本字符串操做的支持:子串截取和字符串鏈接。記住,
lua_pushlstring 能夠接受一個額外的參數,字符串的長度來實現字符串的截取,因此,如
果你想將字符串 s 從 i 到 j 位置(包含 i 和 j)的子串傳遞給 lua,只須要:
lua_pushlstring(L, s+i, j-i+1);
下面這個例子,假如你想寫一個函數來根據指定的分隔符分割一個字符串,並返回
一個保存全部子串的 table,好比調用:
split("hi,,there", ",")
應該返回表{"hi", "", "there"}。咱們能夠簡單的實現以下,下面這個函數不須要額外
的緩衝區,能夠處理字符串的長度也沒有限制。
static int l_split (lua_State *L) {
const char *s = luaL_checkstring(L, 1);
const char *sep = luaL_checkstring(L, 2);
const char *e;
int i = 1;
lua_newtable(L); /* result */
/* repeat for each separator */
while ((e = strchr(s, *sep)) != NULL) {
lua_pushlstring(L, s, e-s); /* push substring */
lua_rawseti(L, -2, i++);
s = e + 1; /* skip separator */
}
/* push last substring */
lua_pushstring(L, s);
lua_rawseti(L, -2, i);
return 1; /* return the table */
}
在 Lua API 中提供了專門的用來鏈接字符串的函數 lua_concat。等價於 Lua 中的..操
做符:自動將數字轉換成字符串,若是有必要的時候還會自動調用 metamethods。另外,
她能夠同時鏈接多個字符串。調用 lua_concat(L,n)將鏈接(同時會出棧)棧頂的 n 個值,並
將最終結果放到棧頂。
另外一個有用的函數是 lua_pushfstring:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
const char *lua_pushfstring (lua_State *L,
const char *fmt, ...);
206
這個函數某種程度上相似於 C 語言中的 sprintf,根據格式串 fmt 的要求建立一個新
的字符串。與 sprintf 不一樣的是,你不須要提供一個字符串緩衝數組,Lua 爲你動態的創
建新的字符串,按他實際須要的大小。也不須要擔憂緩衝區溢出等問題。這個函數會將
結果字符串放到棧內,並返回一個指向這個結果串的指針。當前,這個函數只支持下列
幾個指示符: %%(表示字符 '%')、%s(用來格式化字符串)、%d(格式化整數)、%f
(格式化 Lua 數字,即 doubles)和 %c(接受一個數字並將其做爲字符),不支持寬度
和精度等選項。
當咱們打算鏈接少許的字符串的時候,lua_concat 和 lua_pushfstring 是頗有用的,然
而,若是咱們須要鏈接大量的字符串(或者字符),這種一個一個的鏈接方式效率是很低
的,正如咱們在 11.6 節看到的那樣。咱們能夠使用輔助庫提供的 buffer 相關函數來解決
這個問題。Auxlib 在兩個層次上實現了這些 buffer。第一個層次相似於 I/O 操做的 buffers:
集中全部的字符串(或者但個字符)放到一個本地 buffer 中,當本地 buffer 滿的時候將
其傳遞給 Lua(使用 lua_pushlstring)。第二個層次使用 lua_concat 和咱們在 11.6 節中看
到的那個棧算法的變體,來鏈接多個 buffer 的結果。
爲了更詳細地描述 Auxlib 中的 buffer 的使用,咱們來看一個簡單的應用。下面這段
代碼顯示了 string.upper 的實現(來自文件 lstrlib.c):
static int str_upper (lua_State *L) {
size_t l;
size_t i;
luaL_Buffer b;
const char *s = luaL_checklstr(L, 1, &l);
luaL_buffinit(L, &b);
for (i=0; i<l; i++)
luaL_putchar(&b, toupper((unsigned char)(s[i])));
luaL_pushresult(&b);
return 1;
}
使用 Auxlib 中 buffer 的第一步是使用類型 luaL_Buffer 聲明一個變量,而後調用
luaL_buffinit 初始化這個變量。初始化以後,buffer 保留了一份狀態 L 的拷貝,所以當我
們調用其餘操做 buffer 的函數的時候不須要傳遞 L。宏 luaL_putchar 將一個單個字符放
入 buffer。Auxlib 也提供了 luaL_addlstring 以一個顯示的長度將一個字符串放入 buffer,
而 luaL_addstring 將一個以 0 結尾的字符串放入 buffer。最後,luaL_pushresult 刷新 buffer
並將最終字符串放到棧頂。這些函數的原型以下:
void luaL_buffinit (lua_State *L, luaL_Buffer *B);
void luaL_putchar (luaL_Buffer *B, char c);
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l);
void luaL_addstring (luaL_Buffer *B, const char *s);
void luaL_pushresult (luaL_Buffer *B);
207
使用這些函數,咱們不須要擔憂 buffer 的分配,溢出等詳細信息。正如咱們所看到
的,鏈接算法是有效的。函數 str_upper 能夠毫無問題的處理大字符串(大於 1MB)。
當你使用auxlib中的buffer時,沒必要擔憂一點細節問題。你只要將東西放入buffer,程
序會自動在Lua棧中保存中間結果。因此,你不要認爲棧頂會保持你開始使用buffer的那
個狀態。另外,雖然你能夠在使用buffer的時候,將棧用做其餘用途,但每次你訪問buffer
的時候,這些其餘用途的操做進行的push/pop操做必須保持平衡6。有一種狀況,即你打
算將從Lua返回的字符串放入buffer時,這種狀況下,這些限制有些過於嚴格。這種狀況
下,在將字符串放入buffer以前,不能將字符串出棧,由於一旦你從棧中未來自於Lua的
字符串移出,你就永遠不能使用這個字符串。同時,在將一個字符串出棧以前,你也不
可以將其放入buffer,由於那樣會將棧置於錯誤的層次(because then the stack would be in
the wrong level)。換句話說你不能作相似下面的事情:
luaL_addstring(&b, lua_tostring(L, 1));
/* BAD CODE */
(譯者:上面正好構成了一對矛盾),因爲這種狀況是很常見的,auxlib 提供了特殊
的函數來將位於棧頂的值放入 buffer:
void luaL_addvalue (luaL_Buffer *B);
固然,若是位於棧頂的值不是字符串或者數字的話,調用這個函數將會出錯。
27.3 在 C 函數中保存狀態
一般來講,C 函數須要保留一些非局部的數據,也就是指那些超過他們做用範圍的
數據。C 語言中咱們使用全局變量或者 static 變量來知足這種須要。然而當你爲 Lua 設
計一個程序庫的時候,全局變量和 static 變量不是一個好的方法。首先,不能將全部的
(通常意義的,原文 generic)Lua 值保存到一個 C 變量中。第二,使用這種變量的庫不
能在多個 Lua 狀態的狀況下使用。
一個替代的解決方案是將這些值保存到一個 Lua 全局變兩種,這種方法解決了前面
的兩個問題。Lua 全局變量能夠存聽任何類型的 Lua 值,而且每個獨立的狀態都有他
本身獨立的全局變量集。然而,並非在全部狀況下,這種方法都是使人滿意地解決方
案,由於 Lua 代碼可能會修改這些全局變量,危及 C 數據的完整性。爲了不這個問題,
Lua 提供了一個獨立的被稱爲 registry 的表,C 代碼能夠自由使用,但 Lua 代碼不能訪問
他。
6
譯註:即有多少次push就要有多少次pop。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
208
27.3.1 The Registry
registry 一 直 位 於 一 個 由 LUA_REGISTRYINDEX 定 義 的 值 所 對 應 的 假 索 引
(pseudo-index)的位置。一個假索引除了他對應的值不在棧中以外,其餘都相似於棧中的
索引。Lua API 中大部分接受索引做爲參數的函數,也均可以接受假索引做爲參數—除
了那些操做棧自己的函數,好比 lua_remove,lua_insert。例如,爲了獲取以鍵值 "Key" 保
存在 registry 中的值,使用下面的代碼:
lua_pushstring(L, "Key");
lua_gettable(L, LUA_REGISTRYINDEX);
registry 就是普通的 Lua 表,所以,你能夠使用任何非 nil 的 Lua 值來訪問她的元素。
然而,因爲全部的 C 庫共享相同的 registry ,你必須注意使用什麼樣的值做爲 key,否
則會致使命名衝突。一個防止命名衝突的方法是使用 static 變量的地址做爲 key:C 連接
器保證在全部的庫中這個 key 是惟一的。函數 lua_pushlightuserdata 將一個表明 C 指針的
值放到棧內,下面的代碼展現了使用上面這個方法,如何從 registry 中獲取變量和向
registry 存儲變量:
/* variable with an unique address */
static const char Key = 'k';
/* store a number */
lua_pushlightuserdata(L, (void *)&Key); /* push address */
lua_pushnumber(L, myNumber);
/* push value */
/* registry[&Key] = myNumber */
lua_settable(L, LUA_REGISTRYINDEX);
/* retrieve a number */
lua_pushlightuserdata(L, (void *)&Key);
/* push address */
lua_gettable(L, LUA_REGISTRYINDEX); /* retrieve value */
myNumber = lua_tonumber(L, -1); /* convert to number */
咱們會在 28.5 節中更詳細的討論 light userdata。
固然,你也能夠使用字符串做爲 registry 的 key,只要你保證這些字符串惟一。當你
打算容許其餘的獨立庫房問你的數據的時候,字符串型的 key 是很是有用的,由於他們
須要知道 key 的名字。對這種狀況,沒有什麼方法能夠絕對防止名稱衝突,但有一些好
的習慣能夠採用,好比使用庫的名稱做爲字符串的前綴等相似的方法。相似 lua 或者 lualib
的前綴不是一個好的選擇。另外一個可選的方法是使用 universal unique identifier(uuid),
不少系統都有專門的程序來產生這種標示符(好比 linux 下的 uuidgen)。一個 uuid 是一
個由本機 IP 地址、時間戳、和一個隨機內容組合起來的 128 位的數字(以 16 進制的方
式書寫,用來造成一個字符串),所以它與其餘的 uuid 不一樣是能夠保證的。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
209
27.3.2 References
你應該記住,永遠不要使用數字做爲 registry 的 key,由於這種類型的 key 是保留給
reference 系統使用。Reference 系統是由輔助庫中的一對函數組成,這對函數用來不須要
擔憂名稱衝突的將值保存到 registry 中去。(實際上,這些函數能夠用於任何一個表,但
他們典型的被用於 registry)
調用
int r = luaL_ref(L, LUA_REGISTRYINDEX);
從棧中彈出一個值,以一個新的數字做爲 key 將其保存到 registry 中,並返回這個
key。咱們將這個 key 稱之爲 reference。
顧名思義,咱們使用 references 主要用於:將一個指向 Lua 值的 reference 存儲到一
個 C 結構體中。正如前面咱們所見到的,咱們永遠不要將一個指向 Lua 字符串的指針保
存到獲取這個字符串的外部的 C 函數中。另外,Lua 甚至不提供指向其餘對象的指針,
好比 table 或者函數。所以,咱們不能經過指針指向 Lua 對象。當咱們須要這種指針的時
候,咱們建立一個 reference 並將其保存在 C 中。
要想將一個 reference 的對應的值入棧,只須要:
lua_rawgeti(L, LUA_REGISTRYINDEX, r);
最後,咱們調用下面的函數釋放值和 reference:
luaL_unref(L, LUA_REGISTRYINDEX, r);
調用這個以後,luaL_ref 能夠再次返回 r 做爲一個新的 reference。
reference 系統將 nil 做爲特殊狀況對待,無論何時,你以 nil 調用 luaL_ref 的話,
不會建立一新的 reference ,而是返回一個常量 reference LUA_REFNIL。下面的調用沒
有效果:
luaL_unref(L, LUA_REGISTRYINDEX, LUA_REFNIL);
然而
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL);
像預期的同樣,將一個 nil 入棧。
reference 系統也定義了常量 LUA_NOREF,她是一個表示任何非有效的 reference 的
整數值,用來標記無效的 reference。任何企圖獲取 LUA_NOREF 返回 nil,任何釋放他
的操做都沒有效果。
27.3.3 Upvalues
registry 實現了全局的值,upvalue 機制實現了與 C static 變量等價的東東,這種變量
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
210
只能在特定的函數內可見。每當你在 Lua 中建立一個新的 C 函數,你能夠將這個函數與
任意多個 upvalues 聯繫起來,每個 upvalue 能夠持有一個單獨的 Lua 值。下面當函數
被調用的時候,能夠經過假索引自由的訪問任何一個 upvalues。
咱們稱這種一個 C 函數和她的 upvalues 的組合爲閉包(closure)。記住:在 Lua 代
碼中,一個閉包是一個從外部函數訪問局部變量的函數。一個 C 閉包與一個 Lua 閉包相
近。關於閉包的一個有趣的事實是,你能夠使用相同的函數代碼建立不一樣的閉包,帶有
不一樣的 upvalues。
看一個簡單的例子,咱們在 C 中建立一個 newCounter 函數。(咱們已經在 6.1 節部
分在 Lua 中定義過一樣的函數)。這個函數是個函數工廠:每次調用他都返回一個新的
counter 函數。儘管全部的 counters 共享相同的 C 代碼,可是每一個都保留獨立的 counter
變量,工廠函數以下:
/* forward declaration */
static int counter (lua_State *L);
int newCounter (lua_State *L) {
lua_pushnumber(L, 0);
lua_pushcclosure(L, &counter, 1);
return 1;
}
這裏的關鍵函數是 lua_pushcclosure,她的第二個參數是一個基本函數(例子中衛
counter),第三個參數是 upvalues 的個數(例子中爲 1)。在建立新的閉包以前,咱們必
須將 upvalues 的初始值入棧,在咱們的例子中,咱們將數字 0 做爲惟一的 upvalue 的初
始值入棧。如預期的同樣,lua_pushcclosure 將新的閉包放到棧內,所以閉包已經做爲
newCounter 的結果被返回。
如今,咱們看看 counter 的定義:
static int counter (lua_State *L) {
double val = lua_tonumber(L, lua_upvalueindex(1));
lua_pushnumber(L, ++val);
lua_pushvalue(L, -1);
/* new value */
/* duplicate it */
lua_replace(L, lua_upvalueindex(1)); /* update upvalue */
return 1; /* return new value */
}
這裏的關鍵函數是 lua_upvalueindex(實際是一個宏),用來產生一個 upvalue 的假
索引。這個假索引除了不在棧中以外,和其餘的索引同樣。表達式 lua_upvalueindex(1)
函數第一個 upvalue 的索引。所以,在函數 counter 中的 lua_tonumber 獲取第一個(僅有
的)upvalue 的當前值,轉換爲數字型。而後,函數 counter 將新的值++val 入棧,並將這
個值的一個拷貝使用新的值替換 upvalue。最後,返回其餘的拷貝。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
211
與 Lua 閉包不一樣的是,C 閉包不能共享 upvalues:每個閉包都有本身獨立的變量
集。然而,咱們能夠設置不一樣函數的 upvalues 指向同一個表,這樣這個表就變成了一個
全部函數共享數據的地方。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
212
第 28 章 User-Defined Types in C
在面的一章,咱們討論瞭如何使用 C 函數擴展 Lua 的功能,如今咱們討論如何使用
C 中新建立的類型來擴展 Lua。咱們從一個小例子開始,本章後續部分將以這個小例子
爲基礎逐步加入 metamethods 等其餘內容來介紹如何使用 C 中新類型擴展 Lua。
咱們的例子涉及的類型很是簡單,數字數組。這個例子的目的在於將目光集中到 API
問題上,因此不涉及複雜的算法。儘管例子中的類型很簡單,但不少應用中都會用到這
種類型。通常狀況下,Lua 中並不須要外部的數組,由於哈希表很好的實現了數組。但
是對於很是大的數組而言,哈希表可能致使內存不足,由於對於每個元素必須保存一
個範性的(generic)值,一個連接地址,加上一些以備未來增加的額外空間。在 C 中的
直接存儲數字值不須要額外的空間,將比哈希表的實現方式節省 50%的內存空間。
咱們使用下面的結構表示咱們的數組:
typedef struct NumArray {
int size;
double values[1]; /* variable part */
} NumArray;
咱們使用大小 1 聲明數組的 values,因爲 C 語言不容許大小爲 0 的數組,這個 1 只
是一個佔位符;咱們在後面定義數組分配空間的實際大小。對於一個有 n 個元素的數組
來講,咱們須要
sizeof(NumArray) + (n-1)*sizeof(double) bytes
(因爲原始的結構中已經包含了一個元素的空間,因此咱們從 n 中減去 1)
28.1 Userdata
咱們首先關心的是如何在 Lua 中表示數組的值。Lua 爲這種狀況提供專門提供一個
基本的類型:userdata。一個 userdatum 提供了一個在 Lua 中沒有預約義操做的 raw 內存
區域。
Lua API 提供了下面的函數用來建立一個 userdatum:
void *lua_newuserdata (lua_State *L, size_t size);
lua_newuserdata 函數按照指定的大小分配一塊內存,將對應的 userdatum 放到棧內,
並返回內存塊的地址。若是出於某些緣由你須要經過其餘的方法分配內存的話,很容易
建立一個指針大小的 userdatum,而後將指向實際內存塊的指針保存到 userdatum 裏。我
們將在下一章看到這種技術的例子。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
使用 lua_newuserdata 函數,建立新數組的函數實現以下:
static int newarray (lua_State *L) {
int n = luaL_checkint(L, 1);
size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);
a->size = n;
return 1; /* new userdatum is already on the stack */
}
213
(函數 luaL_checkint 是用來檢查整數的 luaL_checknumber 的變體)一旦 newarray
在 Lua 中被註冊以後,你就能夠使用相似 a = array.new(1000)的語句建立一個新的數組
了。
爲了存儲元素,咱們使用相似 array.set(array, index, value)調用,後面咱們將看到如
何使用 metatables 來支持常規的寫法 array[index] = value。對於這兩種寫法,下面的函數
是同樣的,數組下標從1開始:
static int setarray (lua_State *L) {
NumArray *a = (NumArray *)lua_touserdata(L, 1);
int index = luaL_checkint(L, 2);
double value = luaL_checknumber(L, 3);
luaL_argcheck(L, a != NULL, 1, "`array' expected");
luaL_argcheck(L, 1 <= index && index <= a->size, 2,
"index out of range");
a->values[index-1] = value;
return 0;
}
luaL_argcheck 函數檢查給定的條件,若是有必要的話拋出錯誤。所以,若是咱們使
用錯誤的參數調用 setarray,咱們將獲得一個錯誤信息:
array.set(a, 11, 0)
--> stdin:1: bad argument #1 to 'set' ('array' expected)
下面的函數獲取一個數組元素:
static int getarray (lua_State *L) {
NumArray *a = (NumArray *)lua_touserdata(L, 1);
int index = luaL_checkint(L, 2);
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
luaL_argcheck(L, a != NULL, 1, "'array' expected");
214
luaL_argcheck(L, 1 <= index && index <= a->size, 2,
"index out of range");
lua_pushnumber(L, a->values[index-1]);
return 1;
}
咱們定義另外一個函數來獲取數組的大小:
static int getsize (lua_State *L) {
NumArray *a = (NumArray *)lua_touserdata(L, 1);
luaL_argcheck(L, a != NULL, 1, "`array' expected");
lua_pushnumber(L, a->size);
return 1;
}
最後,咱們須要一些額外的代碼來初始化咱們的庫:
static const struct luaL_reg arraylib [] = {
{"new", newarray},
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};
int luaopen_array (lua_State *L) {
luaL_openlib(L, "array", arraylib, 0);
return 1;
}
這兒咱們再次使用了輔助庫的 luaL_openlib 函數,他根據給定的名字建立一個表,
並使用 arraylib 數組中的 name-function 對填充這個表。
打開上面定義的庫以後,咱們就能夠在 Lua 中使用咱們新定義的類型了:
a = array.new(1000)
print(a)
print(array.size(a))
for i=1,1000 do
array.set(a, i, 1/i)
end
--> userdata: 0x8064d48
--> 1000
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
print(array.get(a, 10)) --> 0.1
215
在一個 Pentium/Linux 環境中運行這個程序,一個有 100K 元素的數組大概佔用
800KB 的內存,一樣的條件由 Lua 表實現的數組須要 1.5MB 的內存。
28.2 Metatables
咱們上面的實現有一個很大的安全漏洞。假如使用者寫了以下相似的代碼:
array.set(io.stdin, 1, 0)。io.stdin 中的值是一個帶有指向流(FILE*)的指針的 userdatum。因
爲它是一個 userdatum,因此 array.set 很樂意接受它做爲參數,程序運行的結果可能致使
內存 core dump(若是你夠幸運的話,你可能獲得一個訪問越界(index-out-of-range)錯
誤)。這樣的錯誤對於任何一個 Lua 庫來講都是不能忍受的。不論你如何使用一個 C 庫,
都不該該破壞 C 數據或者從 Lua 產生 core dump。
爲了區分數組和其餘的 userdata,咱們單獨爲數組建立了一個 metatable 記住 userdata(
也能夠擁有 metatables)。下面,咱們每次建立一個新的數組的時候,咱們將這個單獨的
metatable 標記爲數組的 metatable。每次咱們訪問數組的時候,咱們都要檢查他是否有一
個正確的 metatable。由於 Lua 代碼不能改變 userdatum 的 metatable,因此他不會僞造我
們的代碼。
咱們還須要一個地方來保存這個新的 metatable,這樣咱們纔可以當建立新數組和檢
查一個給定的 userdatum 是不是一個數組的時候,能夠訪問這個 metatable。正如咱們前
面介紹過的,有兩種方法能夠保存 metatable:在 registry 中,或者在庫中做爲函數的
upvalue。在 Lua 中通常習慣於在 registry 中註冊新的 C 類型,使用類型名做爲索引,
metatable 做爲值。和其餘的 registry 中的索引同樣,咱們必須選擇一個惟一的類型名,
避免衝突。咱們將這個新的類型稱爲 "LuaBook.array"。
輔助庫提供了一些函數來幫助咱們解決問題,咱們這兒將用到的前面未提到的輔助
函數有:
int
luaL_newmetatable (lua_State *L, const char *tname);
void luaL_getmetatable (lua_State *L, const char *tname);
void *luaL_checkudata (lua_State *L, int index,
const char *tname);
luaL_newmetatable 函數建立一個新表(將用做 metatable),將新表放到棧頂並創建
表和 registry 中類型名的聯繫。這個關聯是雙向的:使用類型名做爲表的 key;同時使用
表做爲類型名的 key(這種雙向的關聯,使得其餘的兩個函數的實現效率更高)。
luaL_getmetatable 函數獲取 registry 中的 tname 對應的 metatable。最後,luaL_checkudata
檢查在棧中指定位置的對象是否爲帶有給定名字的 metatable 的 usertatum。若是對象不
存在正確的 metatable,返回 NULL(或者它不是一個 userdata);不然,返回 userdata 的
地址。
下面來看具體的實現。第一步修改打開庫的函數,新版本必須建立一個用做數組
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
metatable 的表:
int luaopen_array (lua_State *L) {
luaL_newmetatable(L, "LuaBook.array");
luaL_openlib(L, "array", arraylib, 0);
return 1;
}
216
第二步,修改 newarray,使得在建立數組的時候設置數組的 metatable:
static int newarray (lua_State *L) {
int n = luaL_checkint(L, 1);
size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);
luaL_getmetatable(L, "LuaBook.array");
lua_setmetatable(L, -2);
a->size = n;
return 1; /* new userdatum is already on the stack */
}
lua_setmetatable 函數將表出棧,並將其設置爲給定位置的對象的 metatable。在咱們
的例子中,這個對象就是新的 userdatum。
最後一步,setarray、getarray 和 getsize 檢查他們的第一個參數是不是一個有效的數
組。由於咱們打算在參數錯誤的狀況下拋出一個錯誤信息,咱們定義了下面的輔助函數:
static NumArray *checkarray (lua_State *L) {
void *ud = luaL_checkudata(L, 1, "LuaBook.array");
luaL_argcheck(L, ud != NULL, 1, "`array' expected");
return (NumArray *)ud;
}
使用 checkarray,新定義的 getsize 是更直觀、更清楚:
static int getsize (lua_State *L) {
NumArray *a = checkarray(L);
lua_pushnumber(L, a->size);
return 1;
}
因爲 setarray 和 getarray 檢查第二個參數 index 的代碼相同,咱們抽象出他們的共同
部分,在一個單獨的函數中完成:
static double *getelem (lua_State *L) {
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
NumArray *a = checkarray(L);
int index = luaL_checkint(L, 2);
luaL_argcheck(L, 1 <= index && index <= a->size, 2,
"index out of range");
/* return element address */
return &a->values[index - 1];
}
217
使用這個 getelem,函數 setarray 和 getarray 更加直觀易懂:
static int setarray (lua_State *L) {
double newvalue = luaL_checknumber(L, 3);
*getelem(L) = newvalue;
return 0;
}
static int getarray (lua_State *L) {
lua_pushnumber(L, *getelem(L));
return 1;
}
如今,假如你嘗試相似 array.get(io.stdin, 10)的代碼,你將會獲得正確的錯誤信息:
error: bad argument #1 to 'getarray' ('array' expected)
28.3 訪問面向對象的數據
下面咱們來看看如何定義類型爲對象的 userdata,以至咱們就能夠使用面向對象的
語法來操做對象的實例,好比:
a = array.new(1000)
print(a:size())
a:set(10, 3.4)
print(a:get(10))
--> 3.4
--> 1000
記住 a:size()等價於 a.size(a)。因此,咱們必須使得表達式 a.size 調用咱們的 getsize
函數。這兒的關鍵在於__index 元方法(metamethod)的使用。對於表來講,無論什麼
時候只要找不到給定的 key,這個元方法就會被調用。對於 userdata 來說,每次被訪問
的時候元方法都會被調用,由於 userdata 根本就沒有任何 key。
假如咱們運行下面的代碼:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
local metaarray = getmetatable(array.new(1))
metaarray.__index = metaarray
metaarray.set = array.set
metaarray.get = array.get
metaarray.size = array.size
218
第一行,咱們僅僅建立一個數組並獲取他的 metatable,metatable 被賦值給 metaarray
(咱們不能從 Lua 中設置 userdata 的 metatable,可是咱們在 Lua 中無限制的訪問
metatable) 接下來,。咱們設置 metaarray.__index 爲 metaarray。當咱們計算 a.size 的時候,
Lua 在對象 a 中找不到 size 這個鍵值,由於對象是一個 userdatum。因此,Lua 試着從對
象 a 的 metatable 的__index 域獲取這個值,正好__index 就是 metaarray。可是 metaarray.size
就是 array.size,所以 a.size(a)如咱們預期的返回 array.size(a)。
固然,咱們能夠在 C 中完成一樣的事情,甚至能夠作得更好:如今數組是對象,他
有本身的操做,咱們在表數組中不須要這些操做。咱們實現的庫惟一須要對外提供的函
數就是 new,用來建立一個新的數組。全部其餘的操做做爲方法實現。C 代碼能夠直接
註冊他們。
getsize、getarray 和 setarray 與咱們前面的實現同樣,不須要改變。咱們須要改變的
只是如何註冊他們。也就是說,咱們必須改變打開庫的函數。首先,咱們須要分離函數
列表,一個做爲普通函數,一個做爲方法:
static const struct luaL_reg arraylib_f [] = {
{"new", newarray},
{NULL, NULL}
};
static const struct luaL_reg arraylib_m [] = {
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};
新版本打開庫的函數 luaopen_array,必須建立一個 metatable,並將其賦值給本身的
__index 域,在那兒註冊全部的方法,建立並填充數組表:
int luaopen_array (lua_State *L) {
luaL_newmetatable(L, "LuaBook.array");
lua_pushstring(L, "__index");
lua_pushvalue(L, -2);
/* pushes the metatable */
lua_settable(L, -3); /* metatable.__index = metatable */
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
219
luaL_openlib(L, NULL, arraylib_m, 0);
luaL_openlib(L, "array", arraylib_f, 0);
return 1;
}
這裏咱們使用了 luaL_openlib 的另外一個特徵,第一次調用,當咱們傳遞一個 NULL
做爲庫名時,luaL_openlib 並無建立任何包含函數的表;相反,他認爲封裝函數的表
在棧內,位於臨時的 upvalues 的下面。在這個例子中,封裝函數的表是 metatable 自己,
也就是 luaL_openlib 放置方法的地方。第二次調用 luaL_openlib 正常工做:根據給定的
數組名建立一個新表,並在表中註冊指定的函數(例子中只有一個函數 new)。
下面的代碼,咱們爲咱們的新類型添加一個__tostring 方法,這樣一來 print(a)將打
印數組加上數組的大小,大小兩邊帶有圓括號(好比,array(1000)):
int array2string (lua_State *L) {
NumArray *a = checkarray(L);
lua_pushfstring(L, "array(%d)", a->size);
return 1;
}
函數 lua_pushfstring 格式化字符串,並將其放到棧頂。爲了在數組對象的 metatable
中包含 array2string,咱們還必須在 arraylib_m 列表中添加 array2string:
static const struct luaL_reg arraylib_m [] = {
{"__tostring", array2string},
{"set", setarray},
...
};
28.4 訪問數組
除了上面介紹的使用面向對象的寫法來訪問數組之外,還能夠使用傳統的寫法來訪
問數組元素,不是 a:get(i),而是 a[i]。對於咱們上面的例子,很容易實現這個,由於我
們的 setarray 和 getarray 函數已經依次接受了與他們的元方法對應的參數。一個快速的解
決方法是在咱們的 Lua 代碼中正確的定義這些元方法:
local metaarray = getmetatable(newarray(1))
metaarray.__index = array.get
metaarray.__newindex = array.set
(這段代碼必須運行在前面的最初的數組實現基礎上,不能使用爲了面向對象訪問
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
的修改的那段代碼)
咱們要作的只是使用傳統的語法:
a = array.new(1000)
a[10] = 3.4
print(a[10])
-- setarray
-- getarray
--> 3.4
220
若是咱們喜歡的話,咱們能夠在咱們的 C 代碼中註冊這些元方法。咱們只須要修改
咱們的初始化函數:
int luaopen_array (lua_State *L) {
luaL_newmetatable(L, "LuaBook.array");
luaL_openlib(L, "array", arraylib, 0);
/* now the stack has the metatable at index 1 and
'array' at index 2 */
lua_pushstring(L, "__index");
lua_pushstring(L, "get");
lua_gettable(L, 2); /* get array.get */
lua_settable(L, 1); /* metatable.__index = array.get */
lua_pushstring(L, "__newindex");
lua_pushstring(L, "set");
lua_gettable(L, 2); /* get array.set */
lua_settable(L, 1); /* metatable.__newindex = array.set */
return 0;
}
28.5 Light Userdata
到目前爲止咱們使用的 userdata 稱爲 full userdata。 還提供了另外一種 userdata: lightLua
userdata。
一個 light userdatum 是一個表示 C 指針的值(也就是一個 void *類型的值)。因爲它
是一個值,咱們不能建立他們(一樣的,咱們也不能建立一個數字)。能夠使用函數
lua_pushlightuserdata 將一個 light userdatum 入棧:
void lua_pushlightuserdata (lua_State *L, void *p);
儘管都是 userdata,light userdata 和 full userdata 有很大不一樣。Light userdata 不是一
個緩衝區,僅僅是一個指針,沒有 metatables。像數字同樣,light userdata 不須要垃圾收
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
集器來管理她。
221
有些人把 light userdata 做爲一個低代價的替代實現,來代替 full userdata,可是這不
是 light userdata 的典型應用。首先,使用 light userdata 你必須本身管理內存,由於他們
和垃圾收集器無關。第二,儘管從名字上看有輕重之分,但 full userdata 實現的代價也並
不大,比較而言,他只是在分配給定大小的內存時候,有一點點額外的代價。
Light userdata 真正的用處在於能夠表示不一樣類型的對象。 full userdata 是一個對象當
的時候,它等於對象自身;另外一方面,light userdata 表示的是一個指向對象的指針,同
樣的,它等於指針指向的任何類型的 userdata。因此,咱們在 Lua 中使用 light userdata
表示 C 對象。
看一個典型的例子,假定咱們要實現:Lua 和窗口系統的綁定。這種狀況下,咱們
使用 full userdata 表示窗口(每個 userdatum 能夠包含整個窗口結構或者一個有系統創
建的指向單個窗口的指針)。當在窗口有一個事件發生(好比按下鼠標),系統會根據窗
口的地址調用專門的回調函數。爲了將這個回調函數傳遞給 Lua,咱們必須找到表示指
定窗口的 userdata。爲了找到這個 userdata,咱們能夠使用一個表:索引爲表示窗口地址的
light userdata,值爲在 Lua 中表示窗口的 full userdata。一旦咱們有了窗口的地址,咱們
將窗口地址做爲 light userdata 放到棧內,而且將 userdata 做爲表的索引存到表內。(注意
這個表應該有一個 weak 值,不然,這些 full userdata 永遠不會被回收掉。)
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
222
第 29 章 資源管理
在前面一章介紹的數組實現方法,咱們沒必要擔憂如何管理資源,只須要分配內存。
每個表示數組的 userdatum 都有本身的內存,這個內存由 Lua 管理。當數組變爲垃圾
(也就是說,當程序不須要)的時候,Lua 會自動收集並釋放內存。
生活老是不那麼如意。有時候,一個對象除了須要物理內存之外,還須要文件描述
符、窗口句柄等相似的資源。(一般這些資源也是內存,但由系統的其餘部分來管理)。
在這種狀況下,當一個對象成爲垃圾並被收集的時候,這些相關的資源也應該被釋放。
一些面向對象的語言爲了這種須要提供了一種特殊的機制(稱爲 finalizer 或者析構器)。
Lua 以__gc 元方法的方式提供了 finalizers。這個元方法只對 userdata 類型的值有效。當
一個 userdatum 將被收集的時候,而且 usedatum 有一個__gc 域,Lua 會調用這個域的值
(應該是一個函數):以 userdatum 做爲這個函數的參數調用。這個函數負責釋放與
userdatum 相關的全部資源。
爲了闡明如何將這個元方法和 API 做爲一個總體使用,這一章咱們將使用 Lua 擴展
應用的方式,介紹兩個例子。第一個例子是前面已經介紹的遍歷一個目錄的函數的另外一
種實現。第二個例子是一個綁定 Expat(Expat 開源的 XML 解析器)實現的 XML 解析
器。
29.1 目錄迭代器
前面咱們實現了一個 dir 函數,給定一個目錄做爲參數,這個函數以一個 table 的方
式返回目錄下全部文件。咱們新版本的 dir 函數將返回一個迭代子,每次調用這個迭代
子的時候會返回目錄中的一個入口(entry)。按新版本的實現方式,咱們能夠使用循環
來遍歷整個目錄:
for fname in dir(".") do print(fname)
end
在 C 語言中,咱們須要 DIR 這種結構纔可以迭代一個目錄。經過 opendir 才能建立
一個 DIR 的實例,而且必須顯式的調用 closedir 來釋放資源。咱們之前實現的 dir 用一個
本地變量保存 DIR 的實例,而且在獲取目錄中最後一個文件名以後關閉實例。但咱們新
實現的 dir 中不能在本地變量中保存 DIR 的實例,由於有不少個調用都要訪問這個值,
另外,也不能僅僅在獲取目錄中最後一個文件名以後關閉目錄。若是程序循環過程當中中
斷退出,迭代子根本就不會取得最後一個文件名,因此,爲了保證 DIR 的實例必定可以
被釋放掉,咱們將它的地址保存在一個 userdatum 中,並使用這個 userdatum 的__gc 的
元方法來釋放目錄結構。
儘管咱們實現中userdatum的做用很重要,但這個用來表示一個目錄的userdatum,並
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
223
不須要在Lua可見範圍以內。Dir函數返回一個迭代子函數,迭代子函數須要在Lua的可見
範圍以內。目錄多是迭代子函數的一個upvalue。這樣一來,迭代子函數就能夠直接訪
問這個結構7,可是Lua不能夠(也不須要)訪問這個結構。
總的來講,咱們須要三個 C 函數。第一,dir 函數,一個 Lua 調用他產生迭代器的
工廠,這個函數必須打開 DIR 結構並將他做爲迭代函數的 upvalue。第二,咱們須要一
個迭代函數。第三,__gc 元方法,負責關閉 DIR 結構。通常來講,咱們還須要一個額外
的函數來進行一些初始的操做,好比爲目錄建立 metatable,並初始化這個 metatable。
首先看咱們的 dir 函數:
#include <dirent.h>
#include <errno.h>
/* forward declaration for the iterator function */
static int dir_iter (lua_State *L);
static int l_dir (lua_State *L) {
const char *path = luaL_checkstring(L, 1);
/* create a userdatum to store a DIR address */
DIR **d = (DIR **)lua_newuserdata(L, sizeof(DIR *));
/* set its metatable */
luaL_getmetatable(L, "LuaBook.dir");
lua_setmetatable(L, -2);
/* try to open the given directory */
*d = opendir(path);
if (*d == NULL) /* error opening the directory? */
luaL_error(L, "cannot open %s: %s", path,
strerror(errno));
/* creates and returns the iterator function
(its sole upvalue, the directory userdatum,
is already on the stack top */
lua_pushcclosure(L, dir_iter, 1);
return 1;
}
7
譯註:指目錄結構,即userdatum
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
224
這兒有一點須要注意的,咱們必須在打開目錄以前建立 userdatum。若是咱們先打開
目錄,而後調用 lua_newuserdata 會拋出錯誤,這樣咱們就沒法獲取 DIR 結構。按照正確
的順序,DIR 結構一旦被建立,就會馬上和 userdatum 關聯起來;以後無論發生什麼,
__gc 元方法都會自動的釋放這個結構。
第二個函數是迭代器:
static int dir_iter (lua_State *L) {
DIR *d = *(DIR **)lua_touserdata(L, lua_upvalueindex(1));
struct dirent *entry;
if ((entry = readdir(d)) != NULL) {
lua_pushstring(L, entry->d_name);
return 1;
}
else return 0; /* no more values to return */
}
__gc 元方法用來關閉目錄,但有一點須要當心:由於咱們在打開目錄以前建立
userdatum,因此無論 opendir 的結果是什麼,userdatum 未來都會被收集。若是 opendir
失敗,未來就沒有什麼能夠關閉的了:
static int dir_gc (lua_State *L) {
DIR *d = *(DIR **)lua_touserdata(L, 1);
if (d) closedir(d);
return 0;
}
最後一個函數打開這個只有一個函數的庫:
int luaopen_dir (lua_State *L) {
luaL_newmetatable(L, "LuaBook.dir");
/* set its __gc field */
lua_pushstring(L, "__gc");
lua_pushcfunction(L, dir_gc);
lua_settable(L, -3);
/* register the `dir' function */
lua_pushcfunction(L, l_dir);
lua_setglobal(L, "dir");
return 0;
}
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
225
整個例子有一個注意點。開始的時候,dir_gc 看起來應該檢查他的參數是不是一個
目錄。不然,一個惡意的使用者可能用其餘類型的參數(好比,文件)調用這個函數導
致嚴重的後果。然而,在 Lua 程序中沒法訪問這個函數:他被存放在目錄的 metatable
中,Lua 程序歷來不會訪問這些目錄。
29.2 XML 解析
如今,咱們將要看到一個xml解析器的簡單實現,稱爲lxp8 ,它包括了Lua和Expat
(http://www.libexpat.org/)。Expat是一個開源的C語言寫成的XML 1.0 的解析器。它實現
了SAX(http://www.saxproject.org/),SAX是XML簡單的API,是基於事件的API,這意
味着一個SAX解析器讀取有一個XML文檔,而後反饋給應用程序他所發現的。舉個例子,
咱們要通知Expat解析這樣一個字符串:
<tag cap="5">hi</tag>
它將會產生三個事件:當它讀取子字符串 "<tag cap="5">hi</tag>",產生一個讀取
到開始元素的事件;當它解析 "hi" 時,產生一個讀取文本事件(有時也稱爲字符數據
事件);當解析 "end" 時,產生一個讀取結束元素的事件。而每一個事件,都會調用應用
程序適當的句柄。
這裏咱們不會涉及到整個 Expat 庫,咱們只會集中精力關注那些可以闡明和 Lua 相
互做用的新技術的部分。當咱們實現了核心功能後,在上面進行擴展將會變得很容易。
雖然 Expat 解析 XML 文檔時會有不少事件,咱們將會關心的僅僅是上面例子提到的三
個事件(開始元素,結束元素,文本數據),咱們須要調用的 API 是 Expat 衆多 API 中
不多的幾個。首先,咱們須要建立和析構 Expat 解析器的函數:
#include <xmlparse.h>
XML_Parser XML_ParserCreate (const char *encoding);
void XML_ParserFree (XML_Parser p);
這裏函數參數是可選的;在咱們的使用中,咱們直接選用 NULL 做爲參數。當咱們
有了一個解析器的時候,咱們必須註冊回調的句柄:
XML_SetElementHandler(XML_Parser p,
XML_StartElementHandler start,
XML_EndElementHandler end);
XML_SetCharacterDataHandler(XML_Parser p,
XML_CharacterDataHandler hndl);
8
譯註:估計是lua xml parser的簡寫。
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
226
第一個函數登記了開始元素和結束元素的句柄。第二個函數登記了文本數據(在
XML 語法中的字符數據)的句柄。全部回掉的句柄經過第一個參數接收用戶數據。開始
元素的句柄一樣接收到標籤的名稱和它的屬性做爲參數:
typedef void (*XML_StartElementHandler)(void *uData,
const char *name,
const char **atts);
這些屬性來自於以 '\0' 結束的字符串組成的數組,這些字符串分別對應了一對以屬
性名和屬性值組成的屬性。結束元素的句柄只有一個參數,就是標籤名。
typedef void (*XML_EndElementHandler)(void *uData,
const char *name)
最終,一個文本句柄僅僅以字符串做爲額外的參數。該文本字符串不能是以'\0'結束
的字符串,而是顯式指明長度的字符串:
typedef void
(*XML_CharacterDataHandler)(void *uData,
const char *s,
int len);
咱們用下面的函數將這些文本傳給 Expat:
int XML_Parse (XML_Parser p,
const char *s, int len, int isFinal);
Expat 經過成功調用 XML_Parse 一段一段的解析它接收到的文本。XML_Parse 最後
一個參數爲 isFinal,他表示這部分是否是 XML 文檔的最後一個部分了。須要注意的是
不是每段文本都須要經過 0 來表示結束,咱們也能夠經過顯實的長度來斷定。XML_Parse
函數若是發現解析錯誤就會返回一個 0(expat 也提供了輔助的函數來顯示錯誤信息,但
是由於簡單的緣故,咱們這裏都將之忽略掉)。咱們須要 Expat 的最後一個函數是容許我
們設置將要傳給句柄的用戶數據的函數:
void XML_SetUserData (XML_Parser p, void *uData);
好了,如今咱們來看一下如何在 Lua 中使用 Expat 庫。第一種方法也是最直接的一
種方法:簡單的在 Lua 中導入這些函數。比較好的方法是對 Lua 調整這些函數。好比 Lua
是沒有類型的,咱們不須要用不一樣的函數來設置不一樣的調用。可是咱們怎麼樣避免一塊兒
調用那些註冊了的函數呢。替代的是,當咱們建立了一個解析器,咱們同時給出了一個
包含全部回調句柄以及相應的鍵值的回調錶。舉個例子來講,若是咱們要打印一個文檔
的佈局,咱們能夠用下面的回調錶:
local count = 0
callbacks = {
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
StartElement = function (parser, tagname)
io.write("+ ", string.rep(" ", count), tagname, "\n")
count = count + 1
end,
EndElement = function (parser, tagname)
count = count - 1
io.write("- ", string.rep(" ", count), tagname, "\n")
end,
}
227
輸入"<to> <yes/> </to>",這些句柄將會打印出:
+ to
+
-
yes
yes
- to
經過這個 API,咱們不須要維護這些函數的調用。咱們直接在回調錶中維回他們。
所以,整個 API 須要三個函數:一個建立解析器,一個解析一段段文本,最後一個關閉
解析器。(實際上,咱們用解析器對象的方法,實現了最後兩個功能)。對這些 API 函數
的典型使用以下:
p = lxp.new(callbacks)
for l in io.lines() do
assert(p:parse(l))
assert(p:parse("\n"))
end
assert(p:parse())
p:close()
-- finish document
-- create new parser
-- iterate over input lines
-- parse the line
-- add a newline
如今,讓咱們把注意力集中到實現中來。首先,考慮如何在 Lua 中實現解析器。很
天然的會想到使用 userdatum,可是咱們將什麼內容放在 userdata 裏呢?至少,咱們必須
保留實際的 Expat 解析器和一個回調錶。咱們不能將一個 Lua 表保存在一個 userdatum(或
者在任何的 C 結構中),然而,咱們能夠建立一個指向表的引用,並將這個引用保存在
userdatum 中。(咱們在 27.3.2 節已經說過,一個引用就是 Lua 自動產生的在 registry 中
的一個整數)最後,咱們還必須可以將 Lua 的狀態保存到一個解析器對象中,由於這些
解析器對象就是 Expat 回調從咱們程序中接受的全部內容,而且這些回調須要調用 Lua。
一個解析器的對象的定義以下:
#include <xmlparse.h>
typedef struct lxp_userdata {
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
lua_State *L;
XML_Parser *parser;
} lxp_userdata;
/* associated expat parser */
int tableref; /* table with callbacks for this parser */
228
下面是建立解析器對象的函數:
static int lxp_make_parser (lua_State *L) {
XML_Parser p;
lxp_userdata *xpu;
/* (1) create a parser object */
xpu = (lxp_userdata *)lua_newuserdata(L,
sizeof(lxp_userdata));
/* pre-initialize it, in case of errors */
xpu->tableref = LUA_REFNIL;
xpu->parser = NULL;
/* set its metatable */
luaL_getmetatable(L, "Expat");
lua_setmetatable(L, -2);
/* (2) create the Expat parser */
p = xpu->parser = XML_ParserCreate(NULL);
if (!p)
luaL_error(L, "XML_ParserCreate failed");
/* (3) create and store reference to callback table */
luaL_checktype(L, 1, LUA_TTABLE);
lua_pushvalue(L, 1); /* put table on the stack top */
xpu->tableref = luaL_ref(L, LUA_REGISTRYINDEX);
/* (4) configure Expat parser */
XML_SetUserData(p, xpu);
XML_SetElementHandler(p, f_StartElement, f_EndElement);
XML_SetCharacterDataHandler(p, f_CharData);
return 1;
}
函數 lxp_make_parser 有四個主要步驟:
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
229
第一步遵循共同的模式:首先建立一個 userdatum,而後使用 consistent 的值預初始
化 userdatum,最後設置 userdatum 的 metatable。預初始化的緣由在於:若是在初始化的
時候有任何錯誤的話,咱們必須保證析構器(__gc 元方法)可以發如今可靠狀態下發現
userdata 並釋放資源。
第二步,函數建立一個 Expat 解析器,將它保存在 userdatum 中,並檢測錯誤。
第三步,保證函數的第一個參數是一個表(回調錶),建立一個指向表的引用,並將
這個引用保存到新的 userdatum 中。
第四步,初始化 Expat 解析器,將 userdatum 設置爲將要傳遞給回調函數的對象,並
設置這些回調函數。注意,對於全部的解析器來講這些回調函數都同樣。畢竟,在 C 中
不可能動態的建立新的函數,取代的方法是,這些固定的 C 函數使用回調錶來決定每次
應該調用哪一個 Lua 函數。
下一步是解析方法,負責解析一段 XML 數據。他有兩個參數:解析器對象(方法自
己)和一個可選的一段 XML 數據。當沒有數據調用這個方法時,他通知 Expat 文檔已經
解析結束:
static int lxp_parse (lua_State *L) {
int status;
size_t len;
const char *s;
lxp_userdata *xpu;
/* get and check first argument (should be a parser) */
xpu = (lxp_userdata *)luaL_checkudata(L, 1, "Expat");
luaL_argcheck(L, xpu, 1, "expat parser expected");
/* get second argument (a string) */
s = luaL_optlstring(L, 2, NULL, &len);
/* prepare environment for handlers: */
/* put callback table at stack index 3 */
lua_settop(L, 2);
lua_getref(L, xpu->tableref);
xpu->L = L; /* set Lua state */
/* call Expat to parse string */
status = XML_Parse(xpu->parser, s, (int)len, s == NULL);
/* return error code */
lua_pushboolean(L, status);
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
return 1;
}
230
當 lxp_parse 調用 XML_Parse 的時候,後一個函數將會對在給定的一段 XML 數據中
找到的全部元素,分別調用這些元素對應的句柄。因此,lxp_parse 會首先爲這些句柄準
備環境,在調用 XML_Parse 的時候有一些細節:記住這個函數的最後一個參數告訴 Expat
給定的文本段是不是最後一段。當咱們不帶參數調用他時,s 將使用缺省的 NULL,因
此這時候最後一個參數將爲 true。如今讓咱們注意力集中到回調函數 f_StartElement、
f_EndElement 和 f_CharData 上,這三個函數有類似的結構:每個都會針對他的指定事
件檢查 callback 表是否認義了 Lua 句柄,若是有,預處理參數而後調用這個 Lua 句柄。
咱們首先來看 f_CharData 句柄,他的代碼很是簡單。她調用他對應的 Lua 中的句柄
(當存在的時候),帶有兩個參數:解析器 parser 和字符數據(一個字符串)
static void f_CharData (void *ud, const char *s, int len) {
lxp_userdata *xpu = (lxp_userdata *)ud;
lua_State *L = xpu->L;
/* get handler */
lua_pushstring(L, "CharacterData");
lua_gettable(L, 3);
if (lua_isnil(L, -1)) { /* no handler? */
lua_pop(L, 1);
return;
}
lua_pushvalue(L, 1); /* push the parser (`self') */
lua_pushlstring(L, s, len); /* push Char data */
lua_call(L, 2, 0);
}
/* call the handler */
注意,因爲當咱們建立解析器的時候調用了 XML_SetUserData,因此,全部的 C 句
柄都接受 lxp_userdata 數據結構做爲第一個參數。還要注意程序是如何使用由 lxp_parse
設置的環境的。首先,他假定 callback 表在棧中的索引爲 3;第二,假定解析器 parser
在棧中索引爲 1(parser 的位置確定是這樣的,由於她應該是 lxp_parse 的第一個參數)。
f_EndElement 句柄和 f_CharData 相似,也很簡單。他也是用兩個參數調用相應的
Lua 句柄:一個解析器 parser 和一個標籤名(也是一個字符串,但如今是以 '\0' 結尾):
static void f_EndElement (void *ud, const char *name) {
lxp_userdata *xpu = (lxp_userdata *)ud;
lua_State *L = xpu->L;
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
lua_pushstring(L, "EndElement");
lua_gettable(L, 3);
if (lua_isnil(L, -1)) { /* no handler? */
lua_pop(L, 1);
return;
}
lua_pushvalue(L, 1); /* push the parser (`self') */
lua_pushstring(L, name); /* push tag name */
lua_call(L, 2, 0);
}
/* call the handler */
231
最後一個句柄 f_StartElement 帶有三個參數:解析器 parser,標籤名,和一個屬性列
表。這個句柄比上面兩個稍微複雜點,由於它須要將屬性的標籤列表翻譯成 Lua 識別的
內容。咱們是用天然的翻譯方式,好比,相似下面的開始標籤:
<to method="post" priority="high">
產生下面的屬性表:
{ method = "post", priority = "high" }
f_StartElement 的實現以下:
static void f_StartElement (void *ud,
const char *name,
const char **atts) {
lxp_userdata *xpu = (lxp_userdata *)ud;
lua_State *L = xpu->L;
lua_pushstring(L, "StartElement");
lua_gettable(L, 3);
if (lua_isnil(L, -1)) { /* no handler? */
lua_pop(L, 1);
return;
}
lua_pushvalue(L, 1); /* push the parser (`self') */
lua_pushstring(L, name); /* push tag name */
/* create and fill the attribute table */
lua_newtable(L);
while (*atts) {
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
lua_pushstring(L, *atts++);
lua_pushstring(L, *atts++);
lua_settable(L, -3);
}
lua_call(L, 3, 0);
}
/* call the handler */
232
解析器的最後一個方法是 close。當咱們關閉一個解析器的時候,咱們必須釋放解析
器對應的全部資源,即 Expat 結構和 callback 表。記住,在解析器建立的過程當中若是發
生錯誤,解析器並不擁有這些資源:
static int lxp_close (lua_State *L) {
lxp_userdata *xpu;
xpu = (lxp_userdata *)luaL_checkudata(L, 1, "Expat");
luaL_argcheck(L, xpu, 1, "expat parser expected");
/* free (unref) callback table */
luaL_unref(L, LUA_REGISTRYINDEX, xpu->tableref);
xpu->tableref = LUA_REFNIL;
/* free Expat parser (if there is one) */
if (xpu->parser)
XML_ParserFree(xpu->parser);
xpu->parser = NULL;
return 0;
}
注意咱們在關閉解析器的時候,是如何保證它處於一致的(consistent)狀態的,當我
們對一個已經關閉的解析器或者垃圾收集器已經收集這個解析器以後,再次關閉這個解
析器是沒有問題的。實際上,咱們使用這個函數做爲咱們的析構函數。他負責保證每一
個解析器自動得釋放他全部的資源,即便程序員沒有關閉解析器。
最後一步是打開庫,將上面各個部分放在一塊兒。這兒咱們使用和麪向對象的數組例
子(28.3 節)同樣的方案:建立一個 metatable,將全部的方法放在這個表內,表的__index
域指向本身。這樣,咱們還須要一個解析器方法的列表:
static const struct luaL_reg lxp_meths[] = {
{"parse", lxp_parse},
{"close", lxp_close},
{"__gc", lxp_close},
{NULL, NULL}
Copyright ® 2005, Translation Team, www.luachina.net
Programming in Lua
};
233
咱們也須要一個關於這個庫中全部函數的列表。和 OO 庫相同的是,這個庫只有一
個函數,這個函數負責建立一個新的解析器:
static const struct luaL_reg lxp_funcs[] = {
{"new", lxp_make_parser},
{NULL, NULL}
};
最終,open 函數必需要建立 metatable,並經過__index 指向表自己,而且註冊方法
和函數:
int luaopen_lxp (lua_State *L) {
/* create metatable */
luaL_newmetatable(L, "Expat");
/* metatable.__index = metatable */
lua_pushliteral(L, "__index");
lua_pushvalue(L, -2);
lua_rawset(L, -3);
/* register methods */
luaL_openlib (L, NULL, lxp_meths, 0);
/* register functions (only lxp.new) */
luaL_openlib (L, "lxp", lxp_funcs, 0);
return 1;
}
Copyright ® 2005, Translation Team, www.luachina.nethtml