Tại sao toán tử xẻng (<<) được ưa thích hơn plus-bằng (+ =) khi xây dựng chuỗi trong Ruby?


156

Tôi đang làm việc thông qua Ruby Koans.

Các test_the_shovel_operator_modifies_the_original_stringKoan trong about_strings.rb bao gồm những nhận xét sau đây:

Các lập trình viên Ruby có xu hướng ủng hộ toán tử xẻng (<<) hơn toán tử cộng bằng (+ =) khi xây dựng chuỗi. Tại sao?

Tôi đoán là nó liên quan đến tốc độ, nhưng tôi không hiểu hành động dưới mui xe sẽ khiến người điều khiển xẻng nhanh hơn.

Ai đó có thể vui lòng giải thích các chi tiết đằng sau sở thích này?


4
Toán tử xẻng sửa đổi đối tượng String thay vì tạo một đối tượng String mới (tính phí bộ nhớ). Cú pháp có đẹp không? xem Java và .NET có các lớp StringBuilder
Đại tá Panic

Câu trả lời:


257

Bằng chứng:

a = 'foo'
a.object_id #=> 2154889340
a << 'bar'
a.object_id #=> 2154889340
a += 'quux'
a.object_id #=> 2154742560

Vì vậy, <<thay đổi chuỗi ban đầu thay vì tạo một chuỗi mới. Lý do cho điều này là trong ruby a += blà cách viết tắt cú pháp cho a = a + b(tương tự với các <op>=toán tử khác ) là một bài tập. Mặt khác, <<một bí danh concat()làm thay đổi máy thu tại chỗ.


3
Cảm ơn, noodl! Vì vậy, về bản chất, << nhanh hơn vì nó không tạo ra các đối tượng mới?
erinbrown

1
Điểm chuẩn này nói rằng Array#joinchậm hơn so với sử dụng <<.
Andrew Grimm

5
Một trong những người của EdgeCase đã đăng một lời giải thích với số hiệu suất: A Little More About String
Cincinnati Joe

8
Liên kết @CincinnatiJoe ở trên dường như bị phá vỡ, đây là một liên kết mới: Một chút nữa về chuỗi
jasoares

Đối với người dùng java: toán tử '+' trong Ruby tương ứng với việc nối thêm thông qua đối tượng StringBuilder và '<<' tương ứng với việc ghép các đối tượng String
nanosoft

79

Bằng chứng hiệu suất:

#!/usr/bin/env ruby

require 'benchmark'

Benchmark.bmbm do |x|
  x.report('+= :') do
    s = ""
    10000.times { s += "something " }
  end
  x.report('<< :') do
    s = ""
    10000.times { s << "something " }
  end
end

# Rehearsal ----------------------------------------
# += :   0.450000   0.010000   0.460000 (  0.465936)
# << :   0.010000   0.000000   0.010000 (  0.009451)
# ------------------------------- total: 0.470000sec
# 
#            user     system      total        real
# += :   0.270000   0.010000   0.280000 (  0.277945)
# << :   0.000000   0.000000   0.000000 (  0.003043)

70

Một người bạn đang học Ruby như ngôn ngữ lập trình đầu tiên của anh ấy đã hỏi tôi câu hỏi tương tự trong khi xem qua String in Ruby trên loạt Ruby Koans. Tôi đã giải thích nó cho anh ta bằng cách sử dụng sự tương tự sau đây;

Bạn có một ly nước đầy một nửa và bạn cần đổ đầy lại ly của mình.

Cách đầu tiên bạn làm điều đó bằng cách lấy một ly mới, đổ đầy nửa chừng bằng nước từ vòi và sau đó sử dụng ly đầy nửa thứ hai này để đổ đầy ly uống của bạn. Bạn làm điều này mỗi khi bạn cần đổ đầy lại ly của mình.

Cách thứ hai bạn lấy một nửa ly đầy của bạn và chỉ cần đổ đầy nó bằng nước trực tiếp từ vòi.

Vào cuối ngày, bạn sẽ có nhiều kính để lau hơn nếu bạn chọn chọn một chiếc kính mới mỗi khi bạn cần đổ đầy lại ly của mình.

Điều tương tự áp dụng cho toán tử xẻng và toán tử cộng bằng nhau. Cộng với người vận hành bằng nhau chọn một 'kính' mới mỗi lần cần đổ đầy kính trong khi người vận hành xẻng chỉ lấy cùng một ly và đổ đầy lại. Vào cuối ngày, bộ sưu tập 'kính' nhiều hơn cho toán tử Plus bằng nhau.


2
Tương tự lớn, yêu nó.
GMA

5
tương tự lớn nhưng kết luận khủng khiếp. Bạn sẽ phải thêm rằng kính được làm sạch bởi người khác để bạn không phải quan tâm đến chúng.
Filip Bartuzi

