3

有没有办法在 Ruby 中通过值而不是通过引用传递对象?例如,

class Person
  attr_accessor :name
end
def get_name(obj)
  obj.name = "Bob"
  puts obj.name
end
jack = Person.new
jack.name = "Jack"
puts jack.name
get_name(jack)
puts jack.name

输出应该是

Jack
Bob
Jack

代替

Jack
Bob
Bob

任何帮助,将不胜感激。

4

3 回答 3

5

, Ruby通过引用而不是 value 传递

如果需要模拟传值,可以使用Ruby的Object#clone方法。在这种情况下,你会做这样的事情:

def get_name(obj)
  new_object = obj.clone
  new_object.name = "Bob"
  puts new_object.name
end

这会生成对象的浅拷贝。换句话说,对象的实例变量被复制,但变量引用的对象没有被复制。如果你需要做一个深拷贝,你可以阅读这篇 Stack Overflow 帖子。Ruby 没有单一方法来执行深度复制,但那篇文章描述了如何使用编组和解来进行深度复制。

clonedup工作非常相似,但有一些差异。根据文档:

对象#clone

生成 obj 的浅拷贝——拷贝obj的实例变量,但不拷贝它们引用的对象。复制obj的冻结和污​​染状态。另请参阅Object#dup下的讨论。

对象#dup

生成 obj 的浅拷贝——拷贝obj的实例变量,但不拷贝它们引用的对象。dup 复制obj的污染状态。另请参阅Object#clone下的讨论。一般来说,clone 和 dup 在后代类中可能有不同的语义。虽然 clone 用于复制对象,包括其内部状态,但 dup 通常使用后代对象的类来创建新实例。

此方法可能具有特定于类的行为。如果是这样,该行为将记录在类的 #initialize_copy 方法下。

您可以查看dupclone文档。

编辑

虽然我的回答可能给出了 OP 正在寻找的内容,但就通过引用或值传递的语义而言,它并不完全正确。有关更多讨论,请参阅此页面上的其他答案和评论。您还可以在此处的评论和此帖子中查看讨论以获取更多信息。

于 2013-11-04T18:14:47.583 回答
2

Ruby按值传递的。总是。这是一个简单的程序,它证明了这一事实:

def foo(bar)
  bar = 'reference'
end

baz = 'value'

foo(baz)

puts "Ruby is pass-by-#{baz}"
# Ruby is pass-by-value

您所看到的只是共享可变状态:如果一个对象是可变的,则它可以被变异,如果该对象通过多个引用可见,那么该变异将通过多个引用可见。就那么简单。不管你像我的朋友那样叫我“Jörg”还是像我妈妈那样叫我“儿子”,如果我剪了头发,你们两个都会看到。

如果您不希望您的对象发生变异,则需要使它们不可变。例如:

class Person
  attr_reader :name

  private

  attr_writer :name

  def initialize(name)
    self.name = name
  end
end

def get_name(obj)
  obj.name = "Bob"
  puts obj.name
end

jack = Person.new('Jack')

puts jack.name
# Jack

get_name(jack)
# NoMethodError: private method `name=' called for #<Person:0xdeadbeef081542 @name='Jack'>

puts jack.name
# Jack

现在,您的Person对象不能再被其他对象更改。但是,请注意,您的对象引用的Person对象显然仍然可以更改:

jack.name << ' the Ripper'

puts jack.name
# Jack the Ripper

jack.name.replace('Bob')

puts jack.name
# Bob

如果你想防止这种情况发生,你需要确保你的对象引用的所有Person对象也是不可变的。例如像这样:

class Person
  def initialize(name)
    self.name = name.freeze
  end
end

jack = Person.new('Jack)

jack.name << 'the Ripper'
# RuntimeError: can't modify frozen String

现在,您的Person对象是真正不可变的。(至少像 Ruby 这样具有强大反射能力的语言一样“真实”。)

不幸的是,我们现在对其他人做了我们试图保护自己免受同样的事情:我们正在变异他们的对象!在Person#initialize中,我们String通过freezeing 来改变传入的那个。如果创建 的方法Person想要对 做其他事情String,他们会大吃一惊:

name = 'Jack'

jack = Person.new(name)

name << ' the Ripper'
# RuntimeError: can't modify frozen String

我们可以通过在ingString之前复制第一个来解决这个问题:freeze

class Person
  def initialize(name)
    self.name = name.dup.freeze
  end
end

name = 'Jack'

jack = Person.new(name)

name << ' the Ripper'
# => 'Jack the Ripper'
于 2013-11-05T11:39:47.780 回答
2

在@Michael的出色回答之上,正式回答原始问题:

有没有办法在 Ruby 中通过值而不是通过引用传递对象?

不,绝对不是。不可能。真的没有,算了。

或者更准确地说,如果我们在词汇上挑剔,Ruby 通过值传递对对象的引用,因此传递的对象是可变的。

在 Ruby 中,您必须:

  1. 根据需要手动克隆/复制输入和输出(参见迈克尔的回答);或者
  2. 假设如果您决定不这样做,没有人会搞砸您班级的内部结构。

Ruby 主义者倾向于选择选项 2,因为内部结构是这样构建的(attr_reader 等返回对象状态),出于性能原因,并且因为创建您返回或操作的所有内容的深层副本在实践中绝非易事。


回答约尔格的评论:

def foo(bar) bar << ' is now mutated' end;
baz = 'Baz';
foo(baz);
puts baz # Baz is now mutated

添加关于冻结的进一步说明/示例,因为这也会产生意想不到的结果:

foo = {foo: 'bar'}.freeze  # {:foo=>"bar"}
foo[:foo] += 'baz'         # RuntimeError: can't modify frozen Hash
foo[:foo] << 'baz'         # {:foo=>"barbaz"}
于 2013-11-04T18:26:32.437 回答