从零开始: c#纯代码实现完整Json解析器的全过程及注释与自定义格式的支持实现

news/2025/9/20 18:40:19/文章来源:https://www.cnblogs.com/luojin765/p/19102718

从零开始: c#纯代码实现完整Json解析器的全过程及注释与自定义格式的支持实现

大家好!我们要深入探讨一个非常常用的技术:JSON反序列化。别小看这个技术,它可是现代编程中不可或缺的一部。JSON解析不仅仅是简单的数据转换,它还涉及到复杂的词法分析和文法解析。这些技术是编译器设计的基础,但这不是我们今天要深入探讨的内容。
我们想通过一些简化的方法和直觉的思考,以纯c#代码为例,分享实现自己的可自定义的JSON解析器的过程,希望大家可以更好地理解数据结构和算法,提升编程能力。
文中完整的代码和项目,已基于MIT协议开源。你可以自由地使用、修改和分发。你可以根据自己的需求进行定制,随意集成。所以,让我们开始吧。

一、先来认识一下JSON

1.1 什么是JSON?

JSON就像是一种"数据语言",用来在不同的程序之间传递信息。比如:

{"name": "小明","age": 20,"isStudent": true,"hobbies": ["篮球", "音乐", "编程"]
}

你看,这就是一个JSON对象,它很像我们C#中的类. 但JSON里面的取值只有几种有限的基础类型:

string:就是文本,比如"你好";
number:数字,比如123, -1.2, 1E-4;
bool:布尔值true或false;
array:包含多个取值的集合
map:包含多个键值对的集合

很清楚的能看到,用什么语言解析json第一步是需要将json中的取值映射到该语言下的对应类型中. 对于c#, 我们仅需要考虑几个简单类型即可:

string对应text in C#
double,int对应number in C#
bool对应bool in C#
array对应 List<object> in C#
map对应Dictionary<string,object>in C#

1.2 为什么要反序列化?

  • 想象一下:你的朋友用微信给你发了一条消息,这条消息需要从"网络格式"转换成你能看懂的文字。JSON反序列化就是做类似的事情.
  • 另外就是关于数据的存储,因为复杂的结构化数据不能一直放在内存中,当要进入磁盘持久化时,可以选择将对象存储为JSon,清晰易读,现在很多程序的配置文件都是这么用的.

1.3 为什么要尝试单独实现?

很多自定义的场景,包括但不限于:

  • 特殊场景的极限性能考虑,轻量级无需反射
  • 特殊注释的实现 ( 根据JSON标准(RFC 8259),JSON格式不支持注释。这就是为什么很多严格的JSON解析器遇到注释会报错。)
  • 无需考虑源生成,简单场景直接AOT编译
  • 高度频繁修改的对象, 无需修改映射的实体对象.

二、解析主流程

整体流程就像拆快递包裹:拿到大包裹 -> 拆开大包裹 -> 如果大包裹里有小包裹 -> 再拆开小包裹

开始拆快递(Parse方法)↓
拿出小刀准备开箱(创建JsonReader)↓
判断里面是什么(ReadValue方法)↓
根据包装形状决定怎么拆:📦 如果是方盒子{ } → 拆对象(ReadObject)📦 如果是长盒子[ ] → 拆数组(ReadArray)  📦 如果是带""的 → 拆字符串(ReadString)📦 如果是数字 → 拆数字(ReadNumber)📦 如果是true/false → 拆布尔值(ReadBoolean)📦 如果是null → 拆空盒子(ReadNull)↓
把所有东西整理好↓
交给用户(返回结果)

我们暂时用List<object>,Dictionary<string,object>两个对象来描述json. 如1.1中所示的Json可以表现为:

Dictionary<string, object> json = new Dictionary<string, object>(){{ "name","小明" },{ "age",20 },{"isStudent", true},{"hobbies", new List<object>{"篮球", "音乐", "编程" } }};

是不是很简单呢?

类的整体架构如下:

LumJsonDeserializer│└── JsonReader (ref struct)├── ReadValue()        // 读取任意值├── ReadObject()       // 读取对象├── ReadArray()        // 读取数组├── ReadString()       // 读取字符串├── ReadNumber()       // 读取数字├── ReadBoolean()      // 读取布尔值└── ReadNull()         // 读取null

三、解析的具体实现

