首页 / 知识

关于语言不可知:通过引用或通过值传递?

2023-04-11 15:54:00

Pass by reference or pass by value?

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

所以这是我对你们所有人的问题,用你最喜欢的语言,它是如何实际完成的? 什么是可能的陷阱?

当然,你最喜欢的语言可以是你曾经玩过的任何东西:流行的,模糊的,深奥的,新的,旧的......


这是我对Java编程语言的贡献。

首先是一些代码:

1
2
3
4
5
6
public void swap(int x, int y)
{
  int tmp = x;
  x = y;
  y = tmp;
}

调用此方法将导致:

1
2
3
4
5
6
7
8
9
10
11
int pi = 3;
int everything = 42;

swap(pi, everything);

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

"Output:
pi: 3
everything: 42"

即使使用'真实'对象也会显示类似的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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的价值!
这意味着对象是通过引用传递的,事实并非如此!
正确的陈述是:Object引用按值传递。

更多来自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.

figure 1
(来源:javaworld.com)

结论或长话短说:

  • Java按值传递参数
  • "by value"是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

这是c#编程语言的另一篇文章

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

1
2
3
4
5
private void swap(string a, string b) {
  string tmp = a;
  a = b;
  b = tmp;
}

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

1
2
3
4
5
6
7
string x ="foo";
string y ="bar";
swap(x, y);

"output:
x: foo
y: bar"

但是,与java不同,c#确实为开发人员提供了通过引用传递参数的机会,这是通过在参数类型之前使用'ref'关键字来完成的:

1
2
3
4
5
private void swap(ref string a, ref string b) {
  string tmp = a;
  a = b;
  b = tmp;
}

此交换将更改引用参数的值:

1
2
3
4
5
6
7
string x ="foo";
string y ="bar";
swap(x, y);

"output:
x: bar
y: foo"

c#也有一个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关键字通过引用传递
  • 从value传递的参数的内部方法将改变对象(如果该方法本身改变某些值)

有用的链接:

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


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

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

1
2
3
4
5
6
7
8
9
>>> 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的参数是一个可变类型,函数内的重新赋值也不是就地操作,因此传递的参数值不会改变:

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

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

1
2
3
4
5
6
7
>>> 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'

由于我还没有看过Perl的答案,我以为我会写一个。

在引擎盖下,Perl可以有效地作为传递引用。作为函数调用参数的变量是引用传递的,常量作为只读值传递,表达式的结果作为临时值传递。通过列表赋值从@_shift构造参数列表的惯用惯用法倾向于将其隐藏在用户之外,从而给出了pass-by-value的外观:

1
2
3
4
5
6
7
8
sub incr {
  my ( $x ) = @_;
  $x++;
}

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

这将打印Value is now 1,因为$x++增加了在incr()函数内声明的词法变量,而不是传入的变量。这种按值传递的样式通常是大多数时候想要的,作为函数修改他们的参数在Perl中很少见,应该避免使用样式。

但是,如果由于某种原因特别需要这种行为,可以通过直接操作@_数组的元素来实现,因为它们将是传递给函数的变量的别名。

1
2
3
4
5
6
7
sub incr {
  $_[0]++;
}

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

这次它将打印Value is now 2,因为$_[0]++表达式增加了实际的$value变量。它的工作方式是在引擎盖下@_不像大多数其他数组那样是一个真正的数组(例如可以通过my @array获得),而是它的元素直接由传递给函数调用的参数构建。如果需要,这允许您构造传递引用语义。作为普通变量的函数调用参数按原样插入到此数组中,并将更复杂表达式的常量或结果作为只读临时值插入。

然而,在实践中这样做非常罕见,因为Perl支持参考值;也就是说,引用其他变量的值。通常,通过传入对该变量的引用来构造对变量具有明显副作用的函数更为清晰。这对于呼叫站点的读者来说是一个明确的指示,即传递引用语义是有效的。

1
2
3
4
5
6
7
8
sub incr_ref {
  my ( $ref ) = @_;
  $$ref++;
}

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

这里\运算符产生的引用与C中& address-of运算符的方式大致相同。


这里有一个很好的解释.NET。

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


不要忘记还有名称传递,并通过值结果传递。

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

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


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

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

绝大多数语言,包括C,Java,Python,Ruby,JavaScript,Scheme,OCaml,Standard ML,Go,Objective-C,Smalltalk等,都只是按值传递。传递指针值(某些语言称之为"引用")不算作引用传递;我们只关心传递的东西,指针,而不是指向的东西。

像C ++,C#,PHP这样的语言默认是像上面的语言一样传递值,但是函数可以使用&ref显式地声明参数是传递引用。

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


按价值

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

引用

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

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


PHP也是通过值传递的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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() ."
";

输出:

1
a b

但是在PHP4中,对象被视为原始对象。意思是:

1
2
3
4
5
6
7
8
9
<?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"

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

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

1
2
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.)

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

1
2
3
4
5
6
float *FtoC(float temp)
{
    float c;
    c = (temp-32)*9/5;
    return &c;
}

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

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


语言值传递引用编程语言

最新内容

相关内容

热门文章

推荐文章

标签云

猜你喜欢