Ruby是传递引用还是传递值?

283
@user.update_languages(params[:language][:language1], 
                       params[:language][:language2], 
                       params[:language][:language3])
lang_errors = @user.errors
logger.debug "--------------------LANG_ERRORS----------101-------------" 
                + lang_errors.full_messages.inspect

if params[:user]
  @user.state = params[:user][:state]
  success = success & @user.save
end
logger.debug "--------------------LANG_ERRORS-------------102----------" 
                + lang_errors.full_messages.inspect

if lang_errors.full_messages.empty?
@user 对象在 update_lanugages 方法中向 lang_errors 变量添加错误。 当我保存 @user 对象时,最初存储在 lang_errors 变量中的错误会丢失。
虽然我的尝试更多是一种hack方式(似乎没有起作用),但我想了解为什么变量值会被清空。我理解传递引用所以想知道如何在不被清空的情况下保留该变量的值。

我还注意到,我能够在克隆对象中保留该值。 - Sid
2
你应该看Abe Voelker的回答。但是在这个问题上跑了一圈后,我会这样说。当你将一个对象Foo传递给一个过程时,对象的引用副本被传递,bar,按值传递。你不能改变Foo指向的对象,但可以改变它所指向的对象的内容。因此,如果你传递一个数组,数组的内容可以被改变,但你不能改变所引用的数组。能够使用Foo的方法而不必担心破坏Foo上的其他依赖关系,这很好。 - bobbdelsol
14个回答

2
Two references refer to same object as long as there is no reassignment. 

在同一对象中进行的任何更新都不会使引用指向新的内存,因为它仍然在同一块内存中。 以下是几个例子:

    a = "first string"
    b = a



    b.upcase! 
    => FIRST STRING
    a
    => FIRST STRING

    b = "second string"


a
    => FIRST STRING
    hash = {first_sub_hash: {first_key: "first_value"}}
first_sub_hash = hash[:first_sub_hash]
first_sub_hash[:second_key] = "second_value"

    hash
    => {first_sub_hash: {first_key: "first_value", second_key: "second_value"}}

    def change(first_sub_hash)
    first_sub_hash[:third_key] = "third_value"
    end

    change(first_sub_hash)

    hash
    =>  {first_sub_hash: {first_key: "first_value", second_key: "second_value", third_key: "third_value"}}

2
很多优秀的答案深入探讨了Ruby的“按值传递引用”的原理,但我更喜欢通过示例来学习和理解。希望这能有所帮助。
def foo(bar)
  puts "bar (#{bar}) entering foo with object_id #{bar.object_id}"
  bar =  "reference"
  puts "bar (#{bar}) leaving foo with object_id #{bar.object_id}"
end

bar = "value"
puts "bar (#{bar}) before foo with object_id #{bar.object_id}"
foo(bar)
puts "bar (#{bar}) after foo with object_id #{bar.object_id}"

# Output
bar (value) before foo with object_id 60
bar (value) entering foo with object_id 60
bar (reference) leaving foo with object_id 80 # <-----
bar (value) after foo with object_id 60 # <-----

当我们进入这个方法时,可以看到我们的 bar 仍然指向字符串"value"。但是,我们将一个字符串对象 "reference" 分配给了 bar,它有一个新的 object_id。在这种情况下,foo 内部的 bar 具有不同的作用域,无论我们在方法中传递了什么,都不再被 bar 访问,因为我们重新分配了它并将其指向内存中保存字符串 "reference" 的新位置。
现在考虑一下相同的方法。唯一的区别是我们在方法内部所做的事情。
def foo(bar)
  puts "bar (#{bar}) entering foo with object_id #{bar.object_id}"
  bar.replace "reference"
  puts "bar (#{bar}) leaving foo with object_id #{bar.object_id}"
end

bar = "value"
puts "bar (#{bar}) before foo with object_id #{bar.object_id}"
foo(bar)
puts "bar (#{bar}) after foo with object_id #{bar.object_id}"

# Output
bar (value) before foo with object_id 60
bar (value) entering foo with object_id 60
bar (reference) leaving foo with object_id 60 # <-----
bar (reference) after foo with object_id 60 # <-----

