Array Slicing in Ruby: Erklärung für unlogisches Verhalten (entnommen aus Rubykoans.com)

232

Ich habe die Übungen in Ruby Koans durchlaufen und war von der folgenden Ruby-Eigenart beeindruckt, die ich wirklich unerklärlich fand:

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

Warum ist das array[5,0]nicht gleich array[4,0]? Gibt es einen Grund, warum sich das Array-Slicing so seltsam verhält, wenn Sie an der (Länge + 1) -ten Position beginnen?

Pascal Van Hecke
quelle
Sieht aus wie die erste Zahl ist der Index, zweite Zahl zu beginnen ist , wie viele Elemente in Scheiben schneiden
Austin

Antworten:

185

Slicing und Indizierung sind zwei verschiedene Vorgänge, und wenn Sie das Verhalten des einen vom anderen ableiten, liegt Ihr Problem darin.

Das erste Argument in Slice identifiziert nicht das Element, sondern die Stellen zwischen Elementen und definiert Bereiche (und nicht Elemente selbst):

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

4 ist immer noch innerhalb des Arrays, gerade noch; Wenn Sie 0 Elemente anfordern, erhalten Sie das leere Ende des Arrays. Es gibt jedoch keinen Index 5, sodass Sie von dort aus nicht schneiden können.

Wenn Sie indexieren (wie array[4]), zeigen Sie auf Elemente selbst, sodass die Indizes nur von 0 auf 3 gehen.

Amadan
quelle
8
Eine gute Vermutung, es sei denn, dies wird von der Quelle gesichert. Da ich nicht snarky bin, würde ich mich für einen Link interessieren, wenn überhaupt, um das "Warum" zu erklären, wie es das OP und andere Kommentatoren fragen. Ihr Diagramm ist sinnvoll, außer dass Array [4] Null ist. Array [3] ist: Gelee. Ich würde erwarten, dass Array [4, N] Null ist, aber es ist [] wie das OP sagt. Wenn es ein Ort ist, ist es ein ziemlich nutzloser Ort, weil Array [4, -1] gleich Null ist. Mit Array [4] können Sie also nichts anfangen.
Squarism
5
@squarism Ich habe gerade eine Bestätigung von Charles Oliver Nutter (@headius auf Twitter) erhalten, dass dies die richtige Erklärung ist. Er ist ein großer JRuby-Entwickler, daher würde ich sein Wort als ziemlich maßgeblich betrachten.
Hank Gay
18
Das Folgende ist die Rechtfertigung für dieses Verhalten: klingen.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
Matt Briançon
4
Richtige Erklärung. Ähnliche Diskussionen über Ruby-Core: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune
18
Wird auch als "Zaunposten" bezeichnet. Der fünfte Zaunpfosten (ID 4) existiert, das fünfte Element jedoch nicht. Das Schneiden ist eine Zaunpfostenoperation, das Indizieren ist eine Elementoperation.
Matty K
27

Dies hat mit der Tatsache zu tun, dass Slice ein Array zurückgibt, relevante Quelldokumentation von 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

was mir nahe legt, dass wenn Sie den Start angeben, der außerhalb der Grenzen liegt, er null zurückgibt, also in Ihrem Beispiel array[4,0]nach dem 4. vorhandenen Element fragt, aber ein Array von null Elementen zurückgibt. Während array[5,0]fragt nach einem Index außerhalb der Grenzen, so dass er null zurückgibt. Dies ist möglicherweise sinnvoller, wenn Sie sich daran erinnern, dass die Slice-Methode ein neues Array zurückgibt und die ursprüngliche Datenstruktur nicht ändert.

BEARBEITEN:

Nachdem ich die Kommentare überprüft hatte, entschied ich mich, diese Antwort zu bearbeiten. Slice ruft das folgende Code-Snippet auf, wenn der arg-Wert zwei ist:

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

