代碼失控與狀態機(下)

来源:https://www.cnblogs.com/Zongsoft/archive/2018/09/04/coding-outcontrol-statemachine-2.html
-Advertisement-
Play Games

序言 在 "《代碼失控與狀態機(上)》" 的文末,我們留了一個解析「成員訪問表達式」的“作業”,那麼,通過本文我們一起來完成這個作業。 首先,為什麼要苦哈哈的寫一個這樣看上去沒什麼用的解析器?因為在某些 IoC 或 AOP 容器中 (不幸的是我需要實現一個這樣的 IoC 容器) ,常需要動態求解成員 ...


序言

《代碼失控與狀態機(上)》的文末,我們留了一個解析「成員訪問表達式」的“作業”,那麼,通過本文我們一起來完成這個作業。

首先,為什麼要苦哈哈的寫一個這樣看上去沒什麼用的解析器?因為在某些 IoC 或 AOP 容器中(不幸的是我需要實現一個這樣的 IoC 容器),常需要動態求解成員訪問表達式的值,而解析表達式就是第一步。其實這個“作業”正是編譯器技術中詞法解析的簡化版,自己手動擼一遍,對理解《編譯原理》的前端處理技巧是一個很好的入門練手。

其次,我現在正在造一個 ORM 數據引擎,該數據引擎有個很酷的特性就是在 CRUD 中支持類似 GraphQL 這樣的功能(即數據模式表達式),所以我需要寫一個類 GraphQL 的解析器,這應該算是一個很有價值的案例。

如上,手寫各種“表達式”解析器是很有現實意義和價值的。

源碼

基礎知識

BNF(Backus-Naur Form)巴克斯範式,在電腦科學領域,BNF 是一種適用於上下文無關的語法的符號表述,常用於描述計算編程語言的語法(文法)、文檔格式、指令集以及通信協議等,總之它適用於需要準確描述的地方,如果不用這個東西,我們幾乎沒辦法準確而簡潔的表述像電腦編程語言這樣需要精準表達的東西。

除了基本的 BNF 以外,人們為了更簡潔的表達而進行了擴展和增強,譬如:EBNF(Extended Backus–Naur Form)、ABNF(Augmented Backus–Naur Form),我找了幾篇文章供大家參考(尤其是前三篇):

除非是去寫編程語言的編譯器,通常我們不用閱讀和編寫像 YACC(Yet Another Compiler Compiler) 或 ANTLR(ANother Tool for Language Recognition) 這些工具中的那些非常“精準”的 BNF 的語法。有關 YACC 和 ANTLR 的一個具體案例,我推薦下麵這篇文章(不用摳細節,主要關註語法定義部分)

《TiDB 源碼閱讀系列文章(五)TiDB SQL Parser 的實現》

我推薦大家閱讀和採用各 SQL 手冊中使用的 BNF 方言來學習應用,因為它們語法約定簡單,對付一般應用場景足夠用。下麵是它們的鏈接(個人比較偏好我軟的 Transact-SQL),敬請食用。

語法規範

關於“成員訪問表達式”的詳細語法(文法)可以參考《C#語言規範》,下麵讓我們先看看之前寫的那個成員表達式的例子:

PropertyA
.ListProperty[100]
.MethodA(PropertyB, 'String\'Constant for Arg2', 200, ['key'].XXX.YYY)
.Children['arg1', PropertyC.Foo]

我嘗試用自然語言來表述上面代碼的意思:

  1. 訪問某個對象中名為 PropertyA 的成員(屬性或欄位);
  2. 訪問上面成員值對象中名為 ListProperty 的成員(該成員為列表類型或該成員所屬類型有個索引器);
  3. 訪問上面成員值對象中名為 MethodA 的方法(方法的參數數目不限,此例為4個參數);
  4. 訪問上面方法返回值對象中名為 Children 的成員(該成員為列表類型或該成員所屬類型有個索引器)。

補充說明:

  • 方法參數數量不定(零或多個),參數類型可以是常量(字元串、數字)或成員訪問表達式;
  • 列表屬性或索引器參數至少一個多則不限,參數類型同方法參數;
  • 字元串常量使用單引號或雙引號標註,支持 \ 反斜杠轉義符;
  • 數字常量支持尾綴標註,即“L”表示長整型、“M”或“m”表示 decimal 類型等。