注意区别了吗?我们所做的是:修改了该变量指向的String对象的内容。方法内部仍然存在不同作用域的bar。因此,小心处理传入方法的变量。如果您在原地修改传入的变量(gsub!,replace等),请在方法名称中使用感叹号!来表示。例如:“def foo!”

P.S.:

重要的是要记住foo内部和外部的“bar”是“不同的”“bar”。它们的范围是不同的。在方法内部,您可以将“bar”重命名为“club”,结果将是相同的。
我经常看到变量在方法内外被重复使用,虽然这样做没问题,但会降低代码的可读性并且是一种代码异味。我强烈建议不要像我上面的示例中那样做 :),而是应该这样做
def foo(fiz)
  puts "fiz (#{fiz}) entering foo with object_id #{fiz.object_id}"
  fiz =  "reference"
  puts "fiz (#{fiz}) leaving foo with object_id #{fiz.object_id}"
end

bar = "value"
puts "bar (#{bar}) before foo with object_id #{bar.object_id}"
foo(bar)
puts "bar (#{bar}) after foo with object_id #{bar.object_id}"

# Output
bar (value) before foo with object_id 60
fiz (value) entering foo with object_id 60
fiz (reference) leaving foo with object_id 80
bar (value) after foo with object_id 60

1
Ruby是解释性语言。变量是对数据的引用,而不是数据本身。这有助于使用同一变量来存储不同类型的数据。 lhs = rhs的赋值操作会复制rhs上的引用,而不是数据。这与其他语言(例如C)不同,其中赋值将从rhs向lhs进行数据复制。
因此,对于函数调用,传递的变量(比如x)确实会被复制到函数中的局部变量中,但x是一个引用。然后将有两个引用副本,都引用相同的数据。一个在调用者中,一个在函数中。
在函数中的赋值会将新引用复制到函数版本的x中。在此之后,调用者版本的x保持不变。它仍然是对原始数据的引用。
相反,如果在x上使用.replace方法,则会导致Ruby进行数据复制。如果在任何新赋值之前使用replace,则调用者也将在其版本中看到数据更改。
类似地,只要传入变量的原始引用保持完好无损,实例变量将与调用者所看到的相同。在对象框架内,实例变量始终具有最新的引用值,无论是由调用者提供还是在传递给类的函数中设置。
“按值调用”或“按引用调用”在这里变得混淆,因为对“=”的混淆。在编译语言中,“=”是数据副本。在这个解释性语言中,“=”是引用副本。在你的例子中,你传入了引用,然后通过“=”进行引用复制,这会破坏原始传入的引用,然后人们谈论它,好像“=”是数据副本。
为了保持一致的定义,我们必须坚持使用“.replace”作为数据副本。从“.replace”的角度来看,我们确实看到这是按引用传递的。此外,如果我们在调试器中走过去,我们会看到引用被传递,因为变量是引用。
然而,如果我们必须将'='作为参考框架,那么确实我们会看到传入的数据直到赋值操作,而在赋值之后我们就再也看不到它了,而调用者的数据仍然保持不变。从行为上来说,只要我们不认为传入的值是复合的,这就是按值传递,因为我们无法在单个赋值中保留其中一部分并更改另一部分(因为该赋值会更改引用并使原始内容超出范围)。此外,在对象中的实例变量将是引用,就像所有变量一样。因此,我们将被迫谈论“按值传递引用”,并使用相关的措辞。

0

是的,但是...

Ruby传递一个对象的引用,由于Ruby中的所有内容都是对象,因此可以说它是按引用传递的。

我不同意这里声称它是按值传递的帖子,这对我来说似乎是纠结于语义的游戏。

然而,在实际效果上,它“隐藏”了行为,因为Ruby提供的大多数操作“开箱即用”,例如字符串操作,会产生对象的副本:

> astringobject = "lowercase"

> bstringobject = astringobject.upcase
> # bstringobject is a new object created by String.upcase

> puts astringobject
lowercase

> puts bstringobject
LOWERCASE

这意味着大部分时间,原始对象保持不变,看起来像 Ruby 是“按值传递”的。

当然,在设计自己的类时,了解此行为的细节对于功能行为、内存效率和性能都非常重要。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接