Ruby'de dizi dilimleme: mantıksız davranış için açıklama (Rubykoans.com'dan alınmıştır)


232

Ruby Koans'taki alıştırmaları yapıyordum ve gerçekten açıklanamaz bulduğum aşağıdaki Ruby tuhaflığından etkilendim :

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

Öyleyse neden array[5,0]eşit değil array[4,0]? Dizi uzunluğunun (uzunluk + 1) inci konumda başladığınızda bu garip davranmasının bir nedeni var mı ?



ilk sayı başlamak için endeksi, ikinci sayı kaç dilim dilimdir
austin

Yanıtlar:


185

Dilimleme ve dizine ekleme iki farklı işlemdir ve birinin davranışını diğerinden çıkarmak, probleminizin yattığı yerdir.

Dilimdeki ilk argüman öğeyi değil, elemanlar arasındaki yerleri açıklar (ve elemanların kendileri değil) tanımlar:

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

4 hala dizi içinde, ancak zar zor; 0 öğe talep ederseniz dizinin boş ucunu alırsınız. Ama indeks 5 yok, bu yüzden oradan dilimleyemezsiniz.

Dizin (gibi array[4]) yaptığınızda, öğelerin kendisini işaret edersiniz, bu nedenle endeksler yalnızca 0'dan 3'e gider.


8
Kaynak tarafından desteklenmedikçe iyi bir tahmin. Snarky değil, ben sadece OP ve diğer yorumcular soruyor gibi "neden" açıklamak için herhangi bir bağlantı ilgilenen olacaktır. Diyagramınız Array [4] sıfırdan farklıdır. Dizi [3]: jöle. Array [4, N] 'nin sıfır olmasını beklerdim ama OP'nin dediği gibi []. Bir yerse, oldukça işe yaramaz bir yer çünkü Array [4, -1] sıfır. Böylece Array [4] ile hiçbir şey yapamazsınız.
squarism

5
@squarism Charles Oliver Nutter'dan (Twitter'da @headius) doğru bir açıklama olduğunu onayladım. O büyük bir JRuby geliştiricisi, bu yüzden onun sözünü oldukça yetkili buluyorum.
Hank Gay

18
Bu davranış için gerekçe şudur
Matt Briançon

4
Doğru açıklama. Ruby-core hakkında benzer tartışmalar: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune

18
"Çit gönderme" olarak da bilinir. Beşinci çit direği (id 4) mevcuttur, ancak beşinci öğe yoktur. Dilimleme bir çit sonrası işlemidir, indeksleme bir eleman işlemidir.
Matty K

27

bunun dilimin bir dizi, Array # dilimindeki ilgili kaynak belgelerini döndürmesi ile ilgilidir:

 *  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

Bu da bana, eğer sınırları aşan bir başlangıç ​​yaparsanız, sıfır döndüreceğini gösterir, bu nedenle örneğinizde array[4,0]var olan 4. elemanı ister ancak sıfır eleman dizisini döndürmeyi ister. Sınır array[5,0]dışında bir dizin ister, böylece sıfır döndürür. Dilim yönteminin orijinal veri yapısını değiştirmeyen yeni bir dizi döndürdüğünü hatırlarsanız, bu belki daha mantıklıdır .

DÜZENLE:

Yorumları inceledikten sonra bu cevabı düzenlemeye karar verdim. Dilim , arg değeri iki olduğunda aşağıdaki kod snippet'ini çağırır :

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

yöntemin tanımlandığı array.csınıfa bakarsanız rb_ary_subseq, uzunluk dizin dışındaysa, nil döndürdüğünü görürsünüz:

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

Bu durumda, 4 geçtiğinde olan budur, 4 eleman olup olmadığını kontrol eder ve böylece sıfır dönüşünü tetiklemez. Ardından ikinci argümanı sıfıra ayarlanırsa boş bir dizi döndürür. 5 iletilirse dizide 5 öğe bulunmadığından sıfır argümanı değerlendirilmeden önce sıfır döndürür. Kod burada hat 944 at.

Bunun bir hata olduğuna ya da en azından öngörülemez olduğuna inanıyorum ve 'En Az Sürpriz İlkesi' değil. Birkaç dakika aldığımda en azından yakut çekirdeğe başarısız bir test yaması göndereceğim.


2
Ancak ... [4,0] dizisindeki 4 ile gösterilen öğe de mevcut değildir ... - çünkü aslında 5 elementtir (0 tabanlı sayım, örneklere bakın). Yani sınırların dışında da.
Pascal Van Hecke

1
haklısın. Geri döndüm ve kaynağa baktım ve ilk argüman c kodu içinde uzunluk olarak değil, dizin olarak işleniyor gibi görünüyor. Bunu yansıtmak için cevabımı düzenleyeceğim. Bence bu bir hata olarak gönderilebilir.
Jed Schneider

