Ruby에서의 배열 슬라이싱 : 비논리적 행동에 대한 설명 (Rubykoans.com에서 가져옴)


232

나는 Ruby Koans 에서 연습을 했는데 정말 설명 할 수없는 다음의 루비 문제에 부딪쳤다 .

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]않습니까? (길이 + 1) 번째 위치 에서 시작할 때 배열 슬라이싱이 이상하게 동작하는 이유가 있습니까 ?



첫 번째 숫자는 시작하는 색인이고 두 번째 숫자는 슬라이스 할 요소의 수입니다
austin

답변:


185

슬라이싱 및 인덱싱은 서로 다른 두 가지 작업이며 서로의 동작을 유추하는 것은 문제가있는 부분입니다.

slice의 첫 번째 인수는 요소가 아니라 요소 사이의 위치를 ​​식별하여 범위를 정의하고 요소 자체는 정의하지 않습니다.

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

4는 여전히 배열 안에 있습니다. 0 개의 요소를 요청하면 배열의 빈 끝이 나타납니다. 그러나 인덱스 5가 없으므로 거기서 슬라이스 할 수 없습니다.

index (와 같은 array[4]) 를 수행하면 요소 자체를 가리 키므로 인덱스는 0에서 3으로 만 이동합니다.


8
이것이 소스에 의해 백업되지 않는 한 좋은 추측입니다. 삐걱 거리지 않고 OP와 같은 다른 사람들이 묻는 이유와 같은 이유를 설명하는 링크에 관심이 있습니다. Array [4]가 nil 인 것을 제외하고 다이어그램이 의미가 있습니다. 배열 [3]은 : jelly입니다. Array [4, N]은 nil 일 것으로 예상되지만 OP가 말한 것처럼 []입니다. 그것이 장소라면, Array [4, -1]이 nil이기 때문에 그것은 쓸모없는 장소입니다. 따라서 Array [4]로는 아무것도 할 수 없습니다.
squarism

5
@squarism Charles Oliver Nutter (@twitter의 Twitter @headius)로부터 이것이 올바른 설명임을 확인했습니다. 그는 큰 JRuby 개발자이기 때문에 그의 말을 꽤 권위 있다고 생각합니다.
행크 게이


4
올바른 설명. 루비 코어에 대한 비슷한 토론 : redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune

18
"울타리 게시"라고도합니다. 다섯 번째 울타리 포스트 (ID 4)는 존재하지만 다섯 번째 요소는 존재하지 않습니다. 슬라이싱은 펜스 포스트 작업이며 인덱싱은 요소 작업입니다.
Matty 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]존재하는 네 번째 요소를 요청하지만 0 개의 요소 배열을 반환하도록 요청합니다. 동안은 array[5,0]이 전무 반환 있도록 범위를 벗어 인덱스를 요청합니다. 슬라이스 메서드가 원래 데이터 구조를 변경하지 않고 배열을 반환한다는 것을 기억하면 더 의미가 있습니다 .

편집하다:

의견을 검토 한 후이 답변을 편집하기로 결정했습니다. 슬라이스 값이 2 일 때 슬라이스는 다음 코드 스 니펫을 호출합니다 .

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 리턴을 트리거하지 않습니다. 그런 다음 두 번째 arg가 0으로 설정되면 계속 진행하여 빈 배열을 반환합니다. 5가 전달되면 배열에 5 개의 요소가 없으므로 0 인수가 평가되기 전에 nil을 리턴합니다. 여기 944 행에 코드를 작성 하십시오 .

나는 이것이 버그이거나 적어도 예측할 수 없으며 '최소한의 서프라이즈 원칙'이 아니라고 생각합니다. 몇 분이 지나면 최소한 실패한 테스트 패치를 루비 코어에 제출합니다.


2
그러나 ... 배열 [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

이것은 배열 슬라이스가 rvalue가 아닌 유효한 lvalue가 될 수 있다고 생각할 때 의미가 있습니다.

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]반환 하면 불가능합니다 . 그러나 범위를 벗어 났으므로 리턴 합니다 (4 요소 배열의 4 번째 요소 뒤에 삽입하는 것은 의미가 있지만 4 요소 배열의 5 번째 요소 뒤에 삽입하는 것은 의미가 없습니다).nil[]array[5,0]nil