具体入口如下:

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(),       // null_ when IsDigit(current) || current == '-' => ReadNumber(), // 数字'/' => ThrowUnexpectedComment(), // 意外注释_ => ThrowUnexpectedCharacter(current) // 意外字符};}
}

ReadValue()作为总入口,根据当前字符类型分发到具体的读取方法,即核心分发器.

3.1 读取JSON对象,以ReadObject()为例

ReadObject() → ReadString() → ReadValue() → (递归)

ReadObject()最终将创建 Dictionary<string, object?>对象,他主要母的是读取键值对,键必须是字符串.

 private Dictionary<string, object?> ReadObject()
{// 创建一个新的字典来存储解析后的键值对var obj = new Dictionary<string, object?>();_position++; // 跳过对象开始的大括号 '{'SkipWhitespaceAndComments(); // 跳过可能的空白字符和注释// 检查是否立即遇到结束大括号 '}'(空对象情况)if (TryConsume('}'))return obj; // 如果是空对象,直接返回空字典// 开始循环处理对象中的每个键值对while (true){SkipWhitespaceAndComments(); // 跳过键之前的空白字符和注释// 验证当前位置是否是双引号(JSON键必须是字符串)if (_span[_position] != '"')ThrowFormatException("Expected string key in object"); // 如果不是双引号,抛出格式异常var key = ReadString(); // 读取键的字符串值SkipWhitespaceAndComments(); // 跳过键后面的空白字符和注释Consume(':'); // 消费并验证冒号分隔符,如果没有找到则抛出异常SkipWhitespaceAndComments(); // 跳过冒号后面的空白字符和注释var value = ReadValue(); // 读取值(可以是任何JSON类型:字符串、数字、布尔值、对象、数组等)obj[key] = value; // 将键值对添加到字典中SkipWhitespaceAndComments(); // 跳过值后面的空白字符和注释// 检查是否遇到对象结束的大括号 '}'if (TryConsume('}'))break; // 如果找到结束大括号,跳出循环Consume(','); // 消费并验证逗号分隔符,用于分隔多个键值对SkipWhitespaceAndComments(); // 跳过逗号后面的空白字符和注释}return obj; // 返回解析完成的字典
}

当需要读取json模式中的值对象时, 这个方法会再次递归调用ReadValue().是不是非常简单?
当然我们除了ReadObject(), 还有ReadValue(), ReadObject(),ReadArray(),ReadString(),ReadNumber(),ReadBoolean(),ReadNull()都需要一一实现, 具体可自行查看代码.

3.2 辅助方法.

  • SkipWhitespaceAndComments() - 跳过空白和注释
 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 - 处理掉预期的字符如",)].
    比如当处于字符串中时
    private bool TryConsume(char expected){SkipWhitespaceAndComments();if (_position < _span.Length && _span[_position] == expected){_position++;return true;}return false;}

四 转义及特殊字符处理

4.1 转义字符

转义字符指的是当json的字符串值对象中含有的特殊含义的字符串,常见的比如有字符串 {"name:":"\"萤火\"初芒"}, 读取出来的字符串应该是含有引号的 "萤火"初芒。但是字符串总中的引号会干扰正常解析流程,造成程序误以为提前引号对提前关闭而出错。
因此需要单独针对转义符号\进行处理。具体方法是,当字符串解析过程ReadString()中,如果遇到转义符号\时,暂不处理,提前跳过标记。

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()方法中,处理的转义符号主要有以下集中:

 '"' => '"',    //引号'\\' => '\\',  //斜杠'/' => '/',  //斜杠'b' => '\b', 'f' => '\f','n' => '\n', //换行'r' => '\r', //换行't' => '\t','u' => ProcessUnicodeEscape(span, ref spanIndex),  //处理unicode字符,\u8424\u706b\u521d\u8292 -> 萤火初芒

4.2 数字处理

数字的处理比较简单,可以用库去实现,单这里列出了逐字符解析数字的过程。考虑了负数、小数点、科学计数等。
为了更好的展示自定义的功能,我们加入了对特殊数字表达的解析,如{"name:":.9527}。这样有一个好处,就是存储记录的时候省去了开头的一个0。一般的通用标准库是不支持对纯小数点开头的值.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);// 方案1:优先尝试解析为整数if (!isDouble && TryParseInteger(numberSpan, out long intValue))return intValue;// 方案2:使用 double.TryParse(优化版)if (TryParseDouble(numberSpan, out double doubleValue))return doubleValue;ThrowFormatException("Invalid number format");return 0;
}

