通过引用或通过值传递?

sve*_*ven 51 language-agnostic oop parameters pass-by-reference pass-by-value

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

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

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

sve*_*ven 31

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

首先是一些代码:

public void swap(int x, int y)
{
  int tmp = x;
  x = y;
  y = tmp;
}
Run Code Online (Sandbox Code Playgroud)

调用此方法将导致:

int pi = 3;
int everything = 42;

swap(pi, everything);

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

"Output:
pi: 3
everything: 42"
Run Code Online (Sandbox Code Playgroud)

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

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"
Run Code Online (Sandbox Code Playgroud)

因此很明显,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"
Run Code Online (Sandbox Code Playgroud)

棘手成功改变了pnt1的价值!这意味着对象是通过引用传递的,事实并非如此!正确的说法应该是:对象引用是按值传递.

更多来自Tony Sintes:

该方法成功地改变了pnt1的值,即使它是按值传递的; 但是,pnt1和pnt2的交换失败了!这是混乱的主要原因.在main()方法中,pnt1和pnt2只不过是对象引用.当您将pnt1和pnt2传递给tricky()方法时,Java会像任何其他参数一样按值传递引用.这意味着传递给方法的引用实际上是原始引用的副本.下面的图1显示了Java将对象传递给方法后指向同一对象的两个引用.

图1
(来源:javaworld.com)

结论或长话短说:

  • Java 按值传递参数
  • "by value"是java将参数传递给方法的唯一方法
  • 使用作为参数给出的对象的方法将改变对象,因为引用指向原始对象.(如果该方法本身改变某些值)

有用的链接:


sve*_*ven 20

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

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

private void swap(string a, string b) {
  string tmp = a;
  a = b;
  b = tmp;
}
Run Code Online (Sandbox Code Playgroud)

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

string x = "foo";
string y = "bar";
swap(x, y);

"output: 
x: foo
y: bar"
Run Code Online (Sandbox Code Playgroud)

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

private void swap(ref string a, ref string b) {
  string tmp = a;
  a = b;
  b = tmp;
} 
Run Code Online (Sandbox Code Playgroud)

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

string x = "foo";
string y = "bar";
swap(x, y);

"output: 
x: bar
y: foo"
Run Code Online (Sandbox Code Playgroud)

c#也有一个out关键字,ref和out之间的区别是微妙的. 来自msdn:

取出参数的方法的调用者 不需要分配给在调用之前作为out参数传递的变量; 但是,被调用者需要在返回之前分配out参数.

相比之下,ref参数认为是被调用者最初分配的.因此,被调用者 在使用之前不需要分配给ref参数.Ref参数传入和传出方法.

像java一样,一个小的缺陷是仍然可以使用内部方法更改通过值传递的对象

结论:

  • 默认情况下,c#按值传递其参数
  • 但是当需要的参数也可以使用ref关键字通过引用传递
  • 从value传递的参数的内部方法将改变对象(如果该方法本身改变某些值)

有用的链接:


yuk*_*ude 19

Python使用pass-by-value,但由于所有这些值都是对象引用,因此净效果类似于pass-by-reference.但是,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']
Run Code Online (Sandbox Code Playgroud)

该行a = "Red"仅为a字符串值创建一个本地名称,"Red"并且对传入的参数(现在隐藏,因为a必须从那时起引用本地名称)没有影响.无论参数是可变的还是不可变的,赋值都不是就地操作.

b参数是对可变列表对象的引用,该.append()方法执行列表的就地扩展,并对新"Blue"字符串值进行处理.

(因为字符串对象是不可变的,所以它们没有任何支持就地修改的方法.)

一旦函数返回,重新赋值a就没有效果,而扩展名则b清楚地显示了传递引用样式的调用语义.

如前所述,即使参数for a是一个可变类型,函数内的重新赋值也不是就地操作,因此传递的参数值不会改变:

>>> a = ["Purple", "Violet"]
>>> do_something(a, b)
>>> print a, b
['Purple', 'Violet'] ['Black', 'Burgundy', 'Blue', 'Blue']
Run Code Online (Sandbox Code Playgroud)

如果您不希望被调用函数修改列表,则应使用不可变元组类型(由字面形式的括号标识,而不是方括号),这不支持就地.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'
Run Code Online (Sandbox Code Playgroud)

  • 从我在网上快速浏览Python参数传递讨论中看到的内容来看,大多数Python人都不知道通过引用传递什么.Python绝对是值得传递的.价值观的不变性是一个单独的问题.然后有些人对字典绑定感到困惑,并且不理解绑定到字典中的值的引用的符号与持有对值的引用的变量是相同的.通过引用传递的是传递对变量的引用而不是值的引用; 或者用符号说法,你传递一个可变名称绑定. (2认同)

Leo*_*erd 7

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

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

sub incr {
  my ( $x ) = @_;
  $x++;
}

my $value = 1;
incr($value);
say "Value is now $value";
Run Code Online (Sandbox Code Playgroud)

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

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

sub incr {
  $_[0]++;
}

my $value = 1;
incr($value);
say "Value is now $value";
Run Code Online (Sandbox Code Playgroud)

这次它将打印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";
Run Code Online (Sandbox Code Playgroud)

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


Kar*_*uin 6

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

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


Mat*_*kel 5

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

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

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


new*_*cct 5

无论您所说的按值传递还是按引用传递,都必须在不同语言中保持一致。跨语言使用的最常见和一致的定义是,通过引用传递,您可以“正常”将变量传递给函数(即无需显式获取地址或类似的内容),并且该函数可以分配给(而不是变异)函数内部参数的内容,它与分配给调用范围中的变量具有相同的效果。

从这个角度来看,语言分为以下几类:每个组具有相同的传递语义。如果您认为两种语言不应该放在同一组中,我挑战您想出一个区分它们的例子。

绝大多数语言,包括CJavaPythonRubyJavaScriptSchemeOCamlStandard MLGoObjective-CSmalltalk等都是仅传值的。传递指针值(某些语言称其为“引用”)不算作引用传递;我们只关心传递的东西,指针,而不是指向的东西。

C++C#PHP等语言默认情况下是按值传递,与上述语言类似,但函数可以使用&或显式声明参数为按引用传递ref

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