通过引用还是通过值?

时间:2020-03-05 18:37:41  来源:igfitidea点击:

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

所以这是我向大家提出的问题,用我们最喜欢的语言,实际上是如何完成的?可能的陷阱是什么?

我们最喜欢的语言当然可以是我们曾经玩过的任何东西:流行,晦涩,深奥,新颖,古老...

解决方案

回答

这是我对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会按值传递其参数,因为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的更多作品:

The method successfully alters the
  value of pnt1, even though it is
  passed by value; however, a swap of
  pnt1 and pnt2 fails! This is the major
  source of confusion. In the main()
  method, pnt1 and pnt2 are nothing more
  than object references. When you pass
  pnt1 and pnt2 to the tricky() method,
  Java passes the references by value
  just like any other parameter. This
  means the references passed to the
  method are actually copies of the
  original references. Figure 1 below
  shows two references pointing to the
  same object after Java passes an
  object to a method.

图1 http://www.javaworld.com/javaworld/javaqa/2000-05/images/03-qa-0512-pass2b.gif

结论或者长话短说:

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

有用的链接:

  • http://www.javaworld.com/javaworld/javaqa/2000-05/03-qa-0526-pass.html
  • http://www.ibm.com/developerworks/java/library/j-passbyval/
  • http://www.ibm.com/developerworks/library/j-praxis/pr1.html
  • http://javadude.com/articles/passbyvalue.htm

回答

按价值

  • 比参考速度慢,因为系统必须复制参数
  • 仅用于输入

引用

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

回答

别忘了还有通过名称传递和通过价值结果传递。

按值传递结果与按值传递相似,不同之处在于,在作为参数传递的原始变量中设置了值。它可以在某种程度上避免干扰全局变量。显然在分区内存中更好,在这种情况下,按引用传递可能会导致页面错误(参考)。

按名称传递表示仅在实际使用值时才计算值,而不是在过程开始时计算值。 Algol使用了pass-by-name,但是一个有趣的副作用是,编写交换过程非常困难(参考)。另外,每次访问通过名称传递的表达式时,都会对其进行重新求值,这也会产生副作用。

回答

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

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"

但是,与java cdoes不一样,开发人员可以通过引用传递参数,这是通过在参数类型之前使用'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"

calso有一个out关键字,ref和out之间的区别很小。
来自msdn:

The caller of a method which takes an
  out parameter is not required to
  assign to the variable passed as the
  out parameter prior to the call;
  however, the callee is required to
  assign to the out parameter before
  returning.

In contrast ref parameters are
  considered initially assigned by the
  callee. As such, the callee is not
  required to assign to the ref
  parameter before use. Ref parameters
  are passed both into and out of a
  method.

一个小陷阱是,就像在Java中一样,按值传递的对象仍然可以使用其内部方法进行更改

结论:

  • C#默认通过值传递其参数
  • 但是在需要的时候也可以使用ref关键字通过引用传递参数
  • 由值传递的参数的内部方法将更改对象(如果该方法本身更改了某些值)

有用的链接:

  • http://msdn.microsoft.com/zh-CN/vcsharp/aa336814.aspx
  • http://www.c-sharpcorner.com/UploadFile/saragana/Willswapwork11162005012542AM/Willswapwork.aspx
  • http://en.csharp-online.net/Value_vs_Reference

回答

.NET有一个很好的解释。

许多人惊讶于引用对象实际上是按值传递的(在两种Cand Java中)。这是堆栈地址的副本。这样可以防止方法更改对象实际指向的位置,但仍然允许方法更改对象的值。在Cit中,可以通过引用传递引用,这意味着我们可以更改实际对象指向的位置。

回答

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"

回答

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'

回答

关于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

明显的缺点是,我们需要以某种方式在被调用函数中知道变量的名称。但是这种技术可以轻松地移动许多数据。这就是为什么尽管从技术上讲并未通过引用,但我称其为"差不多"。