我正在进行Ruby Koans的练习,我被以下Ruby的怪癖所震惊,我发现它真的无法解释:
array = [:peanut, :butter, :and, :jelly]
array[0] #=> :peanut #OK!
array[0,1] #=> [:peanut] #OK!
array[0,2] #=> [:peanut, :butter] #OK!
array[0,0] #=> [] #OK!
array[2] #=> :and #OK!
array[2,2] #=> [:and, :jelly] #OK!
array[2,20] #=> [:and, :jelly] #OK!
array[4] #=> nil #OK!
array[4,0] #=> [] #HUH?? Why's that?
array[4,100] #=> [] #Still HUH, but consistent with previous one
array[5] #=> nil #consistent with array[4] #=> nil
array[5,0] #=> nil #WOW. Now I don't understand anything anymore...
那么为什么数组[5,0]不等于数组[4,0]呢?当你从第(长度+1)个位置开始时,数组切片会表现得这么奇怪,有什么原因吗?
这与slice返回一个数组有关,相关的源文档来自array# slice:
* call-seq:
* array[index] -> obj or nil
* array[start, length] -> an_array or nil
* array[range] -> an_array or nil
* array.slice(index) -> obj or nil
* array.slice(start, length) -> an_array or nil
* array.slice(range) -> an_array or nil
这对我来说意味着,如果你给出的开始是越界的,它将返回nil,因此在你的例子数组[4,0]请求存在的第4个元素,但要求返回一个零元素的数组。当数组[5,0]请求一个越界的索引时,它返回nil。如果您还记得slice方法是返回一个新的数组,而不是改变原始的数据结构,那么这可能更有意义。
编辑:
在看过评论后,我决定编辑这个答案。当arg值为2时,Slice调用以下代码片段:
if (argc == 2) {
if (SYMBOL_P(argv[0])) {
rb_raise(rb_eTypeError, "Symbol as array index");
}
beg = NUM2LONG(argv[0]);
len = NUM2LONG(argv[1]);
if (beg < 0) {
beg += RARRAY(ary)->len;
}
return rb_ary_subseq(ary, beg, len);
}
如果你查看定义了rb_ary_subseq方法的array.c类,你会看到如果长度超出边界,它会返回nil,而不是索引:
if (beg > RARRAY_LEN(ary)) return Qnil;
在这个例子中,这就是传入4时发生的情况,它检查是否有4个元素,因此不会触发nil返回。然后,如果第二个参数被设置为0,它将返回一个空数组。而如果传入5,则数组中没有5个元素,因此在zero参数被求值之前返回nil。代码在第944行。
我认为这是一个错误,或者至少是不可预测的,而不是“最小意外原则”。当我有几分钟的时间,我至少会提交一个失败的测试补丁给ruby core。
至少要注意行为是一致的。从5开始,一切都是一样的;奇怪的只发生在[4,N]。
也许这种模式有帮助,或者我只是累了,根本没用。
array[0,4] => [:peanut, :butter, :and, :jelly]
array[1,3] => [:butter, :and, :jelly]
array[2,2] => [:and, :jelly]
array[3,1] => [:jelly]
array[4,0] => []
在[4,0]处,我们捕获数组的末尾。如果最后一个返回nil,我实际上会发现它相当奇怪,就模式的美丽而言。由于这样的上下文,4是第一个参数的可接受选项,这样就可以返回空数组。但是,一旦我们达到5或5以上,方法可能会立即退出,因为它完全超出了边界。
dr:在Array .c的源代码中,不同的函数会被调用,这取决于你是将1个还是2个参数传递给array# slice,从而导致意外的返回值。
(首先,我想指出的是,我不会用C编程,但多年来一直在使用Ruby。因此,如果您不熟悉C语言,但您花了几分钟时间来熟悉函数和变量的基础知识,那么遵循Ruby源代码实际上并不难,如下所示。这个答案是基于Ruby v2.3的,但与v1.9差不多。)
场景# 1
数组中。长度== 4;Array.slice (4) #=> nil
如果你查看array# slice (rb_ary_aref)的源代码,你会看到当只传入一个参数时(第1277-1289行),rb_ary_entry被调用,传入索引值(可以是正的,也可以是负的)。
然后Rb_ary_entry计算请求元素从数组开始的位置(换句话说,如果传入了一个负索引,它计算正的等效值),然后调用rb_ary_elt来获得请求的元素。
正如预期的那样,当数组len的长度小于或等于索引(这里称为offset)时,rb_ary_elt返回nil。
1189: if (offset < 0 || len <= offset) {
1190: return Qnil;
1191: }
场景# 2
array.length == 4; array.slice(4, 0) #=> []
然而,当传入2个参数(即起始索引beg和切片len的长度)时,将调用rb_ary_subseq。
在rb_ary_subseq中,如果起始索引beg大于数组长度alen,则返回nil:
1208: long alen = RARRAY_LEN(ary);
1209:
1210: if (beg > alen) return Qnil;
否则,计算结果切片len的长度,如果它被确定为零,则返回一个空数组:
1213: if (alen < len || alen < beg + len) {
1214: len = alen - beg;
1215: }
1216: klass = rb_obj_class(ary);
1217: if (len == 0) return ary_new(klass, 0);
因为4的起始下标不大于array。长度,返回一个空数组,而不是人们可能期望的nil值。
问题回答吗?
如果这里真正的问题不是“什么代码导致这种情况发生?”,而是“Matz为什么这样做?”,那么你只需要在下一次RubyConf上请他喝杯咖啡,然后问他。
当你考虑数组slice可以是一个有效的左值,而不仅仅是一个右值时,这是有意义的:
array = [:peanut, :butter, :and, :jelly]
# replace 0 elements starting at index 5 (insert at end or array):
array[4,0] = [:sandwich]
# replace 0 elements starting at index 0 (insert at head of array):
array[0,0] = [:make, :me, :a]
# array is [:make, :me, :a, :peanut, :butter, :and, :jelly, :sandwich]
# this is just like replacing existing elements:
array[3, 4] = [:grilled, :cheese]
# array is [:make, :me, :a, :grilled, :cheese, :sandwich]
如果数组[4,0]返回nil而不是[],这将是不可能的。然而,array[5,0]返回nil,因为它是越界的(插入4元素数组的第4个元素后是有意义的,但插入4元素数组的第5个元素后是没有意义的)。
将切片语法数组[x,y]解读为“从数组中的x个元素开始,选择最多y个元素”。只有当数组至少有x个元素时才有意义。