代碼失控與狀態機(下)

来源: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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...