来学习一下.NET Framework的源代码(1)——绝对值的实现与Decimal类型

  虽然产品就快上线了,但是最近我们Table Manager北京的这几个人貌似没什么任务哦。现在来学习一下.NET Framework的代码。

  作为前几期,我们先来学习一下System.Math这个类中几个比较有意思的方法。


  如何求绝对值?

  首先,浮点类型数据的绝对值是用C++写的,.NET只是简单调用了一下C++的代码,看不到代码。

  整型是比较好处理的,如果为负直接乘以-1就完事了。不过这样的话并没有考虑周全,我们看看M$是怎么写的:

Public Shared Function Abs(ByVal value As Integer) As Integer
    If (value >= 0) Then
        Return value
    End If
    Return Math.AbsHelper(value)
End Function

  正数没什么问题,负数的时候调用了AbsHelper,这是个什么玩意呢?原来是这样:

Private Shared Function AbsHelper(ByVal value As Integer) As Integer
    If (value = -2147483648) Then
        Throw New OverflowException(Environment.GetResourceString("Overflow_NegateTwosCompNum"))
    End If
    Return -value
End Function

  哦,原来是考虑了边界的情况。int类型的取值范围是[-2^31,2^31-1],要是我写肯定就直接return -value了,所以还是考虑不周。

  Decimal这个数据类型比较有意思:

Return New Decimal(d.lo, d.mid, d.hi, (d.flags And &H7FFFFFFF))

  先来了解一下Decimal吧。Decimal提供数字的最大数量的有效数位,用于不能容忍浮点数舍入误差的场合,比如Money。Decimal数据类型占16个字节,实际上是由4个整型组成:flags、hi、lo、mid。后三个肯定是代表高、中、低的32位,而flags就比较奇怪,如果只是表示符号,干嘛不用个Byte呢?这个类型有一个ToInt64方法:

Public Shared Function ToInt64(ByVal d As Decimal) As Long
    If ((d.flags And &HFF0000) <> 0) Then
        d = Decimal.Truncate(d)
    End If
    If (d.hi = 0) Then
        Dim num As Long = ((d.lo And CLng(&HFFFFFFFF)) Or (d.mid << &H20))
        If (d.flags >= 0) Then
            If (num >= 0) Then
                Return num
            End If
        Else
            num = -num
            If (num <= 0) Then
                Return num
            End If
        End If
    End If
    Throw New OverflowException(Environment.GetResourceString("Overflow_Int64"))
End Function

  首先,M$把flags和x0FF0000进行与操作进行判断,如果16~23位有不为0的位,那么就会进行Truncate,所以可以推断出这些位和小数点的位置有关系。

  首先如果高32位不为0,肯定超过了Int64的范围,直接走到抛异常的语句。之后d.lo And CLng(&HFFFFFFFF)取lo的低32位,并将mid左移32位(0x20)进行与操作,很明显这句话是取整数部分的低64位。

  接下来,如果flags为非负数,并且取出来的低64位也是非负数,那么直接返回低64位。这样做是考虑到了溢出的问题(执行不到Return语句直接到最下一行的Exception了),如果这个Decimal类型的不是负数,但是取出来的低64位的最高位表示的是负数,那么肯定就超过了Int64的正数表示范围。与此对应的,将Decimal转换成UInt64类型的时候就没有这个判断:

Public Shared Function ToUInt64(ByVal d As Decimal) As UInt64
    If ((d.flags And &HFF0000) <> 0) Then
        d = Decimal.Truncate(d)
    End If
    If (d.hi = 0) Then
        Dim num As UInt64 = (CULng(d.lo) Or (CULng(d.mid) << &H20))
        If ((d.flags >= 0) OrElse (num = 0)) Then
            Return num
        End If
    End If
    Throw New OverflowException(Environment.GetResourceString("Overflow_UInt64"))
End Function

  不过在转换成Int32进行负数的判断时(就是Else之后的语句),也是类似的作用,不过有点绕,需要自己考虑一下。

  按照这个逻辑,可以推断出M$直接用flags的符号来判断整个Decimal类型的符号。因此flags的最高位(第31位)才是Decimal类型的符号位,而最开始又分析出了16~23位表示小数点的位置,那么剩下的位是干吗的呢?只有一种可能——暂时没用,留空的。

  至于为什么不用一个Char表示小数点的位置,而用一个Byte表示符号呢?大概是因为“13个字节+1位”的类型不太符合规范,而且剩下的23位可能会留着以后做扩充来使用。

  所以回到最初的求Decimal类型的绝对值的问题,Return New Decimal(d.lo, d.mid, d.hi, (d.flags And &H7FFFFFFF))的意思是:高中低位全部不变,把flags的最高位(符号位)置成0就完事啦!

✏️ 有任何想法?欢迎发邮件告诉老夫:daozhihun@outlook.com