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个回答

477

其他回答都是正确的,但一个朋友向我解释过这个问题,归根结底是关于Ruby如何处理变量,所以我想分享一些我为他写的简单图片/解释(对于长度和可能的过度简化,请见谅):


问题1:当你把一个新变量 str 赋值为'foo'时会发生什么?

str = 'foo'
str.object_id # => 2000

enter image description here

A: 创建了一个标签名为str,它指向对象'foo'。对于这个Ruby解释器的状态而言,该对象位于内存位置2000


Q2: 当您使用=将现有变量str赋值给一个新对象时会发生什么?

str = 'bar'.tap{|b| puts "bar: #{b.object_id}"} # bar: 2002
str.object_id # => 2002

enter image description here

A: 标签 str 现在指向另一个对象。


Q3: 当您将新变量=分配给 str 时会发生什么?

str2 = str
str2.object_id # => 2002

enter image description here

A: 创建了一个名为str2的标签,指向与str相同的对象。


Q4: 如果被strstr2引用的对象被更改会发生什么?

str2.replace 'baz'
str2 # => 'baz'
str  # => 'baz'
str.object_id # => 2002
str2.object_id # => 2002

enter image description here

A: 这两个标签仍然指向同一个对象,但该对象本身已经发生变化(其内容已更改为其他内容)。


这与原问题有什么关系?

这基本上与Q3 / Q4中发生的情况相同。该方法获得了其自己的私有副本变量/标签(str2),该变量是传递给它的(str)。它不能更改标签 str 指向的对象,但可以更改它们都引用的对象的内容以包含其他内容:

str = 'foo'

def mutate(str2)
  puts "str2: #{str2.object_id}"
  str2.replace 'bar'
  str2 = 'baz'
  puts "str2: #{str2.object_id}"
end

str.object_id # => 2004
mutate(str) # str2: 2004, str2: 2006
str # => "bar"
str.object_id # => 2004

3
Robert Heaton最近也在博客中谈到了这个问题:http://robertheaton.com/2014/07/22/is-ruby-pass-by-reference-or-pass-by-value/ - Michael Renner

264
在传统术语中,Ruby是严格按值传递的。然而,在Ruby中,所有东西都是对象,所以Ruby可以表现得像按引用传递的语言。
Ruby打破了“按引用传递”或“按值传递”的传统定义,因为一切都是对象,当它传递东西时,它传递对象的引用。所以实际上,我们可以将Ruby归类为第三种类型的语言,称之为“按对象引用传递”。按照计算机科学术语的严格定义,Ruby是按值传递的。
Ruby没有任何纯粹的、非引用值的概念,因此无法将其传递给方法。变量始终是对对象的引用。为了获得一个不会在你手下发生变化的对象,你需要复制或克隆你传递的对象,从而得到一个没有其他人引用的对象。然而,即使这也不是百分百可靠:标准的克隆方法都只做浅拷贝,所以克隆体的实例变量仍然指向原来的对象。如果被ivars引用的对象发生变化,那么这个变化仍然会出现在副本中,因为它引用的是同样的对象。

94
Ruby是按值传递的。毫无疑问,也没有例外。如果你想知道Ruby(或任何其他语言)是按引用传递还是按值传递,请试一试:def foo(bar) bar = 'reference' end; baz = 'value'; foo(baz); puts "Ruby is pass-by-#{baz}" - Jörg W Mittag
113
@JörgWMittag:是的,但OP的困惑实际上并不涉及到严格的CS意义上的传值或传引用。他所错过的是,你传递的“值”实际上是引用。我觉得仅仅说“这是传值”的话有些学究和会误导OP,因为那并不是他真正想要知道的。但感谢您的解释,因为对于未来的读者来说这很重要,我本应该包括在内。(我总是在犹豫是包括更多信息还是不让人们困惑。) - Chuck
18
不同意@Jorg的说法。Ruby是按引用传递方式,但实际上只是改变了引用。可以尝试这个:def foo(bar) bar.replace 'reference' end; baz = 'value'; foo(baz); puts "Ruby是按引用传递-#{baz}" - pguardiario
18
我认为这只是一个定义问题。你使用了你个人想出的“按引用传递”的定义,而Jörg则使用传统的计算机科学定义。当然,我无权告诉你如何使用语言--我只是认为解释“通常”意味着什么很重要。在传统术语中,Ruby是按值传递的,但这些值本身是引用。我完全理解你和原贴作者为什么喜欢将其视为按引用传递-这只是该术语的传统含义不同。 - Chuck
7
Ruby 中的一切都是对象,因此 Ruby 既不是传值调用也不是传引用调用,至少在 C++ 中使用这些术语的意义上不是。"按对象引用传递" 可能是描述 Ruby 行为更好的方式。但最终,最好别过于强调这些术语,而是要对真正发生的行为有一个好的理解。 - David Winiecki
显示剩余17条评论

