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

52

当学习一门新的编程语言时,你可能会遇到一个问题,那就是这门语言默认是按值传递还是按引用传递。

因此,我的问题是,对于你们所熟悉的编程语言,它是如何实现的?有哪些可能的陷阱

你喜欢的编程语言可以是任何你曾经使用过的:流行的(如Ruby)、冷门的(如Obscure)、古怪的(如Brainfuck)、新颖的(如CUDA)、老旧的(如FORTRAN)等等...


这里已经有一个答案解释了PHP5中的情况。链接 - Mat
4
哇,这个问题有一个指向测试服务器的链接。我认为你需要修复这个链接。 - Andrew Grimm
为什么这个被关闭了? - The Daleks stand with Ukraine
11个回答

31
这里是我为Java编程语言做出的贡献。
首先是一些代码:
public void swap(int x, int y)
{
  int tmp = x;
  x = y;
  y = tmp;
}

调用此方法将导致以下结果:

int pi = 3;
int everything = 42;

swap(pi, everything);

System.out.println("pi: " + pi);
System.out.println("everything: " + everything);

"Output:
pi: 3
everything: 42"

即使使用“真实”对象也会显示类似的结果:
public class MyObj {
    private String msg;
    private int number;

    //getters and setters
    public String getMsg() {
        return this.msg;
    }


    public void setMsg(String msg) {
        this.msg = msg;
    }


    public int getNumber() {
        return this.number;
    }


    public void setNumber(int number) {
        this.number = number;
    }

    //constructor
    public MyObj(String msg, int number) {
        setMsg(msg);
        setNumber(number);
    }
}

public static void swap(MyObj x, MyObj y)
{
    MyObj tmp = x;
    x = y;
    y = tmp;
}

public static void main(String args[]) {
    MyObj x = new MyObj("Hello world", 1);
    MyObj y = new MyObj("Goodbye Cruel World", -1); 

    swap(x, y);

    System.out.println(x.getMsg() + " -- "+  x.getNumber());
    System.out.println(y.getMsg() + " -- "+  y.getNumber());
}


"Output:
Hello world -- 1
Goodbye Cruel World -- -1"

因此,很明显Java是通过值传递其参数的,例如pieverything的值以及MyObj对象的值都没有交换。请注意,"按值传递"是Java向方法传递参数的唯一方式。(例如,像C++这样的语言允许开发人员在参数类型后使用'&'来按引用传递参数)
现在是棘手的部分,或者至少会让大多数新的Java开发人员感到困惑:(摘自javaworld)
原作者:Tony Sintes
public void tricky(Point arg1, Point arg2)
{
    arg1.x = 100;
    arg1.y = 100;
    Point temp = arg1;
    arg1 = arg2;
    arg2 = temp;
}
public static void main(String [] args)
{
    Point pnt1 = new Point(0,0);
    Point pnt2 = new Point(0,0);
    System.out.println("X: " + pnt1.x + " Y: " +pnt1.y); 
    System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);
    System.out.println(" ");
    tricky(pnt1,pnt2);
    System.out.println("X: " + pnt1.x + " Y:" + pnt1.y); 
    System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);  
}


"Output
X: 0 Y: 0
X: 0 Y: 0
X: 100 Y: 100
X: 0 Y: 0"

tricky成功地改变了pnt1的值!这意味着对象是按引用传递的,但事实并非如此!一个正确的说法应该是:对象引用是按值传递的。

Tony Sintes还说:

该方法成功地改变了pnt1的值,即使它是按值传递的;然而,交换pnt1和pnt2失败了!这是主要的混淆源。在main()方法中,pnt1和pnt2只是对象引用。当你将pnt1和pnt2传递给tricky()方法时,Java像任何其他参数一样通过值传递引用。这意味着传递给方法的引用实际上是原始引用的副本。下图1显示了Java将对象传递给方法后,两个引用指向同一个对象。

图1
(来源: javaworld.com)

结论或简言之:

  • Java将参数按值传递
  • "按值"是java中将参数传递给方法的唯一方式
  • 使用作为参数给定的对象的方法将更改对象,因为引用指向原始对象。(如果该方法本身更改了某些值)

有用的链接:


20

这是有关C#编程语言的另一篇文章。

C#默认情况下使用按值传递参数的方式。

private void swap(string a, string b) {
  string tmp = a;
  a = b;
  b = tmp;
}

调用这个版本的交换函数将不会产生任何结果:

string x = "foo";
string y = "bar";
swap(x, y);

"output: 
x: foo
y: bar"

然而,与Java不同,C#允许开发人员使用'ref'关键字在参数类型之前来通过引用传递参数。

private void swap(ref string a, ref string b) {
  string tmp = a;
  a = b;
  b = tmp;
} 

这个交换 改变引用参数的值:

string x = "foo";
string y = "bar";
swap(x, y);

"output: 
x: bar
y: foo"

c#也有一个out关键字,而ref和out之间的区别是微妙的。 来自msdn:

调用带有out参数的方法的调用者在调用前不需要为传递作为out参数的变量赋值;但是,在返回之前,被调用者必须为out参数分配值。

