大家好!我們要深入探討一個(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()
├── ReadArray()
├── ReadString()
├── ReadNumber()
├── ReadBoolean()
└── ReadNull()
三、解析的具體實(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();
var current = _span[_position];
return current switch
{
'{' => ReadObject(),
'[' => ReadArray(),
'"' => ReadString(),
't' or 'f' => ReadBoolean(),
'n' => ReadNull(),
_ when IsDigit(current) || current == '-' => ReadNumber(),
'/' => 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()
{
var obj = new Dictionary<string, object?>();
_position++;
SkipWhitespaceAndComments();
if (TryConsume('}'))
return obj;
while (true)
{
SkipWhitespaceAndComments();
if (_span[_position] != '"')
ThrowFormatException("Expected string key in object");
var key = ReadString();
SkipWhitespaceAndComments();
Consume(':');
SkipWhitespaceAndComments();
var value = ReadValue();
obj[key] = value;
SkipWhitespaceAndComments();
if (TryConsume('}'))
break;
Consume(',');
SkipWhitespaceAndComments();
}
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();
}
else if (next == '*')
{
SkipMultiLineComment();
}
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++;
int start = _position;
int length = 0;
bool hasEscapes = false;
while (_position < _span.Length)
{
var current = _span[_position];
if (current == '"')
break;
if (current == '\\')
{
hasEscapes = true;
_position++;
length++;
if (_position >= _span.Length)
break;
}
_position++;
length++;
}
if (_position >= _span.Length || _span[_position] != '"')
ThrowFormatException("Unterminated string");
var resultSpan = _span.Slice(start, length);
_position++;
if (!hasEscapes)
return new string(resultSpan);
return ProcessStringWithEscapes(resultSpan);
}
在ProcessStringWithEscapes()
方法中,處理的轉(zhuǎn)義符號(hào)主要有以下集中:
'"' => '"',
'\\' => '\\',
'/' => '/',
'b' => '\b',
'f' => '\f',
'n' => '\n',
'r' => '\r',
't' => '\t',
'u' => ProcessUnicodeEscape(span, ref spanIndex),
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++;}
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);
if (!isDouble && TryParseInteger(numberSpan, out long intValue))
return intValue;
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í)體類映射:
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);
config.Save("d:\\aa.json");
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ò)