喜欢天黑却怕鬼 阅读(28) 评论(0)

写在前面

之前自信撸码时踩了一次小坑,代码如下:

  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字符列表搜索'ÿ',果然搜到了。

mark

我们可以发现'ÿ'是拉丁字母辅助的最后一个字符,再往后的的字符基本不会出现,所以在大多数情况下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是不是空格字符。对应下图:

mark

c >= '\t' && c <= '\r' 对照Unicode字符列表就可以理解。下图红框圈出的5个字符都认定为空白字符。

mark

mark

c == '\u00a0' 如下图被认定为空白字符。

mark

c == '\u0085' 如下图被认定为空白字符。

mark

满足①②③④其中任意一个,便会被判定为空白字符。

那么假设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.SpaceSeparatorUnicodeCategory.LineSeparatorUnicodeCategory.ParagraphSeparator其中任意之一,则判定为空白字符,返回true。

总结

采坑不要紧,要紧的是要知道为什么会有这个坑。

软件80%的bug都拜“未将对象引用到对象的实例”所赐,要养成防御性编码的好习惯。


本文为博主学习感悟总结,水平有限,如果不当,欢迎指正。

如果您认为还不错,不妨点击一下下方的推荐按钮,谢谢支持。

转载与引用请注明出处。