array[x,y]" x에서 요소 뒤에서 시작 array, 최대 y요소 선택 "으로 슬라이스 구문 을 읽 습니다 . 이것은 array적어도 x요소 가 있는 경우에만 의미가 있습니다 .


11

수행 메이크업 감각을

해당 슬라이스에 할당 할 수 있어야하므로 문자열의 시작과 끝이 길이가 0 인 식으로 정의됩니다.

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

할당 할 때 두 번째 숫자는 무엇을합니까? 무시되는 것 같습니다. [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee 2016 년

@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

문서는 확실히 더 명확 할 수 있지만 실제 동작은 일관되고 유용합니다. 참고 : String 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

일반적이고 이해하기 쉬운 실수는 단일 인수 색인의 의미가 두 인수 시나리오 (또는 범위)에서 첫 번째 인수 의 의미와 동일하다고 가정합니다 . 그것들은 실제로 같은 것이 아니며 문서에는 이것을 반영하지 않습니다. 오류는 분명히 문서에 있으며 구현에는 없습니다.

단일 인수 : 색인은 문자열 내 단일 문자 위치를 나타냅니다. 주어진 색인에 문자가 없기 때문에 결과는 색인에서 찾은 단일 문자열이거나 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

두 개의 정수 인수 : 인수는 추출하거나 바꿀 문자열 부분을 식별합니다. 특히, 문자열의 폭이 0 인 부분을 식별하여 문자열의 앞이나 끝을 포함하여 기존 문자 앞이나 뒤에 텍스트를 삽입 할 수 있습니다. 이 경우 첫 번째 인수는 문자 위치를 식별 하지 않고 위의 다이어그램에 표시된 것처럼 문자 사이의 공백을 식별합니다. 두 번째 인수는 길이이며 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"

범위의 동작은 매우 흥미 롭습니다. 시작점은 위에서 설명한 것처럼 두 개의 인수가 제공 될 때 첫 번째 인수와 동일하지만 범위의 끝점은 단일 인덱싱에서와 같이 '문자 위치'이거나 두 개의 정수 인수에서와 같이 "가장자리 위치"일 수 있습니다. 차이는 더블 도트 범위 또는 트리플 도트 범위의 사용 여부에 따라 결정됩니다.

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.

이 예제를 다시 살펴보고 double 또는 range 인덱싱 예제에 단일 인덱스 의미론을 사용하고 고집한다면 혼란 스러울 것입니다. 실제 동작을 모델링하기 위해 아스키 다이어그램에 표시되는 대체 번호 매기기를 사용해야합니다.


3
그 스레드의 주요 아이디어를 포함시킬 수 있습니까? (링크가 언젠가 유효하지 않은 경우)
VonC

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

Jim Weirich가 제공 한 설명

그것에 대해 생각하는 한 가지 방법은 인덱스 위치 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.cArray#slice초래하는 1 개 또는 2 개의 인수를 전달하는지에 따라 다른 함수가 호출 됩니다.

(먼저, 나는 C로 코딩하지 않지만 몇 년 동안 루비를 사용하고 있다고 지적하고 싶습니다. 따라서 C에 익숙하지 않지만 기본 사항에 익숙해지는 데 몇 분이 걸립니다. 함수와 변수에 대해서는 아래에 설명 된 것처럼 Ruby 소스 코드를 따르는 것이 어렵지 않습니다.이 답변은 Ruby v2.3을 기반으로하지만 v1.9와 거의 동일합니다.)

시나리오 # 1

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

Array#slice( rb_ary_aref) 에 대한 소스 코드를 보면 하나의 인수 만 전달되면 ( 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입니다 보다 큰 배열 길이 alen, nil반환됩니다

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

그렇지 않으면 결과 슬라이스의 길이 len가 계산되고 0으로 결정되면 빈 배열이 반환됩니다.

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.length있는 nil값 대신 빈 배열이 반환됩니다 .

질문이 있습니까?

여기서 실제 질문이 "어떤 코드로 인해 이런 일이 발생합니까?"가 아니라 "왜 Matz가 이런 식으로 작업 했습니까?"가 아니라면 다음 RubyConf에서 커피 한 잔을 사야합니다. 물어.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.