通过引用传递或通过值?

umodi 发布于 2018-02-09 oop 最后更新 2018-02-09 01:00 356 浏览

在学习一种新的编程语言时,您可能遇到的一个可能的障碍就是这个语言是默认的通过值或者通过引用的问题。 所以,这是我的问题,以你最喜欢的语言,它实际上是如何完成的?什么是可能的陷阱? 当然,您最喜欢的语言可以是您玩过的任何语言:popularobscureesotericnewold ...

已邀请:

umodi

赞同来自:

这是我自己对Java programming language的贡献。 首先一些代码:

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.
figure 1 http://www.javaworld.com/javaworld/javaqa/2000-05/images/03-qa-0512-pass2b.gif 结论
或长话短说:

jautem

赞同来自:

按值

  • 比引用慢,因为系统必须复制参数
  • 仅用于输入
  • 引用
    • 因为只传递了一个指针
    • 用于输入输出
    • 如果与全局变量一起使用,
    • 可能非常危险

vvelit

赞同来自:

不要忘记还有按名称传递按值传递。 按值传递类似于按值传递,其中添加的方面是在作为参数传递的原始变量中设置值。它可以在一定程度上避免干扰全局变量。在分区内存中显然更好,通过引用传递可能导致页面错误(Reference)。 按名称传递意味着这些值只在实际使用时计算,而不是在过程开始时计算。 Algol使用pass-by-name,但是一个有趣的副作用是编写交换过程(Reference)非常困难。此外,每次访问时,通过名称传递的表达式都会被重新评估,这也会产生副作用。

umodi

赞同来自:

这是c# programming language的另一篇文章 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"
然而,unlike java c# does 给开发者提供了通过引用传递参数的机会,这是通过在参数类型之前使用'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之间的区别是微妙的。 from 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一样,一个小陷阱就是通过值传递的对象仍然可以使用内部方法来改变结论

iqui

赞同来自:

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

hiure

赞同来自:

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中,对象被视为primitives。意思是:
<?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"

qnobis

赞同来自:

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'

svelit

赞同来自:

关于J,虽然只有AFAIK通过值传递,但有一种通过引用传递的形式,可以移动大量的数据。您只需将一个名为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
明显的缺点是你需要在被调用的函数中以某种方式知道你的变量的名字。但是这种技术可以无痛地移动大量的数据。这就是为什么,虽然在技术上不通过参考,我称之为“非常多”。

vdicta

赞同来自:

默认情况下,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;
}

qnobis

赞同来自:

由于我还没有看到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中的&运算符地址一样。

ksit

赞同来自:

无论你说传递值或传递引用必须跨语言一致。跨语言使用的最常见和一致的定义是,通过引用传递,可以将变量传递给函数“正常”(即不需要明确地址或类似的东西),函数可以赋值给(不改变内容)函数内的参数,它将具有分配给调用范围内的变量相同的效果。 从这个角度来看,语言分组如下:每个组具有相同的传递语义。如果你认为两种语言不应该放在同一个团体中,我挑战你提出一个区分它们的例子。 绝大多数语言包括 C Java Python Ruby JavaScript Scheme OCaml 标准ML Go Objective-C strong> Smalltalk 等都是传递值。传递一个指针值(有些语言把它称为“引用”)不算作通过引用;我们只关心通过的东西,指针,而不是指向的东西。 诸如 C++ C# PHP 之类的语言在默认情况下是类似于上述语言的按值传递的,但函数可以显式声明参数为通过引用传递,使用&ref Perl 总是通过引用传递;然而,在实践中,人们几乎总是在得到它之后复制这些价值观,从而以一种价值传递的方式来使用它。