成人欧美一区二区三区的电影,日韩一级一欧美一级国产,国产成人国拍亚洲精品,无码人妻精品一区二区三区毛片,伊人久久无码大香线蕉综合

LOGO OA教程 ERP教程 模切知識(shí)交流 PMS教程 CRM教程 開(kāi)發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

c#純代碼實(shí)現(xiàn)完整Json解析器的全過(guò)程及注釋與自定義格式的支持實(shí)現(xiàn)

freeflydom
2025年10月10日 10:27 本文熱度 491

大家好!我們要深入探討一個(gè)非常常用的技術(shù):JSON反序列化。別小看這個(gè)技術(shù),它可是現(xiàn)代編程中不可或缺的一部。JSON解析不僅僅是簡(jiǎn)單的數(shù)據(jù)轉(zhuǎn)換,它還涉及到復(fù)雜的詞法分析和文法解析。這些技術(shù)是編譯器設(shè)計(jì)的基礎(chǔ),但這不是我們今天要深入探討的內(nèi)容。
我們想通過(guò)一些簡(jiǎn)化的方法和直覺(jué)的思考,以純c#代碼為例,分享實(shí)現(xiàn)自己的可自定義的JSON解析器的過(guò)程,希望大家可以更好地理解數(shù)據(jù)結(jié)構(gòu)和算法,提升編程能力。

一、先來(lái)認(rèn)識(shí)一下JSON

1.1 什么是JSON?

JSON就像是一種"數(shù)據(jù)語(yǔ)言",用來(lái)在不同的程序之間傳遞信息。比如:

{
  "name": "小明",
  "age": 20,
  "isStudent": true,
  "hobbies": ["籃球", "音樂(lè)", "編程"]
}

你看,這就是一個(gè)JSON對(duì)象,它很像我們C#中的類. 但JSON里面的取值只有幾種有限的基礎(chǔ)類型:

string:就是文本,比如"你好";
number:數(shù)字,比如123, -1.2, 1E-4;
bool:布爾值true或false;
array:包含多個(gè)取值的集合
map:包含多個(gè)鍵值對(duì)的集合

很清楚的能看到,用什么語(yǔ)言解析json第一步是需要將json中的取值映射到該語(yǔ)言下的對(duì)應(yīng)類型中. 對(duì)于c#, 我們僅需要考慮幾個(gè)簡(jiǎn)單類型即可:

string對(duì)應(yīng)text in C#
double,int對(duì)應(yīng)number in C#
bool對(duì)應(yīng)bool in C#
array對(duì)應(yīng) List<object> in C#
map對(duì)應(yīng)Dictionary<string,object>in C#

1.2 為什么要反序列化?

  • 想象一下:你的朋友用微信給你發(fā)了一條消息,這條消息需要從"網(wǎng)絡(luò)格式"轉(zhuǎn)換成你能看懂的文字。JSON反序列化就是做類似的事情.
  • 另外就是關(guān)于數(shù)據(jù)的存儲(chǔ),因?yàn)閺?fù)雜的結(jié)構(gòu)化數(shù)據(jù)不能一直放在內(nèi)存中,當(dāng)要進(jìn)入磁盤持久化時(shí),可以選擇將對(duì)象存儲(chǔ)為JSon,清晰易讀,現(xiàn)在很多程序的配置文件都是這么用的.

1.3 為什么要嘗試單獨(dú)實(shí)現(xiàn)?

很多自定義的場(chǎng)景,包括但不限于:

  • 特殊場(chǎng)景的極限性能考慮,輕量級(jí)無(wú)需反射
  • 特殊注釋的實(shí)現(xiàn) ( 根據(jù)JSON標(biāo)準(zhǔn)(RFC 8259),JSON格式不支持注釋。這就是為什么很多嚴(yán)格的JSON解析器遇到注釋會(huì)報(bào)錯(cuò)。)
  • 無(wú)需考慮源生成,簡(jiǎn)單場(chǎng)景直接AOT編譯
  • 高度頻繁修改的對(duì)象, 無(wú)需修改映射的實(shí)體對(duì)象.

二、解析主流程

整體流程就像拆快遞包裹:拿到大包裹 -> 拆開(kāi)大包裹 -> 如果大包裹里有小包裹 -> 再拆開(kāi)小包裹

開(kāi)始拆快遞(Parse方法)
    ↓
拿出小刀準(zhǔn)備開(kāi)箱(創(chuàng)建JsonReader)
    ↓
判斷里面是什么(ReadValue方法)
    ↓
