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 ??
Odpowiedzi:
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):
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.źródło
ma to związek z faktem, że plaster zwraca tablicę, odpowiednią dokumentację źródłową z Array # slice:
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 gdyarray[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:
jeśli spojrzysz na
array.c
klasę, w którejrb_ary_subseq
metoda jest zdefiniowana, zobaczysz, że zwraca zero, jeśli długość jest poza zakresem, a nie indeks: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.
źródło
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.
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 kontekstu4
jest 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.źródło
Ma to sens, gdy weźmiesz pod uwagę, że wycinek tablicy może być prawidłową wartością, a nie tylko wartością:
Nie byłoby to możliwe, gdyby
array[4,0]
zwrócononil
zamiast[]
.array[5,0]
Zwraca jednak,nil
ponieważ 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 odx
elementówarray
, wybierz doy
elementów”. Ma to sens tylko wtedy, gdyarray
ma przynajmniejx
elementy.źródło
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.
źródło
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]
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:
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.
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.
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:
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.
źródło
Zgadzam się, że to wydaje się dziwne zachowanie, ale nawet oficjalna dokumentacja
Array#slice
wykazuje takie samo zachowanie, jak w twoim przykładzie, w „szczególnych przypadkach” poniżej:Niestety, nawet ich opis
Array#slice
nie wydaje się zapewniać wglądu, dlaczego działa w ten sposób:źródło
Wyjaśnienie przedstawione przez Jima Weiricha
źródło
Rozważ następującą tablicę:
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żyja[1,0]
. Zasadniczo, w notacjia[i,n]
,i
reprezentuje indeks in
liczbę elementów. Kiedyn=0
okreś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ępa[4,0]
, otrzymasznil
jako 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/ .
źródło
tl; dr: w kodzie źródłowym w
array.c
wywoływane są różne funkcje w zależności od tego, czy przekażesz 1 czy 2 argumenty w celuArray#slice
uzyskania 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_entry
wywoływana jest wartość indeksu (która może być dodatnia lub ujemna).rb_ary_entry
nastę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łujerb_ary_elt
żądany element.Zgodnie z oczekiwaniami
rb_ary_elt
zwraca,nil
gdy długość tablicylen
jest mniejsza lub równa indeksowi (tutaj nazywaneoffset
).Scenariusz nr 2
array.length == 4; array.slice(4, 0) #=> []
Jednak gdy przekazywane są 2 argumenty (tzn. Indeks początkowy
beg
i długość wycinkalen
),rb_ary_subseq
wywoływane jest.W
rb_ary_subseq
, jeśli indeks początkowybeg
jest większy niż długość tablicyalen
,nil
zwracane jest:W przeciwnym razie
len
obliczana jest długość wynikowego wycinka , a jeśli zostanie ustalona na zero, zwracana jest pusta tablica:Ponieważ indeks początkowy 4 nie jest większy niż
array.length
, zwracana jest pusta tablica zamiastnil
oczekiwanej 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.
źródło