Распределение массивов в Ruby: объяснение нелогичного поведения (взято с Rubykoans.com)

Я проходил упражнения в Ruby Koans, и меня поразила следующая рубиновая причуда, которую я нашел очень необъяснимым:

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 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

Ответ 1

Нарезка и индексирование - это две разные операции, и выведение поведения одного из другого - вот где ваша проблема.

Первый аргумент в срезе идентифицирует не элемент, а места между элементами, определяющие пролеты (а не сами элементы):

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

4 все еще находится внутри массива, едва ли; если вы запросите 0 элементов, вы получите пустой конец массива. Но нет индекса 5, поэтому вы не можете нарезать оттуда.

Когда вы индексируете (например, array[4]), вы указываете сами элементы, поэтому индексы идут только от 0 до 3.

Ответ 2

Это связано с тем, что срез возвращает массив, соответствующую исходную документацию из массива # 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 возвращает новый массив, не изменяя исходную структуру данных.

EDIT:

После просмотра комментариев я решил отредактировать этот ответ. Slice вызывает следующий фрагмент кода, когда значение arg равно двум:

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 элементов, поэтому он возвращает nil до вычисления нуля arg. код здесь в строке 944.

Я считаю, что это ошибка или, по крайней мере, непредсказуемая, а не "Принцип наименьшего сюрприза". Когда я получу несколько минут, я по крайней мере отправлю неудачный патч для проверки на рубиновый ядро.

Ответ 3

По крайней мере, обратите внимание, что поведение согласовано. Начиная с 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 и выше, метод, вероятно, немедленно выйдет из-за того, что он полностью и полностью выходит за пределы.

Ответ 4

Это имеет смысл, если вы считаете, что срез массива может быть допустимым lvalue, а не только rvalue:

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 элементов имеет смысл, но вставка после 5-го элемента массива из 4 элементов не является).

Прочитайте синтаксис фрагмента array[x,y] как "начиная с x элементов в array, выберите до y элементов". Это имеет смысл только в том случае, если array имеет не менее x элементов.

Ответ 5

Это имеет смысл

Вы должны иметь возможность назначать эти срезы, поэтому они определяются таким образом, что начало и конец строки имеют рабочие выражения с нулевой длиной.

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

Ответ 6

Я согласен, что это похоже на странное поведение, но даже официальная документация на 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, похоже, не дает никакого представления о том, почему оно работает таким образом:

Элемент Reference - возвращает элемент по индексу или возвращает подмассив, начинающийся с начала и продолжающийся для элементов длины, или возвращает субарей, заданный диапазоном. Отрицательные индексы отсчитываются назад от конца массива (-1 - последний элемент). Возвращает nil, если индекс (или начальный индекс) выходит за пределы диапазона.

Ответ 7

Я нашел объяснение Гари Райт очень полезным. 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

два целых аргумента: аргументы идентифицируют часть строки для извлечь или заменить. В частности, части нулевой ширины строки также могут быть идентифицированы так, чтобы текст мог быть вставлен до или после существующие символы, включая фронт или конец строки. В этом case, первый аргумент не идентифицирует позицию символа, но вместо этого определяет пространство между символами, как показано на диаграмме выше. Второй аргумент - длина, которая может быть равна 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 для моделирования фактического поведения.

Ответ 8

Объяснение, данное Джим Вейрихом

Один из способов подумать о том, что позиция индекса 4 находится на самом краю массива. Когда вы запрашиваете фрагмент, вы возвращаете столько массив, который оставлен. Поэтому рассмотрим массив [2,10], массив [3,10] и array [4,10]... каждый возвращает оставшиеся биты конца array: 2 элемента, 1 элемент и 0 элементов соответственно. Однако, позиция 5 явно находится вне массива, а не на краю, поэтому массив [5,10] возвращает ноль.

Ответ 9

Рассмотрим следующий массив:

>> 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/.