在《代碼失控與狀態機(上)》的文末,咱們留了一個解析「成員訪問表達式」的「做業」,那麼,經過本文咱們一塊兒來完成這個做業。html
首先,爲何要苦哈哈的寫一個這樣看上去沒什麼用的解析器?由於在某些 IoC 或 AOP 容器中(不幸的是我須要實現一個這樣的 IoC 容器),常須要動態求解成員訪問表達式的值,而解析表達式就是第一步。其實這個「做業」正是編譯器技術中詞法解析的簡化版,本身手動擼一遍,對理解《編譯原理》的前端處理技巧是一個很好的入門練手。前端
其次,我如今正在造一個 ORM 數據引擎,該數據引擎有個很酷的特性就是在 CRUD 中支持相似 GraphQL 這樣的功能(即數據模式表達式),因此我須要寫一個類 GraphQL 的解析器,這應該算是一個頗有價值的案例。mysql
如上,手寫各類「表達式」解析器是頗有現實意義和價值的。git
https://github.com/Zongsoft/Zongsoft.CoreLibrary/tree/master/src/Expressions
https://github.com/Zongsoft/Zongsoft.CoreLibrary/tree/feature-data/src/Reflection/Expressions
https://github.com/Zongsoft/Zongsoft.CoreLibrary/blob/feature-data/src/Data/SchemaParser.cs
BNF(Backus-Naur Form)巴克斯範式,在計算機科學領域,BNF 是一種適用於上下文無關的語法的符號表述,經常使用於描述計算編程語言的語法(文法)、文檔格式、指令集以及通訊協議等,總之它適用於須要準確描述的地方,若是不用這個東西,咱們幾乎沒辦法準確而簡潔的表述像計算機編程語言這樣須要精準表達的東西。github
除了基本的 BNF 之外,人們爲了更簡潔的表達而進行了擴展和加強,譬如:EBNF(Extended Backus–Naur Form)、ABNF(Augmented Backus–Naur Form),我找了幾篇文章供你們參考(尤爲是前三篇):sql
除非是去寫編程語言的編譯器,一般咱們不用閱讀和編寫像 YACC(Yet Another Compiler Compiler) 或 ANTLR(ANother Tool for Language Recognition) 這些工具中的那些很是「精準」的 BNF 的語法。有關 YACC 和 ANTLR 的一個具體案例,我推薦下面這篇文章(不用摳細節,主要關注語法定義部分):數據庫
《TiDB 源碼閱讀系列文章(五)TiDB SQL Parser 的實現》express
我推薦你們閱讀和採用各 SQL 手冊中使用的 BNF 方言來學習應用,由於它們語法約定簡單,對付通常應用場景足夠用。下面是它們的連接(我的比較偏好我軟的 Transact-SQL),敬請食用。編程
https://docs.microsoft.com/zh-cn/previous-versions/sql/sql-server-2012/ms177563(v=sql.110))
https://www.postgresql.org/docs/10/static/index.html
https://dev.mysql.com/doc/refman/8.0/en
https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf
關於「成員訪問表達式」的詳細語法(文法)能夠參考《C#語言規範》,下面讓咱們先看看以前寫的那個成員表達式的例子:api
PropertyA .ListProperty[100] .MethodA(PropertyB, 'String\'Constant for Arg2', 200, ['key'].XXX.YYY) .Children['arg1', PropertyC.Foo]
我嘗試用天然語言來表述上面代碼的意思:
PropertyA
的成員(屬性或字段);ListProperty
的成員(該成員爲列表類型或該成員所屬類型有個索引器);MethodA
的方法(方法的參數數目不限,此例爲4個參數);Children
的成員(該成員爲列表類型或該成員所屬類型有個索引器)。補充說明:
\
反斜槓轉義符;如上,即便我寫了這麼長篇的文字,依然沒有精確而完整的完成對「成員表達式」的語法表達,可見咱們必須藉助 BNF 這樣東西才能進行精準表達。下面是它的 BNF 範式(採用的是 Transact-SQL 語法規範)):
expression ::= {member | indexer}[.member | indexer][...n] member ::= identifier | method indexer ::= "[" {expression | constant}[,...n] "]" method ::= identifier([expression | constant][,...n]) identifier ::= [_A-Za-z][_A-Za-z0-9]* constant ::= "string constant" | number number ::= [0-9]+{.[0-9]}?[L|m|M|f|F]
如上,即便咱們採用的不是能直接生成詞法解析器(Parser)的「高精準」的 BNF 表達式,但它依然足夠精確、簡潔。
有了確切的語法規範/文法(即 BNF 範式表達式)以後,咱們就能夠有的放矢的繪製表達式解析器的狀態機圖了。
狀態說明:
由於方法和索引器的參數有多是表達式,所以在實現上須要進行遞歸棧處理,因此流程圖中標有壓棧(Push)、出棧(Pop)的行爲,經過虛線表示對應的激發操做。全部左方括號 [
通路會激發壓棧操做,同時右方括號 ]
通路會激發對應的出棧操做;由於版面問題,上述流程圖並無標註出圓括號(方法參數)通路的出入棧的部分,可是邏輯等同於方括號(索引器)部分。
提示:
關於解析器狀態機的設計,我沒有發現具備普適性的設計指導方案,你們能夠根據本身的理解設定不一樣於上圖的狀態定義;至於對狀態設置粒度的把握,整體原則是要具有邏輯或概念上的自恰性、並方便繪圖和編程實現就能夠了。
位於 Zongsoft.Reflection.Expressions 命名空間中的接口和類總體上與 System.Linq.Expressions 命名空間中的相關類的設計相似。大體類圖以下:
提供解析功能的是 MemberExpressionParser 這個內部靜態類(狀態機類),它的 Parse(string text) 即爲狀態驅動函數,它遍歷輸入參數的文本字符,交給具體的私有方法 DoXXX(context) 進行狀態遷移斷定,如此循環即完成整個解析工做,總體結構與《代碼失控與狀態機(上)》中介紹的狀態機的程序結構一致,具體代碼以下:
public static IMemberExpression Parse(string text, Action<string> onError) { if(string.IsNullOrEmpty(text)) return null; //建立解析上下文對象 var context = new StateContext(text.Length, onError); //狀態遷移驅動 for(int i = 0; i < text.Length; i++) { context.Character = text[i]; switch(context.State) { case State.None: if(!DoNone(ref context, i)) return null; break; case State.Gutter: if(!DoGutter(ref context, i)) return null; break; case State.Separator: if(!DoSeparator(ref context, i)) return null; break; case State.Identifier: if(!DoIdentifier(ref context, i)) return null; break; case State.Method: if(!DoMethod(ref context, i)) return null; break; case State.Indexer: if(!DoIndexer(ref context, i)) return null; break; case State.Parameter: if(!DoParameter(ref context, i)) return null; break; case State.Number: if(!DoNumber(ref context, i)) return null; break; case State.String: if(!DoString(ref context, i)) return null; break; } } //獲取最終的解析結果 return context.GetResult(); }
代碼簡義:
在 Zongsoft.Data 數據引擎裏面有個數據模式(Schema)的概念,它是一種在數據操做中定義數據形狀的表達式,有點相似於 GraphQL 表達式的功能(不含查詢條件)。
譬若有一個名爲 Corporation
的企業實體類,它除了企業編號、名稱、簡稱等單值屬性外,還有企業法人、部門集合等這樣的「一對一」和「一對多」的複合(導航)屬性等。如今假設咱們調用數據訪問類的 Select
方法進行查詢調用:
var entities = dataAccess.Select<Corporation>( Condition.GreaterThanEqual("RegisteredCapital", 100));
以上代碼表示查詢 Corporation
實體對應的表,條件爲 RegisteredCapital
註冊資本大於等於100萬元的記錄,但缺少表達 Corporation
實體關聯的導航屬性的語義。採用數據模式(Schema)來定義操做的數據形狀,大體以下:
var schema = @" CorporationId, Name, Abbr, RegisteredCapital, Principal{Name, FullName, Avatar}, Departments:10(~Level, NumberOfPeople) { Name, Manager { Name, FullName, JobTitle, PhoneNumber } }"; var entities = dataAccess.Select<Corporation>( schema, Condition.GreaterThanEqual("RegisteredCapital", 100) & Condition.Like("Principal.Name", "鍾%"));
經過數據訪問方法中的 schema
參數,咱們能夠方便的定義數據形狀(含一對多導航屬性的分頁和排序設置),這樣就省去了屢次訪問數據庫進行數據遍歷的操做,大大提升了運行效率,同時簡化了代碼。
數據模式中各成員以逗號分隔,若是是複合屬性則能夠用花括號來限定其內部屬性集,對於一對多的複合屬性,還能夠定義其分頁和排序設置。如下是它的 BNF 範式:
schema ::= { * | ! | !identifier | identifier[paging][sorting]["{"schema [,...n]"}"] } [,...n] identifier ::= [_A-Za-z][_A-Za-z0-9]* number ::= [0-9]+ paging ::= ":"{ {*|?}| number[/{?|number}] } sorting ::= "(" { [~|!]identifier }[,...n] ")"
提示:驚歎號表示排除的意思,一個驚歎號表示排除以前的全部成員定義;以驚歎號打頭的成員標識,表示排除以前定義的該成員(若是以前有定義的話,沒有則忽略)。
分頁設置的釋義:
*
返回全部記錄(即不分頁);
?
返回第一頁,頁大小爲系統默認值,等同於1/?
格式(數據引擎默認設置);
n
返回 n 條記錄,等同於1/n
格式;
n/m
返回第 n 頁,每頁 m 行;
n/?
返回第 n 頁,頁大小爲系統默認值;
以上是數據模式表達式的解析器狀態機圖,具體實現代碼這裏就再也不贅述,整體上跟「成員訪問表達式」解析器相似。
在不少應用狀態機場景的編程中,繪製一個狀態機圖對於實現是具備很是重要的指導意義,但願經過這兩個具體的案例能對你們有所啓示。
其實 Linux/Unix 中的命令行,也是一個很好的案例,有興趣的能夠嘗試寫下它的 BNF 和解析狀態機圖。
此次咱們介紹了文本解析相關的狀態機的設計和實現,其實還有與工做流相關的通用狀態機也是一個很是有趣的應用場景,通用狀態機能夠應用在遊戲、工做流、業務邏輯驅動等方面。去年下半年由於業務線的須要,我花了差很少一兩個禮拜的時間實現了一個完備的通用狀態機,自我感受設計得不錯,但由於時間侷促,在狀態泛型實現上有個小瑕疵,之後作完優化後再來介紹它的架構設計和實現,這個系列就先且到此爲止罷。
本文可能會更新,請閱讀原文:https://zongsoft.github.io/blog/zh-cn/zongsoft/coding-outcontrol-statemachine-2,以免因內容陳舊而致使的謬誤,同時亦有更好的閱讀體驗。