64

Ruby使用"对象引用传递"

(使用Python的术语。)

说Ruby使用"按值传递"或"按引用传递"并不太具体,也不太有帮助。我认为,如今大多数人都知道这些术语("值" vs "引用")来源于C++。

在C++中,"按值传递"意味着函数获得变量的副本,对副本进行任何更改都不会改变原始变量。对于对象也是如此。如果您按值传递一个对象变量,则整个对象(包括其所有成员)都将被复制,对成员的任何更改都不会改变原始对象上的那些成员。(如果您按值传递指针,则情况就不同了,但据我所知,Ruby没有指针。)

class A {
  public:
    int x;
};

void inc(A arg) {
  arg.x++;
  printf("in inc: %d\n", arg.x); // => 6
}

void inc(A* arg) {
  arg->x++;
  printf("in inc: %d\n", arg->x); // => 1
}

int main() {
  A a;
  a.x = 5;
  inc(a);
  printf("in main: %d\n", a.x); // => 5

  A* b = new A;
  b->x = 0;
  inc(b);
  printf("in main: %d\n", b->x); // => 1

  return 0;
}

输出:

in inc: 6
in main: 5
in inc: 1
in main: 1
在C++中,“按引用传递”意味着函数可以访问原始变量。它可以分配一个全新的整型字面值,原始变量也将具有该值。

在C++中,“按引用传递”意味着函数可以访问原始变量。它可以分配一个全新的整型字面值,原始变量也将具有该值。

void replace(A &arg) {
  A newA;
  newA.x = 10;
  arg = newA;
  printf("in replace: %d\n", arg.x);
}

int main() {
  A a;
  a.x = 5;
  replace(a);
  printf("in main: %d\n", a.x);

  return 0;
}

输出:

in replace: 10
in main: 10

如果参数不是一个对象,Ruby在传递参数时使用按值传递(以C++的意义)。但是在Ruby中,一切皆为对象,所以实际上没有像C++那样的按值传递。

在Ruby中,使用“按对象引用传递”(使用Python的术语):

  • 在函数内部,可以将任何对象成员分配新值,并且这些更改将在函数返回后持久存在。
  • 在函数内部,将一个全新的对象赋值给变量会导致变量停止引用旧对象。但函数返回后,原变量仍然将引用旧对象。

因此,在C++意义上,Ruby不使用“按引用传递”。如果使用,“在函数内分配新对象给变量”将导致函数返回后忘记旧对象。

class A
  attr_accessor :x
end

def inc(arg)
  arg.x += 1
  puts arg.x
end

def replace(arg)
  arg = A.new
  arg.x = 3
  puts arg.x
end

a = A.new
a.x = 1
puts a.x  # 1

inc a     # 2
puts a.x  # 2

replace a # 3
puts a.x  # 2

puts ''

def inc_var(arg)
  arg += 1
  puts arg
end

b = 1     # Even integers are objects in Ruby
puts b    # 1
inc_var b # 2
puts b    # 1

输出:

1
2
2
3
2

1
2
1

*这就是为什么在Ruby中,如果您想在函数内部修改对象但在函数返回时忘记这些更改,则必须在进行临时更改之前显式地复制该对象。


1
你的答案是最好的。我也想发布一个简单的例子 def ch(str) str.reverse! end; str="abc"; ch(str); puts str #=> "cba" - Fangxing
这是正确的答案!这里也有非常好的解释:https://robertheaton.com/2014/07/22/is-ruby-pass-by-reference-or-pass-by-value。但是我仍然不理解的是: def foo(bar) bar = 'reference' end; baz = 'value'; foo(baz); puts "Ruby is pass-by-#{baz}"。 这将打印出“Ruby is pass-by-value”。但是,在foo内部的变量被重新赋值。如果bar是一个数组,那么重新赋值就不会影响到baz。为什么? - haffla
我不明白你的问题。我认为你应该提出一个全新的问题,而不是在这里评论问。 - David Winiecki
@haffla def foo(bar) bar = 'reference' end; 此处将bar重新赋值为字符串对象'reference',因此bar和baz不再引用同一个字符串对象。添加一些打印object_id语句以更仔细地查看。def foo(bar) puts "重新分配前的bar: #{bar.object_id}"; bar = 'reference'; puts "重新分配后的bar: #{bar.object_id}"; end; baz = 'value'; foo(baz); puts "Ruby是按值传递的-#{baz}"; puts "baz: #{baz.object_id}";重新分配前的bar: 7864800 重新分配后的bar: 7864620 Ruby是按值传递的-value baz: 7864800 - aka

40

Ruby是按值传递的。始终如此。没有例外。没有如果。没有但是。

下面是一个简单的程序,演示了这个事实:

def foo(bar)
  bar = 'reference'
