Разрезание массива в 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]? Есть ли причина, почему нарезка массива ведет себя так странно, когда вы начинаете с (длина + 1) позиции ??

Паскаль Ван Хекке
источник
похоже, первое число - это индекс, с которого нужно начинать, второе число - сколько элементов нарезать
Остин

Ответы:

185

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

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

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

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

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

Amadan
источник
8
Хорошая догадка, если это не подтверждено источником. Не будь язвительным, я был бы заинтересован в ссылке, если таковая имеется, просто чтобы объяснить «почему», как спрашивают ОП и другие комментаторы. Ваша диаграмма имеет смысл, за исключением того, что Array [4] равен нулю. Массив [3]: желе. Я ожидаю, что Array [4, N] будет нулевым, но это, как говорит OP. Если это место, это довольно бесполезное место, потому что Array [4, -1] равен нулю. Так что вы ничего не можете сделать с Array [4].
Квадрат
5
@squarism Я только что получил подтверждение от Чарльза Оливера Наттера (@headius в Twitter), что это правильное объяснение. Он выдающийся JRuby dev, так что я считаю его слово достаточно авторитетным.
Хэнк Гей
18
Ниже приводится обоснование такого поведения: blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
Мэтт Бриансон,
4
Правильное объяснение. Подобные обсуждения на ruby-core: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Марк-Андре Лафортун
18
Также упоминается как «пост забора». Пятый забор (id 4) существует, а пятый элемент - нет. Нарезка - это операция с ограждением, индексация - это операция элемента.
Мэтти К
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

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

РЕДАКТИРОВАТЬ:

После просмотра комментариев я решил отредактировать этот ответ. 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. Затем он продолжается и возвращает пустой массив, если второй аргумент равен нулю. в то время как если передано 5, в массиве нет 5 элементов, поэтому он возвращает nil до того, как будет вычислен нулевой аргумент. код здесь в строке 944.

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

Джед Шнайдер
источник
2
Но ... элемент, обозначенный 4 в массиве [4,0], тоже не существует ... - потому что на самом деле это элемент 5 (отсчет на основе 0, см. Примеры). Так что это тоже за гранью.
Паскаль Ван Хекке
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 и выше, метод, вероятно, сразу же выйдет из-за того, что он полностью и полностью вышел за пределы.

Matchu
источник
12

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

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

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

array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]
DigitalRoss
источник
1
Вы также можете присвоить диапазону этот срез, который возвращается как ноль, поэтому было бы полезно расширить это объяснение. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas
что делает второй номер при назначении? кажется, игнорируется. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Дрю Верли
@drewverlee это не игнорируется:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen
10

Мне также очень помогло объяснение Гари Райта. 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, чтобы смоделировать реальное поведение.

напор
источник
3
Можете ли вы включить основную идею этой темы? (в случае ссылки один день становится недействительным)
VonC
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, кажется, не дает никакого представления о том, почему это работает так:

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

Марк Рушаков
источник
7

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

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

suvankar
источник
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/ .

Tairone
источник
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

Если вы посмотрите на исходный код for 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 и Спроси его.

Скотт Шупбах
источник