在学习一种新的编程语言时,您可能会遇到的一个可能的障碍是该语言是默认情况下是按值传递还是按引用传递。
所以这是我向你们所有人提出的问题,用你们最喜欢的语言,它实际上是如何完成的?可能的陷阱是什么?
在学习一种新的编程语言时,您可能会遇到的一个可能的障碍是该语言是默认情况下是按值传递还是按引用传递。
所以这是我向你们所有人提出的问题,用你们最喜欢的语言,它实际上是如何完成的?可能的陷阱是什么?
这是我自己对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 通过value传递其参数,因为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"
tricky成功改变了 pnt1 的值!这意味着对象是通过引用传递的,事实并非如此!正确的说法是:对象引用是按值传递的。
更多来自托尼·辛特斯:
该方法成功地改变了 pnt1 的值,即使它是按值传递的;但是,pnt1 和 pnt2 的交换失败了!这是混乱的主要来源。在 main() 方法中,pnt1 和 pnt2 只不过是对象引用。当您将 pnt1 和 pnt2 传递给 tricky() 方法时,Java 按值传递引用,就像任何其他参数一样。这意味着传递给该方法的引用实际上是原始引用的副本。下面的图 1 显示了在 Java 将对象传递给方法后指向同一个对象的两个引用。
(来源:javaworld.com)
结论或长话短说:
有用的链接:
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"
创建了一个本地名称 ,并且对传入的参数没有影响(它现在是隐藏的,因为从那时起必须引用本地名称)。无论参数是可变的还是不可变的,赋值都不是就地操作。a
"Red"
a
该b
参数是对可变列表对象的引用,该.append()
方法执行列表的就地扩展,附加新的"Blue"
字符串值。
(因为字符串对象是不可变的,它们没有任何支持就地修改的方法。)
一旦函数返回,对的重新赋值a
没有任何效果,而对的扩展b
清楚地显示了传递引用风格的调用语义。
如前所述,即使参数 fora
是可变类型,函数内的重新赋值也不是就地操作,因此传递参数的值不会发生变化:
>>> 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'
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 中一样,按值传递的对象仍然可以使用它们的内部方法进行更改
结论:
有用的链接:
由于我还没有看到 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 中的地址操作符非常相似。
.NET这里有一个很好的解释。
很多人对引用对象实际上是按值传递(在 C# 和 Java 中)感到惊讶。它是堆栈地址的副本。这可以防止方法更改对象实际指向的位置,但仍允许方法更改对象的值。在 C# 中,可以通过引用传递引用,这意味着您可以更改实际对象指向的位置。
无论您所说的按值传递还是按引用传递,都必须在不同语言之间保持一致。跨语言使用的最常见和一致的定义是,通过引用传递,您可以“通常”将变量传递给函数(即不显式获取地址或类似的东西),并且函数可以分配给(不变异的内容)函数内部的参数,它将与分配给调用范围内的变量具有相同的效果。
从这个角度来看,语言分为以下几类;每个组具有相同的传递语义。如果您认为不应该将两种语言放在同一个组中,我挑战您想出一个区分它们的示例。
包括C、Java、Python、Ruby、JavaScript、Scheme、OCaml、Standard ML、Go、Objective-C、Smalltalk等在内的绝大多数语言都是仅按值传递的。传递指针值(某些语言称其为“引用”)不算作按引用传递;我们只关心传递的东西,指针,而不是指向的东西。
默认情况下,C++、C#、PHP等语言与上述语言一样是按值传递,但函数可以使用&
或显式声明参数为按引用传递ref
。
Perl总是通过引用传递;然而,在实践中,人们几乎总是在获取值后复制值,从而以传递值的方式使用它。
关于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
明显的缺点是您需要在被调用函数中以某种方式知道变量的名称。但是这种技术可以轻松地移动大量数据。这就是为什么,虽然技术上不是通过引用传递,但我称之为“差不多”。
按价值
引用
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"
默认情况下,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;
}