“通过引用传递”究竟是什么意思?

谁有权决定?

编辑:显然我没有成功地完善我的问题。
不是在问Java的论证是如何传递的。 我知道看起来像持有对象的变量实际上是一个包含对象引用的变量,并且该引用是按值传递的。 这里有很多很好的解释(在链接的线程和其他线程中)和其他地方。

问题是关于术语传递的技术含义 。 (结束编辑)

我不确定这是否是SO的正确问题,如果没有道歉,但我不知道更好的地方。 在这里的其他问题中已经说了很多,例如, Java是“传递参考”还是“传递价值”? 并通过引用或通过值传递? ,但我没有找到该术语含义的权威答案。

我认为“通过引用传递”意味着“将引用(通常是指针)传递给对象”,因此被调用者可以修改调用者看到的对象,而“按值传递”意味着复制对象,并让被调用者玩得开心(明显的问题:如果对象包含引用,深层复制或浅层)。
唱FW出现很多 地方 说 “通过参考传递”就是说, 这里有一些论点,它意味着更多,但定义仍然读取

一种ParameterPassing模式,其中将实际参数的引用(或者如果你想在政治上不正确,指针)传递给forms参数; 当被调用者需要forms参数时,它取消引用指针以获取它。

我没有找到很多地方给出更强的术语定义,在这个页面上,我发现“forms参数的左值被设置为实际参数的左值”。 并且,如果我理解正确, 这里使用相同的定义(“forms参数仅作为实际参数的别名。”)

事实上,我发现使用更强定义的唯一地方是反对这样一种观点,即在Java中,对象是通过引用传递的(这可能是由于我缺乏google-fu)。

所以,如果我把事情弄清楚,那就是通过参考传递

class Thing { ... } void byReference(Thing object){ ... } Thing something; byReference(something); 

