我想了解R在传递函数参数、创建变量副本等方面使用的逻辑,以及与内存使用相关的情况。什么时候它实际上创建变量副本,而仅仅是传递该变量的引用?我特别关注的情况是:
f <- function(x) {x+1}
a <- 1
f(a)
a
是直接传递还是传递的 a 的引用?
x <- 1
y <- x
复制的引用?什么情况下不是这种情况?
如果有人能向我解释一下,我将非常感激。
我想了解R在传递函数参数、创建变量副本等方面使用的逻辑,以及与内存使用相关的情况。什么时候它实际上创建变量副本,而仅仅是传递该变量的引用?我特别关注的情况是:
f <- function(x) {x+1}
a <- 1
f(a)
a
是直接传递还是传递的 a 的引用?
x <- 1
y <- x
复制的引用?什么情况下不是这种情况?
如果有人能向我解释一下,我将非常感激。
在传递变量时,始终是通过副本而不是引用进行的。但有时,直到实际发生赋值才会创建一个副本。该过程的真正描述是“通过承诺”进行的。请查看文档。
?force
?delayedAssign
一个实际的影响是,要避免需要至少两倍于您对象名义上占用的RAM数量几乎是很困难的,甚至是不可能的。修改一个大对象通常需要制作临时副本。
更新:2015年:我同意Matt Dowle所说的,他的data.table包提供了一条避免复制重复问题的替代路线。如果这是所要求的更新,那么在建议提出时我没有理解它。
R语言3.2.1中apply
和Reduce
的评估规则发生了最近变化。具体内容可参考这里的新闻: Returning anonymous functions from lapply - what is going wrong?
而jhetzel在评论中引用的有趣的论文现已在此处。
data.table
包,查看其中的?copy
函数。使用:=
运算符可以通过引用进行赋值,查看?":="
以及一系列的set*
函数。与需要2-3个对象副本的传统方法不同,我们的目标是只需要一个列值或更少的工作内存即可完成操作。 - Matt Dowle虽然回答晚了,但这是语言设计中非常重要的一个方面,在网络上没有得到足够的关注(或者至少是通常的来源)。
x <- c(0,4,2)
lobstr::obj_addr(x)
# [1] "0x7ff25e82b0f8"
y <- x
lobstr::obj_addr(y)
# [1] "0x7ff25e82b0f8"
x
和y
都指向同一个标识符。x <- c(1, 2, 3)
很容易将其理解为:“创建一个名为‘x’的对象,包含值1、2和3”。不幸的是,这是一种简化,会导致关于R在幕后实际执行的操作的错误预测。更准确地说,这段代码正在做两件事:它正在创建一个对象,一个值向量c(1, 2, 3)
。然后将该对象绑定到名称x
上。换句话说,对象或值没有名称;实际上是名称具有值。所以,如果我们现在通过向向量附加一个值来修改y
的值,y
现在指向一个不同的“对象”。这与文档中关于复制操作只有在“新对象被修改”时才会发生(惰性)的说法一致。 y
指向的地址与先前不同。
y <- c(y, -3)
print(lobstr::obj_addr(y))
# [1] "0x7ff25e825b48"
@onlyphantom 这非常有帮助!对象可以在不被复制的情况下从函数中来回传递,这也是我来这里的原因:
tmp <- function(x, create) {
if(!create){
x
}else{
"new"
}
}
x = c(0,4,2)
y = tmp(x, F)
lobstr::obj_addr(x) == lobstr::obj_addr(y) # y points to x!
oldAddr = lobstr::obj_addr(x)
x = tmp(x, F)
lobstr::obj_addr(x) == oldAddr # TRUE!
tmp = function(x, label = deparse(x), force=TRUE) {
if(force){
label
}
x <- x + 1
print(label); return(x)
}
tmp(2)
[1] "2"
[1] 3
tmp(2, force=F)
[1] "3"
[1] 3
R版本3.6.3
tracemem
可以帮助探索,.Internal(inspect(x))
也有帮助,需要理解NAM
ED 字段;我的总体原则是“一变再复制”,比如说y <- x
不会触发复制(因为原始数据没有变化),x
和y
指向的内存被NAMED
,修改它们中的任何一个都会触发复制。 - Martin Morgan