起源

一切都要从那个线上bug说起,执行decimal.TryParse时程序并没有按照预期输出结果💔💔💔

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
string input = "1,2,3";
decimal output;
if (decimal.TryParse(input, out output))
{
    Console.WriteLine($"{output}");
}
else
{
    Console.WriteLine($"Unable to decimal {input}");
}
//这里意外的输出了 123

因为按照我的理解这里是不能成功转换的,所以排查问题的着重点根本就没放在这,以至于整个排查过程也显得颇为辛苦。

查看究竟

去MSDN上翻帮助文档,发现TryParse有这样一个重载方法 TryParse示例1.png

看到Globalization的时候就知道凉了一半,在往下看示例,果然,货币符号千分位小数点 全出来了。 TryParse示例2.png

现在问题很明显了,我的代码没有转换失败很有可能就是因为逗号被识别成了千分位。 反编译一下,发现转换的时候确实使用了一个默认值 System.Globalization.NumberStyles.NumberTryParse示例3.png

继续来看这个枚举值,微软官方文档里给的说明如下:

指示使用AllowLeadingWhiteAllowTrailingWhiteAllowLeadingSignAllowTrailingSignAllowDecimalPointAllowThousands样式。 这是复合数字样式。 TryParse示例4.png

我们从最后一个类型AllowThousands可以看出来,默认确实使用了千分位,所以在上述示例中"1,2,3"的确能够成功的完成转换。

问题延申

默认使用了千分位解析没错,但是"1,2,3"这个字符串的逗号也没在实际的千分位位置,这又是咋回事呢,想不通。😨😨😨

继续往下看,看看AllowThousands又是如何定义的。

指示数字字符串可以具有组分隔符,例如将百位与千位分隔开来的符号。 如果NumberStyles值包括AllowCurrencySymbol标志,要分析的字符串包括货币符号,则有效组分隔符字符由CurrencyGroupSeparator属性确定,且每个组中的位数由CurrencyGroupSizes属性确定。 否则,有效的组分隔符字符由NumberGroupSeparator属性确定,每组的位数由NumberGroupSizes属性确定。

从文档中我们知道每组数字的位数由NumberGroupSizes来决定。 回过头来继续看刚才反编译的代码,FormatInfo的参数使用的NumberFormatInfo.CurrentInfo,现在我们看下NumberGroupSizes的值到底是多少(调试发现默认千分位每组的位数是3)。 TryParse示例5.png

到这里我已经是摸不到头绪了,所以去网上搜索了一下,但是结果也不理想,总结一下网上就两种结论:

  1. 有逗号时可以转换成功
  2. 如果要验证千分位的位置,使用正则来辅助

打破砂锅干到底

网上找不到满意的答案,那就只能研究一下.Net的源码了,所以我对decimal.TryParse方法进行了调试,终于弄清了这个原因。下面放一部分关键代码:

TryParse示例6.png TryParse示例7.png

实际上在转换的时候,.NET将需要转换的字符串转换成了字符指针,然后逐字符去做对比。当要对比的字符等于千分位的分隔符时直接跳过,继续对比下一位。因此,不管字符串中有多少个分隔符都是可以转换的,比如下面这个例子:

1
2
3
decimal value = decimal.Parse("1,,,,,,,2,,,,3");
Console.WriteLine(value);
//输出 123

当然,实际上这个转换的过程远比我描述的复杂,我这里只是针对性的分析了一下而已。 至此,整个问题都找到了答案,圆满收官😏😏