通过引用或通过值传递?

时间:2008-08-05 08:56:01

标签: language-agnostic oop parameters pass-by-reference pass-by-value

在学习新的编程语言时,您可能遇到的一个可能的障碍是,默认情况下,该语言是按值传递还是按引用传递

所以这是我对你们所有人的问题,用你最喜欢的语言, 实际上是怎么做的?什么是可能的陷阱

您最喜欢的语言当然可以是您玩过的任何语言:popularobscureesotericnewold ..

11 个答案:

答案 0 :(得分:31)

以下是我对Java programming language的贡献。

首先是一些代码:

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将其参数按值传递为 pi 所有 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"

棘手成功更改了pnt1的值! 这意味着对象是通过引用传递的,事实并非如此! 正确的陈述是: 对象引用按值传递。

来自Tony Sintes的更多信息:

  

该方法成功改变了   pnt1的值,即使它是   通过价值;然而,交换   pnt1和pnt2失败了!这是主要的   混乱的根源。在主()   方法,pnt1和pnt2仅此而已   比对象引用。当你通过   pnt1和pnt2到tricky()方法,   Java按值传递引用   就像任何其他参数一样。这个   表示传递给的引用   方法实际上是副本   原始参考。如下图1所示   显示两个指向的引用   Java传递后的同一个对象   反对方法。

figure 1
(来源:javaworld.com

结论或长话短说:

  • Java将参数按值
  • 传递给它
  • “按值”是java中用于将参数传递给方法的唯一方式
  • 使用来自对象方法作为参数将更改对象作为引用指向原始对象。 (如果该方法本身改变某些值)

有用的链接:

答案 1 :(得分:20)

以下是c# programming language

的另一篇文章

c#将其参数按值(默认情况下)

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

调用此版本的swap将因此没有结果:

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

"output: 
x: foo
y: bar"

然而,unlike 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之间的差异是微妙的。 from msdn:

  

一个方法的调用者   输出参数不是必需的   分配给作为传递的变量   在呼叫之前输出参数;   但是,被叫者需要   之前分配给out参数   返回。

  

相比之下参数是   被认为最初被分配   被叫方。因此,被叫方不是   需要分配给参考   使用前的参数。参考参数   被传入和传出   方法

与java一样,一个小的缺陷是,仍然可以使用内部方法更改通过值传递的对象

结论:

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

有用的链接:

答案 2 :(得分:19)

Python 使用pass-by-value,但由于所有这些值都是对象引用,因此净效果类似于pass-by-reference。但是,Python程序员更多地考虑对象类型是 mutable 还是 immutable 。可变对象可以就地更改(例如,字典,列表,用户定义的对象),而不可变对象不能(例如,整数,字符串,元组)。

以下示例显示了一个传递两个参数的函数,一个不可变字符串和一个可变列表。

>>> 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"仅为字符串值a创建本地名称"Red",并且对传入的参数(现在隐藏,因为{{ 1}}必须从那时起引用本地名称)。无论参数是可变的还是不可变的,赋值都不是就地操作。

a参数是对可变列表对象的引用,b方法执行列表的就地扩展,并添加新的.append()字符串值。< / p>

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

一旦函数返回,"Blue"的重新赋值就没有效果,而a的扩展清楚地显示了引用样式的调用语义。

如前所述,即使b的参数是可变类型,函数内的重新赋值也不是就地操作,因此传递的参数值不会发生变化:

a

如果您不希望被调用函数修改列表,则应使用不可变元组类型(由字面形式的括号标识,而不是方括号),这不支持就地{ {1}}方法:

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

答案 3 :(得分:7)

由于我还没有看过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中&地址运算符的方式大致相同。

答案 4 :(得分:6)

.NET有good explanation here

很多人都很惊讶参考对象实际上是通过值传递的(在C#和Java中)。它是堆栈地址的副本。这可以防止方法更改对象实际指向的位置,但仍允许方法更改对象的值。在C#中,可以通过引用传递引用,这意味着您可以更改实际对象指向的位置。

答案 5 :(得分:5)

不要忘记还有按名称传递传递值 - 结果

传递值 - 结果类似于传递值,添加的方面是在作为参数传递的原始变量中设置值。它可以在某种程度上避免干扰全局变量。在分区内存中显然更好,其中按引用传递可能导致页面错误(Reference)。

按名称传递意味着值仅在实际使用时计算,而不是在过程开始时计算。 Algol使用了pass-by-name,但有趣的副作用是编写交换过程非常困难(Reference)。此外,每次访问时都会通过名称传递的表达式进行重新评估,这也会产生副作用。

答案 6 :(得分:4)

无论你说什么作为按值传递或按引用传递都必须在不同语言中保持一致。跨语言使用的最常见和一致的定义是,通过引用传递,您可以“正常”将变量传递给函数(即,不显式地获取地址或类似的东西),并且函数可以分配给(不改变内容)函数内部的参数,它与调用作用域中的变量具有相同的效果。

从这个角度来看,语言分组如下;每个组具有相同的传递语义。如果您认为不应将两种语言放在同一组中,我会向您提出一个区分它们的示例。

绝大多数语言包括 C Java Python Ruby JavaScript < / strong>,计划 OCaml 标准ML 转到 Objective-C Smalltalk 等都是仅按值传递。传递指针值(某些语言称之为“引用”)不算作引用传递;我们只关心传递的东西,指针,而不是指向的东西。

C ++ C# PHP 等语言默认按上述语言传递值,但函数可以明确声明要使用&ref进行参考传递的参数。

Perl 总是按引用传递;然而,在实践中,人们几乎总是在获得它之后复制它们,从而以价值传递的方式使用它。

答案 7 :(得分:4)

按值

    由于系统必须复制参数,因此
  • 比引用慢
  • 仅用于输入

参考

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

答案 8 :(得分:3)

关于J,虽然只有AFAIK传递值,但有一种通过引用传递的形式,可以移动大量数据。您只需将已知为语言环境的内容传递给动词(或函数)。它可以是类的实例,也可以只是通用容器。

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

明显的缺点是你需要在被调用函数中以某种方式知道变量的名称。但是这种技术可以轻松地移动大量数据。这就是为什么,虽然技术上没有通过引用传递,但我称之为“差不多”。

答案 9 :(得分: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中,对象被视为primitives。这意味着:

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

答案 10 :(得分:-1)

默认情况下,ANSI / ISO C使用 - 它取决于您声明函数及其参数的方式。

如果将函数参数声明为指针,则函数将通过引用传递,如果将函数参数声明为非指针变量,则函数将按值传递。

void swap(int *x, int *y);   //< Declared as pass-by-reference.
void swap(int x, int y);     //< Declared as pass-by-value (and probably doesn't do anything useful.)

如果创建一个返回指向在该函数中创建的非静态变量的指针的函数,则可能会遇到问题。以下代码的返回值将是未定义的 - 无法知道分配给函数中创建的临时变量的内存空间是否被覆盖。

float *FtoC(float temp)
{
    float c;
    c = (temp-32)*9/5;
    return &c;
}

但是,您可以返回对静态变量或参数列表中传递的指针的引用。

float *FtoC(float *temp)
{
    *temp = (*temp-32)*9/5;
    return temp;
}