Taglio di array in Ruby: spiegazione del comportamento illogico (tratto da Rubykoans.com)


232

Stavo attraversando gli esercizi in Ruby Koans e sono stato colpito dal seguente stranezza di Ruby che ho trovato davvero inspiegabile:

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

Quindi perché array[5,0]non è uguale a array[4,0]? C'è qualche motivo per cui gamma affettare si comporta in questo strano quando si inizia a (lunghezza + 1) esima posizione ??



sembra che il primo numero sia l'indice da cui iniziare, il secondo numero è quanti elementi tagliare
austin,

Risposte:


185

L'affettatura e l'indicizzazione sono due operazioni diverse, e inferire il comportamento dell'una dall'altra è il problema.

Il primo argomento in slice identifica non l'elemento ma i punti tra gli elementi, definendo gli span (e non gli elementi stessi):

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

4 è ancora all'interno dell'array, appena; se si richiedono 0 elementi, si ottiene la fine vuota dell'array. Ma non esiste un indice 5, quindi non puoi tagliare da lì.

Quando fai un indice (come array[4]), stai indicando gli elementi stessi, quindi gli indici vanno solo da 0 a 3.


8
Una buona ipotesi a meno che questo non sia supportato dalla fonte. Non essendo snarky, sarei interessato a un link se solo per spiegare il "perché" come l'OP e altri commentatori stanno chiedendo. Il diagramma ha senso tranne che Array [4] è nullo. La matrice [3] è: gelatina. Mi aspetto che Array [4, N] sia zero, ma è [] come dice l'OP. Se è un posto, è un posto piuttosto inutile perché Array [4, -1] è zero. Quindi non puoi fare nulla con Array [4].
squarismo,

5
@squarism Ho appena ricevuto conferma da Charles Oliver Nutter (@headius su Twitter) che questa è la spiegazione corretta. È un grande appassionato di JRuby, quindi considererei la sua parola piuttosto autorevole.
Hank Gay,

18
Di seguito è la giustificazione per questo comportamento: blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
Matt Briançon

4
Spiegazione corretta. Discussioni simili su ruby-core: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune

18
Chiamato anche "distacco di recinzione". Il quinto recinto-palo (id 4) esiste, ma il quinto elemento no. L'affettatura è un'operazione trave, l'indicizzazione è un'operazione di elemento.
Matty K,

27

questo ha a che fare con il fatto che slice restituisce un array, relativa documentazione di origine da 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

il che mi suggerisce che se si dà l'inizio che è fuori dai limiti, esso restituirà zero, quindi nel tuo esempio array[4,0]chiede il 4 ° elemento esistente, ma chiede di restituire una matrice di zero elementi. Mentre array[5,0]chiede un indice fuori limite, quindi restituisce zero. Questo forse ha più senso se ricordi che il metodo slice sta restituendo un nuovo array, senza alterare la struttura dei dati originale.

MODIFICARE:

Dopo aver esaminato i commenti, ho deciso di modificare questa risposta. Slice chiama quanto segue frammento di codice quando il valore arg è due:

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

se guardi nel array.c classe in cui rb_ary_subseqè definito il metodo, vedi che restituisce zero se la lunghezza è fuori dai limiti, non l'indice:

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

In questo caso, questo è ciò che accade quando viene passato 4, verifica che ci siano 4 elementi e quindi non attiva il ritorno zero. Quindi continua e restituisce un array vuoto se il secondo arg è impostato su zero. mentre se viene passato 5, non ci sono 5 elementi nella matrice, quindi restituisce zero prima che venga valutata l'arg zero. codice qui alla riga 944.

Credo che questo sia un bug, o almeno imprevedibile e non il "Principio della minima sorpresa". Quando avrò qualche minuto, almeno invierò una patch di test non riuscita a ruby ​​core.


2
Ma ... l'elemento indicato dal 4 nell'array [4,0] non esiste neanche ... - perché in realtà è l'elemento 5the (conteggio basato su 0, vedere gli esempi). Quindi è anche fuori limite.
Pascal Van Hecke,

1
hai ragione. Sono tornato indietro e ho guardato l'origine, e sembra che il primo argomento sia gestito nel codice c come la lunghezza, non l'indice. Modificherò la mia risposta, per riflettere questo. Penso che questo potrebbe essere presentato come un bug.
Jed Schneider,

