Array slicing in Ruby: penjelasan untuk perilaku tidak logis (diambil dari Rubykoans.com)

232

Saya menjalani latihan di Ruby Koans dan saya dikejutkan oleh Ruby quirk berikut yang saya temukan benar-benar tidak dapat dijelaskan:

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

Jadi mengapa array[5,0]tidak sama dengan array[4,0]? Apakah ada alasan mengapa array yang mengiris berperilaku aneh ini ketika Anda mulai di (panjang + 1) th posisi ??

Pascal Van Hecke
sumber
sepertinya angka pertama adalah indeks untuk memulai, angka kedua adalah berapa banyak elemen yang harus diiris
austin

Jawaban:

185

Mengiris dan mengindeks adalah dua operasi yang berbeda, dan menyimpulkan perilaku satu dari yang lain adalah tempat masalah Anda.

Argumen pertama dalam slice mengidentifikasi bukan elemen tetapi tempat di antara elemen, mendefinisikan rentang (dan bukan elemen itu sendiri):

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

4 masih dalam array, nyaris saja; jika Anda meminta 0 elemen, Anda mendapatkan ujung kosong array. Tetapi tidak ada indeks 5, jadi Anda tidak bisa memotongnya dari sana.

Ketika Anda melakukan indeks (seperti array[4]), Anda menunjuk pada elemen itu sendiri, sehingga indeks hanya naik dari 0 menjadi 3.

Amadan
sumber
8
Tebakan yang bagus kecuali jika ini didukung oleh sumbernya. Tidak bersikap angkuh, saya akan tertarik pada tautan jika ada hanya untuk menjelaskan "mengapa" seperti OP dan komentator lain bertanya. Diagram Anda masuk akal kecuali Array [4] adalah nihil. Array [3] adalah: jelly. Saya berharap Array [4, N] menjadi nol tetapi [] seperti kata OP. Jika itu sebuah tempat, itu adalah tempat yang tidak berguna karena Array [4, -1] adalah nol. Jadi, Anda tidak dapat melakukan apa pun dengan Array [4].
squarism
5
@quarism Saya baru saja mendapat konfirmasi dari Charles Oliver Nutter (@headius di Twitter) bahwa ini adalah penjelasan yang benar. Dia adalah JRuby dev yang hebat, jadi aku menganggap kata-katanya cukup otoritatif.
Hank Gay
18
Berikut ini adalah pembenaran untuk perilaku ini: blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
Matt Briançon
4
Penjelasan yang benar. Diskusi serupa tentang ruby-core: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune
18
Juga disebut sebagai "pagar-posting." Pos pagar kelima (id 4) ada, tetapi elemen kelima tidak. Mengiris adalah operasi pagar-pos, pengindeksan adalah operasi elemen.
Matty K
27

ini ada hubungannya dengan fakta bahwa slice mengembalikan array, dokumentasi sumber yang relevan dari 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

yang menunjukkan kepada saya bahwa jika Anda memberikan permulaan yang di luar batas, ia akan mengembalikan nol, jadi dalam contoh Anda array[4,0]meminta elemen ke-4 yang ada, tetapi meminta untuk mengembalikan array elemen nol. Sementara array[5,0]meminta indeks di luar batas sehingga mengembalikan nol. Ini mungkin lebih masuk akal jika Anda ingat bahwa metode slice mengembalikan array baru , bukan mengubah struktur data asli.

EDIT:

Setelah meninjau komentar saya memutuskan untuk mengedit jawaban ini. Slice memanggil potongan kode berikut ketika nilai arg adalah dua:

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

jika Anda melihat di array.ckelas di mana rb_ary_subseqmetode didefinisikan, Anda melihat bahwa itu mengembalikan nihil jika panjangnya di luar batas, bukan indeks:

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

Dalam hal ini inilah yang terjadi ketika 4 dilewatkan, ia memeriksa bahwa ada 4 elemen dan dengan demikian tidak memicu pengembalian nol. Kemudian berjalan dan mengembalikan array kosong jika arg kedua diatur ke nol. sementara jika 5 dilewatkan, tidak ada 5 elemen dalam array, sehingga mengembalikan nil sebelum arg nol dievaluasi. kode di sini di baris 944.

Saya percaya ini adalah bug, atau setidaknya tidak dapat diprediksi dan bukan 'Prinsip Terkejut'. Ketika saya mendapatkan beberapa menit, saya setidaknya akan mengirimkan patch tes yang gagal ke inti ruby.

Jed Schneider
sumber
2
Tapi ... elemen yang ditunjukkan oleh array 4 in [4,0] juga tidak ada ... - karena sebenarnya elemen 5 the (penghitungan berbasis 0, lihat contoh). Jadi di luar batas juga.
Pascal Van Hecke
1
kamu benar. Saya kembali dan melihat sumbernya, dan sepertinya argumen pertama ditangani di dalam kode c sebagai panjangnya, bukan indeksnya. Saya akan mengedit jawaban saya, untuk mencerminkan ini. Saya pikir ini bisa diajukan sebagai bug.
Jed Schneider
23

