Array slicing in Ruby: spiegazione del comportamento illogico (tratto da Rubykoans.com)

Stavo seguendo gli esercizi di Ruby Koans e sono rimasto colpito dal seguente capriccio 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 ragione per cui l’allineamento dell’array si comporta in modo strano quando si inizia alla (lunghezza + 1) posizione?

La segmentazione e l’indicizzazione sono due operazioni diverse e il modo in cui si verifica l’una dall’altra è il problema.

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

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

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

Quando fai index (come array[4] ), stai puntando agli elementi stessi, quindi gli indici passano solo da 0 a 3.

questo ha a che fare con il fatto che slice restituisce un array, la documentazione sorgente rilevante dalla slice # dell’array:

  * 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 date l’inizio che è fuori limite, esso restituirà nil, quindi nel vostro array[4,0] esempio array[4,0] richiede il quarto elemento che esiste, ma chiede di restituire un array di elementi zero. Mentre array[5,0] richiede un indice fuori limite, quindi restituisce zero. Questo forse ha più senso se si ricorda che il metodo slice sta restituendo un nuovo array, non alterando la struttura dei dati originale.

MODIFICARE:

Dopo aver esaminato i commenti ho deciso di modificare questa risposta. Slice chiama il seguente snippet 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 si guarda nella class array.c cui è definito il metodo rb_ary_subseq , si vede che restituisce nil se la lunghezza è fuori limite, non l’indice:

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

In questo caso è ciò che accade quando 4 viene passato, controlla che ci siano 4 elementi e quindi non triggers il ritorno nullo. 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 nil prima che venga valutato l’argomento 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, sottoporrò almeno una patch di prova fallita al core ruby.

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

Forse questo modello 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 a 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 e il massimo, il metodo probabilmente esce immediatamente dalla natura di essere totalmente e completamente fuori dai limiti.

Questo ha senso quando si considera che una sezione dell’array può essere un lvalue valido, non solo un valore di rvalore:

 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 ansible se array[4,0] restituisse nil invece di [] . Tuttavia, array[5,0] restituisce nil perché è fuori limite (l’inserimento dopo il quarto elemento di un array di 4 elementi è significativo, ma l’inserimento dopo il quinto elemento di un array di 4 elementi non lo è).

Leggi l’ array[x,y] syntax delle array[x,y] come “che inizia dopo x elementi nella array , seleziona fino a elementi y “. Questo è significativo solo se l’ array ha almeno x elementi.

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] 

Sono d’accordo che questo sembra un comportamento strano, ma anche la documentazione ufficiale sulla Array#slice dimostra 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 della Array#slice non sembra offrire alcuna idea sul perché funzioni in questo modo:

Riferimento elemento: restituisce l’elemento all’indice o restituisce un sottoarray che inizia con inizio e continua per elementi lunghezza oppure restituisce un sottoarray specificato per intervallo . Gli indici negativi contano all’indietro dalla fine dell’array (-1 è l’ultimo elemento). Restituisce zero se l’indice (o l’indice di partenza) non rientra nell’intervallo.

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/classs/Array.html

I documenti potrebbero sicuramente essere più chiari, ma il comportamento effettivo è auto-consistente e utile. Nota: sto assumendo 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 dare per scontato che la semantica dell'indice del singolo argomento sia la stessa della semantica del primo argomento nello scenario a due argomenti (o intervallo). Non sono la stessa cosa nella 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 è o la stringa di un singolo carattere trovata nell'indice o nil perché non c'è un carattere nell'indice dato.

  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 porzione della stringa da estrarre o sostituire. In particolare, è ansible identificare anche porzioni a larghezza zero della stringa in modo che il testo possa essere inserito prima o dopo i caratteri esistenti, compresi quelli anteriori o finali. 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 di partenza è 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 la singola indicizzazione o la "posizione del bordo" come con due argomenti interi. La differenza è determinata dall'utilizzo della gamma a doppio punto o del punto triplo:

 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 si torna indietro attraverso questi esempi e si insiste e si utilizza la semantica dell'indice singolo per gli esempi di indicizzazione del doppio o dell'intervallo, ci si confonderà. Devi usare la numerazione alternativa che mostro nel diagramma ascii per modellare il comportamento reale.

Una spiegazione fornita da Jim Weirich

Un modo per pensarci è che la posizione 4 dell’indice è proprio al limite dell’array. Quando si chiede una fetta, 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 restanti bit della fine dell’array: 2 elementi, 1 elemento e 0 elementi rispettivamente. Tuttavia, la posizione 5 è chiaramente al di fuori dell’array e non sul bordo, quindi l’array [5,10] restituisce zero.

Considera la seguente matrice:

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

È ansible inserire un elemento all’inizio (testa) dell’array assegnandolo a a[0,0] . Per mettere l’elemento tra "a" e "b" , usa a[1,0] . Fondamentalmente, nella notazione a[i,n] , i rappresenta un indice en un 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 object alla sua estremità usando la notazione descritta sopra? Semplice, assegna il valore a a[3,0] . Questa è la coda dell’array.

Quindi, se provi ad accedere all’elemento a a[3,0] , otterrai [] . In questo caso sei ancora nel raggio dell’array. Ma se provate ad accedere a[4,0] , otterrete nil come valore di ritorno, dal momento che non siete più nel raggio dell’array.

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

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

(Prima di tutto, vorrei far notare che non scrivo codice in C, ma ho usato Ruby per anni. Quindi se non hai familiarità con C, ma ti bastano pochi minuti per familiarizzare con le basi di funzioni e variabili non è poi così difficile seguire il codice sorgente di Ruby, come dimostrato di seguito.Questa risposta è basata su Ruby v2.3, ma è più o meno lo stesso di nuovo alla v1.9.)

Scenario 1

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

Se si guarda il codice sorgente per la Array#slice ( rb_ary_aref ), si vede che quando viene passato solo un argomento ( righe 1277-1289 ), viene chiamato rb_ary_entry , passando il valore dell’indice (che può essere positivo o negativo).

rb_ary_entry calcola 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_elt per ottenere l’elemento richiesto.

Come previsto, rb_ary_elt restituisce nil quando la lunghezza dell’array è 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 (cioè l'indice iniziale beg e la lunghezza della slice len ), viene chiamato rb_ary_subseq .

In rb_ary_subseq , se l'indice di partenza beg è maggiore della lunghezza alen , viene restituito nil :

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

Altrimenti viene calcasting la lunghezza della slice slice len e, se è determinata a zero, viene restituito un array vuoto:

 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 restituito un array vuoto al posto del valore nil che ci si potrebbe aspettare.

Risposta alla domanda?

Se la vera domanda qui non è "Che codice fa accadere questo?", Ma piuttosto, "Perché Matz ha fatto in questo modo?", Beh, dovrai solo comprargli una tazza di caffè al prossimo RubyConf e chiedi a lui.