23

Almeno notare che il comportamento è coerente. Da 5 in poi tutto si comporta allo stesso modo; la stranezza si verifica solo a [4,N].

Forse questo schema aiuta, o forse sono solo stanco e non aiuta affatto.

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

A [4,0], prendiamo la fine dell'array. In realtà lo troverei piuttosto strano, per quanto riguarda la bellezza nei modelli, se l'ultimo tornasse nil. A causa di un contesto come questo, 4è un'opzione accettabile per il primo parametro in modo che l'array vuoto possa essere restituito. Una volta che abbiamo raggiunto il 5 in su, tuttavia, il metodo probabilmente esce immediatamente per natura di essere totalmente e completamente fuori limite.


12

Ciò ha senso se si considera che una porzione di array può essere un valore valido, non solo un valore:

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]

Questo non sarebbe possibile se array[4,0]restituito nilanziché []. Tuttavia, array[5,0]restituisce nilperché non ha limiti (l'inserimento dopo il 4 ° elemento di un array di 4 elementi è significativo, ma l'inserimento dopo il 5 ° elemento di un array di 4 elementi non lo è).

Leggi la sintassi della sezione array[x,y]come "iniziando dopo gli xelementi in array, seleziona fino agli yelementi". Questo ha senso solo se arrayha almeno xelementi.


11

Questo ha senso

Devi essere in grado di assegnare a tali sezioni, in modo che siano definite in modo tale che l'inizio e la fine della stringa abbiano espressioni di lunghezza zero funzionanti.

array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]

1
È inoltre possibile assegnare all'intervallo quella sezione che restituisce come zero, quindi sarebbe utile espandere questa spiegazione. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas,

cosa fa il secondo numero durante l'assegnazione? sembra essere ignorato. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Ha disegnato Verlee il

@drewverlee non viene ignorato:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen

10

Ho trovato molto utile anche la spiegazione di Gary Wright. http://www.ruby-forum.com/topic/1393096#990065

La risposta di Gary Wright è:

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

I documenti certamente potrebbero essere più chiari, ma il comportamento effettivo è coerente e utile. Nota: presumo la versione 1.9.X di String.

Aiuta a considerare la numerazione nel modo seguente:

  -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

L'errore comune (e comprensibile) è anche supporre che la semantica dell'indice a singolo argomento sia la stessa della semantica del primo argomento nello scenario (o intervallo) a due argomenti. Non sono la stessa cosa in pratica e la documentazione non riflette questo. L'errore però è sicuramente nella documentazione e non nell'implementazione:

argomento singolo: l'indice rappresenta una posizione di singolo carattere all'interno della stringa. Il risultato è la stringa di singolo carattere trovata nell'indice o nulla perché non è presente alcun carattere nell'indice specificato.

  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

due argomenti interi: gli argomenti identificano una parte della stringa da estrarre o sostituire. In particolare, è anche possibile identificare parti della stringa di larghezza zero in modo che il testo possa essere inserito prima o dopo i caratteri esistenti, compresi all'inizio o alla fine della stringa. In questo caso, il primo argomento non identifica una posizione di carattere ma identifica invece lo spazio tra i caratteri come mostrato nel diagramma sopra. Il secondo argomento è la lunghezza, che può essere 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"

Il comportamento di un intervallo è piuttosto interessante. Il punto iniziale è lo stesso del primo argomento quando vengono forniti due argomenti (come descritto sopra) ma il punto finale dell'intervallo può essere la 'posizione del carattere' come con l'indicizzazione singola o la "posizione del bordo" come con due argomenti interi. La differenza è determinata dall'uso dell'intervallo di punti doppi o di punti tripli:

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.

Se ripercorri questi esempi e insisti e usi la semantica dell'indice singolo per gli esempi di indicizzazione a doppio o intervallo, ti confonderai. Devi usare la numerazione alternativa che mostro nel diagramma ascii per modellare il comportamento reale.


3
Puoi includere l'idea principale di quella discussione? (nel caso in cui il collegamento un giorno non sia più valido)
VonC

8

Sono d'accordo che questo sembra un comportamento strano, ma anche la documentazione ufficialeArray#slice mostra lo stesso comportamento del tuo esempio, nei "casi speciali" di seguito:

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

