在 Ruby 中,大多数对象都需要内存来存储它们的类和实例变量。一旦分配了这个内存,Ruby 就通过这个内存位置来表示每个对象。当对象被赋值给一个变量或传递给一个函数时,传递的是这个内存的位置,而不是这个内存中的数据。单例方法利用了这一点。当你定义一个单例方法时,Ruby 会默默地用一个新的单例类替换对象类。因为每个对象都存储它的类,Ruby 可以很容易地用一个实现单例方法(并继承自原始类)的新类替换对象的类。
对于立即值的对象,这不再是正确的:true
、false
、nil
、所有符号和小到足以容纳在 Fixnum 中的整数。Ruby 不会为这些对象的实例分配内存,它不会在内部将对象表示为内存中的位置。相反,它根据对象的内部表示来推断对象的实例。这意味着双重:
每个对象的类不再存储在内存中的特定位置,而是由直接对象的类型隐式确定。这就是为什么 Fixnums 不能有单例方法的原因。
具有相同状态的立即对象(例如,整数 2378 的两个 Fixnums)实际上是相同的实例。这是因为实例是由这个状态决定的。
为了更好地理解这一点,请考虑对 Fixnum 进行以下操作:
>> x = 3 + 7
=> 10
>> x.object_id == 10.object_id
=> true
>> x.object_id == (15-5).object_id
=> true
现在,考虑使用字符串:
>> x = "a" + "b"
=> "ab"
>> x.object_id == "ab".object_id
=> false
>> x.object_id == "Xab"[1...3].object_id
=> false
>> x == "ab"
=> true
>> x == "Xab"[1...3]
=> true
Fixnums 的对象 id 相等的原因是它们是具有相同内部表示的直接对象。另一方面,字符串存在于分配的内存中。每个字符串的对象 id 是其对象状态在内存中的位置。
一些底层信息
要理解这一点,您必须了解 Ruby(至少 1.8 和 1.9)在内部如何处理 Fixnums。在 Ruby 中,所有对象在 C 代码中都由类型为 的变量表示VALUE
。Ruby 对 施加以下要求VALUE
:
类型 VALUE 是足以容纳指针的最小整数。这意味着,在 C 中,sizeof(VALUE) == sizeof(void*)
.
任何非立即对象必须在 4 字节边界上对齐。这意味着 Ruby 分配的任何对象都将具有4*i
某个整数的地址i
。这也意味着所有指针在它们的两个最低有效位中都有零值。
第一个要求允许 Ruby 将指向对象的指针和立即值存储在类型变量中VALUE
。第二个要求允许 Ruby 根据两个最低有效位检测 Fixnum 和 Symbol 对象。
为了更具体,考虑 Ruby 对象的内部二进制表示z
,我们将Rz
在 32 位架构中调用它:
MSB LSB
3 2 1
1098 7654 3210 9876 5432 1098 7654 32 10
XXXX XXXX XXXX XXXX XXXX XXXX XXXX AB CD
Ruby 然后解释Rz
, 的表示z
,如下:
如果D==1
,z
则为 Fixnum。该 Fixnum 的整数值存储在表示的高 31 位中,并通过执行算术右移以恢复存储在这些位中的有符号整数来恢复。
测试了三种特殊表示(全部带有D==0
)
- 如果
Rz==0
, 那么z
是false
- 如果
Rz==2
, 那么z
是true
- 如果
Rz==4
, 那么z
是nil
如果ABCD == 1110
,那么 'z' 是一个符号。该符号通过右移八个最低有效位(即,z>>8
在 C 中)转换为唯一 ID。在 32 位架构上,这允许 2^24 个不同的 ID(超过 1000 万个)。在 64 位架构上,这允许 2^48 个不同的 ID。
否则,Rz
表示 Ruby 对象实例在内存中的地址,其类型z
由该位置的类信息确定。