Wycinanie tablic w Ruby: wyjaśnienie nielogicznego zachowania (zaczerpnięte z Rubykoans.com)

232

Przechodziłem ćwiczenia w Ruby Koans i uderzyło mnie następujące dziwactwo Ruby, które okazało się naprawdę niewytłumaczalne:

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

Dlaczego więc array[5,0]nie jest równy array[4,0]? Czy istnieje jakikolwiek powód, dlaczego tablica krojenie zachowuje się dziwnie, kiedy to rozpocznie się w (długość + 1) th pozycji ??

Pascal Van Hecke
źródło
wygląda na to, że pierwsza liczba to indeks, od którego zaczyna się, druga liczba to liczba elementów do pokrojenia
austin

Odpowiedzi:

185

Krojenie i indeksowanie to dwie różne operacje, a wnioskowanie o zachowaniu jednej z nich leży w miejscu, w którym leży twój problem.

Pierwszy argument w plasterku identyfikuje nie element, ale miejsca między elementami, definiując zakresy (a nie same elementy):

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

4 wciąż znajduje się w tablicy, ledwo; jeśli zażądasz 0 elementów, otrzymasz pusty koniec tablicy. Ale nie ma indeksu 5, więc nie można stamtąd wyciąć.

Kiedy indeksujesz (jak array[4]), wskazujesz same elementy, więc indeksy zmieniają się tylko od 0 do 3.

Amadan
źródło
8
Dobre przypuszczenie, chyba że jest to potwierdzone przez źródło. Nie będąc wrednym, byłbym zainteresowany linkiem, który miałby wyjaśnić „dlaczego”, jak OP i inni komentatorzy. Twój schemat ma sens, z wyjątkiem tego, że Array [4] ma wartość zero. Tablica [3] to: galaretka. Spodziewałbym się, że Array [4, N] będzie zero, ale jest [], jak mówi OP. Jeśli jest to miejsce, jest to dość bezużyteczne miejsce, ponieważ Array [4, -1] jest zero. Nie możesz nic zrobić z Array [4].
squarism
5
@squarism Właśnie otrzymałem potwierdzenie od Charlesa Olivera Nuttera (@headius na Twitterze), że jest to prawidłowe wyjaśnienie. Jest znanym twórcą JRuby, więc uważam jego słowo za dość autorytatywne.
Hank Gay
18
Oto uzasadnienie tego zachowania: blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
Matt Briançon
4
Prawidłowe wyjaśnienie Podobne dyskusje na temat ruby-core: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune,
18
Nazywany także „ogrodzeniem”. Piąty słupek ogrodzeniowy (id 4) istnieje, ale piąty element nie. Krojenie jest operacją ogrodzenia, indeksowanie jest operacją elementu.
Matty K
27

ma to związek z faktem, że plaster zwraca tablicę, odpowiednią dokumentację źródłową z 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

co sugeruje mi, że jeśli dasz początek spoza zakresu, zwróci zero, dlatego w twoim przykładzie array[4,0]prosi o czwarty element, który istnieje, ale prosi o zwrócenie tablicy zerowych elementów. Podczas gdy array[5,0]prosi o indeks poza zakresem, więc zwraca zero. Być może ma to większy sens, jeśli pamiętasz, że metoda slice zwraca nową tablicę, a nie zmienia oryginalnej struktury danych.

EDYTOWAĆ:

Po przejrzeniu komentarzy postanowiłem edytować tę odpowiedź. Slice wywołuje następujący fragment kodu, gdy wartość argumentu wynosi dwa:

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);
}

jeśli spojrzysz na array.cklasę, w której rb_ary_subseqmetoda jest zdefiniowana, zobaczysz, że zwraca zero, jeśli długość jest poza zakresem, a nie indeks:

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

W tym przypadku dzieje się tak, gdy 4 jest przekazywane, sprawdza, czy są 4 elementy, a zatem nie wyzwala zerowego powrotu. Następnie kontynuuje i zwraca pustą tablicę, jeśli drugi argument jest ustawiony na zero. podczas gdy jeśli zostanie przekazane 5, w tablicy nie ma 5 elementów, więc zwraca zero przed obliczeniem zerowego arg. kod tutaj w linii 944.

Uważam, że to błąd, a przynajmniej nieprzewidywalny, a nie „Zasada najmniejszej niespodzianki”. Kiedy otrzymam kilka minut, przynajmniej prześlę nieudaną łatkę testową do ruby ​​core.

Jed Schneider
źródło
2
Ale ... element wskazany przez 4 w tablicy [4,0] też nie istnieje ... - ponieważ w rzeczywistości jest to element 5 (liczenie oparte na 0, patrz przykłady). Więc to także poza granicami.
Pascal Van Hecke,
1
masz rację. Wróciłem i spojrzałem na źródło, i wygląda na to, że pierwszy argument jest obsługiwany w kodzie c jako długość, a nie indeks. Przeredaguję swoją odpowiedź, aby to odzwierciedlić. Myślę, że można to zgłosić jako błąd.
Jed Schneider
23

