Cắt mảng trong Ruby: giải thích cho hành vi phi logic (lấy từ Rubykoans.com)


232

Tôi đang trải qua các bài tập trong Ruby Koans và tôi đã bị ấn tượng bởi trò chơi Ruby sau đây mà tôi thấy thực sự không thể giải thích được:

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

Vậy tại sao array[5,0]không bằng array[4,0]? Có bất kỳ lý do tại sao mảng cắt cư xử kỳ lạ này khi bạn bắt đầu tại (chiều dài + 1) lần thứ tư thế ??



Có vẻ như số đầu tiên là chỉ số bắt đầu, số thứ hai là có bao nhiêu phần tử để cắt
austin

Câu trả lời:


185

Cắt lát và lập chỉ mục là hai thao tác khác nhau và suy ra hành vi của cái này từ cái kia là vấn đề của bạn.

Đối số đầu tiên trong lát xác định không phải phần tử mà là vị trí giữa các phần tử, xác định khoảng cách (và không phải chính phần tử):

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

4 vẫn còn trong mảng, chỉ vừa đủ; nếu bạn yêu cầu 0 phần tử, bạn sẽ nhận được kết thúc trống của mảng. Nhưng không có chỉ số 5, vì vậy bạn không thể cắt từ đó.

Khi bạn thực hiện chỉ mục (như array[4]), bạn đang chỉ vào các phần tử, vì vậy các chỉ số chỉ đi từ 0 đến 3.


8
Một dự đoán tốt trừ khi điều này được hỗ trợ bởi nguồn. Không được lén lút, tôi sẽ quan tâm đến một liên kết nếu có chỉ để giải thích "tại sao" như OP và các nhà bình luận khác đang hỏi. Sơ đồ của bạn có ý nghĩa ngoại trừ Array [4] là không. Mảng [3] là: thạch. Tôi hy vọng Array [4, N] sẽ không nhưng nó [] giống như OP nói. Nếu đó là một nơi, đó là một nơi khá vô dụng vì Mảng [4, -1] là con số không. Vì vậy, bạn không thể làm bất cứ điều gì với Array [4].
squarism

5
@squarism Tôi vừa nhận được xác nhận từ Charles Oliver Nutter (@headius trên Twitter) rằng đây là lời giải thích chính xác. Anh ấy là một nhà phát triển lớn của JRuby, vì vậy tôi cho rằng từ của anh ấy khá có thẩm quyền.
Hank Gay

18
Sau đây là lời biện minh cho hành vi này: blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/380637
Matt Briançon

4
Giải thích chính xác. Các cuộc thảo luận tương tự về ruby-core: redmine.ruby-lang.org/issues/4245 , redmine.ruby-lang.org/issues/4541
Marc-André Lafortune

18
Còn được gọi là "đăng hàng rào." Hàng rào thứ năm (id 4) tồn tại, nhưng phần tử thứ năm thì không. Cắt lát là một hoạt động bài hàng rào, lập chỉ mục là một hoạt động yếu tố.
Matty K

27

điều này có liên quan đến thực tế là lát trả về một mảng, tài liệu nguồn có liên quan từ Array # lát:

 *  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

Điều này gợi ý cho tôi rằng nếu bạn đưa ra sự khởi đầu nằm ngoài giới hạn, nó sẽ trả về con số không, do đó trong ví dụ của bạn array[4,0]yêu cầu phần tử thứ 4 tồn tại, nhưng yêu cầu trả về một mảng các phần tử bằng không. Trong khi array[5,0]yêu cầu một chỉ số ngoài giới hạn để nó trả về con số không. Điều này có lẽ có ý nghĩa hơn nếu bạn nhớ rằng phương thức lát đang trả về một cái mới mảng , không làm thay đổi cấu trúc dữ liệu gốc.

BIÊN TẬP:

Sau khi xem xét các ý kiến ​​tôi quyết định chỉnh sửa câu trả lời này. Slice gọi đoạn mã sau khi giá trị arg là hai:

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

nếu bạn nhìn vào array.clớp nơi rb_ary_subseqphương thức được định nghĩa, bạn sẽ thấy rằng nó sẽ trả về nil nếu độ dài nằm ngoài giới hạn, không phải là chỉ mục:

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