五、最后

我们用c#完整实现了一个Json转换的单文件类,无反射,纯字符解析,完美支持aot。基于该json解析类,基于这个类,我们开发了一个简单读取修改保存的配置文件的库,简单的使用示例如下,可配置应用与任何场景,无需提前定义实体类映射:

// 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}}

如果你对这款工具有任何建议或想法,欢迎随时交流!项目已在 GitHub 完全开源(MIT License),如果你觉得有用,欢迎点个 Star ⭐️支持一下! https://github.com/LdotJdot/LumConfig

可以关注微信公众号,更多想法更多内容欢迎交流!

image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/908496.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 32 为什么还有kill不掉的语句?

MySQL有两个kill命令:kill query+线程id,表示终止该线程正在执行的语句;kill (connection)+线程id,表示断开这个线程的连接,如果线程有语句正在执行,会先停止正在执行的语句。有时候可能会遇到:使用了kill,却没…

Axure RP 9 Mac 交互原型设计 - 实践

Axure RP 9 Mac 交互原型设计 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monac…

Ceph IO流程分段上传(1)——InitMultipart - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

第9章 Prompt提示词设计 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

详解Spring Boot DevTools - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

深入解析:rook-ceph自定义添加osd流程

深入解析:rook-ceph自定义添加osd流程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Mon…

1789:算24

题目 总时间限制: 3000ms 内存限制: 65536kB 描述 给出4个小于10个正整数,你可以使用加减乘除4种运算以及括号把这4个数连接起来得到一个表达式。现在的问题是,是否存在一种方式使得得到的表达式的结果等于24。 这里…

Proxy 库解析(二)

refl meta template <bool IsDirect, class R> struct refl_meta {template <class P>requires(IsDirect)constexpr explicit refl_meta(std::in_place_type_t<P>): reflector(std::in_place_type&l…

【Python3教程】Python3高级篇之JSON材料解析

【Python3教程】Python3高级篇之JSON材料解析2025-09-20 18:04 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: b…

大模型服务之下的新旧政务智能系统比较 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

流行的 3D 文件格式及其用途指南

了解 3D 文件格式的重要性 三维 (3D) 技术的市场既广泛又复杂,应用范围从电影后期制作和产品原型制作到虚拟现实 (VR) 和增强现实 (AR) 游戏。该技术各种用例不可或缺的一部分是 3D 文件格式,它决定了 3D 数据…

CentOS7.9上安装MySQL8.4

CentOS 7 已在 2024 年 6 月 30 日结束生命周期 (EOL),官方源已停止维护! CentOS 7 已在 2024 年 6 月 30 日结束生命周期 (EOL),官方源已停止维护! CentOS 7 已在 2024 年 6 月 30 日结束生命周期 (EOL),官方源已…

铁头山羊stm32-HAL库 - 实践

铁头山羊stm32-HAL库 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco",…

IDEA编译Maven任务后target目录没有class

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025CSP-S初赛游记

哈哈,在致知楼(逆天诡异滏阳楼)考的。

JBoltAI框架:企业级AI开发的革新路径与行业实践 - 那年-冬季

JBoltAI框架:企业级AI开发的革新路径与行业实践在人工智能与产业深度融合的当下,企业级AI开发框架正成为推动智能化转型的关键基础设施。JBoltAI作为面向Java技术生态的智能开发框架,通过模块化架构设计与多模型适配…

JBoltAI:重塑视频创作,开启零门槛智能混剪新时代 - 那年-冬季

JBoltAI:重塑视频创作,开启零门槛智能混剪新时代在当今这个视觉内容占据主导地位的时代,视频已成为信息传递、品牌宣传和创意表达的重要载体。然而,传统的视频制作过程往往伴随着高昂的专业门槛、漫长的时间投入以…

深入解析:手搓一个 DELL EMC Unity存储系统健康检查清单

深入解析:手搓一个 DELL EMC Unity存储系统健康检查清单pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas…

实用指南:Spring Boot 读取 YAML 配置文件

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

完整教程:AVL树(平衡二叉搜索树)

完整教程:AVL树(平衡二叉搜索树)2025-09-20 17:48 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !imp…