Rubyでの配列スライス:非論理的な動作の説明(Rubykoans.comから取得)


232

私はRuby Koansでの演習を行っていましたが、次のRubyの癖に本当に説明がつかないと感じました。

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

なぜとarray[5,0]等しくないのarray[4,0]ですか?(length + 1)番目の位置から開始すると、配列のスライスがこの奇妙な動作をする理由はありますか?



最初の数値は開始するインデックス、2番目の数値はスライスする要素の数のようです
オースティン2014

回答:


185

スライスとインデックス作成は2つの異なる操作であり、一方の動作を他方から推測することが問題の原因です。

sliceの最初の引数は、要素ではなく要素間の場所を識別し、スパンを定義します(要素自体ではありません)。

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

4はまだアレイ内にありますが、ほとんどありません。0要素を要求すると、配列の空の端が取得されます。ただし、インデックス5がないため、そこからスライスすることはできません。

(のようにarray[4])インデックスを作成する場合、要素自体を指しているため、インデックスは0から3までしかありません。


8
これがソースによってバックアップされていない限り、適切な推測です。OPや他のコメンテーターが尋ねているような「なぜ」を説明するだけのリンクがあれば、ぎこちないのではなく、リンクに興味があります。Array [4]がnilであることを除いて、この図は意味があります。Array [3]は:jellyです。Array [4、N]はnilになると思いますが、OPが言うように[]です。場所である場合、Array [4、-1]はnilであるため、かなり役に立たない場所です。したがって、Array [4]では何もできません。
2010

5
@squarism Charles Oliver Nutter(Twitterの@headius)からこれが正しい説明であることを確認しました。彼は昔からのJRuby開発者なので、彼の言葉はかなり信頼できると思います。
ハンクゲイ

18
以下は、この行動を正当化する理由であるblade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
マット・ブリアンソン


18
「フェンス投稿」とも呼ばれます。5番目のフェンス支柱(id 4)は存在しますが、5番目の要素は存在しません。スライスはフェンスポスト操作であり、インデックス付けは要素操作です。
マティK

27

これは、sliceが配列、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

これは、範囲外の開始を与えるとnilを返すことを示唆しています。したがって、この例でarray[4,0]は、存在する4番目の要素を要求しますが、ゼロ要素の配列を返すように要求します。while array[5,0]は範囲外のインデックスを要求するため、nilを返します。これは、sliceメソッドが元のデータ構造を変更するのではなく、新しい配列を返すことを覚えている場合、おそらくより理にかなっています。

編集:

コメントを確認した後、私はこの回答を編集することにしました。arg値が2の場合、Sliceは次のコードスニペットを呼び出します。

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

メソッドが定義されているarray.cクラスrb_ary_subseqを見ると、長さが範囲外でインデックスではなくnilを返していることがわかります。

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

この場合、これは4が渡されたときに起こっていることであり、4つの要素があることを確認するため、nilリターンはトリガーされません。続いて、2番目の引数がゼロに設定されている場合は、空の配列を返します。一方、5が渡された場合、配列には5つの要素がないため、ゼロ引数が評価される前にnilを返します。ここの 944行目にコードを記述します

これはバグであるか、少なくとも予測不能であり、「最小の驚きの原則」ではないと考えています。数分経つと、失敗したテストパッチをrubyコアに最低限提出します。


2
しかし... array [4,0]の4で示される要素も存在しません...-これは実際には5要素であるためです(0ベースのカウント、例を参照)。したがって、それも範囲外です。
Pascal Van Hecke

1
あなたが正しい。私は戻ってソースを見ました、そしてそれは最初の引数がインデックスではなく長さとしてcコードの中で処理されているようです。これを反映するために、回答を編集します。これはバグとして提出できると思います。
Jed Schneider

23

少なくとも、動作が一貫していることに注意してください。5以降では、すべてが同じように動作します。奇妙さはでのみ発生し[4,N]ます。

たぶんこのパターンが役立つかもしれませんし、多分私はただ疲れていてまったく役に立たないかもしれません。

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

では[4,0]、配列の終わりをキャッチします。最後のものが戻ってきたら、パターンの美しさに関しては、実際には奇妙だと思いますnil。このようなコンテキストのため4、空の配列を返すことができるように、は最初のパラメーターの受け入れ可能なオプションです。ただし、5以上になると、メソッドは完全に完全に範囲外になるという性質上、すぐに終了する可能性があります。


12

これは、配列スライスが右辺値だけでなく有効な左辺値になる可能性があることを考えると、理にかなっています。

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]

の代わりにarray[4,0]返された場合、これは不可能nilです[]。ただし、範囲外であるためにarray[5,0]戻りますnil(4要素配列の4番目の要素の後に挿入することは意味がありますが、4要素配列の5番目の要素の後に挿入することは意味がありません)。

スライス構文はarray[x,y]、「のx要素の後ろから始まり、要素arrayまで選択する」と読みますy。これは、arrayが少なくともx要素を持っている場合にのみ意味があります。


11

これ理にかなっています

これらのスライスに割り当てることができるようにする必要があるため、文字列の最初と最後に長さゼロの式が機能するようにスライスを定義します。

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

1
nilとして返されるスライスに範囲を割り当てることもできるため、この説明を拡張すると便利です。array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas 14年

