# **第 12 章 数值类**
到现在为止,数值(Numeric)类已经出现过好多次了。接下来我们就来详细讨论一下数值类的加减运算等基本操作、以及一些常用的功能。
-
**数值类的构成**
介绍包含 `Fixnum`、`Float` 等在内的数值类的构成。
-
**数值的字面量(literal)**
介绍在程序中描述数值的各种方法。
-
**算术运算**
介绍四则运算等基本的算术运算、以及数值运算时使用的模块 Math 的用法。
-
**类型转换**
介绍转换数值类型的方法,例如 `Integer` 与 `Float` 的互相转换。
-
**位运算**
介绍进行位运算的运算符。
-
**随机数**
介绍获取随机数时使用的相关功能。
-
**计数**
介绍通过 `Integer` 指定循环次数的方法。
### **12.1 数值类的构成**
在数值类中,有像 -1、0、1、10 这样的表示整数的 `Integer` 类,也有像 0.1、3.141592 这样的具有精度的、表示浮点小数的 `Float` 类。
这些数值类都被定义为了 `Numeric` 类的子类。另外,`Integer` 类又可以分为两种,一种是表示计算机硬件可以处理的数值的 `Fixnum` 类,另外一种是表示比 `Fixnum` 更大的数值的 `Bignum` 类。
![{%}](https://box.kancloud.cn/2015-10-26_562e01efb6396.png)
程序中用到的整数一般都是 `Fixnum` 类范围内的整数。如果使用的整数超过了 `Fixnum` 的范围,Ruby 就会自动将其转换为 `Bignum` 类。因此,在写程序的时候,我们几乎可以忽略上述整数类的区别。下面是计算 2 的 10 次幂以及 2 的 1000 次幂的例子,`**` 是表示乘方的运算符。
> **执行示例**
~~~
> irb --simple-prompt
>> n = 2 ** 10
=> 1024
>> n.class
=> Fixnum
>> m = 2 ** 1000
=> 1071508607186267320948425049060001810561404811705533607443750388370351051124936
1224931983788156958581275946729175531468251871452856923140435984577574698574803934
5677748242309854210746050623711418779541821530464749835819412673987675591655439460
77062914571196477686542167660429831652624386837205668069376
>> m.class
=> Bignum
~~~
Ruby 也可以处理有理数和复数。表示有理数用 `Rational` 类,表示复数用 `Complex` 类。
`Rational` 对象用“`Rational( 分子 , 分母 )`”的形式定义,例如,
![\frac{2}{5}+\frac{1}{3} ](http://latex.codecogs.com/gif.latex?/frac{2}{5}+/frac{1}{3})
上述这样的分数计算,可以用 `Rational` 对象改写成下面那样。我们还可以使用 `Rational#to_f` 方法将其转换为 `Float` 对象。
~~~
a = Rational(2, 5)
b = Rational(1, 3)
p a #=> (2/5)
p b #=> (1/3)
c = a + b
p c #=> (11/15)
p c.to_f #=> 0.7333333333333333
~~~
`Complex` 对象用“`Complex( 实数 , 虚数 )`”的形式定义。以下是计算复数 2i 的 2 次幂的例子:
~~~
c = Complex(0, 2) ** 2
p c #=> (-4+0i)
~~~
### **12.2 数值的字面量**
表 12.1 是表示数值对象的字面量的例子。
**表 12.1 数值对象的字面量**
<table border="1" data-line-num="80 81 82 83 84 85 86 87 88 89 90 91" width="90%"><thead><tr><th> <p class="表头单元格">字面量</p> </th> <th> <p class="表头单元格">作用(括号内为 10 进制的值)</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>123</code></p> </td> <td> <p class="表格单元格">表示10 进制整数</p> </td> </tr><tr><td> <p class="表格单元格"><code>0123</code></p> </td> <td> <p class="表格单元格">表示8 进制整数(83)</p> </td> </tr><tr><td> <p class="表格单元格"><code>0o123</code></p> </td> <td> <p class="表格单元格">表示8 进制整数(83)</p> </td> </tr><tr><td> <p class="表格单元格"><code>0d123</code></p> </td> <td> <p class="表格单元格">表示10 进制整数(123)</p> </td> </tr><tr><td> <p class="表格单元格"><code>0x123</code></p> </td> <td> <p class="表格单元格">表示16 进制整数(291)</p> </td> </tr><tr><td> <p class="表格单元格"><code>0b1111011</code></p> </td> <td> <p class="表格单元格">表示2 进制整数(123)</p> </td> </tr><tr><td> <p class="表格单元格"><code>123.45</code></p> </td> <td> <p class="表格单元格">浮点小数</p> </td> </tr><tr><td> <p class="表格单元格"><code>1.23e4</code></p> </td> <td> <p class="表格单元格">浮点小数的指数表示法(1.23×10 的4 次幂=12300.0)</p> </td> </tr><tr><td> <p class="表格单元格"><code>1.23e-4</code></p> </td> <td> <p class="表格单元格">浮点小数的指数表示法(1.23×10 的-4 次幂=0.000123)</p> </td> </tr></tbody></table>
单纯的数字罗列表示 10 进制整数。以 `0b` 开头的数值表示 2 进制数,以 `0` 或者 `0o` 开头的数值表示 8 进制数,以 `0d` 开头的数值表示 10 进制数,以 `0x` 开头的数值表示 16 进制数。字面量中的 `_` 会被自动忽略。因此,在使用每 3 位数字间隔一下这样的数值表示方法时会十分方便。
~~~
p 1234567 #=> 1234567
p 1_234_567 #=> 1234567
p 0b11111111 #=> 255
p 01234567 #=> 342391
p 0x12345678 #=> 305419896
~~~
包含小数点的数值为浮点小数。我们还可以采用有效数字与指数配合的科学计数法来表示浮点小数。格式为“有效数字”+“英文字母 e(或者 E)”+“表示指数的整数”。
~~~
p 1.234 #=> 1.234
p 1.234e4 #=> 12340.0
p 1234e-4 #=> 0.0001234
~~~
### **12.3 算数运算**
表 12.2 列出的是数值对象间进行基本的算术运算时用到的运算符。
**表 12.2 算数运算的运算符**
<table border="1" data-line-num="112 113 114 115 116 117 118 119 120" width="90%"><thead><tr><th> <p class="表头单元格">运算符</p> </th> <th> <p class="表头单元格">运算</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>+</code></p> </td> <td> <p class="表格单元格">加法运算</p> </td> </tr><tr><td> <p class="表格单元格"><code>-</code></p> </td> <td> <p class="表格单元格">减法运算</p> </td> </tr><tr><td> <p class="表格单元格"><code>*</code></p> </td> <td> <p class="表格单元格">乘法运算</p> </td> </tr><tr><td> <p class="表格单元格"><code>/</code></p> </td> <td> <p class="表格单元格">除法运算</p> </td> </tr><tr><td> <p class="表格单元格"><code>%</code></p> </td> <td> <p class="表格单元格">取余运算</p> </td> </tr><tr><td> <p class="表格单元格"><code>**</code></p> </td> <td> <p class="表格单元格">乘方运算</p> </td> </tr></tbody></table>
`Integer` 对象与 `Float` 对象的运算结果为 `Float` 对象。`Integer` 对象之间、`Float` 对象之间的运算结果分别为 `Integer` 对象、`Float` 对象。
~~~
p 1 + 1 #=> 2
p 1 + 1.0 #=> 2.0
p 2 - 1 #=> 1
p 2 - 1.0 #=> 1.0
p 3 * 2 #=> 6
p 3 * 2.0 #=> 6.0
p 3 * -2.0 #=> -6,0
p 5 / 2 #=> 2
p 5 / 2.0 #=> 2.5
p 5 % 2 #=> 1
p 5 % 2.0 #=> 1.0
p 5 ** 2 #=> 25
p 5 ** 0.5 #=> 2.23606797749979
p 5 ** -2.0 #=> 0.04
p 5 ** -2 #=> 0.04
~~~
这里需要注意的是,指数为负整数的乘方返回的结果是表示有理数的 `Rational` 对象。
~~~
p 5 ** -2.0 #=> 0.04
p 5 ** -2 #=> (1/25)
~~~
### **除法**
除了 / 和 % 以外,数值对象中还有一些与除法相关的方法。
-
***x*.`div`(*y*)**
返回 *x* 除以 *y* 后的商的整数。
~~~
p 5.div(2) #=> 2
p 5.div(2.2) #=> 2
p -5.div(2) #=> -3
p -5.div(2.2) #=> -3
~~~
-
***x*.`quo`(*y*)**
返回 *x* 除以 *y* 后的商,如果 *x*、*y* 都是整数则返回 `Rational` 对象。
~~~
p 5.quo(2) #=> (5/2)
p 5.quo(2.2) #=> 2.2727272727272725
p -5.quo(2) #=> (-5/2)
p -5.quo(2.2) #=> -2.2727272727272725
~~~
-
***x*.`modulo`(*y*)**
与 `x % y` 等价。
-
***x*.`divmod`(*y*)**
将 *x* 除以 *y* 后的商和余数作为数组返回。商是将 *x* / *y* 的结果去掉小数点后的部分而得到的值。余数的符号与 *y* 的符号一致,余数的值为 *x* % *y* 的结果。假设有运算式如下,
*ans*`=`*x*.`divmod(`*y*`)`
这时,下面的等式是成立的。
*x*`==`*ans*`[0] *`*y* + *ans*`[1]`
~~~
p 10.divmod(3.5) #=> [2, 3.0]
p 10.divmod(-3.5) #=> [-3, -0.5]
p -10.divmod(3.5) #=> [-3, 0.5]
p -10.divmod(-3.5) #=> [2, -3.0]
~~~
-
***x*.`remainder`(*y*)**
返回 *x* 除以 *y* 的余数,结果的符号与 *x* 的符号一致。
~~~
p 10.remainder(3.5) #=> 3.0
p 10.remainder(-3.5) #=> 3.0
p -10.remainder(3.5) #=> -3.0
p -10.remainder(-3.5) #=> -3.0
~~~
另外,除数为 0 时,`Integer` 类会返回错误,而 `Float` 类则会返回 `Infinity`(无限大)或者 `NaN`(Not a Number)。如果再用这两个值进行运算,那么结果只会返回 `Infinity` 或者 `NaN`。程序把输入的数据直接用于运算的时候,除数有可能会为 0,我们应当注意避免这样的情况发生。
~~~
p 1 / 0 #=> 错误(ZeroDivisionError)
p 1 / 0.0 #=> Infinity
p 0 / 0.0 #=> NaN
p 1.divmod(0) #=> 错误(ZeroDivisionError)
p 1.divmod(0.0) #=> 错误(FloatDomainError)
~~~
### **12.4 Math 模块**
`Math` 模块提供了三角函数、对数函数等常用的函数运算的方法。该模块中定义了模块函数和常量,例如,求平方根时,可以采用下述方法。
~~~
p Math.sqrt(2) #=> 1.4142135623730951
~~~
表 12.3 为 Math 模块定义的方法。
**表 12.3 Math 模块定义的方法**
<table border="1" data-line-num="208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236" width="90%"><thead><tr><th> <p class="表头单元格">方法名</p> </th> <th> <p class="表头单元格">作用</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>acos(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">反余弦函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>acosh(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">反双曲余弦函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>asin(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">反正弦函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>asinh(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">反双曲正弦函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>atan(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">反正切函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>atan2(<em>x</em>, <em>y</em>)</code></p> </td> <td> <p class="表格单元格">表示 4 个象限的反正切函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>atanh(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">反双曲正切函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>cbrt(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">立方根</p> </td> </tr><tr><td> <p class="表格单元格"><code>cos(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">余弦函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>cosh(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">双曲余弦函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>erf(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">误差函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>erfc(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">余补误差函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>exp(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">指数函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>frexp(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">把一个浮点数分解为尾数和指数</p> </td> </tr><tr><td> <p class="表格单元格"><code>gamma(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">伽玛函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>hypot(<em>x</em>, <em>y</em>)</code></p> </td> <td> <p class="表格单元格">计算三角形的斜边长度</p> </td> </tr><tr><td> <p class="表格单元格"><code>ldexp(<em>x</em>, <em>y</em>)</code></p> </td> <td> <p class="表格单元格">返回 <em>x</em> 乘以 2 的 <em>y</em> 次幂的值</p> </td> </tr><tr><td> <p class="表格单元格"><code>lgamma(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">伽马函数的自然对数</p> </td> </tr><tr><td> <p class="表格单元格"><code>log(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">底数为 e 的对数(自然对数)</p> </td> </tr><tr><td> <p class="表格单元格"><code>log10(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">底数为 10 的对数(常用对数)</p> </td> </tr><tr><td> <p class="表格单元格"><code>log2(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">底数为 2 的对数</p> </td> </tr><tr><td> <p class="表格单元格"><code>sin(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">正弦函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>sinh(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">双曲正弦函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>sqrt(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">平方根</p> </td> </tr><tr><td> <p class="表格单元格"><code>tan(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">正切函数</p> </td> </tr><tr><td> <p class="表格单元格"><code>tanh(<em>x</em>)</code></p> </td> <td> <p class="表格单元格">双曲正切函数</p> </td> </tr></tbody></table>
另外,`Math` 模块还定义了表 12.4 的常量。
**表 12.4 Math 模块定义的常量**
<table border="1" data-line-num="241 242 243 244 245" width="90%"><thead><tr><th> <p class="表头单元格">常量名</p> </th> <th> <p class="表头单元格">作用</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>PI</code></p> </td> <td> <p class="表格单元格">圆周率(3.141592653589793)</p> </td> </tr><tr><td> <p class="表格单元格"><code>E</code></p> </td> <td> <p class="表格单元格">自然对数的底数(2.718281828459045)</p> </td> </tr></tbody></table>
### **12.5 数值类型转换**
将 `Integer` 对象转换为 `Float` 对象时,可以使用 `to_f` 方法。相反,使用 `to_i` 方法则可以将 `Float` 对象转换为 `Integer` 对象(`Integer#to_i` 方法和 `Float#to_f` 方法返回与接收者一样的值)。另外,也可以把字符串转换为数值。
~~~
p 10.to_f #=> 10.0
p 10.8.to_i #=> 10
p -10.8.to_i #=> -10
p "123".to_i #=> 123
p "12.3".to_f #=> 12.3
~~~
`Float#to_i` 方法返回的结果会把小数点以后的值去掉。我们用 `round` 方法对小数进行四舍五入的处理。
~~~
p 1.2.round #=> 1
p 1.8.round #=> 2
p -1.2.round #=> -1
p -1.8.round #=> -2
~~~
返回比接收者大的最小整数用 `ceil` 方法,返回比接收者小的最大整数用 `floor` 方法。
~~~
p 1.5.ceil #=> 2
p -1.5.ceil #=> -1
p 1.5.floor #=> 1
p -1.5.floor #=> -2
~~~
我们还可以将数值转换为 `Rational` 对象和 `Complex` 对象,分别使用 `to_r` 和 `to_c` 方法,如下所示。
~~~
p 1.5.to_r #=> (3/2)
p 1.5.to_c #=> (1.5+0i)
~~~
### **12.6 位运算**
`Interger` 类可以进行表 12.5 所示的位运算。
**表 12.5 Integer 类的位运算符**
<table border="1" data-line-num="281 282 283 284 285 286 287 288 289" width="90%"><thead><tr><th> <p class="表头单元格">运算符</p> </th> <th> <p class="表头单元格">运算</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>~</code></p> </td> <td> <p class="表格单元格">按位取反(一元运算符)</p> </td> </tr><tr><td> <p class="表格单元格"><code>&</code></p> </td> <td> <p class="表格单元格">按位与</p> </td> </tr><tr><td> <p class="表格单元格"><code>|</code></p> </td> <td> <p class="表格单元格">按位或</p> </td> </tr><tr><td> <p class="表格单元格"><code>^</code></p> </td> <td> <p class="表格单元格">按位异或 <code>((a&~b|~a&b))</code></p> </td> </tr><tr><td> <p class="表格单元格"><code>>></code></p> </td> <td> <p class="表格单元格">位右移</p> </td> </tr><tr><td> <p class="表格单元格"><code><<</code></p> </td> <td> <p class="表格单元格">位左移</p> </td> </tr></tbody></table>
~~~
def pb(i)
# 使用printf 的%b 格式
# 将整数的末尾8 位用2 进制表示
printf("%08b\n", i & 0b11111111)
end
b = 0b11110000
pb(b) #=> 11110000
pb(~b) #=> 00001111
pb(b & 0b00010001) #=> 00010000
pb(b | 0b00010001) #=> 11110001
pb(b ^ 0b00010001) #=> 11100001
pb(b >> 3) #=> 00011110
pb(b << 3) #=> 10000000
~~~
> **专栏**
> **位与字节**
> 在计算机的世界中,我们经常会接触到“位”与“字节”,接下来我们就来介绍一下它们所代表的意义。
> - > **位(bit)**
> 位是计算机中最小的数据单位,表示 ON 或 OFF,或者 0 或 1。据说原本是“Binary digit”的简称。
> - > **位与 2 进制**
> 虽然位中只包含两种信息,但将位的信息两两组合的话,就可以表示 00、01、10、11 这四种信息。同样,3 位一组的话可以表示八种信息,4 位一组的话可以表示十六种信息。随着位的数量的增加,可以表示的信息的数量也会成倍的增加。
> 像这样,只用 0 和 1 的计数方式称为 2 进制。我们一般使用的计数方式是 10 进制,这种计数方式总共可表示从 0 到 9 十个数。下面是 2 进制与 10 进制的对照表。
> **表 10 进制、2 进制、8 进制、16 进制对照表**
| 10 进制 | 2 进制 | 8 进制 | 16 进制 |
|-----|-----|-----|-----|
| ` 0` | ` 0` | ` 0` | ` 0` |
| ` 1` | ` 1` | ` 1` | ` 1` |
| ` 2` | ` 10` | ` 2` | ` 2` |
| ` 3` | ` 11` | ` 3` | ` 3` |
| ` 4` | ` 100` | ` 4` | ` 4` |
| ` 5` | ` 101` | ` 5` | ` 5` |
| ` 6` | ` 110` | ` 6` | ` 6` |
| ` 7` | ` 111` | ` 7` | ` 7` |
| ` 8` | ` 1000` | `10` | ` 8` |
| ` 9` | ` 1001` | `11` | ` 9` |
| `10` | ` 1010` | `12` | ` A` |
| `11` | ` 1011` | `13` | ` B` |
| `12` | ` 1100` | `14` | ` C` |
| `13` | ` 1101` | `15` | ` D` |
| `14` | ` 1110` | `16` | ` E` |
| `15` | ` 1111` | `17` | ` F` |
| `16` | `10000` | `20` | `10` |
| `17` | `10001` | `21` | `11` |
> - > **8 进制与 16 进制**
> 计算机处理的信息是用 2 进制表示的。但是如果全部都只用 0、1 来表示的话,位数就会变得非常大,不便于人们理解。因此就可以使用 8 进制和 16 进制来解决这个问题。8 进制使用 0 到 7 共 8 个数,用 1 位数表示 3 个位(bit)。16 进制使用 0 到 15 共 16 个数,用 1 位数表示 4 个位(bit)。由于一般我们使用的数字只有 10 个,因此在 16 进制中,10 到 15 之间的数字就用英文字母 A 到 F 表示。
> - > **字节(byte)**
> 计算机在表示数的时候,会把一定数量的位(bit)的组合——字节作为计数单位。一个字节有多少位并没有共通的标准。在以前,根据制造商和机种的不同,1 个字节包含的位的数量也不一样,但现在 1 个字节有 8 位已经是业界的常识了。1 个字节可以表示的 10 进制数是从 0 到 255,8 进制数是从 000 到 377,16 进制数是从 00 到 FF。由于 2 个 16 进制位刚好等于 8 位(1 个字节),因此用 16 进制来表示以字节为单位的数据会非常方便。
### **12.7 随机数**
有时候随机性可能会帮助我们解决一些问题。随机性一般有以下特质。
-
**没有规则和法则依据**
-
**一定范围内的数会均等地出现**
拿掷骰子为例,我们不能预测下一个投出的是哪一面,但骰子各个面投出的几率都是一样的。我们把这样的情况称为随机,随机得到的数值称为随机数。在掷骰子或者洗扑克牌那样需要偶然性的情况下,或者像加密后的密码那样希望得到一些难以被预测的数据时,一般都会用到随机数。
我们可以用 `Random.rand` 方法得到随机数。不指定参数时,`Random.rand` 方法返回比 1 小的浮点小数。参数为正整数时,返回 0 到该正整数之间的数值。
~~~
p Random.rand #=> 0.13520495197709
p Random.rand(100) #=> 31
p Random.rand(100) #=> 84
~~~
程序不能生成真正的随机数,只能通过某种算法生成看起来像随机数的值,这样的随机数称为模拟随机数。生成模拟随机数需要以某个值为基础,这个值称为随机数的种子。模拟随机数终究只是通过计算得到的数值,只要随机数的种子一样,那么得到值就有可能重复出现。使用 `Random.new` 方法初始化随机数生成器,然后再使用 `Random#rand` 方法,就可以对 `Random` 对象指定随机数种子,从而生成随机数。
~~~
r1 = Random.new(1) # 初始化随机数组
p [r1.rand, r1.rand]
#=> [0.417022004702574, 0.7203244934421581]
r2 = Random.new(1) # 再次初始化随机数组
p [r2.rand, r2.rand]
#=> [0.417022003702574, 0.7203244934421581]
~~~
`Random.new` 方法不指定参数的情况下,则会用随机生成的随机数种子初始化 `Random` 对象,因此每次得到的随机数组也会不一样。
~~~
r1 = Random.new
p [r1.rand, r1.rand]
#=> [0.49452535392946817, 0.34141702823098863]
r2 = Random.new
p [r2.rand, r2.rand]
#=> [0.9464262066747281, 0.01911968591048996]
~~~
在信息安全领域中,“优质的随机”是一个重要的课题。生成用于加密 key 的随机数时,不能重复出现是非常重要的,因此就需要我们慎重地选择难以被预测的随机种子。在一些特殊的情况下可能会需要初始化 `Random` 对象,而一般情况下直接用最开始介绍的 `Random.rand` 方法就足够了。
### **12.8 计数**
除了数值计算外,`Integer` 类还能计算处理的次数、数组的元素个数等。接下来介绍的方法就是按照数值指定的次数执行循环处理的迭代器。
-
***n*.`times`{|*i*| … }**
循环 *n* 次,从 0 到 *n*-1 的值会被依次赋值给块变量。
~~~
ary = []
10.times do |i|
ary << i
end
p ary #=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
~~~
-
***from*.`upto`(*to*){|*i*| … }**
从 *from* 开始循环对 *i* 进行加 1 处理,直到 *i* 等于 to。*from* 比 *to* 大时不会执行循环处理。
~~~
ary = []
2.upto(10) do |i|
ary << i
end
p ary #=> [2, 3, 4, 5, 6, 7, 8, 9, 10]
~~~
-
***from*.`downto`(*to*){…}**
从 *from* 开始循环对 *i* 进行减 1 处理,直到 *i* 等于 *to*。*from* 比 *to* 小时不会执行循环处理。
~~~
ary = []
10.downto(2) do |i|
ary << i
end
p ary #=> [10, 9, 8, 7, 6, 5, 4, 3, 2]
~~~
-
***from*.`step`(*to*, *step*){…}**
从 *from* 开始循环对 *i* 进行加 *step* 处理,直到 *i* 等于 *to*。*step* 为正数时,*from* 比 *to* 大时不会执行循环处理。*step* 为负数时,*from* 比 *to* 小时不会执行循环处理。
~~~
ary = []
2.step(10, 3) do |i|
ary << i
end
p ary #=> [2, 5, 8]
ary = []
10.step(2, -3) do |i|
ary << i
end
p ary #=> [10, 7, 4]
~~~
如果不对 `times`、`upto`、`downto`、`step` 的各方法指定块,则会返回 `Enumerator` 对象。这样,之前通过 `step` 方法的块获取的一连串数值,就同样也可以通过 `Enumerator#collect` 方法获取。关于 `Enumerator` 对象,我们会在第 14 章中介绍。
~~~
ary = 2.step(10).collect{|i| i * 2}
p ary #=> [4, 6, 8, 10, 12, 14, 16, 18, 20]
~~~
### **12.9 近似值误差**
处理浮点小数时很容易因误差产生问题。这里我们来看看具体的例子,执行下面的程序后会产生意想不到的结果。
~~~
a = 0.1 + 0.2
b = 0.3
p [a, b] #=> [0.3, 0.3]
p a == b #=> false
~~~
虽然我们期待 0.1 + 0.2 与 0.3 的比较结果为 `true`,但实际结果却是 `false`。为什么会这样呢?
在 10 进制中,就像 1/10、1/100、1/1000……这样,我们会用 10 取幂后的倒数来表示数值。而另一方面,`Float` 类的浮点小数则是用 2 取幂后的倒数来表示,如 1/2、1/4、1/8……。因此,在处理 1/5、1/3 这种用 2 进制无法正确表示的数值时,结果就会产生误差。而如果要用 2 进制的和来表示这类数值的话,计算机就必须在适当的位置截断计算结果,这样就产生了近似值误差。
如果可以把小数转换为两个整数相除的形式,那么通过使用 `Rational` 类进行运算,就可以避免近似值误差。
~~~
a = Rational(1, 10) + Rational(2, 10)
b = Rational(3, 10)
p [a, b] #=> [(3/10), (3/10)]
p a == b
~~~
另外,Ruby 还提供了 `bigdecimal` 库,可以有效处理拥有更多小数位的 10 进制数。
> **专栏**
> **Comparable 模块**
> Ruby 的比较运算符(`==`、`<=` 等)实际上都是方法。`Comparable` 模块里封装了比较运算符,将其 Mix-in 到类后,就可以实现实例间互相比较的方法(下表)。Comparable 在英语中就是“可以比较”的意思。
> **表 Comparable 模块封装的方法**
| | `<>` | `==` | `>=` | `>` | `between?` |
|-----|-----|-----|-----|-----|-----|
> `Comparable` 模块中的各运算符都会使用 `<=>` 运算符的结果。`<=>` 运算符如果能像下表那样定义的话,上表中的各个方法就都可以使用。
> **表 表 a <=> b 的结果**
| `a <>`时 | -1( 比0 小) |
|-----|-----|
| `a == b`时 | 0 |
| `a > b`时 | 1(比 0 大) |
> 下面的 `Vector` 类表示拥有 *x* 和 *y* 两个坐标的向量。为了比较向量间的坐标,这里定义了 `<=>` 运算符。然后,通过包含(include)`Comparable` 模块,就可以实现上表中的比较方法。
~~~
class Vector
include Comparable
attr_accessor :x, :y
def initialize(x, y)
@x, @y = x, y
end
def scalar
Math.sqrt(x ** 2 + y ** 2)
end
def <=> (other)
scalar <=> other.scalar
end
end
v1 = Vector.new(2, 6)
v2 = Vector.new(4, -4)
p v1 <=> v2 #=> 1
p v1 < v2 #=> false
p v1 > v2 #=> true
~~~
> 在本书介绍过的类中,`Numeric`、`String`、`Time` 都包含了 `Comparable` 模块。
### **练习题**
1. 表示温度的单位有摄氏、华氏两种。请定义将摄氏转换为华氏的方法 `cels2fahr`。摄氏与华氏的转换公式如下:
**华氏=摄氏 × 9 ÷ 5 + 32**
2. 与 1 相反,请定义将华氏转换为摄氏的方法 `fahr2cels`。然后从 1 摄氏度到 100 摄氏度,请按照每隔 1 摄氏度输出一次的方式,输出对应的华氏温度。
3. 请定义返回掷骰子(1 到 6 随机整数)的结果的 `dice` 方法。
4. 请定义合计掷 10 次骰子的结果的 `dice10` 方法。
5. 请定义调查整数 `num` 是否为素数的 `prime?(num)` 方法。素数的定义为除了 1 和自己外不能被整除的数。个位整数中,2、3、5、7 为素数。
> 参考答案:请到图灵社区本书的“随书下载”处下载([http://www.ituring.com.cn/book/1237](http://www.ituring.com.cn/book/1237))。
- 推荐序
- 译者序
- 前言
- 本书的读者对象
- 第 1 部分 Ruby 初体验
- 第 1 章 Ruby 初探
- 第 2 章 便利的对象
- 第 3 章 创建命令
- 第 2 部分 Ruby 的基础
- 第 4 章 对象、变量和常量
- 第 5 章 条件判断
- 第 6 章 循环
- 第 7 章 方法
- 第 8 章 类和模块
- 第 9 章 运算符
- 第 10 章 错误处理与异常
- 第 11 章 块
- 第 3 部分 Ruby 的类
- 第 12 章 数值类
- 第 13 章 数组类
- 第 14 章 字符串类
- 第 15 章 散列类
- 第 16 章 正则表达式类
- 第 17 章 IO 类
- 第 18 章 File 类与 Dir 类
- 第 19 章 Encoding 类
- 第 20 章 Time 类与 Date 类
- 第 21 章 Proc 类
- 第 4 部分 动手制作工具
- 第 22 章 文本处理
- 第 23 章 检索邮政编码
- 附录
- 附录 A Ruby 运行环境的构建
- 附录 B Ruby 参考集
- 后记
- 谢辞