回到Ruby系列文章


Ruby数值类型

Ruby中所有数值都是Numeric类的子类对象,数值都是不可变对象

数值类型的继承关系如下:

Integer是整数,Float是浮点数类型,Rational是分数。

对于整数(Integer):

  • 在Ruby 2.4以前,它要么是Fixnum,要么是Bignum:Fixnum是比较小整数的类型(32位机器是31个二进制位即0~2^31-1,64位机器是62个二进制位即0~2^62-1大约46京,这已经是巨大无比的数了),Bignum是较大整数的类型。Fixnum和Bignum之间在需要的时候会自动转换:当一个初始为Bignum类型的变量值变小后,会自动转换成Fixnum类型;同理,Fixnum的变量也会在它变得足够大时转换成Bignum类型。
  • 在Ruby 2.4及之后的版本中,Fixnum和Bignum已经合并统一为Integer,也不再对用户暴露fixnum和bignum类型。

注:关于Fixnum和Bignum在内存中的存放

在Ruby内部,那些不可变的、占用空间小的对象(比指针大小还小),比如Fixnum、FalseClass、TrueClass、NilClass的对象,它们的值直接存放在变量位置处,而不是在栈中额外使用一个变量保存指针的方式引用它。
例如a=3;b=3,数值对象3已经直接存放在栈帧中,再赋值3给b的时候,可以直接扫描栈帧(因为较小,也或许使用某种高效查找算法),发现3已经存在,于是直接使用栈中的3赋值给b变量,所以a和b是同一个对象。但如果数值超出了Fixnum的范围(比如2^63),数值对象都将存于堆中,然后在栈帧中使用一个指针指向它,比如a=2^63;b=2^63,给b赋值时,不可能扫描整个堆,所以不知道已经存在一个2^63,只能再次分配一个2^63然后在栈帧中使用一个指针指向该对象,所以则a和b不再是同一个对象。

实际上,Ruby中的整数可以变得任意大。但是浮点数不会任意大,浮点数位数达到一点程度后会溢出到正、负无穷

对于分数,只需要在某个数值后面加上一个后缀字母r,就会自动转换为分数形式。例如0.3r等价于分数(3/10)2r等价于2/1r等价于分数形式(2/1)。在Kernel模块中有一个Rational方法,可以用来将各种类型转换成分数形式。

Ruby数值类型转换

当算术运算的两个操作数中,有一个是Float类型,那么整数类型将转换成Float类型进行运算,运算结果也都是Float类型

1
2
3
4
5
6
7
8
9
10
11
>> 2 * 2.5
=> 5.0

>> 1.0 + 2
=> 3.0

>> 5.0/2.5
=> 2.0

>> 5.0/2
=> 2.5

对于整数除法,即两个数都是整数的除法,那么除法得到的结果将是截断后的整数。对于结果为负数的整数除法,将取比它精确结果更小的整数。也就是说,Ruby中的整数除法采取的是地板除法(floor)。所以,(-a)/b等价于a/(-b),但可能不等价于-(a/b)

1
2
3
4
5
6
7
8
9
10
11
>> 5/2
=> 2
>> 5.0/3
=> 1.6666666666666667

>> -3/2 # (-a)/b
=> -2
>> 3/-2 # a/(-b)
=> -2
>> -(3/2) # -(a/b)
=> -1

浮点数是不精确的,所以不要在浮点数参与运算的时候对浮点数做等值比较。非要比较,可以通过减法运算,跟一个足够小的值做大小比较,但也别小过头了。例如,『(0.4-0.1)』和0.3做等值比较:

1
2
3
4
5
6
7
8
9
10
>> (0.4 - 0.1) == 0.3
=> false

>> 0.4 - 0.1
=> 0.30000000000000004

>> ( 0.4 - 0.1 ) - 0.3 < 0.00001 # 这是正确的浮点数参与运算的等值比较方式
=> true
>> ( 0.4 - 0.1 ) - 0.3 < 0.0000000000000000000001
=> false

可以使用Rational分数来比较:

1
2
3
4
5
6
>> (0.4r-0.1r) == 0.3r     #=> true

>> 0.4r #=> (2/5)
>> 0.1r #=> (1/10)
>> 0.4r-0.1r #=> (3/10)
>> 0.3r #=> (3/10)

也可以使用BigDecimal类来进行运算,它采用的是十进制表示法来表示浮点数,而Float采用的是二进制表示法表示。只不过BigDecimal的运算速度要比正常的浮点数速度慢上很多个数量级,当然,对于普通的财务运算等领域也足够了,只是在进行科学运算的时候,BigDecimal就不够了。另外,BigDecimal不是内置类,只是一个标准库,需要先导入才能使用。

1
2
require "bigdecimal"
(BigDecimal("0.4") - BigDecimal("0.1")) == BigDecimal("0.3") #=> true

以下是几种浮点数运算方式在进行等值比较时的效率高低比较(比较100W次):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 直接使用浮点数比较,比较是不精确的
$ time ruby -e '1000000.times {|x| (0.4-0.1) == 0.3 }'

real 0m0.147s
user 0m0.063s
sys 0m0.078s

