源碼發布
搞了一個下午,終於搞定了這個號稱中國的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下載
http://files.cnblogs.com/blqw/blqw.Json.rar
源碼發布
搞了一個下午,終於搞定了這個號稱中國的github...以后源碼直接在這里發布了(github實在用不來,英文實在太爛了)
https://code.csdn.net/jy02305022/blqw-json
各位看官博友,看完之后如果對你有所啟發請別忘了點一下推薦,讓其他人也可以看到
如果有不同意見歡迎留言一起討論