Trong trường hợp này, đây là những gì đang xảy ra khi 4 được truyền vào, nó kiểm tra xem có 4 phần tử và do đó không kích hoạt trả về nil. Sau đó nó tiếp tục và trả về một mảng trống nếu đối số thứ hai được đặt thành 0. trong khi nếu 5 được truyền vào, không có 5 phần tử trong mảng, vì vậy nó sẽ trả về nil trước khi ước lượng zero được ước tính. mã ở đây tại dòng 944.

Tôi tin rằng đây là một lỗi, hoặc ít nhất là không thể đoán trước và không phải là 'Nguyên tắc bất ngờ tối thiểu'. Khi tôi nhận được một vài phút, tôi sẽ ít nhất gửi một bản vá thử nghiệm thất bại đến lõi ruby.


2
Nhưng ... phần tử được chỉ ra bởi 4 trong mảng [4.0] cũng không tồn tại ... - bởi vì nó thực sự là phần tử thứ 5 (đếm dựa trên 0, xem các ví dụ). Vì vậy, nó là ra khỏi giới hạn là tốt.
Pascal Van Hecke

1
bạn đúng. Tôi đã quay lại và xem xét nguồn, và có vẻ như đối số đầu tiên được xử lý bên trong mã c là độ dài chứ không phải chỉ mục. Tôi sẽ chỉnh sửa câu trả lời của tôi, để phản ánh điều này. Tôi nghĩ rằng điều này có thể được gửi như là một lỗi.
Jed Schneider

23

Ít nhất lưu ý rằng các hành vi là phù hợp. Từ 5 trở lên mọi thứ đều hoạt động như nhau; sự kỳ lạ chỉ xảy ra ở[4,N] .

Có lẽ mô hình này có ích, hoặc có thể tôi chỉ mệt mỏi và nó không giúp ích gì cả.

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

Tại [4,0], chúng tôi bắt cuối của mảng. Tôi thực sự thấy nó khá kỳ lạ, theo như vẻ đẹp trong các mẫu, nếu cái cuối cùng trở lại nil. Do bối cảnh như thế này, 4là một tùy chọn chấp nhận được cho tham số đầu tiên để mảng trống có thể được trả về. Tuy nhiên, khi chúng tôi đạt 5 điểm trở lên, phương thức có thể thoát ra ngay lập tức do bản chất hoàn toàn và hoàn toàn nằm ngoài giới hạn.


12

Điều này có ý nghĩa khi bạn xem xét hơn một mảng mảng có thể là một giá trị hợp lệ, không chỉ là một giá trị:

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]

Điều này sẽ không thể nếu được array[4,0]trả lại nilthay vì []. Tuy nhiên, array[5,0]trả về nilvì nó nằm ngoài giới hạn (chèn sau phần tử thứ 4 của mảng 4 phần tử là có ý nghĩa, nhưng chèn sau phần tử thứ 5 của mảng 4 phần tử thì không).

Đọc cú pháp lát array[x,y]là "bắt đầu sau xcác phần tử trong array, chọn tối đa ycác phần tử". Điều này chỉ có ý nghĩa nếu arraycó ít nhất xcác yếu tố.


11

Điều này ý nghĩa

Bạn cần có khả năng gán cho các lát đó, để chúng được xác định theo cách sao cho phần đầu và phần cuối của chuỗi có các biểu thức có độ dài bằng không.

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

1
Bạn cũng có thể gán cho phạm vi mà lát trả về là 0, vì vậy sẽ rất hữu ích khi mở rộng giải thích này. array[5,0]=:foo # array is now [:peanut, :butter, :and, :jelly, nil, :foo]
mfazekas

Số thứ hai làm gì khi gán? nó dường như bị bỏ qua [26] pry(main)> array[4,5] = [:love, :hope, :peace] => [:peanut, :butter, :and, :jelly, :love, :hope, :peace]
Drew Verlee

@drewverlee nó không bị bỏ qua:array = [:a, :b, :c, :d, :e]; array[1,2] = :x, :x; array => [:a, :x, :x, :d, :e]
fanaugen 7/07/2015