Paling tidak perhatikan bahwa perilakunya konsisten. Dari 5 ke atas semuanya bertindak sama; keanehan hanya terjadi pada [4,N].

Mungkin pola ini membantu, atau mungkin saya hanya lelah dan tidak membantu sama sekali.

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

Di [4,0], kami menangkap ujung array. Saya benar-benar merasa agak aneh, sejauh keindahan dalam pola, jika yang terakhir kembali nil. Karena konteks seperti ini, 4merupakan opsi yang dapat diterima untuk parameter pertama sehingga array kosong dapat dikembalikan. Namun, begitu kita mencapai angka 5 dan ke atas, metode tersebut kemungkinan langsung keluar dengan sifat sepenuhnya dan sepenuhnya di luar batas.

Matchu
sumber
12

Ini masuk akal ketika Anda mempertimbangkan bahwa irisan array dapat menjadi nilai yang valid, bukan hanya nilai:

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]

Hal ini tidak akan mungkin jika array[4,0]kembali nilbukan []. Namun, array[5,0]kembali nilkarena di luar batas (memasukkan setelah elemen ke-4 dari array 4-elemen bermakna, tetapi memasukkan setelah elemen ke-5 dari array 4 elemen tidak).

Baca sintaks slice array[x,y]sebagai "memulai setelah xelemen dalam array, pilih hingga yelemen". Ini hanya bermakna jika arraymemiliki setidaknya xelemen.

Frank Szczerba
sumber
11

Ini tidak masuk akal

Anda harus dapat menetapkan irisan-irisan itu, sehingga mereka didefinisikan sedemikian rupa sehingga awal dan akhir string memiliki ekspresi panjang nol bekerja.

array[4, 0] = :sandwich
array[0, 0] = :crunchy
=> [:crunchy, :peanut, :butter, :and, :jelly, :sandwich]
DigitalRoss
sumber
1
Anda juga dapat menetapkan rentang yang mengiris yang kembali sebagai nol, sehingga akan bermanfaat untuk memperluas penjelasan ini. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas
apa yang dilakukan nomor kedua saat menetapkan? tampaknya diabaikan. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee
@ Davidverlee itu tidak diabaikan:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen
10

Saya menemukan penjelasan oleh Gary Wright juga sangat membantu. http://www.ruby-forum.com/topic/1393096#990065

Jawaban oleh Gary Wright adalah -

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

Dokumen tentu bisa lebih jelas tetapi perilaku yang sebenarnya konsisten dan bermanfaat. Catatan: Saya mengasumsikan versi 1.9.X dari String.

Ini membantu untuk mempertimbangkan penomoran dengan cara berikut:

  -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

Kesalahan umum (dan dapat dimengerti) terlalu berasumsi bahwa semantik indeks argumen tunggal sama dengan semantik argumen pertama dalam dua skenario argumen (atau rentang). Mereka bukan hal yang sama dalam praktiknya dan dokumentasi tidak mencerminkan hal ini. Kesalahannya jelas ada dalam dokumentasi dan bukan dalam implementasi:

argumen tunggal: indeks mewakili posisi karakter tunggal dalam string. Hasilnya adalah string karakter tunggal yang ditemukan di indeks atau nihil karena tidak ada karakter pada indeks yang diberikan.

  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

dua argumen integer: argumen mengidentifikasi bagian dari string untuk diekstraksi atau diganti. Secara khusus, bagian lebar nol dari string juga dapat diidentifikasi sehingga teks dapat dimasukkan sebelum atau setelah karakter yang ada termasuk di bagian depan atau akhir string. Dalam hal ini, argumen pertama tidak mengidentifikasi posisi karakter tetapi malah mengidentifikasi ruang antar karakter seperti yang ditunjukkan pada diagram di atas. Argumen kedua adalah panjang, yang bisa 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"

Perilaku rentang cukup menarik. Titik awal sama dengan argumen pertama ketika dua argumen diberikan (seperti yang dijelaskan di atas) tetapi titik akhir rentang dapat berupa 'posisi karakter' seperti dengan pengindeksan tunggal atau "posisi tepi" seperti dengan dua argumen integer. Perbedaannya ditentukan oleh apakah rentang titik ganda atau rentang titik tiga digunakan:

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.

Jika Anda kembali melalui contoh-contoh ini dan bersikeras dan menggunakan semantik indeks tunggal untuk contoh pengindeksan ganda atau rentang Anda hanya akan bingung. Anda harus menggunakan penomoran alternatif yang saya tunjukkan dalam diagram ascii untuk memodelkan perilaku aktual.

vim
sumber
3
Bisakah Anda memasukkan ide utama utas itu? (dalam kasus tautan suatu hari menjadi tidak valid)
VonC
8