# 直接使用浮点数做不等值比较,比较是精确的
# (多次测试,速度比上面要慢一点点,因为多了次减法运算)
$ time ruby -e '1000000.times {|x| (0.4-0.1) - 0.3 < 0.00001 }'

real 0m0.158s
user 0m0.094s
sys 0m0.063s

# 使用分数字面量,比较是精确的
$ time ruby -e '1000000.times {|x| (0.4r-0.1r) == 0.3r }'

real 0m0.248s
user 0m0.188s
sys 0m0.094s

# 使用Kernel中的Rational()
$ time ruby -e '1000000.times {|x| (Rational("0.4") - Rational("0.1")) == Rational("0.3") }'

real 0m0.630s
user 0m0.563s
sys 0m0.063s

# 使用bigdecimal
$ time ruby -r"bigdecimal" -e '1000000.times do |x|
(BigDecimal("0.4") - BigDecimal("0.1")) == BigDecimal("0.3")
end'

real 0m1.079s
user 0m0.953s
sys 0m0.125s

可见,使用分数字面量或浮点数不等值比较的效率是比较可取的,而使用Kernel.Rational()或BigDecimal()的效率相比之下都比较差。

对于Ruby中的取模%运算,也是支持浮点数的。

1
2
>> 1.5 % 0.4
=> 0.29999999999999993

指数运算时,采取的是Fortran里的优先级模式,和Perl是一样的:从右向左计算。例如3 ** 4 ** 2等价于3 ** (4 ** 2),即其值为3 ** 16

1
2
3
4
>> 3 ** 4 ** 2
=> 43046721
>> 3 ** 16
=> 43046721

指数运算的指数支持浮点数、负数,只是指数涉及到整数除法运算时需要注意,因为对于整数除法,Ruby默认采用的是floor除法:

1
2
3
4
5
x ** 4
x ** -1 # 即x分之1
x ** (1/3.0) # 即x的立方根
x ** (1/4) # 1/4=0,等价于x ** 0,即结果为1
x**(1.0/4.0) # 即x的四次方根

虽然,数值是不可变对象,但是对于整数来说,它支持索引查找各个二进制位上的值,只是不允许修改。其中,x[0]是该数值的最低有效位(即最右边),可以无限向右取,只不过超出范围后其二进制位都为0。

1
2
3
4
5
6
7
8
9
10
11
12
>> x=12345
>> printf "%b\n", x
11000000111001 # 这是十进制12345对应的二进制数

>> x[0] #=> 1
>> x[1] #=> 0
>> x[4] #=> 1
>> x[5] #=> 1
>> x[6] #=> 0
>> x[11] #=> 0
>> x[12] #=> 1
>> x[123] #=> 0 # 超出范围的值

所以,要判断一个数是否为偶数,就非常容易了:

1
x[0] == 0  # 偶数返回true,奇数返回false

Ruby 2.7添加的Integer#[]

在Ruby 2.7之前,只允许使用x[0]的方式取得数值x的某个二进制比特位,从Ruby 2.7开始,该方法进行了更多的扩展,它可以使用Range表达式。

1
2
3
Integer#[m]        # 取索引位m的比特位
Integer#[m,n] # 从索引位m开始取长度为n的比特位
Integer#[m..n] # 从索引位m开始取至索引位n的比特位

需注意:

  • 在比特位中:低位是小索引值,高位是大索引值。例如,4的二进制为100,4[0]取低位0,4[2]取高位1
  • 在范围表达式中:如果上边界小于下边界,等价于上边界无穷,即4[1..0]和4[1..-1]和4[1..]是等价的,都表示从index=1开始取至最高位

通过取二进制指定范围的比特位,使得以前的二进制位移、位与操作变得异常清晰、简单。但无法实现位取反、位或、位异或操作。

例如,File::Stat对象的mode返回的是16位比特位,其中高4位是文件类型,低12位是权限位。如果想要获取低12位的权限值:

1
2
3
4
5
6
7
f = File.new("/tmp/a.log")
fstat = f.stat
fmode = fstat.mode #=> 33188

fmode & 0777 #=> 420,转换为8进制即为644
fmode[0..12] #=> 420
fmode[0,12] #=> 420

如果想要左位移3位,则指定负数-3作为下边界,并指定最大正边界作为范围即可:

1
4[-3..]   #=> 32

如果想右位移3位,则去掉低位的3个比特位即可,即指定3作为下边界:

1
32[3..]

随机数

使用rand()可生成随机数:

  • 如果不传递参数,则生成[0,1)范围内的随机符点数
  • 如果传递正数N(N>=1),则生成[0,N)范围内的随机整数
  • 如果传递一个范围,则生成范围内的随机整数
  • 如果传递负数,将先取绝对值转换为正数
  • 可使用srand(S)指定rand()的随机数种子,相同的随机数种子生成的随机数序列一致。例如,srand(100)后,生成了10个随机数序列,再次设置srand(100),将得到10个相同的随机数序列
1
2
3
4
5
srand(100)
[rand(100),rand(100),rand(100)] #=> [8, 24, 67]

srand(100)
[rand(100),rand(100),rand(100)] #=> [8, 24, 67]

此外,还有Random类提供了一些随机数功能。