MojoUnityJson是使用C#实现的JSON解析器 ,算法思路来自于游戏引擎Mojoc的C语言实现Json.h。借助C#的类库,可以比C的实现更加的简洁和全面,尤其是处理Unicode转义字符(\u开头)的解析,C#的StringBuilder本身就支持了Unicode码点(code point)。
MojoUnityJson使用递归下降的解析模式,核心解析代码只有450行(去掉空行可能只有300多行),支持标准的JSON格式。算法实现力求简洁明了,用最直接最快速的方法达到目的,没有复杂的概念和模式。除了解析JSON,还提供了一组方便直观的API来访问JSON数据,整体实现只有一个文件,仅依赖System.Collections.Generic
,System.Text
,System
三个命名空间,MojoUnityJson可以很容易的嵌入到其它项目里使用。
本文主要介绍一下,超级简单又高效,并且看一眼就完全明白的解析算法,几乎可以原封不动的复制粘贴成其它语言版本的实现。
使用一个简单的结构体,用来在解析的过程中,传递一些上下文数据。
private struct Data
{
// 需要解析的JSON字符串
public string json;
// 当前JSON字符串解析的位置索引
public int index;
// 缓存一个StringBuilder,用来抠出JSON的一段字符。
public StringBuilder sb;
public Data(string json, int index)
{
this.json = json;
this.index = index;
this.sb = new StringBuilder();
}
}
我们把JSON的值抽象成以下几个类型:
public enum JsonType
{
Object,
Array,
String,
Number,
Bool,
Null,
}
// 解析 JsonValue
private static JsonValue ParseValue(ref Data data);
// 解析 JsonObject
private static JsonValue ParseObject(ref Data data);
// 解析 JsonArray
private static JsonValue ParseArray(ref Data data);
// 解析 string
private static JsonValue ParseString(ref Data data);
// 解析 number
private static JsonValue ParseNumber(ref Data data)
这就是全部的解析流程,在ParseValue中会根据字符判断类型,分别调用下面几个不同的解析函数。JsonValue就对应一个JSON的值,它有一个JsonType代表了这个值的类型。这是一个递归的过程,在ParseValue,ParseObject和ParseArray过程中,会递归的调用ParseValue。JSON一定是始于一个,Object或Array,当这个最顶层的值解析完毕的时候,整个JSON也就解析完成了。
解析过程中,会有很多为了格式化存在的空白字符,需要剔除这些,才能获得有信息的字符,这是一个重复的过程,需要一个函数统一处理。
private static void SkipWhiteSpace(ref Data data)
{
while (true)
{
switch (data.json[data.index])
{
case ' ' :
case '\t':
case '\n':
case '\r':
// 每次消耗一个字符,就向后推进JSON的索引
data.index++;
continue;
}
break;
}
}
private static JsonValue ParseValue(ref Data data)
{
// 跳过空白字符
SkipWhiteSpace(ref data);
var c = data.json[data.index];
switch (c)
{
case '{':
// 表示Object
return ParseObject(ref data);
case '[':
// 表示Array
return ParseArray (ref data);
case '"':
// 表示string
return ParseString(ref data);
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '-':
// 表示数值
return ParseNumber(ref data);
case 'f': // 表示可能是false
if
(
data.json[data.index + 1] == 'a' &&
data.json[data.index + 2] == 'l' &&
data.json[data.index + 3] == 's' &&
data.json[data.index + 4] == 'e'
)
{
data.index += 5;
// 表示是false
return new JsonValue(JsonType.Bool, false);
}
break;
case 't': // 表示可能是true
if
(
data.json[data.index + 1] == 'r' &&
data.json[data.index + 2] == 'u' &&
data.json[data.index + 3] == 'e'
)
{
data.index += 4;
// 表示是true
return new JsonValue(JsonType.Bool, true);
}
break;
case 'n': // 表示可能是null
if
(
data.json[data.index + 1] == 'u' &&
data.json[data.index + 2] == 'l' &&
data.json[data.index + 3] == 'l'
)
{
data.index += 4;
// 表示可能是null
return new JsonValue(JsonType.Null, null);
}
break;
}
// 不能处理了
throw new Exception(string.Format("Json ParseValue error on char '{0}' index in '{1}' ", c, data.index));
}
private static JsonValue ParseObject(ref Data data)
{
// Object 对应 C#的Dictionary
var jsonObject = new Dictionary(JsonObjectInitCapacity);
// skip '{'
data.index++;
do
{
// 跳过空白字符
SkipWhiteSpace(ref data);
if (data.json[data.index] == '}')
{
// 空的Object, "{}"
break;
}
DebugTool.Assert
(
data.json[data.index] == '"',
"Json ParseObject error, char '{0}' should be '\"' ",
data.json[data.index]
);
// skip '"'
data.index++;
var start = data.index;
// 解析Object的key值
while (true)
{
var c = data.json[data.index++];
switch (c)
{
case '"':
// check end '"'
break;
case '\\':
// skip escaped quotes
data.index++;
continue;
default:
continue;
}
// already skip the end '"'
break;
}
// get object key string
// 抠出key字符串
var key = data.json.Substring(start, data.index - start - 1);
// 跳过空白
SkipWhiteSpace(ref data);
DebugTool.Assert
(
data.json[data.index] == ':',
"Json ParseObject error, after key = {0}, char '{1}' should be ':' ",
key,
data.json[data.index]
);
// skip ':'
data.index++;
// set JsonObject key and value
// 递归的调用ParseValue获得Object的value值
jsonObject.Add(key, ParseValue(ref data));
// 跳过空白
SkipWhiteSpace(ref data);
if (data.json[data.index] == ',')
{
// Object的下一对KV
data.index++ ;
}
else
{
// 跳过空白
SkipWhiteSpace(ref data);
DebugTool.Assert
(
data.json[data.index] == '}',
"Json ParseObject error, after key = {0}, char '{1}' should be '{2}' ",
key,
data.json[data.index],
'}'
);
break;
}
}
while (true);
// skip '}' and return after '}'
data.index++;
return new JsonValue(JsonType.Object, jsonObject);
}
private static JsonValue ParseArray(ref Data data)
{
// JsonArray 对应 List
var jsonArray = new List(JsonArrayInitCapacity);
// skip '['
data.index++;
do
{
// 跳过空白
SkipWhiteSpace(ref data);
if (data.json[data.index] == ']')
{
// 空 "[]"
break;
}
// add JsonArray item
// 递归处理List每个元素
jsonArray.Add(ParseValue(ref data));
// 跳过空白
SkipWhiteSpace(ref data);
if (data.json[data.index] == ',')
{
// 解析下一个元素
data.index++;
}
else
{
// 跳过空白
SkipWhiteSpace(ref data);
DebugTool.Assert
(
data.json[data.index] == ']',
"Json ParseArray error, char '{0}' should be ']' ",
data.json[data.index]
);
break;
}
}
while (true);
// skip ']'
data.index++;
return new JsonValue(JsonType.Array, jsonArray);
}
private static JsonValue ParseString(ref Data data)
{
// skip '"'
data.index++;
var start = data.index;
string str;
// 处理字符串
while (true)
{
switch (data.json[data.index++])
{
case '"': // 字符串结束
// check end '"'
if (data.sb.Length == 0)
{
// 没有使用StringBuilder,直接抠出字符串
str = data.json.Substring(start, data.index - start - 1);
}
else
{
// 有特殊字符在StringBuilder
str = data.sb.Append(data.json, start, data.index - start - 1).ToString();
// clear for next string
// 清空字符,供下次使用
data.sb.Length = 0;
}
break;
case '\\':
{
// check escaped char
var escapedIndex = data.index;
char c;
// 处理各种转义字符
switch (data.json[data.index++])
{
case '"':
c = '"';
break;
case '\'':
c = '\'';
break;
case '\\':
c = '\\';
break;
case '/':
c = '/';
break;
case 'n':
c = '\n';
break;
case 'r':
c = '\r';
break;
case 't':
c = '\t';
break;
case 'u':
// 计算unicode字符的码点
c = GetUnicodeCodePoint
(
data.json[data.index],
data.json[data.index + 1],
data.json[data.index + 2],
data.json[data.index + 3]
);
// skip code point
data.index += 4;
break;
default:
// not support just add in pre string
continue;
}
// add pre string and escaped char
// 特殊处理的字符和正常的字符,一起放入StringBuilder
data.sb.Append(data.json, start, escapedIndex - start - 1).Append(c);
// update pre string start index
start = data.index;
continue;
}
default:
continue;
}
// already skip the end '"'
break;
}
return new JsonValue(JsonType.String, str);
}
处理字符串麻烦的地方在于,转义字符需要特殊处理,否则转义字符就会原样显示,而不能呈现特殊的作用。好在StringBuilder功能非常强大,提供了处理各种类型的接口。
在JSON中,Unicode字符可以是\u开头跟随4个字符组成的转义字符。这4个字符代表的,其实是一个16进制的数字,就是码点(code point)。码点在StringBuilder的Append重载函数中是直接支持的。所以,我们只要把\u后面的4个字符,转换成码点传递给Append就可以了。
///
/// Get the unicode code point.
///
private static char GetUnicodeCodePoint(char c1, char c2, char c3, char c4)
{
// 转义字符\u后面的4个char是一个16进制的数字。
// 4个char转换为int后,映射到16进制数字的高位到低位,然后相加得到码点。
// 注意这里需要return char类型,才能被Append正确处理。
return (char)
(
UnicodeCharToInt(c1) * 0x1000 +
UnicodeCharToInt(c2) * 0x100 +
UnicodeCharToInt(c3) * 0x10 +
UnicodeCharToInt(c4)
);
}
///
/// Single unicode char convert to int.
///
private static int UnicodeCharToInt(char c)
{
// 使用switch case 减少 if else 的判断
switch (c)
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return c - '0';
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
return c - 'a' + 10;
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
return c - 'A' + 10;
}
throw new Exception(string.Format("Json Unicode char '{0}' error", c));
}
private static JsonValue ParseNumber(ref Data data)
{
var start = data.index;
// 收集数值字符
while (true)
{
switch (data.json[++data.index])
{
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '-':
case '+':
case '.':
case 'e':
case 'E':
continue;
}
break;
}
// 抠出数值字符串
var strNum = data.json.Substring(start, data.index - start);
float num;
// 当成float处理,当然也可以用double
if (float.TryParse(strNum, out num))
{
return new JsonValue(JsonType.Number, num);
}
else
{
throw new Exception(string.Format("Json ParseNumber error, can not parse string [{0}]", strNum));
}
}
只有一句话,把Json字符串解析成JsonValue对象,然后JsonValue对象包含了所有的数值。
var jsonValue = MojoUnity.Json.Parse(jsonString);
// JsonValue 当做 string
public string AsString();
// JsonValue 当做 float
public float AsFloat();
// JsonValue 当做 int
public int AsInt();
// JsonValue 当做 bool
public bool AsBool();
// JsonValue 当做 null
public bool IsNull();
// JsonValue 当做 Dictionary
public Dictionary AsObject();
// JsonValue 当做 Dictionary 并根据 key 获取 value 当做JsonValue
public JsonValue AsObjectGet(string key);
// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 Dictionary
public Dictionary AsObjectGetObject(string key);
// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 List
public List AsObjectGetArray(string key);
// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 string
public string AsObjectGetString(string key);
// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 float
public float AsObjectGetFloat(string key);
// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 int
public int AsObjectGetInt(string key);
// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 bool
public bool AsObjectGetBool(string key);
// JsonValue 当做 Dictionary 并根据 key 获取 value 当做 null
public bool AsObjectGetIsNull(string key);
// JsonValue 当做 List
public List AsArray();
// JsonValue 当做 List 并获取 index 的 value 当做 JsonValue
public JsonValue AsArrayGet(int index);
// JsonValue 当做 List 并获取 index 的 value 当做 Dictionary
public Dictionary AsArrayGetObject(int index);
// JsonValue 当做 List 并获取 index 的 value 当做 List
public List AsArrayGetArray(int index);
// JsonValue 当做 List 并获取 index 的 value 当做 string
public string AsArrayGetString(int index);
// JsonValue 当做 List 并获取 index 的 value 当做 float
public float AsArrayGetFloat(int index);
// JsonValue 当做 List 并获取 index 的 value 当做 int
public int AsArrayGetInt(int index);
// JsonValue 当做 List 并获取 index 的 value 当做 bool
public bool AsArrayGetBool(int index);
// JsonValue 当做 List 并获取 index 的 value 当做 null
public bool AsArrayGetIsNull(int index);
MojoUnityJson目的就是完成简单而单一的JSON字符串解析功能,能够读取JSON的数据就是最重要的功能。在网上也了解了一些开源的C#实现的JSON库,不是功能太多太丰富,就是实现有些繁琐了,于是就手动实现了MojoUnityJson。
欢迎大家关注公众号“创小董”我会继续分享更多经历、经验、解决办法。