如上,即使我寫了這麼長篇的文字,依然沒有精確而完整的完成對“成員表達式”的語法表達,可見我們必須藉助 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 範式表達式)之後,我們就可以有的放矢的繪製表達式解析器的狀態機圖了。

成員訪問表達式解析器狀態機圖

狀態說明:

  • Identifier:標識態,表示處於成員(屬性、欄位、方法)名稱狀態;
  • Separator:分隔符態,表示處於成員分隔符(即圓點)狀態;
  • Gutter:空隙態,表示索引器或方法參數結束後,所處於的空隙狀態;
  • Indexer:索引器態,表示處於的索引器內部的就緒狀態,它可以繼續接受一個有效的非終結符,也可以是一個終結符;
  • Parameter:參數態,表示處於索引器或方法參數的完結狀態,它必須等待一個終結符(逗號或括弧);
  • String:字元串常量態,表示處於字元串常量的內部,它可以接受任意字元,如果遇到終結符(匹配的單引號或雙引號)則轉入參數態;
  • Number:數字常量態,表示處於數字字面量,它可以接受任意數字字元,如果遇到終結符(尾綴符)則轉入參數態。

因為方法和索引器的參數有可能是表達式,因此在實現上需要進行遞歸棧處理,所以流程圖中標有壓棧(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();
}

代碼簡義:

  • 其中表示狀態的枚舉與上面的解析器狀態機流程圖的定義完全一致。
  • 內部的 StateContext 結構用來保存解析過程中的各種數據、狀態、字元緩存等,以及與上下文相關的操作方法等。
  • 內部的 StateVector 結構用來保存解析過程中的標記開關(布爾)的狀態,譬如當前數值常量的類型、當前字元是否位於字元串常量的轉義符態、標識(Identifier)中間是否含有空白字元等。

其他延展

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,以避免因內容陳舊而導致的謬誤,同時亦有更好的閱讀體驗。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • JavaScript 簡介 JavaScript是互聯網上最流行的腳本語言,可用於HTML和web,伺服器,PC,等等。 JavaScript是腳本語言 JavaScript是一種輕量級的編程語言 JavaScript是可插入HTML頁面的編程代碼 JavaScript插入HTML頁面後,可由所有的 ...
  • 原因:execCommand('insertHTML') IE11 不相容 解決方法: var range = I.window.$.getSelection().getRangeAt(0); range.deleteContents(); range = range.cloneRange(); v ...
  • JS判斷當前設備是 PC IOS Andriod ...
  • auto-workflow 用於快速構建各種關係圖的庫,比如流程圖,可視化執行流等 github地址:https://github.com/auto-workflow/AWorkflow 快速開始 npm install aworkflow 或者引用dist文件夾下的產出文件 訪問demo npm ...
  • function getnextElement(node){ if(node.nodeType==1) {return node;} if(node.nextSibling){return getnextElement(node.nextSibling) } return null;} ...
  • CAP理論 1. Consistency(一致性), 數據一致更新,所有數據變動都是同步的 2. Availability(可用性), 好的響應性能 3. Partition tolerance(分區容錯性) 可靠性 參考資料: "CAP理論" 服務發現比較 | | consul | zookeep ...
  • 距離上次發文,已經過去了三年半,這幾年技術更新節奏異常迅猛,.Net進入了跨平臺時代,前端也被革命性的顛覆。 回顧 2015年,正當我還沉迷於JQuery + EasyUi的封裝時,突然意識到技術已經過時。 JQuery在面對更加複雜的UI需求時顯得力不從心,EasyUi雖然組件比較完善,但界面風格 ...
  • 源碼: https://github.com/tylike/CIIP https://gitee.com/ciip/CIIP 一,CIIP的目標是什麼? 更加簡單,快速的建立信息類管理系統.讓實施人員可能承擔多數工作,降低開發人員的勞動強度. 二,CIIP改動了哪些? 2016年到本次更新之前,CI ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...