根据第一个定义大致对应(在C中)

 struct RawThing { ... }; typedef RawThing *Thing; void byReference(Thing object){ // do something } // ... struct RawThing whatever = blah(); Thing something = &whatever; byReference(something); // pass whatever by reference // we can change the value of what something (the reference to whatever) points to, but not // where something points to 

从这个意义上讲,说Java通过引用传递对象就足够了。 但根据第二个定义,传递参考意味着或多或少

 struct RawThing { ... }; typedef RawThing *RawThingPtr; typedef RawThingPtr *Thing; void byReference(Thing object){ // do something } // ... RawThing whatever = blah(); RawThingPtr thing_pointer = &whatever; byReference(&thing_pointer); // pass whatever by reference // now we can not only change the pointed-to (referred) value, // but also where thing_pointer points to 

而且由于Java只允许你有对象的指针(限制你可以用它们做什么),但没有指针指针,从这个意义上说,Java通过引用传递对象是完全错误的。

所以,

  1. 我是否已充分理解上述传递参考的定义?
  2. 还有其他定义吗?
  3. 是否有共识哪个定义是“正确的”,如果是,哪个?

当然,不同的人目前对“传递参考”的含义有不同的定义。 这就是为什么他们不同意某些东西是否通过引用传递。

但是,无论您使用何种定义,都必须在语言中使用它。 你不能说一种语言具有传值,并且在另一种语言中具有完全相同的语义,并且说它是通过引用传递的。 指出语言之间的类比是解决这一争议的最佳方式,因为虽然人们可能对特定语言中的传递模式有强烈的意见,但当你将相同的语义与其他语言进行对比时,它有时会带来反直觉的结果,迫使他们重新思考他们的定义。

  • 一个主要观点是Java只是按值传递。 (在Internet上随处搜索,您将找到这种观点。)此视图是对象不是值,但始终通过引用进行操作,因此它是按值分配或传递的引用。 该视图认为,pass-by-reference的测试是否可以分配给调用范围中的变量。

如果一个人同意这个观点,那么人们也必须考虑大多数语言,包括Python,Ruby,OCaml,Scheme,Smalltalk,SML,Go,JavaScript,Objective-C等多种语言, 作为传值 。 如果其中任何一个让你觉得奇怪或违反直觉,我挑战你指出为什么你认为在Java中的对象中任何一种语言中的对象的语义不同。 (我知道其中一些语言可能明确声称它们是传递引用;但它与它们所说的无关;必须根据实际行为对所有语言应用一致的定义。)

  • 如果您采用相反的观点,即Java中的对象是传递引用,那么您还必须将C视为传递引用。

以Java为例:

 class Thing { int x; } void func(Thing object){ object.x = 42; object = null; } Thing something = null; something = new Thing(); func(something); 

在C中,它将等同于:

 typedef struct { int x; } Thing; void func(Thing *object){ object->x = 42; object = NULL; } Thing *something = NULL; something = malloc(sizeof Thing); memset(something, 0, sizeof(something)); func(something); // later: free(something); 

我声称上述内容在语义上是等价的; 只有语法不同。 唯一的语法差异是:

  1. C需要显式*来表示指针类型; Java的引用(指向对象的指针)类型不需要显式*
  2. C使用->通过指针访问字段; Java只是使用.
  3. Java使用new为堆上的新对象动态分配内存; C使用malloc来分配它,然后我们需要初始化内存。
  4. Java有垃圾收集

请注意,重要的是,

  1. 在这两种情况下,使用对象调用函数的语法都是相同的: func(something) ,无需执行任何操作,例如获取地址或任何内容。
  2. 在这两种情况下,对象都是动态分配的(它可能超出了函数的范围)。 和
  3. 在这两种情况下, object = null; 函数内部不影响调用范围。

因此在两种情况下语义都是相同的,所以如果你调用Java pass-by-reference,你也必须调用C pass-by-reference。

谁有权决定? 没有人,也没有人。 你自己决定; 作家决定他或她的书; 读者决定是否同意作者。

要理解这个术语,需要深入了解语言(并用C代码解释它们而不是忽视这一点)。 参数传递样式是指编译器通常用于创建特定行为的机制。 通常定义以下内容:

  • 按值传递:在输入子例程时,参数将复制到参数中
  • 传递结果:输入子程序时参数未定义,子程序返回时参数复制到参数
  • pass by value-result:将参数复制到entry处的参数中,并将参数复制到返回的参数中
  • 通过引用传递:对参数变量的引用被复制到参数中; 对参数变量的任何访问都透明地转换为对参数变量的访问

(术语注释:参数是子例程中定义的变量,参数是调用中使用的表达式。)

教科书通常也会按名称定义传递,但这种情况很少见,也不容易解释。 通过需要也存在。

参数传递样式的重要性在于它的作用:在传递值时,对参数所做的任何更改都不会传递给参数; 在通过结果传递时,对参数所做的任何更改都会传递给最后的参数; 在通过引用传递时,对参数所做的任何更改都会在创建时传递给参数。

某些语言定义了多种传递样式,允许程序员分别为每个参数选择其首选样式。 例如,在Pascal中,默认样式是按值传递,但程序员可以使用var关键字来指定按引用传递。 其他一些语言指定一种传递方式。 还有一些语言为不同类型指定不同的样式(例如,在C中,传递值是默认值,但数组是通过引用传递的)。

现在,在Java中,从技术上讲,我们有一种带有pass-by-value的语言,对象变量的值是对象的引用。 是否使Java通过引用传递对象变量是一个品味问题。

这两个C示例实际上都演示了pass-by-value,因为C没有pass-by-reference。 只是你传递的值是一个指针。 通过引用传递的语言如Perl:

 sub set_to_one($) { $_[0] = 1; # set argument = 1 } my $a = 0; set_to_one($a); # equivalent to: $a = 1 

这里,变量$a实际上是通过引用传递的,因此子例程可以修改它。 它不会修改$a通过间接指向的某个对象; 相反,它会修改$a本身。

Java在这方面就像C一样,只是在Java对象中是“引用类型”,所以你所拥有的(以及你所能传递的所有东西)都是指向它们的指针。 像这样的东西:

 void setToOne(Integer i) { i = 1; // set argument = 1 } void foo() { Integer a = 0; setToOne(a); // has no effect } 

实际上不会改变; 它只会重新分配给i

实际上, 通过引用传递将引用传递给值 – 而不是它的副本 – 作为参数。


我想在继续之前,应该定义某些事情。 我可能会使用它们,而不是习惯使用它们。

  • 对象是数据分子。 它占用存储空间,可能包含其他对象,但具有自己的标识,可以作为单个单元引用和使用。

  • 引用是对象的别名或句柄。 在语言层面,参考文献大多与它所指的东西相似; 根据语言的不同,编译器/解释器/运行时/ gnomes会在需要实际对象时自动取消引用它。

  • 是评估表达式的结果。 它是一个具体的对象,可以存储,传递给函数等。(OOP想知道,注意我在这里使用“对象”这个通用的“数据分子”意义,而不是OOP“类实例”的意义。 )

  • 变量是对预分配的命名引用

    特别注意: 变量不是值。 尽管如此,变量通常不会改变。 他们的价值就是变化。 他们很容易混淆,这在一定程度上certificate了参考幻觉通常有多好。

  • 引用类型变量 (Java,C#,…)是一个值,它是一个引用变量


当您将变量作为参数传递时,大多数语言将默认创建变量值的副本并传递副本。 被调用者将其参数的名称绑定到该副本。 这被称为“通过价值”(或更明确地说,“通过复制”)。 呼叫两侧的两个变量最终会有不同的存储位置,因此是完全不同的变量(只是因为它们通常以相等的值开始相关)。

另一方面,通过引用传递不会复制。 相反,它传递变量本身(减去名称)。 也就是说,它将引用传递给变量别名的相同值。 (这通常通过隐式传递指向变量存储的指针来完成,但这只是一个实现细节;调用者和被调用者不必知道或关心它是如何发生的。)被调用者将其参数的名称绑定到该位置。 最终结果是双方使用相同的存储位置(仅可能是不同的名称)。 因此,被调用者对其变量所做的任何更改也都会对调用者的变量进行。 例如,在面向对象语言的情况下,可以为变量赋予完全不同的值。

大多数语言(包括Java)本身不支持这种语言。 哦,他们喜欢他们这样做……但那是因为那些从来没有真正通过引用传递的人 ,往往不会在这样做和通过值传递参考之间找到微妙的区别。 使用这些语言引起混淆的地方是引用类型变量。 Java本身永远不会直接使用引用类型对象,而是引用这些对象。 区别在于“包含”所述对象的变量。 引用类型变量的值是这样的引用(或者,有时,特殊引用值,表示“无”)。 当Java传递这样的引用时,虽然它不复制对象,但它仍然复制值(即:函数获取的引用是变量引用的值的副本)。 也就是说,它传递了一个引用 ,但是它是按值传递 。 这允许通过引用传递的大多数事物允许但不是全部


我能想到的最明显的测试是真正的传递参考支持,那就是“交换测试”。 本机支持通过引用传递的语言必须提供足够的支持来编写swap其参数值的函数swap 。 代码相当于:

 swap (x, y): <-- these should be declared as "reference to T" temp = x x = y y = temp -- value1 = (any valid T) value2 = (any other valid T) a = value1 b = value2 swap(a, b) assert (a == value2 and b == value1) 
  1. 必须是可能的并且成功运行 - 对于允许复制和重新分配的任何类型T - 使用语言的赋值和严格相等运算符(包括由T指定的任何重载); 和
  2. 不得要求调用者转换或“包装”args(例如:通过显式传递指针)。 要求将args标记为通过引用传递是可以的。

(显然,没有可变变量的语言不能以这种方式进行测试 - 但这很好,因为它们并不重要 。两者之间的巨大语义差异是被调用者的变量是如何可修改的。在任何情况下,变量的值都不可修改,差异只是实现细节或优化。)

注意,这个答案中的大多数谈话都是关于“变量”的。 许多语言(如C ++)也允许通过引用传递匿名值。 机制是一样的; 值占用存储空间,引用是它的别名。 它不一定在调用者中有一个名字。

维基百科给出了一个非常明确的引用引用定义,我无法改进:

在逐个引用的评估(也称为pass-by-reference)中,函数接收对用作参数的变量的隐式引用,而不是其值的副本。 这通常意味着函数可以修改(即赋值给)用作参数的变量 – 其调用者将看到它。

请注意,您的示例都不是引用调用,因为在C中分配forms参数永远不会修改调用者看到的参数。

但这足以复制粘贴,阅读完整的讨论(带有示例)

http://en.wikipedia.org/wiki/Evaluation_strategy#Call_by_reference

Java不通过引用传递。 您总是传递副本/按值。 但是,如果您传递一个对象,那么您将获得该引用的副本。 因此,您可以直接编辑对象,但是如果覆盖本地引用,则不会覆盖原始对象引用。

通过引用传递参数意味着参数的指针嵌套比局部变量的指针嵌套更深。 如果您有一个具有类类型的变量,则该变量是指向实际值的指针。 基本类型的变量包含值本身。

现在,如果按值传递这些变量,则保持指针嵌套:对象引用保留指向对象的指针,原始变量保留值本身。

将变量作为引用传递意味着指针嵌套变得更深:您将指针传递给对象引用,以便您可以更改对象引用; 或者您传递指向基元的指针,以便您可以更改其值。

这些定义用于C#和Object Pascal ,它们都具有通过引用传递变量的关键字。

回答你的问题:因为最后的变量 – whatever是第一个例子还是第二个例子中的thing_pointer – 都通过指针( & )传递给函数,两者都通过引用传递。

如果您熟悉C,可能以下类比解释了Java的工作原理。 这仅适用于类类型(而非基本类型)的对象。

在Java中,我们可以有一个变量并将其传递给函数:

 void f(Object x) { x.bar = 5; // #1j x = new Foo; // #2j } void main() { Foo a; a.bar = 4; f(a); // now a.bar == 5 } 

在C中,这看起来如下:

 void f(struct Foo * p) { p->bar = 5; // #1c p = malloc(sizeof(struct Foo)); // #2c } int main() { struct Foo * w = malloc(sizeof(struct Foo)); w->bar = 4; f(w); /* now w->bar = 5; */ } 

在Java中,class-type的变量总是引用 ,在C中最忠实地映射到指针 。 但是在函数调用中,指针本身是通过复制传递的。 访问 #1j和#1c中的指针会修改原始变量,因此从这个意义上说,您正在传递对变量的引用。 但是,变量本身只是一个指针,它本身是通过复制传递的。 所以当你为它分配别的东西时。 在#2j和#2c中,您只是在f的本地范围内重新绑定引用/指针的副本 。 各个示例中的原始变量aw保持不变。

简而言之:一切都是参考,引用按值传递。

另一方面,在C中,我可以通过声明void v(struct Foo ** r);来实现真正的“通过引用传递” void v(struct Foo ** r); 并呼吁f(&w) ; 这将允许我从f内改变自己。

注1:对于像int这样的基本类型不是这样,它们完全按值传递。

注2:C ++示例有点整洁,因为我可以通过引用传递指针(我不必说struct ): void f(Foo * & r) { r = new Foo; }f(w);