Нарізка масиву в Ruby: пояснення нелогічної поведінки (взято з Rubykoans.com)


232

Я проходив вправи в Рубі-Коансі, і мене вразила наступна примха Рубі, яку я вважав дійсно незрозумілою:

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) й позиції ??



виглядає, що перше число - це індекс, з якого потрібно починати, друге число - скільки елементів потрібно нарізати
austin

Відповіді:


185

Нарізання та індексація - це дві різні операції, і випливати з поведінки однієї з іншої - це ваша проблема.

Перший аргумент у фрагменті ідентифікує не елемент, а місця між елементами, визначаючи проміжки (а не самі елементи):

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

4 все ще знаходиться в масиві, ледь-ледь; якщо ви запитаєте 0 елементів, ви отримуєте порожній кінець масиву. Але немає індексу 5, тому ви не можете звідти відрізати.

Коли ви робите індекс (як array[4]), ви вказуєте на самі елементи, тому індекси йдуть лише від 0 до 3.


8
Хороша здогадка, якщо це не підкріплено джерелом. Я не зацікавлений, я зацікавився б посиланням, якщо якесь саме пояснити "чому", як запитують ОП та інші коментатори. Ваша діаграма має сенс, за винятком того, що масив [4] є нульовим. Масив [3] - це: желе. Я б очікував, що масив [4, N] буде нульовим, але це [], як каже ОП. Якщо це місце, це досить марне місце, оскільки масив [4, -1] є нульовим. Таким чином, ви не можете нічого зробити з Array [4].
скваризм

5
@squarism Я щойно отримав підтвердження від Чарльза Олівера Нуттера (@headius у Twitter), що це правильне пояснення. Він великий розвід JRuby, тому я вважаю його слово досить авторитетним.
Генк Гей

18
Далі є обґрунтування такої поведінки: blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
Метт Бріансон

4
Правильне пояснення. Подібні дискусії на ruby-core: redmine.ruby-lang.org/isissue/4245 , redmine.ruby-lang.org/isissue/4541
Marc-André Lafortune

18
Також називається "огорожа-розміщення". П'ятий стовп огорожі (id 4) існує, але п’ятий елемент - ні. Нарізка - це операція огорожі, індексація - це елемент операції.
Matty K

27

це пов'язано з тим, що фрагмент повертає масив, відповідну вихідну документацію з масиву # фрагмент:

 *  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]просить індекс вийти за межі, щоб він повернув нуль. Це, можливо, має більше сенсу, якщо ви пам’ятаєте, що метод зрізу повертає новий масив, не змінюючи початкову структуру даних.

Редагувати:

Переглянувши коментарі, я вирішив відредагувати цю відповідь. Фрагмент викликає такий фрагмент коду, коли значення 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 елементів, тож він повертає нуль до того, як буде оцінено нульовий аргумент. код тут у рядку 944.

Я вважаю, що це помилка, або, принаймні, непередбачувана, а не «Принцип найменшого сюрпризу». Коли я отримаю декілька хвилин, я принаймні надішлю невдалий тест-патч на рубінове ядро.


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 і вище, метод, ймовірно, негайно виходить із-за того, що він повністю і повністю вийшов за межі.


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]

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

Документи, безумовно, можуть бути більш зрозумілими, але фактична поведінка є послідовною та корисною. Примітка. Я припускаю версію String 1.9.X.

Це допомагає розглянути нумерацію наступним чином:

  -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 явно знаходиться поза масивом, а не на краю, тому масив [5,10] повертає нуль.


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#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 і Запитайте його.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.