为什么像 String#replace 这样的 Ruby 方法会改变变量的副本?

Why do some Ruby methods like String#replace mutate copies of variables?

所以首先我只是在学习 Ruby 并且来自 JavaScript 背景。我有一个问题,我找不到答案。我有这个例子:

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

blue

我的问题是:为什么会这样?我知道设置 b = a 使它们相同 object_id,因此从技术上讲,同一个变量字符串有两个名称。但我从来没有看到使用这种递归值更改的理由。如果我设置 b = a 是因为我想操纵 a 的值而不更改它。

另外,好像有时候一个方法会修改a,但有时候会导致"b"成为一个新的对象。这似乎模棱两可,毫无意义。

我什么时候会用到它?重点是什么?这是否意味着我无法将 a 的值传递给另一个变量,而没有任何更改传播回 a?

它在处理哈希递归的场景中很有用。

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

这里的问题不是所谓的递归,Ruby 变量不是递归的(对于这个词的任何正常含义——即它们不引用自己,你不需要递归例程来顺序与他们一起工作)。计算机编程中的递归是指代码直接或间接调用自身,例如包含对自身调用的函数。

在Ruby中,所有变量都指向对象。这无一例外——尽管有一些内部技巧可以使事情变得更快,但即使写 a=5 也会创建一个名为 a 的变量,并将其 "points" 传递给 Fixnum 对象 5 - 仔细的语言设计意味着您几乎不会注意到这种情况的发生。最重要的是,数字不能改变(你不能把 5 变成 6,它们总是不同的对象),所以你可以 认为 不知何故 a "contains" a 5 即使从技术上讲 a 指向 5.

尽管使用字符串,对象可以改变。您的示例代码的逐步解释可能如下所示:

a = 'red'

创建一个新的 String 对象,其内容为 "red",并将变量 a 指向它。

b = a

将变量 b 指向与 a 相同的对象。

b.replace('blue')

b 指向的对象(以及 a 指向的对象)调用 replace 方法该方法将字符串的内容更改为 "blue" .

b = 'green'; 

创建一个新的 String 对象,其内容为 "green",并将变量 b 指向它。变量 ab 现在指向不同的对象。

print a 

a指向的String对象有内容"blue"。因此,根据语言规范,一切正常。

When will I ever use this?

一直以来。在 Ruby 中,您使用变量临时指向对象,以便调用它们的方法。对象是您要使用的东西,变量是您在代码中用来引用它们的名称。它们是分开的这一事实有时会让您感到困惑(尤其是在 Ruby 中使用字符串,许多其他语言没有这种行为)

and does this mean I can't pass the value of "a" into another variable without any changes recursing back to "a"?

如果你想复制一个字符串,有几种方法可以做到。例如

b = a.clone

b = "#{a}"

然而,在实践中,您很少只想直接复制字符串。你会想做一些与你的代码目标相关的事情。通常在 Ruby 中会有一个方法来完成你需要的操作和 return 一个 new 字符串,所以你会做这样的事情

b = a.something

在其他情况下,您实际上想要对原始对象进行更改。这完全取决于您的代码的目的是什么。就地更改 String 对象可能很有用,因此 Ruby 支持它们。

Furthermore it seems sometimes a method will recurse into "a" and sometimes it will cause "b" to become a new object_id.

从来没有这样。没有方法会改变对象的身份。但是,大多数方法都会 return 一个新对象。有些方法会改变一个对象的内容——你需要更加注意 Ruby 中的那些方法,因为可能会改变其他地方使用的数据——在其他 OO 语言中也是如此,JavaScript对象在这里也不例外,它们的行为方式完全相同。

TL;DR

在您原来的问题(现在已编辑)中,您将递归与变异和传播混淆了。在正确的情况下,以及在预期行为时,这三个概念都是有用的工具。您可能会发现您发布的特定示例令人困惑,因为您不希望字符串就地发生变异,或者更改会传播到指向该对象的所有指针。

泛化方法的能力使动态语言(如 Ruby 中的鸭子类型成为可能。主要的概念障碍是理解变量 指向对象 ,只有使用核心库和标准库的经验才能让您理解对象如何响应特定消息。

Ruby 中的字符串是响应消息的成熟对象,而不仅仅是语言原语。在接下来的部分中,我试图解释为什么这很少成为问题,以及为什么该功能在像 Ruby 这样的动态语言中很有用。我还介绍了一个相关的方法,它会产生您最初期望的行为。

一切都是关于对象分配

My question is why is this a thing. I understand that setting "b=a" makes them them the same object_id so there technically two names for the same variable string.

这在日常编程中很少出现。考虑以下因素:

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"]

这会按您预期的方式工作,因为变量只是对象的占位符。只要您将 objects 分配给变量,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 正在向 ab 指向的同一对象发送消息。由于两个变量都包含相同的对象,因此无论您以 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 对象。它不关心字符串在其他地方被标记为 critter;在内部,该方法使用标签 str 来引用同一对象。

只要您知道方法何时改变接收器以及何时传回新对象,您就不会对结果感到惊讶。稍后详细介绍。

String#replace 的真正作用

在您的具体示例中,我可以看出 String#replace 的文档可能会让人感到困惑。文档说:

replace(other_str) → str
Replaces the contents and taintedness of str with the corresponding values in other_str.

really 的意思是 b.replace 实际上是在改变对象 ("replacing the contents"),而不是 return 为一个新对象赋值给变量。例如:

# 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 之类的方法进行对比,其中 return 对象的 copy,这意味着您将取回具有不同 object_id 的新对象.

改为做什么:分配新对象

如果你想让ab指向不同的对象,你可以使用像[=23=这样的非变异方法] 而是:

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

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

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

在这个相当人为的例子中,b.sub returns 一个 new 字符串对象,然后将其分配给变量 b。这导致将不同的对象分配给每个变量,这是您最初期望的行为。