如何区分哪些变量是通过引用传递的?

How is the distinction made which variables are passed by reference?

提问人:StoneThrow 提问时间:9/25/2021 最后编辑:StoneThrow 更新时间:9/27/2021 访问量:80

问:

在与 和 的漫长历史之后,我最近才成为一个编程语言多语言的人,开始认真地研究新的语言,特别是 ,而且程度要小得多。CC++GroovyPython

回复:“引用传递语言”,我想了解哪些变量按函数传递,哪些变量按值传递的规则(或经验法则^)。

我在 和 中编写了两个等效的程序。这些程序演示了整数是按值传递的,但字典是通过引用传递的:Jenkins/GroovyPython

def dfunc(v) {
  v.foo = "dolor"
  v = [a:"a", b:"b"]
}

def podfunc(v) { v = 42 }

pipeline {
  agent any

  stages {
    stage( "1" ) {
      steps {
        script {
          def my_dict = [foo: "lorem", bar: "ipsum"]
          def my_int = 5

          echo my_dict.toString()
          dfunc(my_dict)
          echo my_dict.toString()

          echo my_int.toString()
          podfunc(my_int)
          echo my_int.toString()
        }
      }
    }
  }
}
def dfunc(d):
    d["foo"] = "dolor"
    d = { "a": "a", "b": "b" }
    
def podfunc(d):
    d = 42

my_dict = { "foo": "lorem", "bar": "ipsum" }
my_int = 5

print(my_dict)
dfunc(my_dict)
print(my_dict)

print(my_int)
podfunc(my_int)
print(my_int)

以上两种方法的输出都是有效的:

{'foo': 'lorem', 'bar': 'ipsum'}
{'foo': 'dolor', 'bar': 'ipsum'}
5
5

(在支撑等方面有特定语言的变化)

我认为另一种看待这个问题的方式——但我不确定这是否正确——也许是这些语言与其说是“按引用传递”,不如说是“按值传递,但某些变量是在堆上隐式创建的”。

换句话说,我推断 在 和 中,当我调用 or 时,在这两种情况下,我在技术上都是按值传递的;它只是在堆栈上创建像隐式整数这样的“基元”,并在堆上创建“其他变量”,如隐式字典。GroovyPythondfunc(d)podfunc(d)my_intmy_dict

这是否正确解释了为什么某些变量似乎是按值传递的,而另一些变量似乎是按引用传递的?

如果是这样,在堆栈/堆上隐式创建了哪些变量,为什么?这个编码器是可控的吗?
在我可怜的偏见眼中,这两个变量的声明没有任何自我证明哪个是在堆栈上创建的,哪个是在堆上创建的:
C

def my_dict = [foo: "lorem", bar: "ipsum"]
def my_int = 5

我能找到的最接近的现有讨论是:
Groovy(或 Java) - 通过引用传递到包装器对象
中 通过引用传递与按值传递有什么区别?
。但要么我没有完全理解现有的答案,要么它们没有完全解决我的问题,如上所述。

谢谢你帮我解决这个问题。


^@juanpa.Arrivillaga 纠正了我:程序没有证明任何内容都是通过引用传递的——这个问题不可避免地变成了关于“引用传递”一词的误用。

python groovy 按引用传递

评论

