Почему некоторые методы Ruby, такие как String#, заменяют измененные копии переменных?

Итак, во-первых, я только изучаю Ruby и использую фон JavaScript. У меня есть проблема, на которую я не могу найти ответ. У меня есть этот пример:

a = 'red'
b = a
b.replace('blue')
b = 'green'
print a

синий

Мой вопрос: почему это вещь? Я понимаю, что настройки b = a делает их одним и тем же object_id, так что технически существует два имени для одной и той же переменной строки. Но я никогда не вижу причин для использования такого рода рекурсивного изменения значения. Если я устанавливаю b = a это потому, что я хочу манипулировать значением а, не меняя его.

Кроме того, иногда кажется, что метод изменит a, но иногда это приведет к тому, что "b" станет новым объектом. Это кажется неоднозначным и не имеет смысла.

Когда я буду когда-либо использовать это? Какой смысл? Означает ли это, что я не могу передать значение a в другую переменную без каких-либо изменений, распространяющихся обратно к a?

3 ответа

Решение

Проблема здесь не называется рекурсией, и переменные Ruby не являются рекурсивными (для любого нормального значения слова - т.е. они не ссылаются на себя, и вам не нужны рекурсивные процедуры для работы с ними). Рекурсия в компьютерном программировании - это когда код вызывает себя, прямо или косвенно, такой как функция, которая содержит вызов для себя.

В Ruby все переменные указывают на объекты. Это без исключения - хотя есть некоторые внутренние приемы, чтобы сделать вещи быстро, даже писать a=5 создает переменную с именем a и "указывает" на Fixnum объект 5 - тщательный языковой дизайн означает, что вы почти не замечаете этого. Самое главное, числа не могут измениться (вы не можете изменить 5 в 6, они всегда разные объекты), так что вы можете думать, что как-то a "содержит" 5 и сойти с рук, хотя технически a указывает на 5,

С помощью Strings объекты могут быть изменены. Пошаговое объяснение вашего примера кода может выглядеть следующим образом:

a = 'red'

Создает новый String объект с содержимым "красный" и переменная points a на него.

b = a

Переменная очков b к тому же объекту, что и a,

b.replace('blue')

Вызывает replace метод объекта, на который указывает b (а также указал a) Метод изменяет содержимое строки на "синий".

b = 'green'; 

Создает новый String объект с содержимым "зелёный" и переменная points b на него. Переменные a а также b Теперь укажите на разные объекты.

print a 

Объект String, на который указывает a имеет содержимое "синий". Так что все работает правильно, в соответствии со спецификацией языка.

Когда я буду когда-либо использовать это?

Все время. В Ruby вы используете переменные, чтобы временно указывать на объекты, чтобы вызывать методы для них. Объекты - это то, с чем вы хотите работать, переменные - это имена в вашем коде, которые вы используете для ссылки на них. Тот факт, что они являются отдельными, может сбивать вас с толку время от времени (особенно в Ruby со строками, где многие другие языки не имеют такого поведения)

и означает ли это, что я не могу передать значение "a" в другую переменную без каких-либо изменений, возвращающихся обратно в "a"?

Если вы хотите скопировать строку, есть несколько способов сделать это. Например

b = a.clone

или же

b = "#{a}"

Однако на практике вы редко просто хотите делать прямые копии строк. Вы захотите сделать что-то еще, что связано с целью вашего кода. Обычно в Ruby существует метод, который выполняет необходимые вам манипуляции и возвращает новую строку, поэтому вы должны сделать что-то подобное

b = a.something

В других случаях вы фактически захотите внести изменения в исходный объект. Все зависит от того, какова цель вашего кода. Изменения на месте в объектах String могут быть полезны, поэтому Ruby их поддерживает.

Кроме того, иногда кажется, что метод вернется в "a", а иногда это приведет к тому, что "b" станет новым object_id.

Это никогда не так. Никакие методы не изменят идентичность объекта. Однако большинство методов возвращает новый объект. Некоторые методы изменят содержимое объекта - это те методы в Ruby, о которых вам нужно больше знать из-за возможности изменения данных, используемых в других местах - то же самое верно и в других языках OO, объекты JavaScript здесь не исключение, они ведут себя точно так же.

Это может быть полезно в сценарии при работе с рекурсией в хэше.

obj = {}
ary = [1,2,3]

temp_obj = obj

ary.each do |entry|
  temp_obj[entry] = {}
  temp_obj = temp_obj[entry]
end

> obj
=> {1=>{2=>{3=>{}}}}

Если вы хотите дублировать, вы можете просто использовать dup

> a = 'red'
=> "red"
> b = a.dup
=> "red"
> b.replace('orange')
=> "orange"
> a
=> "red"
> b
=> "orange"

тем не мение dup не делает deep_copy, как указано в комментариях, см. пример