end

baz = 'value'

foo(baz)

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

15
“这里的错误在于本地参数被重新分配(指向内存中的新位置)” - 这不是一个错误,这是“按值传递”的定义。如果Ruby是“按引用传递”,那么在调用方中重新分配本地方法参数绑定也会重新分配调用者中的本地变量绑定。然而,实际情况并非如此,因此Ruby是“按值传递”的。改变可变值时值发生变化这一事实完全无关紧要,那只是可变状态的工作原理。 Ruby不是纯函数式语言。 - Jörg W Mittag
7
感谢 Jörg 为“按值传递”这一正确定义辩护。尽管 Ruby 总是按值传递,但当这个值实际上是一个引用时,它很容易让我们感到困惑。 - Douglas
10
这是诡辩。"按值传递"和"按引用传递"之间的实际区别是语义上的,而不是句法上的。你会说C数组是按值传递吗?当然不是,即使当你将一个数组的名称传递给函数时,你传递的是一个不可变的指针,只有指针所指向的数据可以被改变。显然,在Ruby中,值类型是按值传递的,而引用类型是按引用传递的。 - dodgethesteamroller
3
@dodgethesteamroller:Ruby和C都是按值传递参数。总是这样,没有例外,不是如果就是但是。按值传递和按引用传递之间的区别在于你传递引用所指向的值还是传递引用本身。C 永远 传递值,从不 传递引用。这个值可能是一个指针,也可能不是,但它被传递的事实并不取决于它是什么类型的值。Ruby 同样 永远 按值传递,从不 按引用传递。该值始终是一个指针,但同样,这是无关紧要的。 - Jörg W Mittag
53
这个答案严格来说是“正确的”,但并不是很“有用”。传递的值始终是指针这一事实并不无关紧要。对于那些正在学习的人来说,这是一种困惑的来源,你的回答对解决这种困惑没有任何帮助。 - user788472
显示剩余16条评论

26

Ruby在严格意义上是传值的,但这些值是引用。

这可以称为“按引用传值”。这篇文章是我读过最好的解释:http://robertheaton.com/2014/07/22/is-ruby-pass-by-reference-or-pass-by-value/

按引用传值可以简要地解释如下:

函数接收对调用者使用的相同内存中的对象的引用。但是,它不会接收调用方存储此对象的盒子;就像按值传递一样,函数提供自己的盒子并为其创建一个新变量。

结果的行为实际上是传递引用和传递值的经典定义的结合。


“按值传递引用”是我用来描述Ruby参数传递的相同短语。我认为这是最准确和简洁的短语。 - Wayne Conrad
这篇文章帮助我认识到 Ruby 是按值传递引用的:https://launchschool.com/blog/object-passing-in-ruby - Pablo

17
已经有一些很好的答案了,但我想发表一对权威人士的定义,同时也希望有人能解释一下这些权威人士Matz(Ruby的创造者)和David Flanagan在他们出色的O'Reilly书籍《Ruby编程语言》中所说的意思。
【来自3.8.1:对象引用】
当你在Ruby中将一个对象传递给方法时,传递给该方法的是一个对象引用。这不是对象本身,也不是指向对象的引用。换句话说,方法参数是按值传递而不是按引用传递,但传递的值是对象引用。
因为对象引用被传递给方法,所以方法可以使用这些引用来修改底层对象。当方法返回时,这些修改就可见了。
这些都对我有意义,直到最后一段,尤其是最后一句话。这最多是误导性的,最糟糕的是令人困惑。在任何情况下,如何修改按值传递的引用可以改变底层对象呢?

1
因为引用没有被修改,而是底层对象被修改了。 - dodgethesteamroller
1
因为对象是可变的。Ruby不是纯函数式语言。这与传递引用和传递值完全无关(除了在纯函数式语言中,传递值和传递引用总是产生相同的结果,因此语言可以使用任何一种或两种方式而你永远不会知道)。 - Jörg W Mittag
2
一个很好的例子是,如果你不是在函数中进行变量赋值,而是将哈希传递给函数并在传递的哈希上执行合并操作。原始哈希最终会被修改。 - elc

15

Ruby是按引用传递的。总是这样。没有例外。没有如果。没有但是。

以下是一个简单的程序,证明了这个事实:

def foo(bar)
  bar.object_id
end

baz = 'value'

puts "#{baz.object_id} Ruby is pass-by-reference #{foo(baz)} because object_id's (memory addresses) are always the same ;)"

=> 由于object_id(内存地址)始终相同,2279146940 Ruby是按引用传递的。
def bar(babar)
  babar.replace("reference")
end

bar(baz)

puts "some people don't realize it's reference because local assignment can take precedence, but it's clearly pass-by-#{baz}"

=> 一些人可能没有意识到这是引用传递,因为局部赋值可以优先执行,但它显然是按引用传递。