割り当てるときに2番目の番号は何をしますか?無視されているようです。[26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee 2015年

@drewverlee 無視されませんarray = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen

10

ゲイリー・ライトの説明も参考になりました。 http://www.ruby-forum.com/topic/1393096#990065

ゲイリー・ライトの答えは-

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

ドキュメントは確かにもっと明確かもしれませんが、実際の振る舞いは首尾一貫していて有用です。注:1.9.Xバージョンのストリングを想定しています。

次の方法で番号付けを検討すると役立ちます。

  -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

よくある(そして理解できる)間違いは、単一引数のインデックスのセマンティクスが2つの引数のシナリオ(または範囲)の最初の引数のセマンティクスと同じであると仮定すること です。それらは実際には同じものではなく、ドキュメントはこれを反映していません。ただし、エラーは間違いなくドキュメントにあり、実装にはありません。

単一の引数:インデックスは文字列内の単一の文字位置を表します。結果は、インデックスで見つかった1文字の文字列か、指定されたインデックスに文字がないためnilになります。

  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

2つの整数引数:引数は、抽出または置換する文字列の一部を識別します。特に、文字列の幅がゼロの部分も識別できるため、文字列の前や終わりなど、既存の文字の前後にテキストを挿入できます。この場合、最初の引数は文字の位置を識別しませが、代わりに上の図に示すように文字間のスペースを識別します。2番目の引数は長さで、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"

範囲の動作はかなり興味深いです。上記のように2つの引数が指定されている場合、開始点は最初の引数と同じですが、範囲の終了点は、単一のインデックスの場合の「文字位置」または2つの整数引数の場合の「エッジ位置」にすることができます。違いは、ダブルドット範囲またはトリプルドット範囲のどちらを使用するかによって決まります。

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.

これらの例に戻り、二重または範囲のインデックスの例に単一のインデックスのセマンティクスを使用すると、混乱するだけです。実際の動作をモデル化するには、ASCIIダイアグラムに表示される代替の番号付けを使用する必要があります。


3
そのスレッドの主なアイデアを含めることができますか?(リンクが1日無効になる場合)
VonC '25

8

これは奇妙な動作のように思われることに同意しますが、上の公式ドキュメントでArray#sliceさえ以下の「特殊なケース」での例と同じ動作を示しています。

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

残念ながら、彼らの説明でさえ、なぜそれがこのように機能Array#sliceするのについての洞察を提供していないようです:

要素の要素を言及は、返しインデックス、または戻りサブアレイから始まる開始 との継続的な長さ の要素、またはリターンで指定サブアレイ範囲。負のインデックスは、配列の最後から逆方向に数えます(-1は最後の要素です)。インデックス(または開始インデックス)が範囲外の場合はnilを返します。


7

ジム・ウェイリッヒによる説明

これについて考える1つの方法は、インデックス位置4が配列の一番端にあるということです。スライスを要求すると、残っている配列と同じ量が返されます。したがって、array [2,10]、array [3,10]、array [4,10]を考えてみてください。それぞれ、配列の最後の残りのビットを返します。それぞれ2要素、1要素、0要素です。ただし、位置5は明らかに端ではなく配列の外にあるため、array [5,10]はnilを返します。


6

次の配列について考えてみます。

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

に割り当てることで、配列の先頭(先頭)に項目を挿入できa[0,0]ます。"a"との間に要素を置くには"b"、を使用しますa[1,0]。基本的に、の表記a[i,n]iは、インデックスとn要素の数を表します。の場合n=0、配列の要素間の位置を定義します。

配列の終わりについて考えた場合、上記の表記法を使用してアイテムをその終わりにどのように追加できますか?単純に、値をに割り当てますa[3,0]。これは配列の末尾です。

したがって、で要素にアクセスしようとするとa[3,0]、が取得され[]ます。この場合、あなたはまだ配列の範囲内にいます。しかし、にアクセスしようとすると、配列の範囲内にいないa[4,0]ためnil、戻り値として取得されます。

詳細については、http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/をご覧ください


0

tl; dr:のソースコードではarray.c、1つまたは2つの引数を渡すかどうかに応じて、さまざまな関数が呼び出さArray#sliceれ、予期しない戻り値が返されます。

(最初に、私はCでコーディングしていないが、何年もRubyを使用していることを指摘しておきます。したがって、Cに慣れていない場合でも、基本に慣れるまでに数分かかります。関数と変数については、以下に示すように、Rubyのソースコードを追跡するのはそれほど難しくありません。この回答はRuby v2.3に基づいていますが、v1.9に戻ってもほぼ同じです。

シナリオ#1

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

Array#slicerb_ary_aref)のソースコードを見ると、引数が1つだけ渡された場合(1277-1289行rb_ary_entryが呼び出され、インデックス値(正または負の値)を渡して呼び出されていることがわかります。

rb_ary_entry次に、要求された要素の位置を配列の先頭から計算し(つまり、負のインデックスが渡された場合は、同等の正の値を計算します)、を呼び出しrb_ary_eltて、要求された要素を取得します。

予想されるように、rb_ary_elt戻りnil配列の長さは場合lenであると等しいまたはそれ未満(ここで呼ばれる指標offset)。

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

シナリオ#2

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

ただし、2つの引数(つまり、開始インデックスbegとスライスの長さlen)が渡さrb_ary_subseqれると、が呼び出されます。

ではrb_ary_subseq、開始インデックスbegが配列の長さより大きい場合alennilが返されます。

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

それ以外の場合は、結果のスライスの長さlenが計算され、ゼロであると判断された場合は、空の配列が返されます。

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

したがって、開始インデックス4はを超えないためarray.lengthnil期待される値の代わりに空の配列が返されます。

質問に答えましたか?

ここでの実際の質問が「これが発生する原因となるコードは何ですか?」ではなく、「なぜMatzがこのようにしたのか」ではない場合、次のRubyConfでコーヒーを1杯購入する必要があります。彼に尋ねる。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.