当学习一门新的编程语言时,你可能会遇到一个问题,那就是这门语言默认是按值传递还是按引用传递。
因此,我的问题是,对于你们所熟悉的编程语言,它是如何实现的?有哪些可能的陷阱?
你喜欢的编程语言可以是任何你曾经使用过的:流行的(如Ruby)、冷门的(如Obscure)、古怪的(如Brainfuck)、新颖的(如CUDA)、老旧的(如FORTRAN)等等...
当学习一门新的编程语言时,你可能会遇到一个问题,那就是这门语言默认是按值传递还是按引用传递。
因此,我的问题是,对于你们所熟悉的编程语言,它是如何实现的?有哪些可能的陷阱?
你喜欢的编程语言可以是任何你曾经使用过的:流行的(如Ruby)、冷门的(如Obscure)、古怪的(如Brainfuck)、新颖的(如CUDA)、老旧的(如FORTRAN)等等...
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"
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将对象传递给方法后,两个引用指向同一个对象。
(来源: javaworld.com)
结论或简言之:
有用的链接:
这是有关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一样,通过值传递的对象仍然可以使用其内部方法更改
结论:
有用的链接:
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'
我还没有看到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
获得的数组)那样是一个真正的数组,而是它的元素直接由传递给函数调用的参数构建而成。如果需要构造按引用传递的语义,这允许您进行构造。将作为普通变量的函数调用参数原样插入此数组,将常量或更复杂表达式的结果插入为只读临时值。sub incr_ref {
my ( $ref ) = @_;
$$ref++;
}
my $value = 1;
incr(\$value);
say "Value is now $value";
在这里,\
运算符的作用方式与 C 语言中的 &
取地址运算符类似,它会返回一个引用。
对于.NET,这里有一个很好的解释。
很多人惊讶地发现,在C#和Java中,引用对象实际上是按值传递的。它是栈地址的副本。这可以防止方法更改对象实际指向的位置,但仍允许方法更改对象的值。在C#中,可以通过引用传递来更改实际对象指向的位置。
&
不是一个“技术细节”——这是最重要的细节。按引用传递是一个非常技术性的术语,涉及语法和语义。只有在直接传递变量时,没有任何额外的操作,才能称之为“按引用传递”。如果你不想在这些事情上严格要求,就不应该使用这些术语。在C语言中,技术上没有按引用传递。这是众所周知的,也没有争议。只需在StackOverflow上搜索即可。 - newacct关于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
按值传递
按引用传递
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"