这是唯一正确的答案,同时也有一些有趣的陷阱:尝试执行 a = 'foobar' ; b = a ; b[5] = 'z',会导致 a 和 b 都被修改。 - Martijn
2
@Martijn:你的论点并不完全正确。我们逐条分析你的代码。a = 'foobar' 创建一个指向 'foobar' 的新引用。b = a 创建了第二个指向与 a 相同数据的引用。b[5] = 'z' 将 b 引用的值的第六个字符更改为 'z'(与 a 引用的值巧合地也被更改)。这就是为什么在你的术语中 "both get modified",或者更准确地说,为什么 "the value referenced by both variables gets modified"。 - Lukas_Skywalker
2
你在 bar 方法中没有对引用做任何操作。你只是修改了引用所指向的对象,而不是引用本身。在 Ruby 中,唯一修改引用的方法是通过赋值。你不能通过调用方法来修改 Ruby 中的引用,因为方法只能在对象上调用,而引用在 Ruby 中不是对象。你的代码示例表明 Ruby 具有共享可变状态(这里不讨论),但它并没有阐明按值传递和按引用传递之间的区别。 - Jörg W Mittag
2
当有人问一个语言是否是“按引用传递”时,他们通常想知道当你将某些东西传递给函数并且函数修改它时,它是否会在函数外部被修改。对于Ruby来说,答案是“是”。这个答案有助于证明@JörgWMittag的答案非常无用。 - Toby 1 Kenobi
@Toby1Kenobi:当然,你可以自由地使用与常见、广泛使用的“按值传递”术语定义不同的个人定义。但是,如果你这样做,你应该准备好让人们感到困惑,特别是如果你忽略了这样一个事实,即你所谈论的概念在某些方面甚至与其他人完全相反。特别是,“按引用传递”并不关心传递的“东西”是否可以被修改,而是关心那个“东西”是什么,特别是它是否是引用... - Jörg W Mittag
显示剩余3条评论

9

参数是原始引用的副本。因此,您可以更改其值,但无法更改原始引用。


3
试试这个:--
1.object_id
#=> 3

2.object_id
#=> 5

a = 1
#=> 1
a.object_id
#=> 3

b = 2
#=> 2
b.object_id
#=> 5

标识符a包含对象1的object_id 3,标识符b包含对象2的object_id 5。

现在执行以下操作:--

a.object_id = 5
#=> error

a = b
#value(object_id) at b copies itself as value(object_id) at a. value object 2 has object_id 5
#=> 2

a.object_id 
#=> 5

现在,a和b都包含相同的object_id,即引用值对象2的object_id 5。因此,Ruby变量包含object_id来引用值对象。 执行以下操作也会出现错误:--
c
#=> error

但是这样做不会报错:--
5.object_id
#=> 11

c = 5
#=> value object 5 provides return type for variable c and saves 5.object_id i.e. 11 at c
#=> 5
c.object_id
#=> 11 

a = c.object_id
#=> object_id of c as a value object changes value at a
#=> 11
11.object_id
#=> 23
a.object_id == 11.object_id
#=> true

a
#=> Value at a
#=> 11

这里标识符a返回值对象11,其对象id为23,即对象ID 23位于标识符a处。现在我们通过使用方法来看一个例子。

def foo(arg)
  p arg
  p arg.object_id
end
#=> nil
11.object_id
#=> 23
x = 11
#=> 11
x.object_id
#=> 23
foo(x)
#=> 11
#=> 23

foo中的arg变量被赋予了x的返回值。这清楚地表明,参数是按值11传递的,当值11本身是一个对象时,它具有唯一的对象ID 23。

现在再看这个例子:--

def foo(arg)
  p arg
  p arg.object_id
  arg = 12
  p arg
  p arg.object_id
end

#=> nil
11.object_id
#=> 23
x = 11
#=> 11
x.object_id
#=> 23
foo(x)
#=> 11
#=> 23
#=> 12
#=> 25
x
#=> 11
x.object_id
#=> 23

在这里,标识符arg首先包含object_id 23来引用11,然后在与value object 12进行内部分配后,它包含object_id 25。但它不会改变在调用方法中使用的标识符x所引用的值。

因此,Ruby是传值调用,并且Ruby变量不包含值,而是包含对value object的引用。


3

需要注意的是,您甚至不必使用“replace”方法来更改原始值。如果您为哈希中的一个键分配另一个值,则会更改原始值。

def my_foo(a_hash)
  a_hash["test"]="reference"
end;

hash = {"test"=>"value"}
my_foo(hash)
puts "Ruby is pass-by-#{hash["test"]}"

我发现另一件事情。如果你传递的是数字类型,所有数字类型都是不可变的,因此是按值传递的。对于上面的字符串起作用的替换函数,在任何数字类型上都不起作用。 - Don Carr

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