Saya setuju bahwa ini tampak seperti perilaku aneh, tetapi bahkan dokumentasi resmi tentangArray#slice menunjukkan perilaku yang sama seperti pada contoh Anda, dalam "kasus khusus" di bawah:

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

Sayangnya, bahkan deskripsi mereka Array#slicetampaknya tidak menawarkan wawasan mengapa itu bekerja seperti ini:

Referensi Elemen — Mengembalikan elemen pada indeks , atau mengembalikan subarray mulai dari awal dan melanjutkan untuk elemen panjang , atau mengembalikan subarray yang ditentukan oleh rentang . Indeks negatif dihitung mundur dari akhir array (-1 adalah elemen terakhir). Mengembalikan nil jika indeks (atau indeks awal) berada di luar kisaran.

Mark Rushakoff
sumber
7

Penjelasan yang diberikan oleh Jim Weirich

Salah satu cara untuk memikirkannya adalah bahwa posisi indeks 4 berada di ujung array. Saat meminta sepotong, Anda mengembalikan array yang tersisa. Jadi pertimbangkan array [2,10], array [3,10] dan array [4,10] ... masing-masing mengembalikan bit sisa dari ujung array: masing-masing 2 elemen, 1 elemen, dan 0 elemen. Namun, posisi 5 jelas di luar array dan tidak di tepi, jadi array [5,10] mengembalikan nihil.

suvankar
sumber
6

Pertimbangkan array berikut:

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

Anda bisa memasukkan item ke awal (kepala) array dengan menugaskannya a[0,0]. Untuk meletakkan elemen di antara "a"dan "b", gunakan a[1,0]. Pada dasarnya, dalam notasi a[i,n], imerupakan indeks dan nsejumlah elemen. Kapann=0 , itu menentukan posisi antara elemen-elemen array.

Sekarang jika Anda berpikir tentang akhir array, bagaimana Anda bisa menambahkan item ke ujungnya menggunakan notasi yang dijelaskan di atas? Sederhana, tetapkan nilainya a[3,0]. Ini adalah ekor dari array.

Jadi, jika Anda mencoba mengakses elemen di a[3,0], Anda akan mendapatkannya []. Dalam hal ini Anda masih dalam kisaran array. Tetapi jika Anda mencoba mengakses a[4,0], Anda akan mendapatkan nilnilai pengembalian, karena Anda tidak lagi berada dalam jangkauan array.

Baca lebih lanjut di http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .

Tairone
sumber
0

tl; dr: dalam kode sumbernya array.c, fungsi yang berbeda dipanggil tergantung pada apakah Anda memberikan 1 atau 2 argumen untuk Array#slicemenghasilkan nilai pengembalian yang tidak terduga.

(Pertama-tama, saya ingin menunjukkan bahwa saya tidak kode dalam C, tetapi telah menggunakan Ruby selama bertahun-tahun. Jadi jika Anda tidak terbiasa dengan C, tetapi Anda membutuhkan waktu beberapa menit untuk membiasakan diri dengan dasar-dasar fungsi dan variabel benar-benar tidak sulit untuk mengikuti kode sumber Ruby, seperti yang ditunjukkan di bawah ini. Jawaban ini didasarkan pada Ruby v2.3, tetapi kurang lebih sama dengan kembali ke v1.9.)

Skenario 1

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

Jika Anda melihat kode sumber untuk Array#slice( rb_ary_aref), Anda melihat bahwa ketika hanya satu argumen yang dilewati ( baris 1277-1289 ), rb_ary_entrydipanggil, meneruskan nilai indeks (yang bisa positif atau negatif).

rb_ary_entrykemudian menghitung posisi elemen yang diminta dari awal array (dengan kata lain, jika indeks negatif dilewatkan, itu menghitung setara positif) dan kemudian panggilan rb_ary_eltuntuk mendapatkan elemen yang diminta.

Seperti yang diharapkan, rb_ary_eltkembali nilketika panjang array lenadalah kurang dari atau sama dengan indeks (di sini disebut offset).

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

Skenario # 2

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

Namun ketika 2 argumen dilewatkan (yaitu indeks awal beg, dan panjang slice len), rb_ary_subseqdisebut.

Dalam rb_ary_subseq, jika indeks mulai begadalah lebih besar dari panjang array alen, nildikembalikan:

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

Kalau tidak, panjang irisan yang dihasilkan lendihitung, dan jika itu ditentukan menjadi nol, array kosong dikembalikan:

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

Jadi karena indeks awal 4 tidak lebih besar dari array.length, array kosong dikembalikan bukan nilnilai yang mungkin diharapkan.

Pertanyaan dijawab?

Jika pertanyaan aktual di sini bukan "Kode apa yang menyebabkan ini terjadi?", Tetapi, "Mengapa Matz melakukannya dengan cara ini?", Anda hanya perlu membelikannya secangkir kopi di RubyConf berikutnya dan Tanyakan dia.

Scott Schupbach
sumber