关于 bit(位) 和 byte(字节) 的解释可以读一下 Working with Bits and Bytes in Ruby
在 Unicode 编码下,使用中文的时候占用 2 个字节。但使用字母的时候,其实只是用了一个字节,就是后面的一个字节。比如 00000000 01000000
,大家会注意这里就造成了浪费,就是在原来的字节前面加 0 就可以了,这样就相当于多了一倍的储存空间,在存储和运输上不太划算。
UTF-8 编码是基于 Unicode 编码的可变长度编码方式。它把 Unicode 编码根据不同的数字大小编码成 1-6 个字节,常用的字母就是 1 个字节,汉字等通常 2-3 个字节。
'hello world'.each_byte.to_a #=> [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
"s".bytes #=> [115]
"中".bytes #=> [228, 184, 173]
当你在看 bytes
文档的时候,可能还会发现一些冷门知识,比如:代码点 codepoints,有兴趣自己去找资料学习吧。
'hello world'.codepoints.to_a #=> [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
"中".codepoints.to_a #=> [20013]
刚才我们知道在计算机内存中,用的是 Unicode 编码,当要存储在硬盘或者传输时,就转换为 UTF-8 模式,当我们在编辑一个文本文件时,还没保存的时候,我们用的就是 Unicode 编码,当我们点击保存时,这时候就转换为 UTF-8 编码了,当我们读取的时候,就又变成了 Unicode 编码,就是这样转换的。
Ruby 默认是 UTF-8。
"some string".encoding #=> #<Encoding:UTF-8>
Encoding.default_external #=> #<Encoding:UTF-8>
在计算机中所有数据都是以二进制的形式储存的,位运算其实就是直接对在内存中的二进制数据进行操作,因此处理数据的速度非常快。
位运算符作用于位,并逐位执行操作。
假设如果 a = 60,且 b = 13,现在以二进制格式,它们如下所示:
a = 0011 1100
b = 0000 1101
a&b #=> 0000 1100
a|b #=> 0011 1101
a^b #=> 0011 0001
~a #=> 1100 0011
下表列出了 Ruby 支持的位运算符 ( '或' 符号我用英文 I 代替的,尴尬):
运算符 | 描述 | 运算规则 |
---|---|---|
& |
与 | 两个位都为 1 时,结果才为 1 |
I | 或 | 两个位都为 0 时,结果才为 0 |
^ |
异或 | 两个位相同为 0,相异为 1 |
~ |
取反 | 0 变 1,1 变 0 |
<< |
左移 | 各二进位全部左移若干位,高位丢弃,低位补 0 |
>> |
右移 | 各二进位全部右移若干位 |
运算符 | 实例 |
---|---|
& |
(a & b) 将得到 12,即为 0000 1100 |
I |
(a I b) 将得到 61,即为 0011 1101 |
^ |
(a ^ b) 将得到 49,即为 0011 0001 |
~ |
(~a ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< |
a << 2 将得到 240,即为 1111 0000 |
>> |
a >> 2 将得到 15,即为 0000 1111 |
注意以下几点:
~
取反是单目操作符,其它 5 种都是双目操作符。>>
操作。对无符号数,高位补 0;有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补 0(逻辑右移),见后面的代码示例。a, b = 15, -15
a >> 2 #=> 3
b >> 2 #=> -4
因为 15 的二进制是 0000 1111
,右移二位成为 __00 1111
,最高位由符号位填充将得到 0000 0011
即十进制的 3。-15 的二进制是 1111 0001
,右移二位成为 __11 1100
,最高位由符号位填充将得到 1111 1100
即十进制的 -4。
上面的例子这里涉及到两个知识点:
第一个问题很简单,以字符串为例,先转换成 ascii 码再转换成 2 进制。
'A'.bytes.first.to_s(2) #=> "1000001"
闹呢,当然不可能这么傻。C 语言允许开发人员直接访问存储变量的内存,而 Ruby 不行。当我们需要在 Ruby 中访问 字节 (byte) 和 位 (bits) 的时候,可以使用 pack
和 unpack
方法。
一般来说,对于 unpack
方法你只要记住两个参数 b*
转换成 2 进制,和 C*
转换成 ascii 码。
'A'.unpack('b*') #=> ["10000010"]
"hello world".unpack('C*') #=> [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
"中".unpack('C*') #=> [228, 184, 173]
真的足够用了,再去研究 B*
和 b*
有什么不同,又会牵扯到 MSB/LSB 的问题,'H' 转换成 16 进制什么的,完全不用在意。
懂了 unpack
那 pack
也就懂了,无非是逆向操作。
[1000001].pack('C') #=> "A"
[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100].pack('C*') #=> "hello world"
首先要弄懂 原码,反码和补码,而我不打算摘抄一大段东西,所以可以直接看
关于第 2 个问题你目前只需要了解到:
>>
操作是算术右移。低位溢出,符号位不变,并用符号位数补溢出的高位。
缘由出来了,今天遇到了这个函数。问这个函数有什么用。
# File activesupport/lib/active_support/security_utils.rb, line 11
def secure_compare(a, b)
return false unless a.bytesize == b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
看完了上面的文章,这函数应该能看懂了吧。当然,除了 a.unpack "C#{a.bytesize}"
这句有点迷糊,刚才明明说好只要记住 C*
和 b*
两个参数足够了啊。
没办法去看文档吧,然后,我懵了!
"aaa".unpack('h2H2c') #=> ["16", "61", 97]
"whole".unpack('xax2aX2aX1aX2a') #=> ["h", "e", "l", "l", "o"]
这什么鬼参数,好歹找到了一篇中文解释 pack 模板字符串。
翻译过来,这一句就跟 a.unpack 'C*'
没区别。
其它没什么好解释的了,按位 异或 比较传入的两个参数是否相等。
我想了半天也没想到这么比较有什么好处,又不想靠 gg 搜答案,晚上跟老伙伴讨论了一波,他也没什么头绪,没办法只好 gg 了。
转了一圈又回来了,原来论坛上有相关帖子,这函数主要是搞定 计时攻击 的,具体可看 计时攻击原理以及 Rails 对应的防范。
延展开去还有关于 IO 打开文件时候的外部编码和内部编码的问题,可以直接看相关阅读中 Ruby 对多语言的支持 一文,虽然文章有点老,还是针对 Ruby 1.9 版本阐述的问题,但是不影响你理解。
再扯其它的就离主题太远了,初衷只是因为今天做笔试题时候发现自己对这块知识基本忘光了,赶快奶自己一波。
如果你知道自己在某一领域上有所欠缺,就应该立刻开始学习相关知识。