迟来的Json反序列化
源码发布
搞了一个下午,终于搞定了这个号称中国的github...以后源码直接在这里发布了(github实在用不来,英文实在太烂了)
https://code.csdn.net/jy02305022/blqw-json
相关回顾
废话
自从上次发表了Json序列化的方案之后,已经整整一个月了。
原本是想序列化写完马上开始写反序列化的,但是来看了大家的回复之后得到了很多启示,所以这一个月直接在做优化的工作(当然还有带BB)。
我发现博客园真是个好地方,以前在QQ空间,点点,微博发表技术文章的时候根本没有人回复,了不起有几个转载的。。。
在这里大家一起参与讨论,才能获得更多的启示和发现,才能更好的提高自己!
blqw.Json方案整体结构
├─JsonBuilder //用于将C#转换为Json字符串
├─QuickJsonBuilder //快速的将任意C#对象转换为Json字符串,继承自JsonBuilder
├─UnsafeStringWriter //程序集可用,未公开对象.以非安全方式访问指针操作字符串直接写入内存,以提高字符串拼接效率
├─JsonParser //用于将Json字符串转换为C#对象
└─UnsafeJsonReader //程序集可用,未公开对象.以非安全方式访问指针遍历内存中的字符串,以提高访问效率
ps:1,2,3是序列化用的,4,5是反序列化用的,项目还引用了Literacy用于IL方式反射访问对象属性
反序列化设计
反序列化相关的类只有2个UnsafeJsonReader,JsonParser 。一个用于读字符串,一个用于生成对象
UnsafeJsonReader 负责从Json字符串中读取指定的内容,读出的内容只能是最基本的String,Number,DateTime,true,false等
JsonParser 负责解析具体对象,并命令UnsafeJsonReader读取需要的内容,如果有必要,得到对象后进行一次转换然后赋值给对象
ps:JsonParser目前不支持转为DataSet或DataTable,因为把我觉得没什么必要,即使转了转过来都是String的也没什么用是吧
- 图例
- 示例代码
//json:{"Name":"blqw","Age":27} class User { public string Name { get; set; } public int Age { get; set; } }
User ToUser(string json) { UnsafeJsonReader reader = new UnsafeJsonReader(json); //构造UnsafeJsonReader,这里只是例子而已,真是情况不是这样的 if (reader.SkipChar('{') == false) //跳过 左花括号 ,此操作忽略所有空格 { //如果跳过空格后第一个字符不是{,返回false,如果是返回true,且跳过{ ThrowMissingCharException('{'); //返回false 则抛出异常 "缺少{符号" } User user = new User(); while (true) { var pn = reader.ReadString(); //读取一个String ,String第一次字符必须是 双引号 或 单引号,否则会抛出异常 if (reader.SkipChar(':') == false) //跳过 冒号 { ThrowMissingCharException(':'); //失败抛出异常 } if (pn == "Name") //判断读出的String是Name还是Age 这只是个例子..... { user.Name = reader.ReadString();//如果是Name 继续出一个String,作为名称 } else if (pn == "Age") { var num = reader.ReadConsts(); //如果是Age,则读出一个常量,可能是true,false,number,null,-Infinity,Infinity //读取失败抛出异常 user.Age = (int)Convert.ChangeType(num, typeof(int));//转为int }
if (reader.SkipChar(',')) //跳过一个 逗号
{ continue; //成功说明还有下一个属性 } else if (reader.Current == '}') //如果失败,直接判断当前字符,如果是 右花括号,说明已经结束了 { break; } else //既不是 逗号 也不是 右花括号 ,那就是作死的节奏了.... { ThrowException("错误的结束符号:" + reader.Current); } } reader.MoveNext(); //能到这里说明遇到右花括号了,跳过这个字符 if (reader.IsEnd()) //判断字符串是否已经结尾了,这个操作依然会跳过空白和回车字符 { return user; } else //如果他还没有结束,我只能遗憾的说,你赢了! { ThrowException(); } }
整体的流程大致就是这么一个情况,当然上面那个是精简的不能再精简的例子,真实情况要复杂很多,不过只要知道思路了对于我们程序猿来说,不是都差不多嘛
异常设计
- 如果User.Age是int类型的,但是Json字符串是这样的{age:"aa"}
这点我参考了大多数人的做法,直接抛出。第一这样做对于性能的影响最小,第二这样做对于调用者来说最直观
我之前的做法是忽略这个属性,后来发现这样做虽然程序没有异常了,但往往错误的时候也不知道,很多值都被直接以默认值的形式插入到数据库去了
- 如果是Json属性不存在对象中 json:{"Name":"blqw", "Address":"gz"},Address不是类User的属性
我使用一个叫SkipValue的方法,在字符串中立即跳过这个属性对应的值部分的字符串
- 如果对象中的属性不在Json字符串中,就不管了,也不会有异常抛出
性能设计
这里是比较重点要说明的,因为这部分是花时间最多的部分。
怎么才能设计出高效的反序列化方法?
- 1.字符串尽量只遍历一次(UnsafeJsonReader就是用来干这个的),重复遍历毫无意义。当然有的时候为了保持程序的可读性不能不这样或者那样设计。这个时候就需要权衡可读性和性能的取舍了。
例如:{"number":"123.165aafdsafdsafdsafds"},有些人会这样处理,先取出123.165aafdsafdsafdsafds,然后再判断是否是数字,这就是多此一举了,在遍历到第一个a的时候到直接就可以给出不是数字的结论了,为什么还要继续? - 2.所有对象只解析一次,在fastJson中,他将所有的字符串先解析成为一个List或者一个Dictionary,然后再吧Dictionary或者List解析成别的对象,这样做虽然可以使得程序可读性更好,便于维护。但是性能上的浪费是显而易见的。
- 3.在字符串转换较慢的类型上重新实现转换方法,比如DateTime,Number(所有数字类型)---这里比较看个人水品了
- 4.了解不同类型在性能上的细微差别,特别是可空值类型,我觉得他就是性能杀手(参考:学习笔记1,学习笔记2)
- 5.使用最适合的方法处理,比如ReadString这个方法会从Json字符串当前位置读取一个String并返回,有的时候我只想跳过,不想返回,当然使用ReadString也是可以达到呀求的,只要不处理返回值就行了。但是这在性能上就有所浪费了,既然不用,那就不要返回,重新实现一个SkipString会比较合适
/// <summary> 读取字符串 /// </summary> public string ReadString() { if (IsEnd())//已到结尾 { ThrowException(); } Char quot = _Current; if (quot != '"' && quot != '\'') { ThrowException(); } MoveNext(); if (_Current == quot) { MoveNext(); return ""; } var index = _Position; MiniBuffer buff = null; do { if (_Current == '\\')//是否是转义符 { if ((WordChars[_Current] & 16) == 0) { ThrowException(); } if (buff == null) { //锁定指针 char* p = stackalloc char[255]; buff = new MiniBuffer(p); } buff.AddString(_P, index, _Position - index); MoveNext(); } MoveNext(); } while (_Current != quot);//是否是结束字符 string str; if (buff == null) { str = new string(_P, index, _Position - index); } else { buff.AddString(_P, index, _Position - index); str = buff.ToString(); } MoveNext(); return str; }
/// <summary> 跳过一个字符串 /// </summary> public void SkipString() { if (IsEnd())//已到结尾 { ThrowException(); } Char quot = _Current; if (quot != '"' && quot != '\'') { ThrowException(); } MoveNext(); while (_Current != quot)//是否是结束字符 { MoveNext(); if (_Current == '\\')//是否是转义符 { if ((WordChars[_Current] & 16) == 0) { ThrowException(); } MoveNext(); } } MoveNext(); }
可以看到这2个方法的代码量都不是一个级别的,性能自然不用说
- 6.使用指针.在类中使用指针一定要注意锁定指针,不然很容易读取到错误的内存块
- 7.善于使用性能分析工具帮助你找出你程序中占用时间长的函数,并合理的修改他
- 8.防止过度设计!(这点我感觉非常重要,最早版本的反序列化一共设计了4个类,就属于过度设计了,不仅增加了代码的复杂度而且降低了性能,都后都精简了)
- 9.合理利用一些小技巧
比如,如何判断一个char是数字还是字母?
常规的方法是
char c = '\0'; if (c >= '0' && c <= '9') { // c是数字 } else if ((c >= 'a' && c <= 'z') || c >= 'A' && c <= 'Z') { //c是字母 } else if (c == '\'' && c == '"') { //c是单引号或双引号 } else if (c == ':') { //c是冒号 } else//if .... { //.... }
修改后的方法
/// <summary> /// <para>1: 数字</para> /// <para>2: 字母</para> /// <para>3: 双引号或单引号</para> /// <para>4: 冒号</para> /// <para>...</para> /// <para></para> /// </summary> readonly static byte[] Chars = new byte[char.MaxValue]; static Program() { for (char c = '0'; c <= '9'; c++) { Chars[c] = 1; } for (char c = 'a'; c <= 'z'; c++) { Chars[c] = 2; } for (char c = 'A'; c <= 'Z'; c++) { Chars[c] = 2; } Chars['\''] = 3; Chars['"'] = 3; Chars[':'] = 4; //... }
char c = '\0'; switch (Chars[c]) { case 1: // c是数字 break; case 2: //c是字母 break; case 3: //c是单引号或双引号 break; case 4: //c是冒号 break; default: //... break; }
效果
- 序列化
- 反序列化
性能
这里提供一份几个常用Json组件的性能测试,也可以看出优化后的性能变化
- 测试对象:
源码下载
包含测试代码,这个以后就不更新了,要更新的去下面的code.csdn下载
https://files.cnblogs.com/blqw/blqw.Json.rar
源码发布
搞了一个下午,终于搞定了这个号称中国的github...以后源码直接在这里发布了(github实在用不来,英文实在太烂了)
https://code.csdn.net/jy02305022/blqw-json
各位看官博友,看完之后如果对你有所启发请别忘了点一下推荐,让其他人也可以看到
如果有不同意见欢迎留言一起讨论
我发布的代码,没有任何版权,遵守WTFPL协议(如有引用,请遵守被引用代码的协议)
qq群:5946699 希望各位喜爱C#的朋友可以在这里交流学习,分享编程的心得和快乐