Wenn Sie in die array.cKlasse schauen, in der die rb_ary_subseqMethode definiert ist, sehen Sie, dass sie null zurückgibt, wenn die Länge außerhalb der Grenzen liegt, nicht der Index:

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

In diesem Fall geschieht dies, wenn 4 übergeben werden. Es prüft, ob 4 Elemente vorhanden sind, und löst daher keine Null-Rückgabe aus. Es geht dann weiter und gibt ein leeres Array zurück, wenn das zweite Argument auf Null gesetzt ist. Wenn 5 übergeben wird, enthält das Array keine 5 Elemente. Daher wird null zurückgegeben, bevor das Argument null ausgewertet wird. Code hier in Zeile 944.

Ich glaube, dies ist ein Fehler oder zumindest unvorhersehbar und nicht das "Prinzip der geringsten Überraschung". Wenn ich ein paar Minuten Zeit habe, werde ich mindestens einen fehlgeschlagenen Test-Patch an Ruby Core senden.

Jed Schneider
quelle
2
Aber ... das durch die 4 im Array [4,0] angegebene Element existiert auch nicht ... - weil es tatsächlich das 5-te Element ist (0-basierte Zählung, siehe Beispiele). Es ist also auch außerhalb der Grenzen.
Pascal Van Hecke
1
Du hast recht. Ich ging zurück und schaute auf die Quelle, und es sieht so aus, als würde das erste Argument im c-Code als Länge und nicht als Index behandelt. Ich werde meine Antwort bearbeiten, um dies zu reflektieren. Ich denke, dies könnte als Fehler eingereicht werden.
Jed Schneider
23

Beachten Sie zumindest, dass das Verhalten konsistent ist. Ab 5 verhält sich alles gleich; Die Verrücktheit tritt nur bei auf [4,N].

Vielleicht hilft dieses Muster, oder vielleicht bin ich nur müde und es hilft überhaupt nicht.

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

Um [4,0]fangen wir das Ende des Arrays. Ich würde es tatsächlich ziemlich seltsam finden, was die Schönheit der Muster angeht, wenn der letzte zurückkommt nil. Aufgrund eines solchen Kontexts 4ist dies eine akzeptable Option für den ersten Parameter, damit das leere Array zurückgegeben werden kann. Sobald wir jedoch 5 und höher erreicht haben, wird die Methode wahrscheinlich sofort beendet, da sie vollständig und vollständig außerhalb der Grenzen liegt.

Matchu
quelle
12

Dies ist sinnvoll, wenn Sie bedenken, dass ein Array-Slice ein gültiger l-Wert sein kann, nicht nur ein r-Wert:

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]

Dies wäre nicht möglich, wenn array[4,0]zurückgegeben nilstatt []. Gibt jedoch array[5,0]zurücknil weil es außerhalb der Grenzen liegt (das Einfügen nach dem 4. Element eines 4-Element-Arrays ist sinnvoll, das Einfügen nach dem 5. Element eines 4-Element-Arrays jedoch nicht).

Lesen Sie die Slice-Syntax array[x,y]als "Beginnen Sie nach xElementen in arrayund wählen Sie bis zu yElementen aus". Dies ist nur sinnvoll, wenn arraymindestens xElemente vorhanden sind.

Frank Szczerba
quelle
11

Das macht Sinn

Sie müssen in der Lage sein, diesen Slices zuzuweisen, damit sie so definiert sind, dass der Anfang und das Ende der Zeichenfolge funktionierende Ausdrücke mit der Länge Null enthalten.

array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]
DigitalRoss
quelle
1
Sie können dem Bereich auch das Slice zuweisen, das als Null zurückgegeben wird. Daher ist es hilfreich, diese Erklärung zu erweitern. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
Mfazekas
Was macht die zweite Nummer beim Zuweisen? es scheint ignoriert zu werden. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee
@rewverlee es wird nicht ignoriert:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen
10

Ich fand die Erklärung von Gary Wright ebenfalls sehr hilfreich. http://www.ruby-forum.com/topic/1393096#990065