根據(jù)包裝形狀決定怎么拆:
   ?? 如果是方盒子{ } → 拆對(duì)象(ReadObject)
   ?? 如果是長(zhǎng)盒子[ ] → 拆數(shù)組(ReadArray)  
   ?? 如果是帶""的 → 拆字符串(ReadString)
   ?? 如果是數(shù)字 → 拆數(shù)字(ReadNumber)
   ?? 如果是true/false → 拆布爾值(ReadBoolean)
   ?? 如果是null → 拆空盒子(ReadNull)
    ↓
把所有東西整理好
    ↓
交給用戶(返回結(jié)果)

我們暫時(shí)用List<object>,Dictionary<string,object>兩個(gè)對(duì)象來(lái)描述json. 如1.1中所示的Json可以表現(xiàn)為:


 Dictionary<string, object> json = new Dictionary<string, object>()
 {
     { "name","小明" },
     { "age",20 },
     {"isStudent", true},
     {"hobbies", new List<object>{"籃球", "音樂(lè)", "編程" } }
 };

是不是很簡(jiǎn)單呢?

類的整體架構(gòu)如下:

LumJsonDeserializer
    │
    └── JsonReader (ref struct)
        ├── ReadValue()        // 讀取任意值
        ├── ReadObject()       // 讀取對(duì)象
        ├── ReadArray()        // 讀取數(shù)組
        ├── ReadString()       // 讀取字符串
        ├── ReadNumber()       // 讀取數(shù)字
        ├── ReadBoolean()      // 讀取布爾值
        └── ReadNull()         // 讀取null

三、解析的具體實(shí)現(xiàn)

具體入口如下:


public static object? Parse(string json)
{
    var reader = new JsonReader(json);
    return reader.ReadValue();
}
private ref struct JsonReader
{
      public object? ReadValue()
      {
          SkipWhitespaceAndComments(); // 跳過(guò)空白和注釋
          var current = _span[_position];
          return current switch
          {
              '{' => ReadObject(),     // 對(duì)象
              '[' => ReadArray(),      // 數(shù)組
              '"' => ReadString(),     // 字符串
              't' or 'f' => ReadBoolean(), // 布爾值
              'n' => ReadNull(),       // null
              _ when IsDigit(current) || current == '-' => ReadNumber(), // 數(shù)字
              '/' => ThrowUnexpectedComment(), // 意外注釋
              _ => ThrowUnexpectedCharacter(current) // 意外字符
          };
      }
}

ReadValue()作為總?cè)肟?,根?jù)當(dāng)前字符類型分發(fā)到具體的讀取方法,即核心分發(fā)器.

3.1 讀取JSON對(duì)象,以ReadObject()為例

ReadObject() → ReadString() → ReadValue() → (遞歸)

ReadObject()最終將創(chuàng)建 Dictionary<string, object?>對(duì)象,他主要母的是讀取鍵值對(duì),鍵必須是字符串.

 private Dictionary<string, object?> ReadObject()
{
    // 創(chuàng)建一個(gè)新的字典來(lái)存儲(chǔ)解析后的鍵值對(duì)
    var obj = new Dictionary<string, object?>();
    _position++; // 跳過(guò)對(duì)象開(kāi)始的大括號(hào) '{'
    SkipWhitespaceAndComments(); // 跳過(guò)可能的空白字符和注釋
    // 檢查是否立即遇到結(jié)束大括號(hào) '}'(空對(duì)象情況)
    if (TryConsume('}'))
        return obj; // 如果是空對(duì)象,直接返回空字典
    // 開(kāi)始循環(huán)處理對(duì)象中的每個(gè)鍵值對(duì)
    while (true)
    {
        SkipWhitespaceAndComments(); // 跳過(guò)鍵之前的空白字符和注釋
        // 驗(yàn)證當(dāng)前位置是否是雙引號(hào)(JSON鍵必須是字符串)
        if (_span[_position] != '"')
            ThrowFormatException("Expected string key in object"); // 如果不是雙引號(hào),拋出格式異常
        var key = ReadString(); // 讀取鍵的字符串值
        SkipWhitespaceAndComments(); // 跳過(guò)鍵后面的空白字符和注釋
        Consume(':'); // 消費(fèi)并驗(yàn)證冒號(hào)分隔符,如果沒(méi)有找到則拋出異常
        SkipWhitespaceAndComments(); // 跳過(guò)冒號(hào)后面的空白字符和注釋
        var value = ReadValue(); // 讀取值(可以是任何JSON類型:字符串、數(shù)字、布爾值、對(duì)象、數(shù)組等)
        obj[key] = value; // 將鍵值對(duì)添加到字典中
        SkipWhitespaceAndComments(); // 跳過(guò)值后面的空白字符和注釋
        // 檢查是否遇到對(duì)象結(jié)束的大括號(hào) '}'
        if (TryConsume('}'))
            break; // 如果找到結(jié)束大括號(hào),跳出循環(huán)
        Consume(','); // 消費(fèi)并驗(yàn)證逗號(hào)分隔符,用于分隔多個(gè)鍵值對(duì)
        SkipWhitespaceAndComments(); // 跳過(guò)逗號(hào)后面的空白字符和注釋
    }
    return obj; // 返回解析完成的字典
}