1
Sự tương tự tuyệt vời, tôi nghĩ rằng nó đi đến một kết luận tốt. Tôi nghĩ ít hơn về việc ai phải lau kính và nhiều hơn về số lượng kính được sử dụng. Bạn có thể tưởng tượng rằng các ứng dụng nhất định đang đẩy giới hạn bộ nhớ trên máy của họ và những máy đó chỉ có thể làm sạch một số lượng kính nhất định tại một thời điểm.
Charlie L

11

Đây là một câu hỏi cũ, nhưng tôi chỉ lướt qua nó và tôi không hoàn toàn hài lòng với các câu trả lời hiện có. Có rất nhiều điểm hay về cái xẻng << nhanh hơn so với ghép + =, nhưng cũng có một sự cân nhắc về ngữ nghĩa.

Câu trả lời được chấp nhận từ @noodl cho thấy << sửa đổi đối tượng hiện có tại chỗ, trong khi + = tạo ra một đối tượng mới. Vì vậy, bạn cần xem xét nếu bạn muốn tất cả các tham chiếu đến chuỗi phản ánh giá trị mới hoặc bạn muốn để riêng các tham chiếu hiện có và tạo một giá trị chuỗi mới để sử dụng cục bộ. Nếu bạn cần tất cả các tham chiếu để phản ánh giá trị được cập nhật, thì bạn cần sử dụng <<. Nếu bạn muốn để các tài liệu tham khảo khác một mình, thì bạn cần sử dụng + =.

Một trường hợp rất phổ biến là chỉ có một tham chiếu duy nhất cho chuỗi. Trong trường hợp này, sự khác biệt về ngữ nghĩa không thành vấn đề và việc tự nhiên thích << vì tốc độ của nó là điều tự nhiên.


10

Bởi vì nó nhanh hơn / không tạo ra một bản sao của chuỗi <-> trình thu gom rác không cần phải chạy.


Trong khi các câu trả lời ở trên cung cấp thêm chi tiết thì đây là câu duy nhất đặt chúng lại với nhau để có câu trả lời hoàn chỉnh. Chìa khóa ở đây dường như theo nghĩa từ ngữ "xây dựng chuỗi" của bạn, nó ngụ ý rằng bạn không muốn hoặc không cần chuỗi gốc.
Drew Verlee

Câu trả lời này dựa trên một tiền đề sai lầm: cả việc phân bổ và giải phóng các đối tượng tồn tại trong thời gian ngắn về cơ bản là miễn phí trong bất kỳ GC hiện đại nào. Nó ít nhất là nhanh như phân bổ ngăn xếp trong C và nhanh hơn đáng kể so với malloc/ free. Ngoài ra, một số triển khai Ruby hiện đại hơn có thể sẽ tối ưu hóa việc phân bổ đối tượng và nối chuỗi hoàn toàn. OTOH, các đối tượng đột biến là khủng khiếp đối với hiệu suất của GC.
Jörg W Mittag

4

Mặc dù phần lớn các câu trả lời +=chậm hơn vì nó tạo ra một bản sao mới, điều quan trọng là phải ghi nhớ điều đó +=<< không thể thay thế cho nhau! Bạn muốn sử dụng mỗi trong các trường hợp khác nhau.

Sử dụng <<cũng sẽ thay đổi bất kỳ biến được chỉ đến b. Ở đây chúng tôi cũng đột biến akhi chúng tôi có thể không muốn.

2.3.1 :001 > a = "hello"
 => "hello"
2.3.1 :002 > b = a
 => "hello"
2.3.1 :003 > b << " world"
 => "hello world"
2.3.1 :004 > a
 => "hello world"

Bởi vì +=tạo một bản sao mới, nó cũng để lại bất kỳ biến nào được trỏ đến nó không thay đổi.

2.3.1 :001 > a = "hello"
 => "hello"
2.3.1 :002 > b = a
 => "hello"
2.3.1 :003 > b += " world"
 => "hello world"
2.3.1 :004 > a
 => "hello"

Hiểu được sự khác biệt này có thể giúp bạn tiết kiệm rất nhiều vấn đề đau đầu khi bạn xử lý các vòng lặp!


2

Mặc dù không phải là câu trả lời trực tiếp cho câu hỏi của bạn, tại sao Thùng hoàn toàn bị đảo lộn luôn là một trong những bài viết yêu thích của tôi về Ruby. Nó cũng chứa một số thông tin về các chuỗi liên quan đến việc thu gom rác.


Cảm ơn bạn vì tiền boa, Michael! Tôi chưa nhận được điều đó ở Ruby, nhưng nó chắc chắn sẽ có ích trong tương lai.
xóa
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.