Чому при побудові рядка в Ruby оператор лопати (<<) надає перевагу над plus-equals (+ =)?


156

Я працюю через Ruby Koans.

test_the_shovel_operator_modifies_the_original_stringKoan в about_strings.rb включає наступний коментар:

Рубінські програмісти, як правило, віддають перевагу оператору лопати (<<) над оператором плюс дорівнює (+ =) під час створення рядків. Чому?

Я здогадуюсь, що це передбачає швидкість, але я не розумію дії під кришкою, що призведе до того, що оператор лопати буде швидшим.

Чи зможе хтось, будь ласка, пояснити деталі, що стоять за цією перевагою?


4
Оператор лопати змінює об'єкт String, а не створює новий об'єкт String (вартість пам'яті). Хіба синтаксис не гарний? пор. У Java та .NET є класи StringBuilder
полковник Паніка

Відповіді:


257

Доказ:

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

Так <<змінюється оригінальний рядок, а не створюється новий. Причиною цього є те, що в рубіні a += bє синтаксична стенограма для a = a + b(те саме стосується інших <op>=операторів), що є призначенням. З іншого боку <<- псевдонім, concat()який змінює приймач на місці.


3
Спасибі, локшина! Отже, по суті, << швидше, тому що не створює нових об'єктів?
erinbrown

1
Цей орієнтир говорить, що Array#joinце повільніше, ніж використання <<.
Ендрю Грімм

5
Один з хлопців EdgeCase опублікував пояснення з номерами виступу: Трохи більше про струни
Цинциннаті Джо

8
Наведене вище посилання @CincinnatiJoe виявляється порушеним, ось нове: Трохи більше про струни
jasoares

Для java folks: "+" оператор у Ruby відповідає додаванню через StringBuilder об'єкт, а "<<" відповідає об'єднанню об'єктів String
nanosoft

79

Доказ продуктивності:

#!/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

Друг, який вивчає Рубі як свою першу мову програмування, задав мені це саме запитання, переходячи через Strings in Ruby в серії Ruby Koans. Я пояснив це йому за допомогою наступної аналогії;

У вас є склянка води, яка наполовину наповнена, і вам потрібно заправити склянку.

Перший спосіб це зробити, взявши новий стакан, наповнивши його наполовину водою з-під крана, а потім використовуючи цей другий напівповний склянку для наповнення питної склянки. Ви робите це кожен раз, коли вам потрібно поповнити склянку.

Другий спосіб ви берете свою половину повного склянки і просто заправляєте її водою прямо з крана.

Зрештою, у вас буде більше склянок для чищення, якщо ви вирішите вибрати новий стакан кожного разу, коли вам потрібно було заправити келих.

Те саме стосується оператора лопати та оператора плюс рівний. Плюс рівний оператор вибирає нову «склянку» щоразу, коли їй потрібно заправляти свою склянку, тоді як оператор лопати просто бере той самий стакан і заправляє. Наприкінці дня більше збірки зі скла для оператора Plus.


2
Чудова аналогія, сподобалася.
GMA

5
чудова аналогія, але жахливі висновки. Вам слід додати, що окуляри чистять хтось інший, так що вам не доведеться про них дбати.
Філіп Бартузі

1
Відмінна аналогія, я думаю, що вона приходить до прекрасного висновку. Я думаю, що менше стосується того, хто повинен почистити скло, і більше про кількість келихів, які взагалі використовуються. Ви можете собі уявити, що певні програми висувають межі пам’яті на своїх машинах і що ці машини можуть одночасно очищати лише певну кількість стекол.
Чарлі Л

11

Це старе питання, але я просто наткнувся на це, і я не повністю задоволений наявними відповідями. Є багато хороших моментів щодо того, що лопата << швидша, ніж конкатенація + =, але є і семантичний розгляд.

Прийнята відповідь від @noodl показує, що << змінює існуючий об'єкт на місці, тоді як + = створює новий об'єкт. Тому вам потрібно врахувати, чи хочете, щоб усі посилання на рядок відображали нове значення, чи ви хочете залишити існуючі посилання в спокої та створити нове значення рядка для локального використання. Якщо вам потрібні всі посилання для відображення оновленого значення, тоді вам потрібно використовувати <<. Якщо ви хочете залишити інші посилання в спокої, тоді вам потрібно використовувати + =.

Дуже поширений випадок полягає в тому, що існує лише одна посилання на рядок. У цьому випадку смислова різниця не має значення і природно віддати перевагу << через її швидкість.


10

Оскільки це швидше / не створює копію рядка <-> сміттєзбірник не потрібно запускати.


Хоча вищенаведені відповіді дають більше деталей, це єдиний, який поєднує їх для повної відповіді. Ключовим тут, здається, є у формулюванні сенс вашого "нарощування рядків", це означає, що ви не хочете або не потребуєте оригінальних рядків.
Дрю Верлі

Ця відповідь ґрунтується на помилковій передумові: і розподіл, і звільнення короткотривалих об'єктів по суті є вільними в будь-якому півдорозі гідного сучасного ГК. Це принаймні так швидко, як розподіл стека в C і значно швидше, ніж malloc/ free. Також деякі більш сучасні реалізації Ruby, ймовірно, повністю оптимізують розподіл об'єктів та конкатенацію рядків. Ото, мутує об'єкти страшний для виконання GC.
Йорг W Міттаг

4

У той час як більшість відповідей кришки +=повільніше , тому що він створює нову копію, це важливо мати на увазі , що +=і << не є взаємозамінні! Ви хочете використовувати кожен у різних випадках.

Використання <<також змінить будь-які змінні, на які вказано b. Тут ми також мутуємо, aколи можемо не захотіти.

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"

Оскільки +=робить нову копію, вона також залишає незмінними будь-які змінні, які вказують на неї.

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"

Розуміння цієї відмінності може врятувати вам багато головних болів, коли ви маєте справу з петлями!


2

Хоча це не пряма відповідь на ваше запитання, чому " Повністю перевернутий контейнер" завжди був однією з моїх улюблених статей Рубі. Він також містить деяку інформацію про рядки щодо вивезення сміття.


Дякую за пораду, Майкл! Я ще не зайшов так далеко в Рубі, але це обов'язково стане в нагоді в майбутньому.
erinbrown
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.