2赞 juanpa.arrivillaga 9/25/2021
请注意,您的示例未演示字典是通过引用传递的。为了演示通过引用传递,对参数的赋值将在调用方中可见在 Python 中,情况从来都不是这样
1赞 Matias Cicero 9/25/2021
@juanpa.arrivillaga 在 C 语言中“通过引用”传递只是一个公认的约定,通过这种约定,您可以传递指向某物的指针而不是某物的值。在这方面,它的行为与 Python 完全相同:您可以更新指针指向的值,但更新指针本身在函数之外没有影响,因为指针是通过值传递的。
1赞 juanpa.arrivillaga 9/25/2021
@MatiasCicero这根本不是引用传递的。这是一个根本性的误解。事实上,C语言非常有名,它只支持按值调用。按值传递指针不是按引用传递。Python 既不使用这两种评估策略。这些只是术语的基本问题。人们经常错误地使用这些术语这一事实并不是延续这种情况的借口。
1赞 juanpa.arrivillaga 9/26/2021
@StoneThrow是的,没错。您可以定义一个 和 that does ,并在其他一些函数中定义 do 和 now 在调用者的范围内将是 ,这在 python 中是不可能的。从本质上讲,在按引用调用中,参数将充当调用方作用域中变量的别名。此外,由于该值不是在 Python 中复制的,因此它也不是按值调用的foo(int& i)i = 42foo(x)x42
1赞 juanpa.arrivillaga 9/26/2021
@StoneThrow是的,您可以将 Python 中的变量视为自动取消引用的指针。IOW python 具有“引用语义”。但请注意,说 Python 是按值调用也是不准确的。学术术语是芭芭拉·里斯科夫(Barbara Liskov)创造的“共享呼叫”(以里斯科夫替代原则而闻名)。但这个术语并不常用。这是一篇非常好的文章,它消除了这三种评估策略的歧义:robertheaton.com/2014/02/09/......

答:

1赞 Renato 9/27/2021 #1

这个答案侧重于 Groovy/JVM,但我相信从概念上讲,它在 Python 和任何其他不直接公开指针的高级语言中的工作方式类似。

Groovy 和 Python(据我所知)没有程序员可以在该语言中使用的指针。然而,非原始类型的变量(至少在 Groovy/Java 中),或者换句话说,对象在某种意义上是“通过引用”传递的......除了不能直接访问指针,因此无法直接修改指针外,只能通过重新分配对象本身指向的数据来修改它们。

对象通常在堆上分配,但 JVM 优化实际上可能会在所谓的转义分析中跳过分配 - 即,如果它可以证明指针仅在本地函数中需要,它可能会将内存直接放在堆栈上(TBH 我不确定何时完成以及它是否常见)。不过,这对程序的语义来说并不重要,因为堆栈和堆分配之间的差异不会暴露给 JVM 程序。

正如你所发现的,基元是按值传递的......不可变对象也是通过值“在语义上”传递的(事实上,当 Project Valhalla 准备就绪时,JVM 将很快添加值类型),尽管实际上它们只是指针:这是因为你不能重新分配你得到的指针,而对象是不可变的意味着你也不能重新分配它本身可能拥有的任何指针: 实际上,它是一种价值。

但是,当你将一个可变对象传递给一个方法时,你被允许改变对象(它的字段)内的“指针”,但不能改变对象引用本身所指向的数据——因为它在 Java/Groovy 中是公开或可访问的......因此,这有点类似于在具有指针的语言中通过引用传递某些内容,但并不相同。

举几个例子:

import groovy.transform.Immutable 
import groovy.transform.Canonical

@Immutable
class Person {
    String name
}

@Canonical
class MutablePerson {
    String name
}

def takesPerson(Person p) {
    // nothing we can change on p as it's immutable,
    // effectively `p` is a "value", not reference.
    println p

    // we are allowed to re-assign p locally, but the
    // caller's reference is not affected. 
    p = null
}

def takesMPerson(MutablePerson p) {
    // we're allowed to change the internal structure of p
    p.name = 'Eva'

    // again, re-assigning p here won't change the caller's ref
    p = null
}

person = new Person(name: 'Joe')
mPerson = new MutablePerson(name: 'Mary')

println "Person before call: $person"
println "MutablePerson before call: $mPerson"

takesPerson person
takesMPerson mPerson

println "Person after call: $person"
println "MutablePerson after call: $mPerson"

结果:

Person before call: Person(Joe)
MutablePerson before call: MutablePerson(Mary)
Person(Joe)
Person after call: Person(Joe)
MutablePerson after call: MutablePerson(Eva)

在我可怜的 C 偏向眼中,这两个变量的声明没有任何内容可以证明在堆栈上创建和在堆上创建的内容:

def my_dict = [foo: "lorem", bar: "ipsum"]
def my_int = 5

