Ruby Ruby 位运算符详解

victor · 2018年03月16日 · 最后由 victor 回复于 2018年03月16日 · 2874 次阅读
本帖已被设为精华帖!

字符编码

进化历史

  1. 计算机只能处理数字,如果要处理文本等,那么必须要把文本转化为二进制的数字才可以处理。
  2. 最开始只有 ASCII 编码,它包含大小写字母,符号,数字。1 个字符占用 1 个字节(byte),一个字节 8 位(bits)。
  3. 因为需要用其它语言,中间过渡了一下 GB2312 之类的编码,一个中文占用 2 个字节(bytes)。
  4. 后来开始使用 Unicode 编码,它包含所有国家语言,所以不会出乱码。ASCII 占用 1 个字节(byte),Unicode 通常 2 个字节(bytes),因为一些生僻字可能需要 3 或 4 个字节(bytes)。

关于 bit(位) 和 byte(字节) 的解释可以读一下 Working with Bits and Bytes in Ruby

字符集和编码方式的区别

UTF-8

在 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>

什么是位运算

在计算机中所有数据都是以二进制的形式储存的,位运算其实就是直接对在内存中的二进制数据进行操作,因此处理数据的速度非常快。

位运算符作用于位,并逐位执行操作。

Ruby 位运算符

假设如果 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 种都是双目操作符。
  • 位操作只能用于整形数据,对float和double类型进行位操作会被编译器报错。
  • 位操作符的运算优先级比较低,因为尽量使用括号来确保运算顺序。
  • >> 操作。对无符号数,高位补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。

pack 和 unpack

上面的例子这里涉及到两个知识点:

  1. 怎么把 15 变成 2进制,另外字母怎么转换成 2进制,中文怎么转换成 2进制?
  2. 符号位是怎么回事?

不同编码之间的处理

第一个问题很简单,以字符串为例,先转换成 ascii 码再转换成 2进制。

'A'.bytes.first.to_s(2) #=> "1000001"

闹呢,当然不可能这么傻。C 语言允许开发人员直接访问存储变量的内存,而 Ruby 不行。当我们需要在 Ruby 中访问 字节(byte) 和 位(bits) 的时候,可以使用 packunpack 方法。

一般来说,对于 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进制什么的,完全不用在意。

懂了 unpackpack 也就懂了,无非是逆向操作。

[1000001].pack('C') #=> "A"
[104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100].pack('C*') #=> "hello world"

符号位

首先要弄懂 原码,反码和补码,而我不打算摘抄一大段东西,所以可以直接看

关于第 2 个问题你目前只需要了解到:

  • 符号位是第一个字节 8 位(bits)的第 1 位,1为负,0为正。
  • ruby 的 >> 操作是算术右移。低位溢出,符号位不变,并用符号位数补溢出的高位。

实际应用

缘由出来了,今天遇到了这个函数。问这个函数有什么用。

# 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 版本阐述的问题,但是不影响你理解。

再扯其它的就离主题太远了,初衷只是因为今天做笔试题时候发现自己对这块知识基本忘光了,赶快奶自己一波。

如果你知道自己在某一领域上有所欠缺,就应该立刻开始学习相关知识。

相关阅读

共收到 3 条回复

编码和字符集都不区分

nouse 回复

谢谢,你说的是对的。我去研究一下这个段落怎么改更合适。本来想的是简单几句话能把字符编码和字符集的历史进化讲清楚,结果反而显示出我的无知。😅

huacnlee 将本帖设为了精华贴 03月16日 18:52
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册