當(dāng)需要讀取json模式中的值對(duì)象時(shí), 這個(gè)方法會(huì)再次遞歸調(diào)用ReadValue().是不是非常簡(jiǎn)單?
當(dāng)然我們除了ReadObject(), 還有ReadValue(), ReadObject(),ReadArray(),ReadString(),ReadNumber(),ReadBoolean(),ReadNull()都需要一一實(shí)現(xiàn), 具體可自行查看代碼.

3.2 輔助方法.

  • SkipWhitespaceAndComments() - 跳過(guò)空白和注釋
 private void SkipWhitespaceAndComments()
 {
     while (_position < _span.Length)
     {
         var current = _span[_position];
         if (char.IsWhiteSpace(current))
         {
             _position++;
         }
         else if (current == '/' && _position + 1 < _span.Length)
         {
             var next = _span[_position + 1];
             if (next == '/')
             {
                 SkipSingleLineComment(); //跳過(guò)單行注釋
             }
             else if (next == '*')
             {
                 SkipMultiLineComment(); //跳過(guò)多行注釋塊
             }
             else
             {
                 break;
             }
         }
         else
         {
             break;
         }
     }
 }
  • TryConsume - 處理掉預(yù)期的字符如",)].
    比如當(dāng)處于字符串中時(shí)
    private bool TryConsume(char expected)
    {
        SkipWhitespaceAndComments();
        if (_position < _span.Length && _span[_position] == expected)
        {
            _position++;
            return true;
        }
        return false;
    }

四 轉(zhuǎn)義及特殊字符處理

4.1 轉(zhuǎn)義字符

轉(zhuǎn)義字符指的是當(dāng)json的字符串值對(duì)象中含有的特殊含義的字符串,常見(jiàn)的比如有字符串 {"name:":"\"螢火\"初芒"}, 讀取出來(lái)的字符串應(yīng)該是含有引號(hào)的 "螢火"初芒。但是字符串總中的引號(hào)會(huì)干擾正常解析流程,造成程序誤以為提前引號(hào)對(duì)提前關(guān)閉而出錯(cuò)。
因此需要單獨(dú)針對(duì)轉(zhuǎn)義符號(hào)\進(jìn)行處理。具體方法是,當(dāng)字符串解析過(guò)程ReadString()中,如果遇到轉(zhuǎn)義符號(hào)\時(shí),暫不處理,提前跳過(guò)標(biāo)記。


 private string ReadString()
 {
     _position++; // 跳過(guò) '"'
     int start = _position;
     int length = 0;
     bool hasEscapes = false;
     // 第一遍:計(jì)算長(zhǎng)度和檢測(cè)轉(zhuǎn)義字符
     while (_position < _span.Length)
     {
         var current = _span[_position];
         if (current == '"')
             break;
         if (current == '\\') //識(shí)別到了轉(zhuǎn)義符號(hào)標(biāo)記
         {
             hasEscapes = true;
             _position++; // 跳過(guò)轉(zhuǎn)義字符
             length++; // 跳過(guò)轉(zhuǎn)義字符
             if (_position >= _span.Length)
                 break;
         }
         _position++;
         length++;
     }
     if (_position >= _span.Length || _span[_position] != '"')
         ThrowFormatException("Unterminated string");
     var resultSpan = _span.Slice(start, length);
     _position++; // 跳過(guò)結(jié)尾的 '"'
     if (!hasEscapes)
         return new string(resultSpan);
     return ProcessStringWithEscapes(resultSpan); //轉(zhuǎn)義字符替換
 }

ProcessStringWithEscapes()方法中,處理的轉(zhuǎn)義符號(hào)主要有以下集中:

 '"' => '"',    //引號(hào)
 '\\' => '\\',  //斜杠
 '/' => '/',  //斜杠
 'b' => '\b', 
 'f' => '\f',
 'n' => '\n', //換行
 'r' => '\r', //換行
 't' => '\t',
 'u' => ProcessUnicodeEscape(span, ref spanIndex),  //處理unicode字符,\u8424\u706b\u521d\u8292 -> 螢火初芒