Die Antwort von Gary Wright lautet -

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

Die Dokumente könnten sicherlich klarer sein, aber das tatsächliche Verhalten ist selbstkonsistent und nützlich. Hinweis: Ich gehe von einer 1.9.X-Version von String aus.

Es ist hilfreich, die Nummerierung folgendermaßen zu berücksichtigen:

  -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

Der häufigste (und verständliche) Fehler besteht darin, anzunehmen, dass die Semantik des Einzelargumentindex mit der Semantik des ersten übereinstimmt Arguments im Szenario (oder Bereich) mit zwei Argumenten übereinstimmt. In der Praxis sind sie nicht dasselbe, und die Dokumentation spiegelt dies nicht wider. Der Fehler liegt jedoch definitiv in der Dokumentation und nicht in der Implementierung:

einzelnes Argument: Der Index repräsentiert eine einzelne Zeichenposition innerhalb der Zeichenfolge. Das Ergebnis ist entweder die einzelne Zeichenfolge, die im Index gefunden wird, oder null, da der angegebene Index kein Zeichen enthält.

  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

Zwei ganzzahlige Argumente: Die Argumente identifizieren einen Teil der Zeichenfolge, der extrahiert oder ersetzt werden soll. Insbesondere können auch Teile der Zeichenfolge mit der Breite Null identifiziert werden, so dass Text vor oder nach vorhandenen Zeichen eingefügt werden kann, einschließlich am Anfang oder Ende der Zeichenfolge. In diesem Fall wird das erste Argument nicht eine Zeichenposition identifizieren , sondern statt dem identifiziert , den Raum zwischen den Zeichen , wie im Diagramm oben dargestellt. Das zweite Argument ist die Länge, die 0 sein kann.

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"

Das Verhalten eines Bereichs ist ziemlich interessant. Der Startpunkt ist der gleiche wie das erste Argument, wenn zwei Argumente angegeben werden (wie oben beschrieben), aber der Endpunkt des Bereichs kann die 'Zeichenposition' wie bei der Einzelindizierung oder die "Kantenposition" wie bei zwei ganzzahligen Argumenten sein. Die Differenz wird dadurch bestimmt, ob der Doppelpunktbereich oder der Dreifachpunktbereich verwendet wird:

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.

Wenn Sie diese Beispiele noch einmal durchgehen und darauf bestehen, die Einzelindexsemantik für die Doppel- oder Bereichsindizierungsbeispiele zu verwenden, werden Sie nur verwirrt. Sie müssen die alternative Nummerierung verwenden, die ich im ASCII-Diagramm zeige, um das tatsächliche Verhalten zu modellieren.

vim
quelle
3
Können Sie die Hauptidee dieses Threads aufnehmen? (im Falle, dass der Link eines Tages ungültig wird)
VonC
8

Ich bin damit einverstanden, dass dies wie ein seltsames Verhalten erscheint, aber selbst die offizielle Dokumentation zuArray#slice zeigt dasselbe Verhalten wie in Ihrem Beispiel in den folgenden "Sonderfällen":

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

Leider Array#slicescheint selbst ihre Beschreibung von keinen Einblick zu geben, warum es so funktioniert:

Element Referenz-Gibt das Element an Index , oder sendet ein Subarray beginnend bei Start und Fort für Längenelemente, oder sendet einen Subarray von spezifizierten Bereich . Negative Indizes zählen ab dem Ende des Arrays rückwärts (-1 ist das letzte Element). Gibt null zurück, wenn der Index (oder der Startindex) außerhalb des Bereichs liegt.

Mark Rushakoff
quelle
7

Eine Erklärung von Jim Weirich

Eine Möglichkeit, darüber nachzudenken, besteht darin, dass sich die Indexposition 4 am äußersten Rand des Arrays befindet. Wenn Sie nach einem Slice fragen, geben Sie so viel des verbleibenden Arrays zurück. Betrachten Sie also das Array [2,10], das Array [3,10] und das Array [4,10] ... und geben jeweils die verbleibenden Bits am Ende des Arrays zurück: 2 Elemente, 1 Element bzw. 0 Elemente. Position 5 befindet sich jedoch deutlich außerhalb des Arrays und nicht am Rand, sodass Array [5,10] Null zurückgibt.