Przynajmniej zauważ, że zachowanie jest spójne. Od 5 roku życia wszystko działa tak samo; dziwność występuje tylko w [4,N].

Może ten wzór pomaga, a może jestem po prostu zmęczony i wcale nie pomaga.

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] => []

W [4,0]łapiemy koniec tablicy. Wydaje mi się, że to dość dziwne, jeśli chodzi o piękno we wzorach, jeśli ostatni powrócił nil. Z powodu takiego kontekstu 4jest dopuszczalną opcją dla pierwszego parametru, aby można było zwrócić pustą tablicę. Gdy jednak osiągniemy 5 i więcej, metoda prawdopodobnie kończy się natychmiast ze względu na to, że jest całkowicie i całkowicie poza zasięgiem.

Matchu
źródło
12

Ma to sens, gdy weźmiesz pod uwagę, że wycinek tablicy może być prawidłową wartością, a nie tylko wartością:

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]

Nie byłoby to możliwe, gdyby array[4,0]zwrócono nilzamiast []. array[5,0]Zwraca jednak, nilponieważ jest poza zakresem (wstawianie po 4-tym elemencie tablicy 4-elementowej jest znaczące, ale wstawianie po 5-tym elemencie tablicy 4-elementowej nie ma znaczenia).

Przeczytaj składnię plastra array[x,y]jako „zaczynając od xelementów array, wybierz do yelementów”. Ma to sens tylko wtedy, gdy arrayma przynajmniej xelementy.

Frank Szczerba
źródło
11

To ma sens

Musisz mieć możliwość przypisania do tych wycinków, aby były one zdefiniowane w taki sposób, że początek i koniec łańcucha mają działające wyrażenia o zerowej długości.

array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]
DigitalRoss
źródło
1
Możesz także przypisać do zakresu, który plasterek zwraca jako zero, więc przydatne byłoby rozwinięcie tego objaśnienia. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas,
co robi drugi numer podczas przypisywania? wydaje się być ignorowane. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee,
@drewverlee nie jest ignorowane:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen
10

Bardzo pomocne okazało się też wyjaśnienie Gary'ego Wrighta. http://www.ruby-forum.com/topic/1393096#990065

Odpowiedź Gary'ego Wrighta brzmi -

http://www.ruby-doc.org/core/classes/Array.html

Dokumenty z pewnością mogłyby być bardziej jasne, ale faktyczne zachowanie jest spójne i przydatne. Uwaga: zakładam, że wersja String.X. 1.9.X.

Pomaga rozważyć numerację w następujący sposób:

  -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

Częstym (i zrozumiałym) błędem jest zbyt duże założenie, że semantyka indeksu pojedynczego argumentu jest taka sama jak semantyka pierwszego argumentu w scenariuszu (lub zakresie) dwóch argumentów. W praktyce to nie to samo, a dokumentacja tego nie odzwierciedla. Błąd jest jednak zdecydowanie w dokumentacji, a nie w implementacji:

pojedynczy argument: indeks reprezentuje pozycję pojedynczego znaku w ciągu. Wynikiem jest albo pojedynczy ciąg znaków znaleziony w indeksie, albo zero, ponieważ w danym indeksie nie ma znaku.

  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

dwa argumenty całkowite: argumenty identyfikują część ciągu do wyodrębnienia lub zamiany. W szczególności można również zidentyfikować części łańcucha o zerowej szerokości, dzięki czemu tekst można wstawić przed lub po istniejących znakach, w tym na początku lub na końcu łańcucha. W takim przypadku pierwszy argument nie identyfikuje pozycji znaku, ale zamiast tego określa odstęp między znakami, jak pokazano na powyższym schemacie. Drugi argument to długość, która może wynosić 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"

Zachowanie zakresu jest dość interesujące. Punkt początkowy jest taki sam jak pierwszy argument, gdy podano dwa argumenty (jak opisano powyżej), ale punktem końcowym zakresu może być „pozycja znaku” jak w przypadku pojedynczego indeksowania lub „pozycja krawędzi” jak w przypadku dwóch argumentów liczb całkowitych. Różnica zależy od tego, czy stosowany jest zakres podwójnych kropek, czy potrójny:

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.

Jeśli wrócisz do tych przykładów i nalegasz na użycie semantyki pojedynczego indeksu dla przykładów indeksowania podwójnego lub zakresu, po prostu się pomylisz. Musisz użyć alternatywnej numeracji, którą pokazuję na schemacie ascii, aby modelować rzeczywiste zachowanie.

wigor
źródło
3
Czy możesz podać główną ideę tego wątku? (w przypadku linku jeden dzień traci ważność)
VonC
8

Zgadzam się, że to wydaje się dziwne zachowanie, ale nawet oficjalna dokumentacjaArray#slice wykazuje takie samo zachowanie, jak w twoim przykładzie, w „szczególnych przypadkach” poniżej:

   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]               #=> []