4.2 數(shù)字處理

數(shù)字的處理比較簡(jiǎn)單,可以用庫(kù)去實(shí)現(xiàn),單這里列出了逐字符解析數(shù)字的過(guò)程??紤]了負(fù)數(shù)、小數(shù)點(diǎn)、科學(xué)計(jì)數(shù)等。
為了更好的展示自定義的功能,我們加入了對(duì)特殊數(shù)字表達(dá)的解析,如{"name:":.9527}。這樣有一個(gè)好處,就是存儲(chǔ)記錄的時(shí)候省去了開(kāi)頭的一個(gè)0。一般的通用標(biāo)準(zhǔn)庫(kù)是不支持對(duì)純小數(shù)點(diǎn)開(kāi)頭的值.9527解析的。具體代碼如下:

   private object ReadNumber()
{
    int start = _position;
    if (TryConsume('-'))
        start = _position;
    
    bool isDouble = _span[_position] == '.';
    if (isDouble) { _position++;}
    // 快速掃描數(shù)字
    while (_position < _span.Length && IsDigit(_span[_position]))
        _position++;
    if (_position < _span.Length && _span[_position] == '.')
    {
        if (isDouble)
        {
            ThrowFormatException("Invalid number format");
        }
        isDouble = true;
        _position++;
        while (_position < _span.Length && IsDigit(_span[_position]))
            _position++;
    }
    if (_position < _span.Length && (_span[_position] == 'e' || _span[_position] == 'E'))
    {
        isDouble = true;
        _position++;
        if (_position < _span.Length && (_span[_position] == '+' || _span[_position] == '-'))
            _position++;
        while (_position < _span.Length && IsDigit(_span[_position]))
            _position++;
    }
    var numberSpan = _span.Slice(start, _position - start);
    // 方案1:優(yōu)先嘗試解析為整數(shù)
    if (!isDouble && TryParseInteger(numberSpan, out long intValue))
        return intValue;
    // 方案2:使用 double.TryParse(優(yōu)化版)
    if (TryParseDouble(numberSpan, out double doubleValue))
        return doubleValue;
    ThrowFormatException("Invalid number format");
    return 0;
}

五、最后

我們用c#完整實(shí)現(xiàn)了一個(gè)Json轉(zhuǎn)換的單文件類,無(wú)反射,純字符解析,完美支持aot?;谠搄son解析類,基于這個(gè)類,我們開(kāi)發(fā)了一個(gè)簡(jiǎn)單讀取修改保存的配置文件的庫(kù),簡(jiǎn)單的使用示例如下,可配置應(yīng)用與任何場(chǎng)景,無(wú)需提前定義實(shí)體類映射:

// Create
LumConfigManager config = new LumConfigManager();
config.Set("findmax", "xx");
config.Set("HotKey", 46);
config.Set("Now", DateTime.Now);
config.Set("TheHotKeys", new int[] { 46, 33, 21 });
config.Set("HotKeys:Mainkey", 426); // Nested configuration
config.Save("d:\\aa.json");
// Read existed file
LumConfigManager loadedConfig = new LumConfigManager("d:\\aa.json");
Console.WriteLine(loadedConfig.GetInt("HotKeys:Mainkey"));
Console.WriteLine(loadedConfig.Get("Now"));
var hotkeys = loadedConfig.Get("TheHotKeys") as IList;
foreach (var key in hotkeys)
{
    Console.WriteLine(key);
}

保存的json文件如下:

{"findmax":"xx","HotKey":46,"Now":"2025/9/11 10:25:50","TheHotKeys":[46,33,21],"HotKeys":{"Mainkey":426}}

如果你對(duì)這款工具有任何建議或想法,歡迎隨時(shí)交流!項(xiàng)目已在 GitHub 完全開(kāi)源(MIT License),如果你覺(jué)得有用,歡迎點(diǎn)個(gè) Star ??支持一下! https://github.com/LdotJdot/LumConfig

?轉(zhuǎn)自https://www.cnblogs.com/luojin765/p/19102718


該文章在 2025/10/10 10:27:36 編輯過(guò)
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對(duì)中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國(guó)內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對(duì)港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場(chǎng)、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場(chǎng)作業(yè)而開(kāi)發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉(cāng)儲(chǔ)管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購(gòu)管理,倉(cāng)儲(chǔ)管理,倉(cāng)庫(kù)管理,保質(zhì)期管理,貨位管理,庫(kù)位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號(hào)管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時(shí)間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved