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


232

我在进行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]?从第(length + 1)位置开始时,数组切片的行为有什么奇怪的原因吗?



看起来第一个数字是开始的索引,第二个数字是要分割的元素数量
奥斯丁2014年

Answers:


185

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

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

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

4仍然在数组中,几乎没有;如果请求0个元素,则得到数组的空端。但是没有索引5,所以您不能从那里切。

做索引(如array[4])时,您指向的是元素本身,因此索引仅从0到3。


8
一个很好的猜测,除非得到源的支持。别太刻薄,我对链接感兴趣,如果有的话可以解释一下OP和其他评论者所问的“为什么”。您的图有意义,但Array [4]为nil。Array [3]是:jelly。我希望Array [4,N]为nil,但就像OP所说的那样[]。如果这是一个地方,那将是一个非常无用的地方,因为Array [4,-1]为nil。因此,您无法使用Array [4]做任何事情。
squarism 2010年

5
@squarism我刚刚得到Charles Oliver Nutter(Twitter上的@headius)的确认,这是正确的解释。他是JRuby的重要开发人员,所以我认为他的话很权威。
Hank Gay



18
也称为“围栏张贴”。第五个栅栏柱(id 4)存在,但第五个元素不存在。切片是栅栏操作,索引是元素操作。
Matty K

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]要求存在的第4个元素,但要求返回零个元素的数组。虽然array[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);
}

如果查看定义array.crb_ary_subseq方法的类,则会发现如果长度超出范围而不是索引,则返回nil:

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

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

我认为这是一个错误,或者至少是不可预测的,而不是“最不惊讶的原则”。几分钟后,我至少会向ruby core提交一个失败的测试补丁。


2
但是... array [4,0]中由4表示的元素也不存在...-因为它实际上是5the元素(基于0的计数,请参见示例)。因此,它也超出了范围。
Pascal Van Hecke 2010年

1
你是对的。我回头查看了源代码,它看起来像第一个参数在C代码中作为长度而不是索引来处理。我将编辑我的答案,以反映这一点。我认为这可以作为错误提交。
杰德·施耐德

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或更高,该方法就可能由于完全和完全超出范围而立即退出。


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元素时才有意义。


11

确实有道理

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

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

1
您还可以将返回的片段分配为nil的范围,因此扩展此说明将很有用。array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas 2014年

分配时第二个数字做什么?它似乎被忽略了。[26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee

@drewverlee,这是不容忽视的:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen 2015年

10

我发现加里·赖特(Gary Wright)的解释也很有帮助。 http://www.ruby-forum.com/topic/1393096#990065

Gary Wright的答案是-

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

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

单个参数:索引表示字符串中单个字符的位置。结果要么是在索引中找到的单个字符串,要么是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

两个整数参数:参数标识要提取或替换的字符串的一部分。特别是,还可以识别字符串的零宽度部分,以便可以在字符串前面或结尾的现有字符之前或之后插入文本。在这种情况下,第一个参数并不能识别一个字符的位置,而是标识如上所示的图中的字符之间的空间。第二个参数是长度,可以为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图中显示的备用编号来对实际行为进行建模。


3
您可以包括该线程的主要思想吗?(如果链接一天无效)
VonC 2012年

8

我同意这看起来像是奇怪的行为,但是即使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个 元素继续的子数组,或返回range所指定的子数组。负索引从数组末尾开始倒计数(-1是最后一个元素)。如果索引(或起始索引)超出范围,则返回nil。


7

Jim Weirich提供的解释

一种考虑的方法是索引位置4在数组的最边缘。当请求切片时,您将返回剩余的数组。因此,请考虑array [2,10],array [3,10]和array [4,10] ...各自返回数组末尾的剩余位:分别为2个元素,1个元素和0个元素。但是,位置5显然数组外部,而不是在边缘,因此array [5,10]返回nil。


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/


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)的源代码,则会看到仅传入一个参数(第1277-1289行)时,rb_ary_entry将调用,并传入索引值(可以为正数或负数)。

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

正如预期的那样,rb_ary_elt返回nil当阵列的长度len小于或等于所述索引(这里称为offset)。

1189:  if (offset < 0 || len <= offset) {
1190:    return Qnil;
1191:  } 

场景2

array.length == 4; array.slice(4, 0) #=> []

但是,当传入2个参数(即,起始索引beg和slice的长度len)时,rb_ary_subseq被调用。

rb_ary_subseq,如果起始索引beg大于数组长度alennil则返回:

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.