23

En azından davranışın tutarlı olduğunu unutmayın. 5'ten itibaren her şey aynı şekilde hareket eder; gariplik sadece 'da olur [4,N].

Belki bu desen yardımcı olur, ya da ben sadece yorgunum ve hiç yardımcı olmuyor.

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

Konumunda [4,0]dizinin sonunu yakalarız. Sonuncusu geri döndüğünde, desenlerdeki güzelliğe gelince, oldukça garip buluyorum nil. Böyle bir bağlam nedeniyle 4, boş dizi döndürülebilmesi için ilk parametre için kabul edilebilir bir seçenektir. Yine de, 5 ve yukarıyı vurduğumuzda, yöntem muhtemelen tamamen ve tamamen sınırların dışına çıkması nedeniyle hemen çıkar.


12

Bir dizi diliminin yalnızca bir değer değil, geçerli bir değer olabileceğini düşündüğünüzde bu mantıklıdır:

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]

Bunun yerine array[4,0]iade edilirse bu mümkün olmazdı . Ancak, sınırların dışında olduğu için döner (4 öğeli dizinin 4. öğesinden sonra ekleme anlamlıdır, ancak 4 öğeli dizinin 5. öğesinden sonra ekleme yapılmaz).nil[]array[5,0]nil

Dilim sözdizimini array[x,y]" xöğeden sonra başlayıp öğelere arraykadar seç y" olarak okuyun. Bu sadece arrayen azından xunsurları varsa anlamlıdır .


11

Bu does mantıklı

Bu dilimlere atayabilmeniz gerekir, böylece dizenin başlangıcı ve sonu çalışan sıfır uzunluklu ifadelere sahip olacak şekilde tanımlanır.

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

1
Nil olarak dönen dilim aralığına da atayabilirsiniz, bu nedenle bu açıklamayı genişletmek yararlı olacaktır. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas

İkinci numara atarken ne yapar? yok sayılıyor gibi görünüyor. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee

@drewverlee o değil ihmal:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen

10

Gary Wright'ın açıklamasını da çok faydalı buldum. http://www.ruby-forum.com/topic/1393096#990065

Gary Wright'ın yanıtı -

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

Dokümanlar kesinlikle daha açık olabilirdi, ancak gerçek davranış kendi kendine tutarlı ve kullanışlı. Not: String'in 1.9.X sürümünü varsayıyorum.

Numaralandırmayı aşağıdaki şekilde düşünmeye yardımcı olur:

  -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

Yaygın (ve anlaşılabilir) hata, tek bağımsız değişken dizinin anlambiliminin , iki bağımsız değişken senaryosundaki (veya aralığındaki) ilk bağımsız değişkenin anlamıyla aynı olduğunu varsayar . Pratikte aynı şey değildirler ve belgeler bunu yansıtmaz. Ancak hata kesinlikle dokümantasyonda ve uygulamada değil:

tek argüman: dizin, dize içindeki tek bir karakter konumunu temsil eder. Sonuç, dizinde bulunan tek karakter dizgisidir veya verilen dizinde hiçbir karakter olmadığından nil olur.

  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

iki tamsayı bağımsız değişkeni: bağımsız değişkenler dizenin ayıklanacak veya değiştirilecek bir bölümünü tanımlar. Özellikle, dizenin sıfır genişlikli kısımları da tanımlanabilir, böylece metin dizenin önü veya sonu dahil olmak üzere mevcut karakterlerden önce veya sonra eklenebilir. Bu durumda, ilk argüman yok değil bir karakter pozisyonunu tanımlamak, ancak yukarıdaki şemada gösterildiği gibi yerine karakterler arasındaki boşluğu tanımlar. İkinci argüman, 0 olabilen uzunluktur.

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"

Bir aralığın davranışı oldukça ilginç. İki argüman sağlandığında (yukarıda açıklandığı gibi) başlangıç ​​noktası ilk argümanla aynıdır, ancak aralığın bitiş noktası, tek indekslemede olduğu gibi 'karakter konumu' veya iki tamsayı argümanında olduğu gibi "kenar konumu" olabilir. Fark, çift nokta aralığının mı yoksa üç nokta aralığının mı kullanıldığına göre belirlenir:

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.

Bu örneklere geri dönüp, çift veya aralıklı dizin oluşturma örnekleri için tek dizin semantiğini kullanmakta ve ısrar ederseniz, kafanız karışacaktır. Ascii diyagramında gösterdiğim alternatif numaralandırmayı gerçek davranışı modellemek için kullanmalısınız.


3
Bu konunun ana fikrini ekleyebilir misiniz? (bağlantının bir gün geçersiz olması durumunda)
VonC

8