与此相反,ref参数被调用方认为是已经初始化的。因此,被调用方在使用ref参数之前不需要对其进行赋值。Ref参数既可以输入又可以输出到方法中。

一个小细节是,与Java一样,通过值传递的对象仍然可以使用其内部方法更改

结论:

  • c#默认情况下按值传递其参数
  • 但当需要时,参数也可以使用ref关键字按引用传递
  • 通过值传递的参数的内部方法将改变对象(如果该方法本身更改了某些值)

有用的链接:


20

Python采用传值方式,但由于所有这样的值都是对象引用,因此净效果类似于传递引用。 然而,Python程序员更多地考虑对象类型是可变的还是不可变的。可变对象可以就地更改(例如字典、列表、用户定义的对象),而不可变对象则不能(例如整数、字符串、元组)。

下面的示例显示了一个函数,该函数接受两个参数,一个不可变字符串和一个可变列表。

>>> def do_something(a, b):
...     a = "Red"
...     b.append("Blue")
... 
>>> a = "Yellow"
>>> b = ["Black", "Burgundy"]
>>> do_something(a, b)
>>> print a, b
Yellow ['Black', 'Burgundy', 'Blue']

a = "Red"语句仅仅是为字符串值"Red"创建了一个本地名称a,对传入的参数没有任何影响(现在该参数被隐藏了,因为之后必须引用本地名称a)。赋值不是就地操作,无论参数是可变还是不可变。

b参数是一个可变列表对象的引用,.append()方法执行就地扩展列表,在列表末尾添加新的"Blue"字符串值。

(因为字符串对象是不可变的,所以它们没有支持就地修改的方法。)

一旦函数返回,a的重新分配没有影响,而b的扩展则清楚地显示了按引用传递的调用语义。

如前所述,即使a的参数是可变类型,函数内的重新分配也不是就地操作,因此不会更改传递的参数值:

>>> a = ["Purple", "Violet"]
>>> do_something(a, b)
>>> print a, b
['Purple', 'Violet'] ['Black', 'Burgundy', 'Blue', 'Blue']

如果您不希望被调用的函数修改您的列表,那么您应该使用不可变元组类型(在字面形式中由括号而不是方括号标识),它不支持就地使用 .append() 方法:

>>> a = "Yellow"
>>> b = ("Black", "Burgundy")
>>> do_something(a, b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in do_something
AttributeError: 'tuple' object has no attribute 'append'

2
从我在网上快速浏览Python参数传递讨论中所读到的内容来看,大多数Python开发人员不知道什么是按引用传递。Python绝对是按值传递。值的不可变性是一个单独的问题。然后还有一些人会被字典绑定搞混,他们不理解符号绑定到字典中值的引用与变量持有值的引用是相同的事情。按引用传递是指传递变量的引用而不是值;或者用符号术语来说,就是传递可变名称绑定。 - Barry Kelly

8

我还没有看到Perl的答案,所以我想写一个。

在幕后,Perl 有效地按引用传递。变量作为函数调用参数被引用传递,常量被作为只读值传递,表达式的结果被作为临时值传递。通常的惯用语法是通过从@_进行列表赋值或通过shift来构造参数列表,这往往会将它隐藏在用户之后,给出传值的外观:

sub incr {
  my ( $x ) = @_;
  $x++;
}

my $value = 1;
incr($value);
say "Value is now $value";

这将打印出Value is now 1,因为$x++已经增加了在incr()函数中声明的词法变量,而不是传递进来的变量。这种按值传递的方式通常是大多数情况下所需的,因为在Perl中修改其参数的函数很少见,应避免使用此风格。
但是,如果特别需要这种行为,则可以通过直接操作@_数组的元素来实现,因为它们将是传递到函数中的变量的别名。
sub incr {
  $_[0]++;
}

my $value = 1;
incr($value);
say "Value is now $value";

这次将输出 Value is now 2,因为 $_ [0] ++ 表达式增加了实际的 $value 变量。这样工作的方式是,在幕后 @_ 不像大多数其他数组(例如通过 my @array 获得的数组)那样是一个真正的数组,而是它的元素直接由传递给函数调用的参数构建而成。如果需要构造按引用传递的语义,这允许您进行构造。将作为普通变量的函数调用参数原样插入此数组,将常量或更复杂表达式的结果插入为只读临时值。
然而,在实践中,这种情况极为罕见,因为 Perl 支持引用值;即指向其他变量的值。通常更清晰的做法是构造一个对变量具有明显副作用的函数,通过传递对该变量的引用来实现。这是对调用者来说,表明传递引用的语义是明确的指示。
sub incr_ref {
  my ( $ref ) = @_;
  $$ref++;
}

my $value = 1;
incr(\$value);
say "Value is now $value";

在这里,\ 运算符的作用方式与 C 语言中的 & 取地址运算符类似,它会返回一个引用。


6

对于.NET,这里有一个很好的解释

很多人惊讶地发现,在C#和Java中,引用对象实际上是按值传递的。它是栈地址的副本。这可以防止方法更改对象实际指向的位置,但仍允许方法更改对象的值。在C#中,可以通过引用传递来更改实际对象指向的位置。


5
不要忘记还有“按名称传递”和“按值结果传递”。 “按值结果传递”类似于“按值传递”,但增加了一个方面,即将值设置在作为参数传递的原始变量中。 在某种程度上,它可以避免与全局变量的干扰。 在分区内存中,它显然比引用传递更好,因为引用传递可能会导致页面错误(参见此处)。
“按名称传递”意味着只有在实际使用时才计算值,而不是在过程开始时。 Algol使用按名称传递,但有趣的副作用是很难编写交换过程(参见此处)。 此外,每次访问时都会重新评估按名称传递的表达式,这也可能产生副作用。

5
你所提到的传值和传引用在不同语言中必须保持一致。跨语言最常见且一致的定义是,使用传引用时,可以“正常”地将变量传递给函数(即无需显式地获取地址或类似方法),并且函数可以对函数内的参数进行赋值(而不是改变其内容),并将与在调用范围内对变量进行赋值具有相同的效果。
从这个角度来看,各种语言被分为以下组; 每个组具有相同的传递语义。如果您认为两种语言不应放在同一组中,请尝试提出区分它们的示例。
绝大多数编程语言都只支持传值,包括 C、Java、Python、Ruby、JavaScript、Scheme、OCaml、Standard ML、Go、Objective-C、Smalltalk 等。传递指针值(某些语言称之为“引用”)不算作传引用;我们只关心传递的对象,即指针,而不是指向的对象。
像 C++、C#、PHP 这样的语言默认也是传值的,但函数可以明确声明参数为传引用,使用 & 或 ref 等符号。
Perl 总是通过引用传递;然而,在实践中,人们几乎总是在获取值后复制它,从而以传值方式使用它。

C不应该与Java等语言在同一组中,因为在C中可以获取变量的地址并将其传递给函数。这使得被调用的函数可以更改变量的值。也就是说,在C中可以进行按引用传递。 - fishinear
1
@fishinear:不对,那是传值调用。它会复制传递的值(一个指针)。 - newacct
1
@fishinear:不对。传值和传引用是处理语法结构的语义概念,与“概念上”的内容无关。在C或Objective-C中不存在传引用。 - newacct
1
@fishinear:你的“概念”没有很好地定义。事实上,在任何语言中都可以进行“概念性按引用传递”。在Java中:很容易。只需使用一个元素的数组代替所有变量。要读取变量,请访问元素0。要写入变量,请写入元素0。当您“按引用传递”时,只需传递该数组即可。 - newacct
1
@fishinear:再说一遍,你不是“将它作为参数传递”。加上&不是一个“技术细节”——这是最重要的细节。按引用传递是一个非常技术性的术语,涉及语法和语义。只有在直接传递变量时,没有任何额外的操作,才能称之为“按引用传递”。如果你不想在这些事情上严格要求,就不应该使用这些术语。在C语言中,技术上没有按引用传递。这是众所周知的,也没有争议。只需在StackOverflow上搜索即可。 - newacct
显示剩余6条评论

4

关于J,据我所知,它只支持传值,但是有一种传引用的形式可用于移动大量数据。你只需将一个称为locale的东西传递给一个动词(或函数)。它可以是类的实例或通用容器。

spaceused=: [: 7!:5 <
exectime =: 6!:2
big_chunk_of_data =. i. 1000 1000 100
passbyvalue =: 3 : 0
    $ y
    ''
)
locale =. cocreate''
big_chunk_of_data__locale =. big_chunk_of_data
passbyreference =: 3 : 0
    l =. y
    $ big_chunk_of_data__l
    ''
)
exectime 'passbyvalue big_chunk_of_data'
   0.00205586720663967
exectime 'passbyreference locale'
   8.57957102144893e_6

明显的缺点是您需要以某种方式知道被调用函数中变量的名称。但是,这种技术可以轻松移动大量数据。这就是为什么虽然在技术上不是按引用传递,我称其为“几乎如此”。

4

按值传递

  • 由于系统需要复制参数,所以比按引用传递慢
  • 仅用于输入

按引用传递

  • 只传递指针,因此速度更快
  • 用于输入和输出
  • 如果与全局变量一起使用,可能非常危险

虽然并没有回答问题,但是对于表述事实加一。 - MPelletier

2

PHP 也是按值传递。

<?php
class Holder {
    private $value;

    public function __construct($value) {
        $this->value = $value;
    }

    public function getValue() {
        return $this->value;
    }
}

function swap($x, $y) {
    $tmp = $x;
    $x = $y;
    $y = $tmp;
}

$a = new Holder('a');
$b = new Holder('b');
swap($a, $b);

echo $a->getValue() . ", " . $b->getValue() . "\n";

输出:

a b

然而在PHP4中,对象被视为类似于原语的东西。这意味着:

<?php
$myData = new Holder('this should be replaced');

function replaceWithGreeting($holder) {
    $myData->setValue('hello');
}

replaceWithGreeting($myData);
echo $myData->getValue(); // Prints out "this should be replaced"

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