在 Java 中,你知道什么时候某些东西可能会进入堆,因为你使用关键字来创建它(与永远不需要进入堆的文字相反,它可以用双引号创建,在这种情况下,它们可能会或可能不会分配内存,这取决于它是否是一个常量 - 没有初始化逻辑的静态最终 - 或者它已被内禁)。但是在Groovy中,这并不是数据可能在堆上分配的唯一情况,因为Groovy比Java有更多的文字,例如列表,集合和Maps,它们也放在堆上(正如我之前提到的,有警告)。newString

但是,如果你只知道在幕后,Groovy 集合文字只是调用 Java,然后将你给出的值添加到其中,那么你就可以合理地猜测堆上将分配哪些内容,哪些不分配。new CollectionType()

对于Groovy程序员来说,更相关的问题是他们可以更改哪些数据,以及重新分配某些东西会产生什么影响。

希望通过上面的示例可以清楚地了解您可以更改的内容:非最终局部变量和类字段。而且,由于方法参数只是 JVM 方法中的局部变量,因此重新赋值它们仅在方法主体的作用域内有效。重新分配类字段在类实例处于“活动状态”时生效,直到再次重新分配为止。

数据结构似乎使这变得更加复杂,但这只是一回事,因为它们都是使用类和字段实现的。当您更改 Groovy Map(或字典)时,它只是在某处重新分配一个字段。

你可以看到,通过实现一个小的数据结构,比如一个极其简化的链接列表:

import groovy.transform.Canonical

@Canonical
class MyLinkedList {
  def value
  def next
}

def singleItemList = new MyLinkedList(value: 'single item')

def list = new MyLinkedList(value: 'first item',
                            next: singleItemList)

println singleItemList
println list

// modify list
list.next = new MyLinkedList(value: 'new item')

println list

// same kind of thing happens when you modify a Map
def map = [foo: 'lorem', bar: 'ipsum']

println map

map.foo = 'maximum'

println map

结果:

MyLinkedList(single item, null)
MyLinkedList(first item, MyLinkedList(single item, null))
MyLinkedList(first item, MyLinkedList(new item, null))
[foo:lorem, bar:ipsum]
[foo:maximum, bar:ipsum]

评论

0赞 juanpa.arrivillaga 9/29/2021
“但是,当你将一个可变对象传递给一个方法时,你就可以更改对象(它的字段)中的”指针“,但不能更改对象引用本身指向的数据——因为它在 Java/Groovy 中是公开或可访问的......因此,这有点类似于在有指针的语言中通过引用传递某些内容,但并不相同。不,这根本不像通过引用传递。
0赞 juanpa.arrivillaga 9/29/2021
这里是按参考传递的关键功能,将打印something_else。即,对通过引用传递的参数的赋值*在作为参数*提供的变量的调用方作用域中*可见。你只是在描述传递的指针。例如,这在 C 中是可能的,但 C 绝对只能按值调用x = somethign; def foo(&var): var = something_else; foo(x); print(x)
0赞 Renato 9/29/2021
你有一个定义,我试图明确这个定义在JVM中是不可能的。但是,由于 JVM 中的任何非原始对象始终是指向实际数据的指针,因此它绝对是一个“引用”。因为当你调用一个方法并将一个 Object 作为参数传入时,该方法会得到与调用方相同的数据,所以可以说你传入的是一个引用。按值传递意味着对“值”的更改不会反映调用方的数据,但它确实反映了调用方的数据。因此,我说按引用传递比按值传递更接近事实。pass by reference
0赞 Renato 9/29/2021
让我们拯救未来的读者这场无用的辩论:stackoverflow.com/questions/373419/......Java 和 C 只有在你使用旧的、严格的短语定义时才是按值传递的,而忽略了现代程序员可能从短语中推断出的内容以及“值”和“引用”的含义。
0赞 juanpa.arrivillaga 9/29/2021
这不是一场辩论。词语是有含义的。它不像C++和C#,这两种支持引用调用的语言,今天没有被大量使用。哎呀,即使是 Fortran 今天仍在使用。仅仅因为程序员错误地使用了这些术语,并不能使它成为一场“辩论”。而且 Python 也不是按值传递的。