我正在进行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)个位置开始时,数组切片会表现得这么奇怪,有什么原因吗?


当前回答

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上请他喝杯咖啡,然后问他。

其他回答

考虑以下数组:

>> array=["a","b","c"]
=> ["a", "b", "c"]

您可以通过将一个项分配给a[0,0]来将其插入到数组的开始(头部)。要将元素放在“a”和“b”之间,使用a[1,0]。基本上,在表示法a[i,n]中,i表示一个索引,n表示一些元素。当n=0时,它定义了数组元素之间的位置。

现在,如果您考虑数组的末尾,您如何使用上面描述的符号将一个项附加到它的末尾?很简单,将值赋给a[3,0]。这是数组的尾部。

因此,如果您尝试访问a[3,0]处的元素,则会得到[]。在这种情况下,您仍然在数组的范围内。但是如果你试图访问a[4,0],你会得到nil作为返回值,因为你不再在数组的范围内了。

更多信息请访问http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/。

至少要注意行为是一致的。从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中的第一个参数不是标识元素,而是标识元素之间的位置,定义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。