10

Tôi thấy lời giải thích của Gary Wright cũng rất hữu ích. http://www.ruby-forum.com/topic/1393096#990065

Câu trả lời của Gary Wright là -

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

Các tài liệu chắc chắn có thể rõ ràng hơn nhưng hành vi thực tế là tự nhất quán và hữu ích. Lưu ý: Tôi đang giả sử phiên bản Chuỗi 1.9.X.

Nó giúp xem xét việc đánh số theo cách sau:

  -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

Lỗi phổ biến (và dễ hiểu) là quá giả định rằng ngữ nghĩa của chỉ số đối số duy nhất giống như ngữ nghĩa của lần đầu tiên đối số trong hai kịch bản đối số (hoặc phạm vi). Chúng không giống nhau trong thực tế và tài liệu không phản ánh điều này. Lỗi mặc dù chắc chắn là trong tài liệu và không phải trong quá trình thực hiện:

đối số duy nhất: chỉ mục biểu thị một vị trí ký tự đơn trong chuỗi. Kết quả là chuỗi ký tự đơn được tìm thấy tại chỉ mục hoặc nil vì không có ký tự nào trong chỉ mục đã cho.

  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

hai đối số nguyên: các đối số xác định một phần của chuỗi cần trích xuất hoặc thay thế. Đặc biệt, các phần có độ rộng bằng không của chuỗi cũng có thể được xác định để văn bản có thể được chèn trước hoặc sau các ký tự hiện có, kể cả ở phía trước hoặc cuối chuỗi. Trong trường hợp này, đối số đầu tiên không xác định vị trí ký tự mà thay vào đó xác định khoảng trắng giữa các ký tự như trong sơ đồ trên. Đối số thứ hai là độ dài, có thể là 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"

Hành vi của một phạm vi là khá thú vị. Điểm bắt đầu giống như đối số đầu tiên khi hai đối số được cung cấp (như được mô tả ở trên) nhưng điểm cuối của phạm vi có thể là 'vị trí ký tự' như với lập chỉ mục đơn hoặc "vị trí cạnh" như với hai đối số nguyên. Sự khác biệt được xác định bởi liệu phạm vi hai chấm hoặc ba chấm được sử dụng:

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.

Nếu bạn quay lại các ví dụ này và nhấn mạnh và sử dụng ngữ nghĩa chỉ mục duy nhất cho các ví dụ lập chỉ mục hai hoặc phạm vi, bạn sẽ chỉ bị nhầm lẫn. Bạn đã phải sử dụng cách đánh số thay thế mà tôi hiển thị trong sơ đồ ascii để mô hình hóa hành vi thực tế.


3
Bạn có thể bao gồm ý tưởng chính của chủ đề đó? (trong trường hợp liên kết một ngày trở nên không hợp lệ)
VonC

8

Tôi đồng ý rằng điều này có vẻ giống như hành vi lạ, nhưng ngay cả tài liệu chính thức về việcArray#slice thể hiện hành vi tương tự như trong ví dụ của bạn, trong "trường hợp đặc biệt" dưới đây:

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

Thật không may, ngay cả mô tả của họ về Array#slicedường như không cung cấp bất kỳ cái nhìn sâu sắc nào về lý do tại sao nó hoạt động theo cách này:

Phần tử tham chiếu phần tử Trả về phần tử tại chỉ mục hoặc trả về một phân đoạn bắt đầu khi bắt đầu và tiếp tục cho các phần tử độ dài hoặc trả về một phân đoạn được chỉ định theo phạm vi . Các chỉ số tiêu cực đếm ngược từ cuối mảng (-1 là phần tử cuối cùng). Trả về nil nếu chỉ mục (hoặc chỉ mục bắt đầu) nằm ngoài phạm vi.


7

Một lời giải thích được cung cấp bởi Jim Weirich

