Разрезание массива в Ruby: объяснение нелогичного поведения (взято с Rubykoans.com) - PullRequest
228 голосов
/ 25 августа 2010

Я выполнял упражнения в 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'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 ]

178 голосов
/ 25 августа 2010

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

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

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

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

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

27 голосов
/ 26 августа 2010

это связано с тем, что 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

, которая подсказывает мне, что если вы дадите старт, выходящий за пределы, онвернет ноль, поэтому в вашем примере array[4,0] запрашивает 4-й элемент, который существует, но просит возвратить массив нулевых элементов.В то время как array[5,0] запрашивает индекс вне границ, он возвращает ноль.Возможно, это имеет больше смысла, если вы помните, что метод slice возвращает массив new , не изменяя исходную структуру данных.

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, вы увидите, что онвозвращает ноль, если длина выходит за пределы, а не индекс:

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

В этом случае это то, что происходит, когда передается 4, он проверяет, что есть 4 элемента и, следовательно, не вызываетноль возврат.Затем он продолжается и возвращает пустой массив, если второй аргумент установлен на ноль.в то время как если передано 5, в массиве нет 5 элементов, поэтому он возвращает nil до того, как будет вычислен нулевой аргумент.код здесь в строке 944.

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

23 голосов
/ 25 августа 2010

По крайней мере, обратите внимание, что поведение является последовательным.С 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 голосов
/ 01 февраля 2012

Это имеет смысл, если учесть, что срез массива может быть допустимым 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 элементов.

11 голосов
/ 07 сентября 2011

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

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

array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]
8 голосов
/ 25 сентября 2012

Мне также очень помогло объяснение Гари Райта. http://www.ruby -forum.com / тема / 1393096 # 990065

Ответ Гэри Райта -

http://www.ruby -doc.org / ядро ​​/ классы / 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 для моделирования реального поведения.

8 голосов
/ 25 августа 2010

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

7 голосов
/ 17 сентября 2011

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

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

6 голосов
/ 10 сентября 2012

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

>> 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 голосов
/ 14 февраля 2018

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#slice (rb_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 и длина среза 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, которое можно ожидать, возвращается пустой массив.

Вопрос ответил?

Если реальный вопрос здесь не «Какой код вызывает это?», А скорее «Почему Мац сделал это таким образом?», То вам просто нужно купить ему чашку кофе в следующий RubyConf и спроси его.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...