【问题标题】:Why do some Ruby methods like String#replace mutate copies of variables?为什么像 String#replace 这样的 Ruby 方法会改变变量的副本?
【发布时间】:2016-06-05 19:27:29
【问题描述】:

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

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

蓝色

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

此外,有时一个方法似乎会修改a,但有时它会导致“b”成为一个新对象。这似乎模棱两可,毫无意义。

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

【问题讨论】:

  • 许多 OO 语言都有这种基于指针的工作方式(变量指向对象,因此您需要了解操作变量与操作对象之间的区别 - 您的问题表明您并不完全在那里然而,我希望有人可以为你说清楚)。 Ruby 的不同寻常之处在于它总是无一例外地适用——变量是总是 指向对象的指针。许多其他语言不会对字符串执行此操作,但会使用数组。
  • 不涉及递归。我建议你改写你的措辞。
  • @KarolyHorvath:但我认为这是任何答案都需要解决的部分问题。我不确定最终结果应该是什么(即问题中应该用什么词来解释问题)。在 Ruby 中寻找递归代码示例的人会很失望地发现这个问题,但也许在这里指出差异和更正术语可能会有所帮助。
  • 你知道 JavaScript 那么为什么 var o = { a: 'b' }; var o2 = o; o2.a = 11 离开 o.a === 11 是一回事?同样的情况,不同的语言。
  • 如果你在做b = a 你想要另一个对象引用。如果你在做b = a.dup 你想要一个独立的copy。您似乎认为“递归”是“传播”的同义词,但事实并非如此。当一个方法调用自己时,递归是最好的说明。

标签: ruby string variable-assignment object-identity


【解决方案1】:

这里的问题不称为递归,并且 Ruby 变量不是递归的(对于这个词的任何正常含义 - 即它们不引用自己,并且您不需要递归例程来使用它们)。计算机编程中的递归是指代码直接或间接调用自身,例如包含对自身的调用的函数。

在 Ruby 中,所有的变量都指向对象。这无一例外 - 尽管有一些内部技巧可以让事情变得更快,即使编写 a=5 也会创建一个名为 a 的变量并将其“指向”Fixnum 对象 5 - 仔细的语言设计意味着你几乎不用不要注意到这种情况发生。最重要的是,数字不能改变(你不能将 5 更改为 6,它们总是不同的对象),所以你可以认为 a“包含”5即使从技术上讲 a 指向 5,也可以侥幸逃脱。

虽然使用字符串,对象可以改变。您的示例代码的分步说明可能如下所示:

a = 'red'

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

b = a

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

b.replace('blue')

b 指向的对象上调用replace 方法(也由a 指向)该方法将String 的内容更改为“蓝色”。

b = 'green'; 

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

print a 

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

我什么时候会用到这个?

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

这是否意味着我不能将“a”的值传递给另一个变量,而没有任何更改递归回“a”?

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

b = a.clone

b = "#{a}"

但是,在实践中,您很少只想直接复制字符串。您将想做与代码目标相关的其他事情。通常在 Ruby 中,会有一个方法来执行你需要的操作并返回一个 new 字符串,所以你会做这样的事情

b = a.something

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

此外,有时一个方法似乎会递归到“a”,有时它会导致“b”成为一个新的object_id。

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

【讨论】:

  • 谢谢,这实际上使事情弄清楚了很多。 b = a.something 部分真的让我点击了。
【解决方案2】:

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

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 并没有像 cmets 中指出的那样进行 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}}

【讨论】:

  • 啊 - 所以我想你必须使用 Marshal 吗?
  • ActiveSupport 针对这种情况提供了deep_dup。对于像 String 这样的扁平对象,dup 就足够了。
【解决方案3】:

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

这符合您的预期,因为变量只是对象的占位符。只要您将 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
将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 的新对象。

替代方法:分配新对象

如果你想让 ab 指向不同的对象,你可以使用像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 返回一个 new 字符串对象,然后将其分配给变量 b。这导致将不同的对象分配给每个变量,这是您最初期望的行为。

【讨论】:

    猜你喜欢
    • 2012-08-24
    • 2016-02-15
    • 1970-01-01
    • 2016-07-13
    • 1970-01-01
    • 1970-01-01
    • 2020-06-20
    • 1970-01-01
    相关资源
    最近更新 更多