Sfortunatamente, anche la loro descrizione Array#slicenon sembra offrire alcuna idea del perché funzioni in questo modo:

Elemento di riferimento-Restituisce l'elemento in corrispondenza dell'indice , o ritorna una sottomatrice a partire da inizio e continua per la lunghezza elementi, o restituisce un sottoarray specificato dal campo . Gli indici negativi contano all'indietro dalla fine dell'array (-1 è l'ultimo elemento). Restituisce nullo se l'indice (o l'indice iniziale) non è compreso nell'intervallo.


7

Una spiegazione fornita da Jim Weirich

Un modo di pensarci è che la posizione dell'indice 4 è al limite dell'array. Quando si richiede una sezione, si restituisce la maggior parte dell'array rimasto. Quindi considera l'array [2,10], l'array [3,10] e l'array [4,10] ... ognuno restituisce i bit rimanenti della fine dell'array: 2 elementi, 1 elemento e 0 elementi rispettivamente. Tuttavia, la posizione 5 è chiaramente al di fuori dell'array e non al limite, quindi l'array [5,10] restituisce zero.


6

Considera il seguente array:

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

È possibile inserire un elemento all'inizio (testa) dell'array assegnandolo a a[0,0]. Per inserire l'elemento tra "a"e "b", utilizzare a[1,0]. Fondamentalmente, nella notazione a[i,n], irappresenta un indice e nun numero di elementi. Quando n=0, definisce una posizione tra gli elementi dell'array.

Ora, se pensi alla fine della matrice, come puoi aggiungere un elemento alla sua fine usando la notazione sopra descritta? Semplice, assegna il valore a a[3,0]. Questa è la coda dell'array.

Quindi, se provi ad accedere all'elemento su a[3,0], otterrai []. In questo caso ci si trova ancora nell'intervallo dell'array. Ma se provi ad accedere a[4,0], otterrai nilcome valore di ritorno, dal momento che non sei più all'interno dell'intervallo dell'array.

Maggiori informazioni su http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .


0

tl; dr: nel codice sorgente in array.c, vengono chiamate diverse funzioni a seconda che si passi 1 o 2 argomenti per Array#sliceottenere valori di ritorno imprevisti.

(Prima di tutto, vorrei sottolineare che non scrivo codice in C, ma uso Ruby da anni. Quindi, se non hai familiarità con C, ma impieghi qualche minuto a familiarizzare con le basi di funzioni e variabili non è poi così difficile seguire il codice sorgente di Ruby, come dimostrato di seguito. Questa risposta si basa su Ruby v2.3, ma è più o meno la stessa indietro alla v1.9.)

Scenario 1

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

Se guardi il codice sorgente per Array#slice( rb_ary_aref), vedi che quando viene passato solo un argomento ( righe 1277-1289 ), rb_ary_entryviene chiamato, passando il valore dell'indice (che può essere positivo o negativo).

rb_ary_entrycalcola quindi la posizione dell'elemento richiesto dall'inizio dell'array (in altre parole, se viene passato un indice negativo, calcola l'equivalente positivo) e quindi chiama rb_ary_eltper ottenere l'elemento richiesto.

Come previsto, rb_ary_eltritorna nilquando la lunghezza dell'array lenè inferiore o uguale all'indice (qui chiamato offset).

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

Scenario n. 2

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

Tuttavia, quando vengono passati 2 argomenti (ovvero l'indice iniziale bege la lunghezza della sezione len), rb_ary_subseqviene chiamato.

In rb_ary_subseq, se l'indice iniziale begè maggiore della lunghezza dell'array alen, nilviene restituito:

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

Altrimenti lenviene calcolata la lunghezza della sezione risultante e, se viene determinata essere zero, viene restituita una matrice vuota:

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

Quindi, poiché l'indice iniziale di 4 non è maggiore di array.length, viene restituita una matrice vuota invece del nilvalore che ci si potrebbe aspettare.

Domanda risposta?

Se la vera domanda qui non è "Quale codice fa sì che ciò accada?", Ma piuttosto "Perché Matz l'ha fatto in questo modo?", Allora dovrai solo offrirgli una tazza di caffè al prossimo RubyConf e chiedi a lui.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.