> a = {hello: {world: 1}}
 => {:hello=>{:world=>1}}
> b = a.dup
 => {:hello=>{:world=>1}}
> b[:hello][:world] = 4
 => 4
> a
 => {:hello=>{:world=>4}}
> b
 => {:hello=>{:world=>4}}

TL;DR

В исходном вопросе, который сейчас редактируется, вы путаете рекурсию с мутацией и распространением. Все три понятия являются полезными инструментами в правильных ситуациях и при ожидаемом поведении. Вы, вероятно, сочтете конкретный пример, который вы опубликовали, запутанным, потому что вы не ожидаете, что строка будет видоизменяться или изменение будет распространяться по всем указателям на этот объект.

Способность обобщать методы - это то, что позволяет типизировать утку в динамических языках, таких как Ruby. Основным концептуальным препятствием является понимание того, что переменные указывают на объекты, и только опыт работы с основными и стандартными библиотеками позволит вам понять, как объекты реагируют на определенные сообщения.

Строки в Ruby - это полноценные объекты, которые отвечают на сообщения, а не просто языковые примитивы. В следующих разделах я попытаюсь объяснить, почему это редко является проблемой, и почему эта функция полезна в динамическом языке, таком как Ruby. Я также расскажу о связанном методе, который создает поведение, которое вы изначально ожидали.

Это все о назначении объектов

Мой вопрос: почему это так? Я понимаю, что установка "b = a" делает их одинаковыми object_id, так что технически существует два имени для одной и той же переменной строки.

Это редко проблема в повседневном программировании. Учтите следующее:

a = 'foo' # assign string to a
b = a     # b now points to the same object as a
b = 'bar' # assign a different string object to to b

[a, b]
#=> ["foo", "bar"]

Это работает так, как вы ожидаете, потому что переменная является просто заполнителем для объекта. Пока вы присваиваете объекты переменным, Ruby делает то, что вы могли интуитивно ожидать.

Объекты получают сообщения

В опубликованном вами примере вы сталкиваетесь с таким поведением, потому что вы действительно делаете следующее:

a = 'foo'       # assign a string to a
b = a           # assign the object held in a to b as well
b.replace 'bar' # send the :replace message to the string object

В этом случае String #replace отправляет сообщение тому же объекту, указанному как a, так и b. Поскольку обе переменные содержат один и тот же объект, строка заменяется независимо от того, вызываете ли вы метод как a.replace или же b.replace,

Возможно, это не интуитивно понятно, но на практике это проблема редко. Во многих случаях такое поведение на самом деле желательно, чтобы вы могли передавать объекты, не заботясь о том, как метод маркирует объект внутри. Это полезно для обобщения метода или для самостоятельного документирования сигнатуры метода. Например:

def replace_house str
  str.sub! 'house', 'guard'
end

def replace_cat str
  str.sub! 'cat', 'dog'
end

critter = 'house cat'    
replace_house critter; replace_cat critter
#=> "guard dog"

В этом примере каждый метод ожидает объект String. Неважно, что строка помечена как твердая в другом месте; внутри метод использует метку str для ссылки на тот же объект.

Пока вы знаете, когда метод мутирует получатель и когда он передает обратно новый объект, вы не будете удивлены результатами. Подробнее об этом через минуту.

Что действительно делает String #replace

В вашем конкретном примере я вижу, как может сбить с толку документация для String #replace. В документации сказано:

заменить (other_str) → str
Заменяет содержимое и зараженность str соответствующими значениями в other_str.

Что это на самом деле означает, что b.replace фактически изменяет объект ("заменяет содержимое"), а не возвращает новый объект для присвоения переменной. Например:

# Assign the same String object to a pair of variables.
a = 'foo'; b = a;

a.object_id
#=> 70281327639900

b.object_id
#=> 70281327639900

b.replace 'bar'
#=> "bar"

b.object_id
#=> 70281327639900

a.object_id == b.object_id
#=> true

Обратите внимание, что object_id никогда не меняется. Конкретный метод, который вы использовали, использует один и тот же объект; он просто меняет свое содержимое. Сравните это с такими методами, как String # sub, которые возвращают копию объекта, что означает, что вы получите новый объект с другим object_id.

Что делать вместо этого: Назначение новых объектов

Если вы хотите, чтобы a и b указывали на разные объекты, вы можете вместо этого использовать метод без мутаций, такой как String # sub:

a = 'foo'; b = a;
b = b.sub 'oo', 'um'
#=> "fum"

[a.object_id, b.object_id]
#=> [70189329491000, 70189329442400]

[a, b]
#=> ["foo", "fum"]

В этом довольно надуманном примере b.sub возвращает новый объект String, который затем присваивается переменной b. Это приводит к тому, что для каждой переменной присваиваются разные объекты, что и ожидалось изначально.

Другие вопросы по тегам