提问人:StoneThrow 提问时间:9/25/2021 最后编辑:StoneThrow 更新时间:9/27/2021 访问量:80
如何区分哪些变量是通过引用传递的?
How is the distinction made which variables are passed by reference?
问:
在与 和 的漫长历史之后,我最近才成为一个编程语言多语言的人,开始认真地研究新的语言,特别是 ,而且程度要小得多。C
C++
Groovy
Python
回复:“引用传递语言”,我想了解哪些变量按函数传递,哪些变量按值传递的规则(或经验法则^)。
我在 和 中编写了两个等效的程序。这些程序演示了整数是按值传递的,但字典是通过引用传递的:Jenkins/Groovy
Python
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 时,在这两种情况下,我在技术上都是按值传递的;它只是在堆栈上创建像隐式整数这样的“基元”,并在堆上创建“其他变量”,如隐式字典。Groovy
Python
dfunc(d)
podfunc(d)
my_int
my_dict
这是否正确解释了为什么某些变量似乎是按值传递的,而另一些变量似乎是按引用传递的?
如果是这样,在堆栈/堆上隐式创建了哪些变量,为什么?这个编码器是可控的吗?
在我可怜的偏见眼中,这两个变量的声明没有任何自我证明哪个是在堆栈上创建的,哪个是在堆上创建的:C
def my_dict = [foo: "lorem", bar: "ipsum"]
def my_int = 5
我能找到的最接近的现有讨论是:
Groovy(或 Java) - 通过引用传递到包装器对象
中 通过引用传递与按值传递有什么区别?
。但要么我没有完全理解现有的答案,要么它们没有完全解决我的问题,如上所述。
谢谢你帮我解决这个问题。
^@juanpa.Arrivillaga 纠正了我:程序没有证明任何内容都是通过引用传递的——这个问题不可避免地变成了关于“引用传递”一词的误用。
答:
这个答案侧重于 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,它们也放在堆上(正如我之前提到的,有警告)。new
String
但是,如果你只知道在幕后,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]
评论
x = somethign; def foo(&var): var = something_else; foo(x); print(x)
pass by reference
评论
foo(int& i)
i = 42
foo(x)
x
42