Bunun garip bir davranış gibi göründüğünü kabul ediyorum, ancak hakkındaki resmi belgelerArray#slice bile aşağıdaki "özel durumlarda" örneğinizle aynı davranışı gösteriyor:

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

Ne yazık ki, açıklamaları bile neden bu şekilde çalıştığına Array#slicedair herhangi bir fikir vermiyor gibi görünüyor :

Eleman at elemanı Referans-İade indeksi veya döner bir altdizilim başlayan başlangıcı ve devam eden uzunluk elemanları veya iadeler tarafından belirtilen bir altdizilim aralığında . Negatif indeksler dizinin sonundan geriye doğru sayar (-1 son öğedir). Dizin (veya başlangıç ​​dizini) aralık dışındaysa nil değerini döndürür.


7

Jim Weirich tarafından sağlanan açıklama

Bunu düşünmenin bir yolu, dizin konumu 4'ün dizinin en kenarında olmasıdır. Bir dilim sorduğunuzda, kalan dizinin çoğunu döndürürsünüz. Bu nedenle dizi [2,10], dizi [3,10] ve dizi [4,10] ... her biri dizinin sonunun kalan bitlerini döndürür: sırasıyla 2 öğe, 1 öğe ve 0 öğe. Ancak, konum 5, kenarın dışında değil , açıkça dizinin dışındadır , bu nedenle [5,10] dizisi nil değerini döndürür.


6

Aşağıdaki diziyi göz önünde bulundurun:

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

Diziye atayarak başlığa (baş) bir öğe ekleyebilirsiniz a[0,0]. Elemanı "a"ve arasına koymak için "b", kullanın a[1,0]. Temel olarak, gösterimde a[i,n], ibir indeksi ve nbir dizi unsuru temsil eder . Ne zaman n=0, dizinin elemanları arasında bir konum tanımlar.

Şimdi dizinin sonu hakkında düşünürseniz, yukarıda açıklanan notasyonu kullanarak bir öğeyi sonuna nasıl ekleyebilirsiniz? Basit, değeri atayın a[3,0]. Bu dizinin kuyruğu.

Yani, adresindeki öğeye erişmeye çalışırsanız a[3,0], alırsınız []. Bu durumda hala dizi aralığındasınız. Ancak erişmeye çalışırsanız a[4,0], nilartık dizi aralığı içinde olmadığınız için dönüş değeri olarak alırsınız .

Bununla ilgili daha fazla bilgiyi http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ adresinde bulabilirsiniz .


0

tl; dr: kaynak kodunda , beklenmeyen dönüş değerlerine yol açan array.c1 veya 2 argüman geçirip geçirmediğinize bağlı olarak farklı işlevler çağrılır Array#slice.

(Öncelikle, C'de kodlamadığımı, ancak Ruby'yi yıllardır kullandığımı belirtmek isterim. Yani C'yi bilmiyorsanız, ancak temel bilgileri öğrenmek için birkaç dakikanızı ayırın işlevler ve değişkenler arasında, aşağıda gösterildiği gibi Ruby kaynak kodunu takip etmek o kadar da zor değildir. Bu cevap Ruby v2.3'e dayanmaktadır, ancak aşağı yukarı v1.9 ile aynıdır.)

Senaryo # 1

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

Array#slice( rb_ary_aref) İçin kaynak koduna bakarsanız, yalnızca bir bağımsız değişken iletildiğinde ( satır 1277-1289 ), rb_ary_entrydizin değerini (pozitif veya negatif olabilir) geçirerek çağrıldığını görürsünüz .

rb_ary_entrydaha sonra istenen öğenin konumunu dizinin başından hesaplar (başka bir deyişle, negatif bir dizin iletilirse, pozitif eşdeğerini hesaplar) ve sonra rb_ary_eltistenen öğeyi almak için çağrı yapar.

Beklendiği gibi, rb_ary_eltdöner nildizinin uzunluğu ne lenolduğu için eşit veya daha az (burada adı indeksi offset).

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

Senaryo # 2

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

Ancak 2 argüman iletildiğinde (yani başlangıç ​​indeksi begve dilimin uzunluğu len) rb_ary_subseqçağrılır.

İn rb_ary_subseqbaşlangıç göstergesi olursa, begbir daha yüksek dizi uzunluğu alen, nildöndürülür:

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

Aksi takdirde, elde edilen dilimin uzunluğu lenhesaplanır ve sıfır olduğu belirlenirse boş bir dizi döndürülür:

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

Bu nedenle, 4'ün başlangıç ​​dizini büyük olmadığından array.length, nilbeklenebileceği değer yerine boş bir dizi döndürülür .

Soru cevaplandı mı?

Buradaki asıl soru "Bu hangi kodun ortaya çıkmasına neden oluyor?" Değil, "Matz neden bu şekilde yaptı?" ona sor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.