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

时间:2010-08-25 16:36:19

标签: arrays ruby

我正在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) th 位置开始时,数组切片的行为是否有任何原因?

10 个答案:

答案 0 :(得分:177)

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

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

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

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

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

答案 1 :(得分:27)

这与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,因此在你的例子中array[4,0]要求存在的第四个元素,但要求返回一个零元素的数组。虽然array[5,0]要求索引超出范围,因此返回nil。如果您记得slice方法返回 new 数组,而不是更改原始数据结构,这可能更有意义。

编辑:

在审核评论后,我决定编辑这个答案。当arg值为2时,Slice调用以下code snippet

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);
}

如果查看定义了array.c方法的rb_ary_subseq类,如果长度超出范围,则会看到它返回nil,而不是索引:

if (beg > RARRAY_LEN(ary)) return Qnil;

在这种情况下,这是传入4时发生的事情,它检查有4个元素,因此不会触发nil返回。然后,如果第二个arg设置为零,它将继续并返回一个空数组。如果传入5,则数组中没有5个元素,因此在计算零arg之前返回nil。代码here位于第944行。

我认为这是一个错误,或者至少是不可预测的,而不是“最小惊喜原则”。当我得到几分钟时,我将至少向ruby核心提交一个失败的测试补丁。

答案 2 :(得分:23)

至少注意到行为是一致的。从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级以上,该方法很可能会立即完全脱离边界而退出。

答案 3 :(得分:12)

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

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个元素之后不插入)。

将切片语法array[x,y]读取为“从x中的array元素开始,最多选择y元素”。仅当array至少包含x元素时才有意义。

答案 4 :(得分:11)

确实有意义

您需要能够分配给这些切片,因此它们的定义方式使得字符串的开头和结尾具有有效的零长度表达式。

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

答案 5 :(得分:8)

我同意这似乎是一种奇怪的行为,但即使the official documentation on 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的描述似乎也没有提供任何关于为什么以这种方式工作的见解:

  

元素引用 - 返回 index 处的元素,或返回从 start 开始并继续 length 元素的子数组,或返回一个子数组由范围指定。负索引从数组末尾向后计数(-1是最后一个元素)。如果索引(或起始索引)超出范围,则返回nil。

答案 6 :(得分:8)

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

加里赖特的回答是 -

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

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

以下列方式考虑编号是有帮助的:

  -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

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

单个参数:索引表示单个字符位置 在字符串中。结果是单个字符串 在索引处找到或者为零,因为给定的字符没有 索引。

  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

两个整数参数:参数标识字符串的一部分 提取或替换。特别是,字符串的零宽度部分 也可以识别,以便可以在之前或之后插入文本 现有字符,包括字符串的前端或末尾。在这 例如,第一个参数标识一个字符位置但是 而是标识字符之间的空格,如图所示 以上。第二个参数是长度,可以是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图来模拟实际行为。

答案 7 :(得分:7)

Jim Weirich

提供的解释
  

考虑它的一种方法是指数位置4位于最边缘   数组。在要求切片时,您将返回尽可能多的切片   剩下的数组。所以考虑数组[2,10],数组[3,10]和   array [4,10] ...每个返回结尾的剩余位   数组:分别为2个元素,1个元素和0个元素。然而,   位置5显然阵列之外,而不是在边缘,所以   array [5,10]返回nil。

答案 8 :(得分:6)

考虑以下数组:

>> 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/了解详情。

答案 9 :(得分:0)

tl; dr:在array.c的源代码中,调用不同的函数,具体取决于是否将1或2个参数传递给Array#slice,导致意外的返回值。

(首先,我想指出我不用C语言编写代码,但多年来一直使用Ruby。所以如果你不熟悉C语言,但是你花了几分钟时间熟悉自己有了函数和变量的基础知识,遵循Ruby源代码并不难,如下所示。这个答案基于Ruby v2.3,但与v1.9大致相同。)

场景#1

array.length == 4; array.slice(4) #=> nil

如果查看Array#slicerb_ary_aref)的源代码,您会看到当只传入一个参数(lines 1277-1289)时,会调用rb_ary_entry,传递索引值(可以是正数或负数)。

rb_ary_entry然后从数组的开头计算所请求元素的位置(换句话说,如果传入负索引,则计算正数当量),然后调用rb_ary_elt获取请求的元素。

正如预期的那样,当数组nil的长度小于或等于索引时,rb_ary_elt会返回len(此处称为offset })。

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.length,因此返回一个空数组而不是可能期望的nil值。

问题解答了?

如果这里的实际问题不是“什么代码导致这种情况发生?”,而是“为什么Matz这样做?”,那么你只需要给他买一杯咖啡。下一个RubyConf并问他。