我正在进行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 = [: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个元素时才有意义。

其他回答

切片和索引是两种不同的操作,从其中一个推断另一个的行为是您的问题所在。

slice中的第一个参数不是标识元素,而是标识元素之间的位置,定义span(而不是元素本身):

  :peanut   :butter   :and   :jelly
0         1         2      3        4

4仍然在数组中,只是勉强;如果你请求0个元素,你会得到数组的空末端。但是没有索引5,所以你不能从这里开始切片。

当你做索引(如数组[4]),你是指向元素本身,所以索引只从0到3。

我同意这看起来像是奇怪的行为,但即使是array# slice的官方文档也在下面的“特殊情况”中演示了与你的例子相同的行为:

   a = [ "a", "b", "c", "d", "e" ]
   a[2] +  a[0] + a[1]    #=> "cab"
   a[6]                   #=> nil
   a[1, 2]                #=> [ "b", "c" ]
   a[1..3]                #=> [ "b", "c", "d" ]
   a[4..7]                #=> [ "e" ]
   a[6..10]               #=> nil
   a[-3, 3]               #=> [ "c", "d", "e" ]
   # special cases
   a[5]                   #=> nil
   a[5, 1]                #=> []
   a[5..10]               #=> []

不幸的是,即使他们对array# slice的描述似乎也没有提供任何关于它为什么以这种方式工作的见解:

元素引用—返回位于下标处的元素,或返回从start开始并继续为长度元素的子数组,或返回由range指定的子数组。负索引从数组的末尾开始计数(-1是最后一个元素)。如果索引(或起始索引)超出范围,则返回nil。

当你考虑数组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个元素时才有意义。

至少要注意行为是一致的。从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以上,方法可能会立即退出,因为它完全超出了边界。

这与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。