Ruby中的数组切片:对不合逻辑行为的解释(摘自Rubykoans.com)

我正在通过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... 

那么为什么array[5,0]不等于array[4,0] ? 当你在(长度+ 1)位置开始时,arrays切片的行为有多奇怪吗?

切片和索引是两个不同的操作,推断出另一个的行为就是问题所在。

slice中的第一个参数不是元素,而是元素之间的地方,定义了跨度(而不是元素本身):

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

4还在阵内,只是勉强; 如果您请求0个元素,则会获得数组的空端。 但没有索引5,所以你不能从那里切片。

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

这与slice从slice#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 

这向我build议,如果你给出了超出范围的开始,它将返回nil,因此在你的示例array[4,0]要求存在第四个元素,但要求返回一个零元素数组。 而array[5,0]要求索引越界,所以它返回nil。 如果你记得slice方法返回一个新的数组,而不是改变原来的数据结构,这可能更有意义。

编辑:

在审查了评论后,我决定编辑这个答案。 当参数值为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个元素,因此不会触发无返回。 然后,如果第二个参数设置为零,则返回空数组。 而如果传入5,则数组中没有5个元素,因此在计算零个arg之前返回nil。 在944行代码。

我相信这是一个错误,或者至less是不可预测的,而不是“最低惊奇原则”。 当我几分钟之后,我将至less提交一个失败的testing补丁到ruby核心。

至less要注意行为是一致的。 从五点开始,一切都是一样的。 奇怪只发生在[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点以上,那么这个方法可能会立即退出,完全超越界限。

这是有道理的,当你考虑比数组切片可以是一个有效的左值,而不只是一个右值:

 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] 

这是不可能的,如果array[4,0]返回nil而不是[] 。 但是, array[5,0]返回nil因为它超出了范围(在4元素数组的第4个元素有意义之后插入,而在4元素数组的第5个元素之后没有插入)。

读取slice语法array[x,y]为“在array x元素之后开始,最多selecty元素”。 这只有在array至less有x元素时才有意义。

有道理的

您需要能够分配给这些切片,因此它们的定义方式使得string的开始和结尾都具有正在使用的零长度expression式。

 array[4, 0] = :sandwich array[0, 0] = :crunchy => [:crunchy, :peanut, :butter, :and, :jelly, :sandwich] 

我同意,这似乎是奇怪的行为,但即使在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的描述似乎也没有提供任何有关它为什么这样工作的见解:

元素参考 – 返回索引处的元素,或者返回从开始处开始继续处理长度元素的子数组,或者返回由range指定的子数组。 负指数从数组末尾向后计数(-1是最后一个元素)。 如果索引(或起始索引)超出范围,则返回nil。

我发现Gary Wright的解释也很有帮助。 http://www.ruby-forum.com/topic/1393096#990065

加里·赖特的答案是 –

http://www.ruby-doc.org/core/classes/Array.html

文件当然可以更清楚,但实际行为是自洽的和有用的。 注意:我假定String的版本是1.9.X。

这有助于以下列方式考虑编号:

  -4 -3 -2 -1 <-- numbering for single argument indexing 0 1 2 3 +---+---+---+---+ | a | b | c | d | +---+---+---+---+ 0 1 2 3 4 <-- numbering for two argument indexing or start of range -4 -3 -2 -1 

常见的(也是可以理解的)错误是假定单参数索引的语义与两个参数场景(或范围)中的第一个参数的语义相同。 他们在实践中不是一回事,文件也没有反映这一点。 错误虽然是在文档中,而不是在执行中:

单个参数:索引表示string中的单个字符位置。 结果是在索引处find的单个string或nil,因为在给定索引处没有字符。

  s = "" s[0] # nil because no character at that position s = "abcd" s[0] # "a" s[-4] # "a" s[-5] # nil, no characters before the first one 

两个整数参数:参数标识要提取或replace的string的一部分。 特别地,也可以标识string的零宽度部分,使得可以在包括string的前端或末端的现有字符之前或之后插入文本。 在这种情况下,第一个参数不会标识字符位置,而是标识字符之间的空格,如上图所示。 第二个参数是长度,可以是0。

 s = "abcd" # each example below assumes s is reset to "abcd" To insert text before 'a': s[0,0] = "X" # "Xabcd" To insert text after 'd': s[4,0] = "Z" # "abcdZ" To replace first two characters: s[0,2] = "AB" # "ABcd" To replace last two characters: s[-2,2] = "CD" # "abCD" To replace middle two characters: s[1..3] = "XX" # "aXXd" 

一个范围的行为是非常有趣的。 当提供两个参数(如上所述)时,起始点与第一个参数相同,但范围的终点可以是单个索引的“字符位置”,也可以是两个整数参数的“边缘位置”。 差异取决于是使用双点范围还是三点范围:

 s = "abcd" s[1..1] # "b" s[1..1] = "X" # "aXcd" s[1...1] # "" s[1...1] = "X" # "aXbcd", the range specifies a zero-width portion of the string s[1..3] # "bcd" s[1..3] = "X" # "aX", positions 1, 2, and 3 are replaced. s[1...3] # "bc" s[1...3] = "X" # "aXd", positions 1, 2, but not quite 3 are replaced. 

如果你回过头来看看这些例子,并坚持使用双索引或范围索引的单索引语义,你就会感到困惑。 你必须使用我在ascii图中显示的替代编号来模拟实际的行为。

Jim Weirich提供的解释

考虑一个方法就是索引位置4位于数组的边缘。 当请求一个分片时,你返回剩下的那个数组。 所以考虑数组[2,10],数组[3,10]和数组[4,10] …分别返回数组末尾的剩余位:2个元素,1个元素和0个元素。 然而,位置5显然是在数组之外 ,而不是在边缘,所以array [5,10]返回nil。

考虑下面的数组:

 >> 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/阅读更多。;