Niestety, nawet ich opis Array#slicenie wydaje się zapewniać wglądu, dlaczego działa w ten sposób:

Odwołanie do elementu - zwraca element pod indeksem lub zwraca podtablicę rozpoczynającą się na początku i kontynuującą dla elementów długości lub zwraca podtablicę określoną przez zakres . Wskaźniki ujemne liczą się wstecz od końca tablicy (-1 to ostatni element). Zwraca zero, jeśli indeks (lub indeks początkowy) jest poza zakresem.

Mark Rushakoff
źródło
7

Wyjaśnienie przedstawione przez Jima Weiricha

Jednym ze sposobów myślenia o tym jest to, że pozycja indeksu 4 znajduje się na samej krawędzi tablicy. Prosząc o plasterek, zwracasz tyle pozostałej tablicy, ile pozostało. Rozważmy więc tablicę [2,10], tablicę [3,10] i tablicę [4,10] ... każdy zwraca pozostałe bity końca tablicy: odpowiednio 2 elementy, 1 element i 0 elementów. Jednak pozycja 5 jest wyraźnie poza tablicą, a nie na krawędzi, więc tablica [5,10] zwraca zero.

suvankar
źródło
6

Rozważ następującą tablicę:

>> array=["a","b","c"]
=> ["a", "b", "c"]

Możesz wstawić element na początek (głowę) tablicy, przypisując go do a[0,0]. Aby umieścić element między "a"i "b", użyj a[1,0]. Zasadniczo, w notacji a[i,n], ireprezentuje indeks i nliczbę elementów. Kiedy n=0określa pozycję między elementami tablicy.

Teraz, jeśli myślisz o końcu tablicy, jak możesz dołączyć element do jego końca za pomocą opisanej powyżej notacji? Proste, przypisz wartość do a[3,0]. To jest ogon tablicy.

Tak więc, jeśli spróbujesz uzyskać dostęp do elementu w a[3,0], otrzymasz []. W takim przypadku nadal znajdujesz się w zasięgu tablicy. Ale jeśli spróbujesz uzyskać dostęp a[4,0], otrzymasz niljako wartość zwracaną, ponieważ nie jesteś już w zasięgu tablicy.

Przeczytaj więcej na ten temat na http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .

Tairon
źródło
0

tl; dr: w kodzie źródłowym w array.cwywoływane są różne funkcje w zależności od tego, czy przekażesz 1 czy 2 argumenty w celu Array#sliceuzyskania nieoczekiwanych wartości zwracanych.

(Po pierwsze, chciałbym zauważyć, że nie koduję w C, ale używam Ruby od lat. Więc jeśli nie znasz C, ale poświęcasz kilka minut na zapoznanie się z podstawami funkcji i zmiennych, tak naprawdę nie jest tak trudno podążać za kodem źródłowym Ruby, jak pokazano poniżej. Ta odpowiedź jest oparta na Ruby v2.3, ale jest mniej więcej taka sama jak w wersji v1.9.)

Scenariusz nr 1

array.length == 4; array.slice(4) #=> nil

Jeśli spojrzysz na kod źródłowy dla Array#slice( rb_ary_aref), zobaczysz, że gdy przekazywany jest tylko jeden argument ( linie 1277-1289 ), rb_ary_entrywywoływana jest wartość indeksu (która może być dodatnia lub ujemna).

rb_ary_entrynastępnie oblicza pozycję żądanego elementu od początku tablicy (innymi słowy, jeśli przekazany jest indeks ujemny, oblicza dodatni ekwiwalent), a następnie wywołuje rb_ary_eltżądany element.

Zgodnie z oczekiwaniami rb_ary_eltzwraca, nilgdy długość tablicy lenjest mniejsza lub równa indeksowi (tutaj nazywane offset).

1189:  if (offset < 0 || len <= offset) {
1190:    return Qnil;
1191:  } 

Scenariusz nr 2

array.length == 4; array.slice(4, 0) #=> []

Jednak gdy przekazywane są 2 argumenty (tzn. Indeks początkowy begi długość wycinka len), rb_ary_subseqwywoływane jest.

W rb_ary_subseq, jeśli indeks początkowy begjest większy niż długość tablicy alen, nilzwracane jest:

1208:  long alen = RARRAY_LEN(ary);
1209:
1210:  if (beg > alen) return Qnil;

W przeciwnym razie lenobliczana jest długość wynikowego wycinka , a jeśli zostanie ustalona na zero, zwracana jest pusta tablica:

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

Ponieważ indeks początkowy 4 nie jest większy niż array.length, zwracana jest pusta tablica zamiast niloczekiwanej wartości.

Odpowiedzi na pytanie

Jeśli pytanie nie brzmi „Jaki kod powoduje, że tak się dzieje?”, A raczej „Dlaczego Matz zrobił to w ten sposób?”, To po prostu musisz kupić mu filiżankę kawy na następnym RubyConf i Zapytaj go.

Scott Schupbach
źródło