Một cách để nghĩ về nó là vị trí chỉ số 4 nằm ở rìa của mảng. Khi yêu cầu một lát, bạn trả lại càng nhiều mảng còn lại. Vì vậy, hãy xem xét mảng [2,10], mảng [3,10] và mảng [4,10] ... mỗi mảng trả về các bit còn lại của cuối mảng: lần lượt là 2 phần tử, 1 phần tử và 0 phần tử. Tuy nhiên, vị trí 5 rõ ràng nằm ngoài mảng và không nằm ở cạnh, vì vậy mảng [5,10] trả về nil.


6

Hãy xem xét các mảng sau:

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

Bạn có thể chèn một mục vào đầu (đầu) của mảng bằng cách gán nó vào a[0,0]. Để đặt phần tử giữa "a""b", sử dụng a[1,0]. Về cơ bản, trong ký hiệu a[i,n], iđại diện cho một chỉ mục và nmột số yếu tố. Khi n=0, nó xác định một vị trí giữa các phần tử của mảng.

Bây giờ nếu bạn nghĩ về sự kết thúc của mảng, làm thế nào bạn có thể nối một mục vào cuối của nó bằng cách sử dụng ký hiệu được mô tả ở trên? Đơn giản, gán giá trị cho a[3,0]. Đây là đuôi của mảng.

Vì vậy, nếu bạn cố gắng truy cập phần tử tại a[3,0], bạn sẽ nhận được []. Trong trường hợp này, bạn vẫn ở trong phạm vi của mảng. Nhưng nếu bạn cố gắng truy cập a[4,0], bạn sẽ nhận được nilgiá trị trả về, vì bạn không còn nằm trong phạm vi của mảng nữa.

Đọc thêm về nó tại http://mybrainstormings.wordpress.com/2012/09/10/arrays-in-ruby/ .


0

tl; dr: trong mã nguồn trong array.c, các hàm khác nhau được gọi tùy thuộc vào việc bạn truyền 1 hoặc 2 đối số vào để Array#slicedẫn đến các giá trị trả về không mong muốn.

(Trước hết, tôi muốn chỉ ra rằng tôi không viết mã bằng C, nhưng đã sử dụng Ruby trong nhiều năm. Vì vậy, nếu bạn không quen thuộc với C, nhưng bạn mất vài phút để làm quen với những điều cơ bản về các hàm và biến thực sự không khó để theo mã nguồn Ruby, như được trình bày dưới đây. Câu trả lời này dựa trên Ruby v2.3, nhưng ít nhiều giống với v1.9.)

Cảnh 1

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

Nếu bạn nhìn vào mã nguồn cho Array#slice( rb_ary_aref), bạn sẽ thấy rằng khi chỉ có một đối số được truyền vào ( dòng 1277-1289 ), rb_ary_entryđược gọi, chuyển vào giá trị chỉ mục (có thể dương hoặc âm).

rb_ary_entrysau đó tính toán vị trí của phần tử được yêu cầu từ đầu mảng (nói cách khác, nếu chỉ số âm được truyền vào, nó sẽ tính toán tương đương dương) và sau đó gọi rb_ary_eltđể lấy phần tử được yêu cầu.

Đúng như dự đoán, rb_ary_eltlợi nhuận nilkhi chiều dài của mảng lennhỏ hơn hoặc bằng với chỉ số (ở đây gọi offset).

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

Kịch bản # 2

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

Tuy nhiên, khi 2 đối số được truyền vào (tức là chỉ mục bắt đầu begvà độ dài của lát len), rb_ary_subseqđược gọi.

Trong rb_ary_subseq, nếu chỉ số bắt đầu beglớn hơn chiều dài mảng alen, nilđược trả về:

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

Mặt khác, độ dài của lát kết quả lenđược tính toán và nếu nó được xác định bằng 0, một mảng trống được trả về:

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

Vì vậy, vì chỉ số bắt đầu là 4 không lớn hơn array.length, một mảng trống được trả về thay vì nilgiá trị mà người ta có thể mong đợi.

Câu hỏi đã trả lời?

Nếu câu hỏi thực tế ở đây không phải là "Mã nào khiến điều này xảy ra?", Mà là "Tại sao Matz lại làm theo cách này?", Thì bạn sẽ phải mua cho anh ấy một tách cà phê tại RubyConf tiếp theo và hỏi anh ấy.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.