Suvankar
quelle
6

Betrachten Sie das folgende Array:

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

Sie können ein Element am Anfang (Kopf) des Arrays einfügen, indem Sie es zuweisen a[0,0]. Verwenden Sie, um das Element zwischen "a"und zu "b"setzen a[1,0]. Im Wesentlichen in der Notation a[i,n], istellt einen Index und neine Anzahl von Elementen. Wenn n=0, definiert es eine Position zwischen den Elementen des Arrays.

Wenn Sie nun über das Ende des Arrays nachdenken, wie können Sie ein Element mit der oben beschriebenen Notation an sein Ende anhängen? Einfach, weisen Sie den Wert zu a[3,0]. Dies ist das Ende des Arrays.

Wenn Sie also versuchen, auf das Element unter zuzugreifen a[3,0], erhalten Sie []. In diesem Fall befinden Sie sich immer noch im Bereich des Arrays. Wenn Sie jedoch versuchen, darauf zuzugreifen a[4,0], erhalten Sie einen nilRückgabewert, da Sie sich nicht mehr im Bereich des Arrays befinden.

Weitere Informationen finden Sie unter http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .

Tairone
quelle
0

tl; dr: Im Quellcode in array.cwerden verschiedene Funktionen aufgerufen, je nachdem, ob Sie 1 oder 2 Argumente übergeben, Array#slicewas zu unerwarteten Rückgabewerten führt.

(Zunächst möchte ich darauf hinweisen, dass ich nicht in C codiere, sondern Ruby seit Jahren verwende. Wenn Sie also nicht mit C vertraut sind, nehmen Sie sich ein paar Minuten Zeit, um sich mit den Grundlagen vertraut zu machen Bei Funktionen und Variablen ist es wirklich nicht so schwer, dem Ruby-Quellcode zu folgen, wie unten gezeigt. Diese Antwort basiert auf Ruby v2.3, ist aber mehr oder weniger dieselbe wie in v1.9.)

Szenario 1

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

Wenn Sie sich den Quellcode für Array#slice( rb_ary_aref) ansehen, sehen Sie, dass, wenn nur ein Argument übergeben wird ( Zeilen 1277-1289 ), rb_ary_entryaufgerufen wird und der Indexwert übergeben wird (der positiv oder negativ sein kann).

rb_ary_entryBerechnet dann die Position des angeforderten Elements vom Anfang des Arrays an (mit anderen Worten, wenn ein negativer Index übergeben wird, berechnet er das positive Äquivalent) und ruft dann rb_ary_eltauf, um das angeforderte Element abzurufen.

Wie erwartet, rb_ary_eltkehrt , nilwenn die Länge des Arrays lenist kleiner oder gleich den Index (hier genannt offset).

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

Szenario 2

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

Wenn jedoch 2 Argumente übergeben werden (dh der Startindex begund die Länge des Slice len), rb_ary_subseqwird aufgerufen.

In rb_ary_subseqWenn der Startindex begist größer als die Arraylänge alen, nilwird zurückgegeben:

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

Andernfalls wird die Länge des resultierenden Slice lenberechnet. Wenn der Wert Null ist, wird ein leeres Array zurückgegeben:

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

Da der Startindex von 4 nicht größer als ist array.length, wird anstelle des nilerwarteten Werts ein leeres Array zurückgegeben .

Frage beantwortet?

Wenn die eigentliche Frage hier nicht "Welcher Code verursacht dies?" Ist, sondern "Warum hat Matz das so gemacht?", Müssen Sie ihm bei der nächsten RubyConf und nur eine Tasse Kaffee kaufen Frag ihn.

Scott Schupbach
quelle