寫在前面
之前自信擼碼時踩了一次小坑,代碼如下:
private static void AppServer_NewMessageReceived(WebSocketSession session, string value)
{
if (string.IsNullOrEmpty(value))
{
return;
}
value = HttpUtility.UrlDecode(value);
SuperSocketTemp<string> model = JsonConvert.DeserializeObject<SuperSocketTemp<string>>(value);
//具體業務...
}
就是這段代碼在測試環境拋錯,說起來全是淚啊。這段代碼的具體業務場景是Websocket即時通訊接收來自客戶端的消息,消息以json字符串的形式傳輸。首先判斷是否空字符串,如果不是,為了防止亂碼進行Url解碼,然后反序列化消息解析成需要的數據格式,最后執行具體的業務操作。
測試環境拋的錯是萬惡的“未將對象引用到對象的實例”,很簡單就可以定位到問題的所在——反序列化失敗了,只要在序列化之后執行具體業務邏輯之前加上非空判斷就可以解決掉這個問題。這也怪自己思維還不夠嚴密,沒有養成防御性編碼的習慣。
private static void AppServer_NewMessageReceived(WebSocketSession session, string value)
{
if (string.IsNullOrEmpty(value))
{
return;
}
value = HttpUtility.UrlDecode(value);
SuperSocketTemp<string> model = JsonConvert.DeserializeObject<SuperSocketTemp<string>>(value);
if(model==null)
{
return;
}
//具體業務...
}
通過日志分析反序列失敗的原因,日志中記錄的消息是空白的,但是代碼中明明有string.IsNullOrEmpty(value)
的判斷,為啥還會出現空的情況呢?仔細一看,原來是多個連續的空格,吐血。於是乎立馬把string.IsNullOrEmpty(value)
改為string.IsNullOrWhiteSpace(value)
,當value是多個連續的空格時,直接返回,不會繼續往下執行。
private static void AppServer_NewMessageReceived(WebSocketSession session, string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
value = HttpUtility.UrlDecode(value);
SuperSocketTemp<string> model = JsonConvert.DeserializeObject<SuperSocketTemp<string>>(value);
if(model==null)
{
return;
}
//具體業務...
}
我們都知道,string.IsNullOrEmpty方法是判斷字符串是否為:null或者string.Empty;string.IsNullOrWhiteSpace方法是判斷null或者所有空白字符,功能相當於string.IsNullOrEmpty和str.Trim().Length總和。那么具體方法內部是怎么實現的呢?我們可以通過ILSpy反編譯窺探一番。
string.IsNullOrEmpty源碼分析
// string
/// <summary>Indicates whether the specified string is null or an <see cref="F:System.String.Empty" /> string.</summary>
/// <param name="value">The string to test. </param>
/// <returns>true if the <paramref name="value" /> parameter is null or an empty string (""); otherwise, false.</returns>
[__DynamicallyInvokable]
public static bool IsNullOrEmpty(string value)
{
return value == null || value.Length == 0;
}
string.IsNullOrEmpty實現很簡單,無非就是判斷傳入的字符串參數,當是null或者空字符串string.Empty就返回true;否則返回false。
string.IsNullOrWhiteSpace源碼分析
// string
/// <summary>Indicates whether a specified string is null, empty, or consists only of white-space characters.</summary>
/// <param name="value">The string to test.</param>
/// <returns>true if the <paramref name="value" /> parameter is null or <see cref="F:System.String.Empty" />, or if <paramref name="value" /> consists exclusively of white-space characters. </returns>
[__DynamicallyInvokable]
public static bool IsNullOrWhiteSpace(string value)
{
if (value == null)
{
return true;
}
for (int i = 0; i < value.Length; i++)
{
if (!char.IsWhiteSpace(value[i]))
{
return false;
}
}
return true;
}
string.IsNullOrWhiteSpace的實現就稍微復雜一些,首先當傳入的字符串參數為null時肯定返回true;如果不是就開始遍歷字符串,取出字符執行char.IsWhiteSpace(value[i])方法,如果char.IsWhiteSpace(value[i])方法返回false,就終止遍歷,返回fasle;否則返回true。所以char.IsWhiteSpace方法應該判斷的是傳入的字符是否為空字符,是空字符返回true,不是返回false。我們可以進入char.IsWhiteSpace方法看一下具體實現:
// char
/// <summary>Indicates whether the specified Unicode character is categorized as white space.</summary>
/// <param name="c">The Unicode character to evaluate. </param>
/// <returns>true if <paramref name="c" /> is white space; otherwise, false.</returns>
[__DynamicallyInvokable]
public static bool IsWhiteSpace(char c)
{
if (char.IsLatin1(c))
{
return char.IsWhiteSpaceLatin1(c);
}
return CharUnicodeInfo.IsWhiteSpace(c);
}
可以發現方法內部判斷了char.IsLatin1(c),符合的話執行char.IsWhiteSpaceLatin1(c),看不明白,繼續往下走。
// char
private static bool IsLatin1(char ch)
{
return ch <= 'ÿ';
}
'ÿ'是什么鬼?看不懂。但是char.IsWhiteSpace方法調用了CharUnicodeInfo.IsWhiteSpace(c)方法,那應該是和Unicode有關。而且到了char字符的級別上,更加可以肯定和Unicode編碼有關。從Unicode字符列表搜索'ÿ',果然搜到了。
我們可以發現'ÿ'是拉丁字母輔助的最后一個字符,再往后的的字符基本不會出現,所以在大多數情況下ch <= 'ÿ'
可以滿足的。當滿足ch <= 'ÿ'
時執行下面的方法:
// char
private static bool IsWhiteSpaceLatin1(char c)
{
return c == ' ' || (c >= '\t' && c <= '\r') || c == '\u00a0' || c == '\u0085';
}
IsWhiteSpaceLatin1(char c)負責判斷字符c是否是空白字符。
① c == ' '
很好理解,判斷c是不是空格字符。對應下圖:
② c >= '\t' && c <= '\r'
對照Unicode字符列表就可以理解。下圖紅框圈出的5個字符都認定為空白字符。
③ c == '\u00a0'
如下圖被認定為空白字符。
④ c == '\u0085'
如下圖被認定為空白字符。
滿足①②③④其中任意一個,便會被判定為空白字符。
那么假設char.IsLatin1(c)返回false呢?此時執行CharUnicodeInfo.IsWhiteSpace(c)。
// System.Globalization.CharUnicodeInfo
internal static bool IsWhiteSpace(char c)
{
switch (CharUnicodeInfo.GetUnicodeCategory(c))
{
case UnicodeCategory.SpaceSeparator:
case UnicodeCategory.LineSeparator:
case UnicodeCategory.ParagraphSeparator:
return true;
default:
return false;
}
}
CharUnicodeInfo.GetUnicodeCategory(c)會返回一個UnicodeCategory枚舉類型。
// System.Globalization.CharUnicodeInfo
/// <summary>Gets the Unicode category of the specified character.</summary>
/// <param name="ch">The Unicode character for which to get the Unicode category. </param>
/// <returns>A <see cref="T:System.Globalization.UnicodeCategory" /> value indicating the category of the specified character.</returns>
[__DynamicallyInvokable]
public static UnicodeCategory GetUnicodeCategory(char ch)
{
return CharUnicodeInfo.InternalGetUnicodeCategory((int)ch);
}
CharUnicodeInfo是一個靜態類,根據MSDN說明,Unicode標准定義了許多Unicode字符類別。例如,一個字符可能被分類為大寫字母,小寫字母,小數位數字,字母數字,段落分隔符,數學符號或貨幣符號。所述UnicodeCategory枚舉定義了可能的字符的類別。
使用CharUnicodeInfo類來獲取特定字符的UnicodeCategory值。該CharUnicodeInfo類定義了返回下面的Unicode字符值的方法:
- 字符或代理對所屬的特定類別。返回的值是UnicodeCategory枚舉的成員。
- 數字值。僅適用於數字字符,包括分數,下標,上標,羅馬數字,貨幣分子,圈出的數字和腳本特定的數字。
- 數字值。適用於可與其他數字字符組合的數字字符,以表示編號系統中的整數。
- 十進制數字值。僅適用於表示小數點(基10)系統中的十進制數字的字符。十進制數字可以是十個數字之一,從零到九。這些字符是UnicodeCategory的成員DecimalDigitNumber類別。
當GetUnicodeCategory方法返回的枚舉值是UnicodeCategory.SpaceSeparator、UnicodeCategory.LineSeparator、UnicodeCategory.ParagraphSeparator其中任意之一,則判定為空白字符,返回true。
總結
踩坑不要緊,要緊的是要知道為什么會有這個坑。
軟件80%的bug都拜“未將對象引用到對象的實例”所賜,要養成防御性編碼的好習慣。
本文為博主學習感悟總結,水平有限,如果不當,歡迎指正。
如果您認為還不錯,不妨點擊一下下方的【推薦】按鈕,謝謝支持。
轉載與引用請注明出處。