Я выполнял упражнения в 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) -й позиции ??
Ответы:
Нарезка и индексация - это две разные операции, и вывести поведение одной из другой - вот где ваша проблема.
Первый аргумент в slice определяет не элемент, а места между элементами, определяя промежутки (а не сами элементы):
4 все еще в массиве, только едва; если вы запросите 0 элементов, вы получите пустой конец массива. Но нет индекса 5, поэтому вы не можете нарезать оттуда.
Когда вы делаете индекс (например
array[4]
), вы указываете на сами элементы, поэтому индексы идут только от 0 до 3.источник
это связано с тем, что slice возвращает массив, соответствующую исходную документацию из Array # slice:
что подсказывает мне, что если вы дадите старт, который находится за пределами, он вернет ноль, поэтому в вашем примере
array[4,0]
запрашивает 4-й элемент, который существует, но просит вернуть массив из нулевых элементов. В то время какarray[5,0]
запрашивает индекс вне границ, поэтому он возвращает ноль. Возможно, это имеет больше смысла, если вы помните, что метод slice возвращает новый массив, а не изменяет исходную структуру данных.РЕДАКТИРОВАТЬ:
После просмотра комментариев я решил отредактировать этот ответ. Slice вызывает следующий фрагмент кода, когда значение arg равно двум:
если вы посмотрите в
array.c
классе, гдеrb_ary_subseq
метод определен, вы увидите, что он возвращает nil, если длина выходит за границы, а не индекс:В этом случае это то, что происходит, когда передается 4, он проверяет, что есть 4 элемента и, следовательно, не запускает возврат nil. Затем он продолжается и возвращает пустой массив, если второй аргумент равен нулю. в то время как если передано 5, в массиве нет 5 элементов, поэтому он возвращает nil до того, как будет вычислен нулевой аргумент. код здесь в строке 944.
Я считаю, что это ошибка, или, по крайней мере, непредсказуемая, а не «Принцип наименьшего сюрприза». Когда у меня будет несколько минут, я по крайней мере отправлю неудачный тестовый патч на ядро ruby.
источник
По крайней мере, обратите внимание, что поведение является последовательным. С 5 и выше все действует одинаково; странность происходит только в
[4,N]
.Может быть, этот шаблон помогает, или, может быть, я просто устал, и это не помогает вообще.
В
[4,0]
, мы ловим конец массива. Я бы на самом деле счел это довольно странным с точки зрения красоты в шаблонах, если бы последний вернулсяnil
. Из-за контекста, подобного этому,4
приемлемый параметр для первого параметра позволяет возвращать пустой массив. Как только мы достигнем 5 и выше, метод, вероятно, сразу же выйдет из-за того, что он полностью и полностью вышел за пределы.источник
Это имеет смысл, если учесть, что срез массива может быть допустимым значением lvalue, а не просто значением rvalue:
Это не было бы возможно , если
array[4,0]
возвращеноnil
вместо[]
. Однакоarray[5,0]
возвращает,nil
потому что он находится за пределами (вставка после 4-го элемента 4-элементного массива имеет смысл, но вставка после 5-го элемента 4-элементного массива - нет).Прочитайте синтаксис среза
array[x,y]
как «начиная сx
элементов вarray
, выберите доy
элементов». Это имеет смысл только еслиarray
имеет хотя быx
элементы.источник
Это имеет смысл
Вы должны быть в состоянии назначить этим срезам, чтобы они были определены таким образом, чтобы начало и конец строки имели рабочие выражения нулевой длины.
источник
array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
[26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
Мне также очень помогло объяснение Гари Райта. http://www.ruby-forum.com/topic/1393096#990065
Ответ Гари Райта -
http://www.ruby-doc.org/core/classes/Array.html
Документы, конечно, могут быть более понятными, но фактическое поведение является последовательным и полезным. Примечание: я предполагаю, что 1.9.X версия String.
Это помогает рассмотреть нумерацию следующим образом:
Распространенной (и понятной) ошибкой также является допущение, что семантика индекса с одним аргументом совпадает с семантикой первого аргумента в сценарии с двумя аргументами (или в диапазоне). На практике это не одно и то же, и в документации это не отражено. Ошибка определенно есть в документации, а не в реализации:
единственный аргумент: индекс представляет позицию одного символа в строке. Результатом является либо одиночная символьная строка, найденная в индексе, либо ноль, потому что в данном индексе нет символа.
два целочисленных аргумента: аргументы идентифицируют часть строки для извлечения или замены. В частности, части строки нулевой ширины также могут быть идентифицированы, так что текст может быть вставлен до или после существующих символов, в том числе в начале или конце строки. В этом случае первый аргумент не идентифицирует позицию символа, а вместо этого определяет пространство между символами, как показано на диаграмме выше. Второй аргумент - это длина, которая может быть 0.
Поведение диапазона довольно интересно. Начальная точка совпадает с первым аргументом, когда предоставляются два аргумента (как описано выше), но конечной точкой диапазона может быть «позиция символа», как при одиночном индексировании, или «позиция края», как с двумя целочисленными аргументами. Разница определяется тем, используется ли диапазон с двумя точками или диапазон с тремя точками:
Если вы вернетесь к этим примерам и будете настаивать на использовании семантики единого индекса для примеров двойного или диапазона индексации, вы просто запутаетесь. Вы должны использовать альтернативную нумерацию, которую я показываю на диаграмме ascii, чтобы смоделировать реальное поведение.
источник
Я согласен, что это кажется странным поведением, но даже официальная документация
Array#slice
демонстрирует то же поведение, что и в вашем примере, в «особых случаях» ниже:К сожалению, даже их описание
Array#slice
, кажется, не дает никакого представления о том, почему это работает так:источник
Объяснение, предоставленное Джимом Вейрихом
источник
Рассмотрим следующий массив:
Вы можете вставить элемент в начало (начало) массива, назначив его
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/ .
источник
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
Сценарий № 2
array.length == 4; array.slice(4, 0) #=> []
Однако, когда передаются 2 аргумента (т.е. начальный индекс
beg
и длина срезаlen
),rb_ary_subseq
вызывается.В
rb_ary_subseq
, если начальный индексbeg
является больше , чем длина массиваalen
,nil
возвращается:В противном случае
len
вычисляется длина полученного среза , и если он определен равным нулю, возвращается пустой массив:Таким образом, поскольку начальный индекс 4 не больше чем
array.length
, вместоnil
значения, которое можно ожидать, возвращается пустой массив .Вопрос ответил?
Если реальный вопрос здесь не «Какой код вызывает это?», А «Почему Мац сделал это таким образом?», То вам просто нужно купить ему чашку кофе на следующем RubyConf и Спроси его.
источник