通过引用传递或通过值?
在学习一门新的编程语言时,可能遇到的障碍之一就是这个语言默认是按值传递还是按引用传递的问题 。
所以,这是我的问题,以你最喜欢的语言向你们所有人怎么做? 有什么可能的陷阱 ?
当然,你最喜欢的语言可以是你曾经玩过的任何东西: stream行的 , 晦涩的 , 深奥的 , 新的 , 旧的 …
这是我自己对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中将parameter passing给方法的唯一方法。 (例如像c ++这样的语言允许开发者在参数的types之后使用' & '引用参数)
现在是棘手的部分 ,或者至less是会混淆大部分新的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的值,尽pipe它是按值传递的; 但是,交换pnt1和pnt2失败! 这是混乱的主要来源。 在main()方法中,pnt1和pnt2只不过是对象引用。 当您将pnt1和pnt2传递给tricky()方法时,Java就像其他参数那样通过值传递引用。 这意味着传递给方法的引用实际上是原始引用的副本。 下面的图1显示了在Java将一个对象传递给一个方法后,两个引用指向同一个对象。
图1 http://www.javaworld.com/javaworld/javaqa/2000-05http://img.dovov.com03-qa-0512-pass2b.gif
结论或长话短说:
- Java 按值传递参数
- “通过值”是java中传递参数给方法的唯一方法
- 使用给定参数中的方法将会改变对象,因为参考指向原始对象。 (如果该方法本身改变某些值)
有用的链接:
这里是另一篇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# 不同的 是 ,开发者有机会通过引用传递参数,这是通过在参数types之前使用'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参数的方法的调用者在调用之前不需要分配给作为outparameter passing的variables; 但是,被调用者在返回之前需要分配给out参数。
和
相反, 参考参数被认为是由被调用者初始分配的。 因此,被调用者在使用前不需要分配给ref参数。 参数参数传入和传出方法。
像java一样,一个小陷阱就是依靠值传递的对象仍然可以使用它们的内部方法来改变
结论:
- c#默认通过值传递它的参数
- 但是当需要的参数也可以通过引用传递使用ref关键字
- 通过值传递的参数的内部方法将改变对象(如果该方法本身改变某些值)
有用的链接:
Python使用按值传递,但是由于所有这些值都是对象引用,所以净效果与通过引用类似。 但是,Python程序员更多地考虑对象types是可变的还是不可变的 。 可变对象可以就地改变(例如,字典,列表,用户定义的对象),而不可变的对象不能(例如,整数,string,元组)。
下面的例子展示了一个传递两个参数,一个不可变string和一个可变列表的函数。
>>> 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"
仅为string值"Red"
创build一个本地名称a
,并且对传入的参数(现在是隐藏的,因为从此之后必须引用本地名称) 。 无论参数是可变的还是不可变的,赋值都不是一个就地操作。
b
参数是对可变列表对象的引用, .append()
方法执行列表的原地扩展,并添加新的"Blue"
string值。
(因为string对象是不可变的,所以它们没有任何支持就地修改的方法。)
一旦函数返回,a的重新赋值没有任何作用,而b
的扩展清楚地显示了按引用风格的调用语义。
如前所述,即使a
的参数是一个可变types,函数内的重新赋值也不是一个就地操作,所以不会改变传入的参数的值:
>>> a = ["Purple", "Violet"] >>> do_something(a, b) >>> print a, b ['Purple', 'Violet'] ['Black', 'Burgundy', 'Blue', 'Blue']
如果你不想让被调用的函数修改你的列表,你会改用不可变的元组types(由字面forms的圆括号来标识,而不是方括号),它不支持原地的.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可以有效地传递参考。 作为函数调用参数的variables被引用传递,常量作为只读值传递,expression式的结果作为临时对象传递。 通过@_
列表分配构build参数列表的惯用习惯用法,或者通过shift
倾向于将它隐藏起来,给出了按值传递的外观:
sub incr { my ( $x ) = @_; $x++; } my $value = 1; incr($value); say "Value is now $value";
这将打印Value is now 1
因为$x++
增加了在incr()
函数中声明的词法variables,而不是传入的variables。这种按值传递的样式通常是大多数时候需要的函数,作为函数修改它们的参数在Perl中是很less见的,应该避免这种风格。
但是,如果出于某种原因,这种行为是特别需要的,可以通过直接操作@_
数组的元素来实现,因为它们将是传递给函数的variables的别名。
sub incr { $_[0]++; } my $value = 1; incr($value); say "Value is now $value";
这次它将打印Value is now 2
,因为$_[0]++
expression式增加了实际的$value
variables。 这样做的方式是,@ @下的引擎不像大多数其他数组那样是一个真正的数组(比如可以通过my @array
获得),而是直接在传递给函数调用的参数之外构build它的元素。 这允许您构build传递引用语义(如果需要的话)。 作为普通variables的函数调用参数是按原样插入到该数组中的,并且将更复杂expression式的常量或结果作为只读临时对象插入。
然而,在实践中这是非常罕见的,因为Perl支持参考值; 即引用其他variables的值。 通常情况下,构造一个对variables有明显副作用的函数,通过传入一个对该variables的引用,会更加清晰。 这对于读者来说是明确的指示,通过引用的语义是有效的。
sub incr_ref { my ( $ref ) = @_; $$ref++; } my $value = 1; incr(\$value); say "Value is now $value";
这里的\
运算符产生的引用&
C中的&
address-of运算符非常相似。
.NET有一个很好的解释 。
很多人都惊讶于引用对象实际上是通过值传递的(在C#和Java中)。 这是一个堆栈地址的副本。 这可以防止方法改变对象实际指向的位置,但仍允许方法改变对象的值。 在C#中可能通过引用传递引用,这意味着您可以更改实际对象指向的位置。
不要忘记,也有名字 传递 ,并通过价值的结果 。
按值传递类似于按值传递,其中添加的方面是在作为parameter passing的原始variables中设置值。 它可以在一定程度上避免干扰全局variables。 分区内存显然是更好的,通过引用传递可能导致页面错误( 引用 )。
按名称传递意味着这些值只在实际使用时计算,而不是在过程开始时计算。 Algol使用pass-by-name,但是一个有趣的副作用是编写交换程序( Reference )非常困难。 此外,每次访问时,通过名称传递的expression式都会被重新评估,这也会产生副作用。
按价值
- 因为系统必须复制参数,所以比参考文件慢
- 仅用于input
通过参考
- 因为只有一个指针被传递而更快
- 用于input和输出
- 如果与全局variables一起使用可能会非常危险
无论你说传递值或传递引用必须跨语言一致。 跨语言使用的最常见和一致的定义是,在传递引用的情况下,可以将variables传递给函数“正常”(即不显式地使用地址或类似的东西),函数可以赋值 )函数内部的参数的内容,它与分配给调用范围中的variables具有相同的效果。
从这个angular度来看,语言分组如下: 每个组具有相同的传递语义。 如果你认为两种语言不应该放在同一个团体中,我挑战你提出一个区分它们的例子。
包括C , Java , Python , Ruby , JavaScript , Scheme , OCaml , Standard ML , Go , Objective-C , Smalltalk等在内的绝大多数语言都只是传值 。 传递一个指针值(有些语言把它称为“引用”)不算作通过引用; 我们只关心通过的东西,指针,而不是指向的东西。
像C ++ , C# , PHP这样的语言在默认情况下像上面的语言那样是按值传递的,但是函数可以使用&
或者ref
来显式地声明参数是通过引用传递的。
Perl总是通过引用; 然而,在实践中,人们几乎总是在得到它之后复制这些价值观,从而以一种价值传递的方式来使用它。
关于J ,虽然只有AFAIK通过价值传递,但有一种通过参考的forms,可以移动大量的数据。 您只需将一个名为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
明显的缺点是你需要在被调用的函数中以某种方式知道你的variables的名字。 但是这种技术可以无痛地移动大量的数据。 这就是为什么,虽然在技术上不通过参考,我称之为“非常多”。
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";
输出:
ab
然而在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使用 – 它取决于你如何声明你的函数及其参数。
如果将函数参数声明为指针,那么该函数将作为参考传递,如果将函数参数声明为非指针variables,则该函数将是按值传递的。
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.)
如果创build一个返回指向该函数中创build的非静态variables的指针的函数,则可能会遇到问题。 以下代码的返回值将是未定义的 – 无法知道分配给在函数中创build的临时variables的内存空间是否被覆盖。
float *FtoC(float temp) { float c; c = (temp-32)*9/5; return &c; }
但是,您可以返回对参数列表中传递的静态variables或指针的引用。
float *FtoC(float *temp) { *temp = (*temp-32)*9/5; return temp; }