JsonUtility,一个快速、轻量级的 C# JSON 插件






4.80/5 (9投票s)
无依赖,快速,轻量级的 JSON 解析和轻查询
引言
有关新功能的详细信息,请参阅末尾的更新部分。
如今,LINQ 对于 C# 开发者来说已是司空见惯,我几乎不必解释元组类型、对象查询和树形处理的价值。
但是,在深层系统级代码中,或者在你希望移植到其他地方运行而 LINQ 不可用的代码中,如何使用这些功能呢?如果我想在我的解析器生成器中使用像动态对象树这样的便捷功能,并搜索查询对象,而又不想让生成代码的消费者强制要求 LINQ 或复杂的 AST 对象模型才能进行解析器中的错误恢复怎么办?如果我想为查询基于 JSON 的 Web 服务创建更低级别的调用,而又不想承担通常伴随而来的所有负担怎么办?或者,如果我的代码运行在手机上,我无法访问所有花哨的功能怎么办?
如果我只需要一个小巧的对象模型,以及一点点查询功能怎么办?如果我只是需要一种无需所有这些麻烦就能简单使用元组和树的方法怎么办?
如果我还可以选择使用 LINQ 来处理我的元组等数据怎么办?
现代 .NET 语言功能的另一个主要缺点是,完成一些非常简单的任务就需要大量的底层基础设施和开销。就像上面提到的那样。
如果我们有一个最基本的 JSON 解析器,它使用列表和字典作为分支来构建简单的 System.Object
树,那会怎么样?
但即使是现在,甚至安装了最新版本的 Silverlight,也并非到处都能得到一致的支持,因为微软在后来版本中更改了 JSON 功能。
而且,如果所有你需要的东西都只占 80%,为何还需要一堆额外的依赖?更糟糕的是,如果你必须以 .NET 1.x 为目标,那么几乎没有什么 JSON 可用。
背景
熟悉 JSON 是必要的,大致熟悉 LINQ、AST(抽象语法树)和元组则有助于理解这个问题。
Using the Code
JsonUtility.cs 是一个用于处理基本 JSON 功能的单一类。
它处理核心的 JSON 数据类型——布尔值、(null)、数字、字符串、数组和对象/字典。
它可以从字符串或流中解析,并写入字符串和流,包括美化打印。
它包含一个基本、非常简单的查询机制,称为 Get
。Get
允许你仅按字段或索引进行查询,但允许你在一次操作中查询它们组成的路径。
代码能够无回溯或查找地进行解析,因此可以从仅前进的流进行工作,但目前,它必须在返回解析结果之前在内存中构建完整的对象模型,此时才能查询树。未来可能会加入访问者支持来规避此限制,但目前,请记住保持简单。优点是,修改内存中的树非常简单。
如果你希望 JsonUtility 支持泛型等 2.0 功能,请务必在项目中包含 NET20 定义。这样做将使例程倾向于返回 2.0 的列表和字典,并在适用时使用类型化的字符枚举器进行解析。性能提升是可衡量的,但不是惊人的。我提供的使用 JsonUtility 的示例代码也会愉快地使用其中任何一种。无论哪种方式,实际返回的 JSON 对象模型都不是类型的,但 2.0 版本使用 IList<Object>
和 IDictionary<String,Object>
而不是 IList
和 IDictionary
。
确保包含魔术。
using Grimoire;
以下是可选的,但我喜欢这样做。如果你不想支持 .NET 1.x,只需删除条件预处理器,只包含 2.0 定义。你甚至不需要使用 JObject
之类的定义,但它们可以清楚地表明你的源 var
是 JSON 树的一部分。
using JNumber=System.Double;
using JString=System.String;
using JBoolean=System.Boolean;
#if NET20 || NET40
using JArray=System.Collections.Generic.List<object>;
using JObject=System.Collections.Generic.Dictionary<string,object>;
using JField=System.Collections.Generic.KeyValuePair<string,object>;
#else
using JArray=System.Collections.ArrayList;
using JObject=System.Collections.Hashtable;
using JField=System.Collections.DictionaryEntry;
#endif
如你所见,解析很简单。输入可以是 string
或 TextReader
,或者任何可枚举的字符源,如字符数组。在这里,我们使用它来解析基于 JSON 的 Web 服务的响应体。
object json;
WebRequest req = WebRequest.Create(url);
using (WebResponse wr = req.GetResponse ())
using (Stream wrs = wr.GetResponseStream ())
using (StreamReader r = new StreamReader (wrs))
json=JsonUtility.Parse (r);
// get the count of the array at json .seasons
int sc = ((JArray)JsonUtility.Get (json, "seasons")).Count;
它的速度也相当快。在解析到内存树方面,它应该可以与其他高性能的 .NET JSON 实现媲美。
一旦你得到了一个树,你不仅可以使用 Get
进行简单的字段选择,还可以将子树写入 TextWriter
、StringBuilder
或 String
,并可选择美化打印结果。这也很快速,无论你使用哪种方法,尽管写入 stream
可能是最快的,因为不一定需要在内存中复制写入的内容。
关注点
你可能会注意到 JsonUtility.cs 中有很多重复的代码。这是因为 JsonUtility 本身是一个解析器生成器输出的原型,是我正在创建的更大项目的一部分。JsonUtility 最初是一个测试用例,用于做出一些关于性能的设计决策,包括在真实世界条件下使用类型化和非类型化枚举器的权衡。正如我所说,生成器是独立项目的一部分,它生成的解析器类型差异很大。这段代码对于可以用递归下降生成的 LL 文法来说是一个理想的输出。目标是提高解析速度并创建内存树,同时使代码足够规则,以便可以将其烘焙到代码生成功能中。
大多数解析代码都非常直接,可能比标准解析器稍微不那么精确。它应该解析所有符合标准的 JSON 树,并在遇到格式不正确的 JSON 时尽快拒绝它。错误处理不太详细,但可能会在未来的版本中得到改进。只要输入树是有效的(没有循环引用,也没有非本机 JSON 数据类型),它在任何情况下都会生成有效的 JSON 字符串。
你会看到递归下降解析方法被标记为 ParseXXXXX,并接受一个枚举器作为输入。
基类方法 Parse(#enumerator e)
直接在该例程中解析布尔值和 null
终端,而将跳过空格、字符串处理、数字处理、数组和对象处理分派到各自的 SkipWhitespace
、ParseString
、ParseNumber
、ParseArray
和 ParseObject
方法。
唯一打破此模式的解析方法是 ParseCombineDigits
,这是一个自定义方法,用于解析数字,它将数字的累积和数字的构建合并到一个步骤中以提高性能。该方法将诸如“342
”之类的数字解析为 double
值。当你使用不同的“part
”参数调用它时,你是在告诉它解析浮点值中不同的部分,例如,对于 123.456E+7
,ParseCombineDigits
将被调用 3 次,其中 part
参数为 0
用于“123
”,1
用于“456
”,再次使用 2
用于“7
”,每次修改累加器值,以便最终结果包含合并到正确的 64 位浮点值中的值。此方法由 ParseNumber
使用。
Get
方法非常简单。它们只是获取当前对象,并允许根据对象类型进行间接访问子项。它使用类型比较(轻微的反射,但不是类型检查)来确定如何进行。如果传递了多个字段参数,Get
方法会简单地重复此过程,允许通过递归进行路径遍历。
拥有无负担的 JSON 的好处之一是,它使我能够轻松地采用元组和树,而无需花费大量额外精力来考虑设计要求和依赖项。我可以直接将其插入并开始使用,而且它的占用空间非常小。这促使我甚至在 C# 中适当地将 JSON 用于方法参数等用途,而不是构建复杂的类结构。能够使用微服务来表示问题域的各个部分而无需额外的工作非常有帮助,并且在调试器中查看这些 JSON 树也非常简单。在某些情况下,它改变了我编写代码的方式。
更新
由于这段代码至少引起了一些兴趣,我为此加入了 NET/DLR 动态支持以及 JsonNode.cs 中的 JsonNode 包装器,你可以使用它来简化一些基本用法。目前它只提供只读访问 JSON,但提供了一些便利功能。
我还做了我一直想做的事情——向 JSON 语法添加了一个令牌类型,允许字段名在不带引号的情况下指定。这样你就可以写“{x:5,y:1}
”并能解析。现在,对象字段键可以是字符串,也可以是以下标识符:可以以 \_ 或字母开头,并且后续字符可以包含任何字母、数字、下划线或连字符。大约符合这个正则表达式 [A-Za-z_][A-Za-z\-_0-9]*,但字母和数字可以是任何 Unicode 字母或数字。
// assumes NET40 is #defined
...
dynamic dom = JsonNode.Parse("{left:{left:10,op:'+',right:5},op:'-',right:2}");
Console.WriteLine("dynamic ref {0}, type = {1}",
(int)dom.left.right,
dom.left.right.NodeType
);
或
var dom = JsonNode.Parse("{left:{left:10,op:'+',right:5},op:'-',right:2}");
Console.WriteLine("{0}, type = {1}",
(int)dom["left","right"],
dom["left","right"].NodeType
);
以及更底层的 JsonUtility.cs 方法(与 JsonNode.cs 不同,它们没有 NodeType
功能)。
object json = JsonUtility.Parse("{left:{left:10,op:'+',right:5},op:'-',right:2}");
Console.WriteLine("{0}, type = ?",
JsonUtility.Get(json,"left","right")
);
历史
- 初始 alpha 版本
- 1 月 18 日,添加了
JsonNode
,稍微扩展了 JSON 语法,并增加了 DLR 动态支持