R,深拷贝与浅拷贝,按引用传递

26

我想了解R在传递函数参数、创建变量副本等方面使用的逻辑,以及与内存使用相关的情况。什么时候它实际上创建变量副本,而仅仅是传递该变量的引用?我特别关注的情况是:

f <- function(x) {x+1}
a <- 1
f(a)

a 是直接传递还是传递的 a 的引用?

x <- 1
y <- x

复制的引用?什么情况下不是这种情况?

如果有人能向我解释一下,我将非常感激。


3
您可能会发现 R 语言定义中的这个部分很有帮助。 - joran
2
Morandat和他的同事们在这篇论文中对R语言中参数的惰性求值进行了有趣而又批判性的讨论:http://www.cs.purdue.edu/homes/jv/pubs/ecoop12.pdf - jthetzel
4
在适当编译的 R 上使用 tracemem 可以帮助探索,.Internal(inspect(x)) 也有帮助,需要理解 NAMED 字段;我的总体原则是“一变再复制”,比如说 y <- x 不会触发复制(因为原始数据没有变化),xy 指向的内存被 NAMED ,修改它们中的任何一个都会触发复制。 - Martin Morgan
3个回答

19

在传递变量时,始终是通过副本而不是引用进行的。但有时,直到实际发生赋值才会创建一个副本。该过程的真正描述是“通过承诺”进行的。请查看文档。

?force
?delayedAssign

一个实际的影响是,要避免需要至少两倍于您对象名义上占用的RAM数量几乎是很困难的,甚至是不可能的。修改一个大对象通常需要制作临时副本。

更新:2015年:我同意Matt Dowle所说的,他的data.table包提供了一条避免复制重复问题的替代路线。如果这是所要求的更新,那么在建议提出时我没有理解它。

R语言3.2.1中applyReduce的评估规则发生了最近变化。具体内容可参考这里的新闻: Returning anonymous functions from lapply - what is going wrong?

而jhetzel在评论中引用的有趣的论文现已在此处


12
使用data.table包,查看其中的?copy函数。使用:=运算符可以通过引用进行赋值,查看?":="以及一系列的set*函数。与需要2-3个对象副本的传统方法不同,我们的目标是只需要一个列值或更少的工作内存即可完成操作。 - Matt Dowle

13

虽然回答晚了,但这是语言设计中非常重要的一个方面,在网络上没有得到足够的关注(或者至少是通常的来源)。

x <- c(0,4,2)
lobstr::obj_addr(x)
# [1] "0x7ff25e82b0f8"
y <- x
lobstr::obj_addr(y)
# [1] "0x7ff25e82b0f8"

注意,这里的“内存地址”是相同的,即对象存储的内存位置。因此,您可以确认xy都指向同一个标识符。
Hadley Wickham的《Advanced R》一书涉及到了这个问题:
考虑以下代码: x <- c(1, 2, 3) 很容易将其理解为:“创建一个名为‘x’的对象,包含值1、2和3”。不幸的是,这是一种简化,会导致关于R在幕后实际执行的操作的错误预测。更准确地说,这段代码正在做两件事:它正在创建一个对象,一个值向量c(1, 2, 3)。然后将该对象绑定到名称x上。换句话说,对象或值没有名称;实际上是名称具有值。
请注意,它们的内存地址是短暂的,并且每次新的R会话都会更改。
现在是重要的部分。
在R语义中,对象按值复制。这意味着修改副本不会影响原始对象。由于在内存中复制数据是一项昂贵的操作,因此R中的副本尽可能懒惰。只有在实际修改新对象时才会发生复制。 来源:[R lang documentation][1]
[1]: https://cran.r-project.org/doc/manuals/r-release/R-lang.html

所以,如果我们现在通过向向量附加一个值来修改y的值,y现在指向一个不同的“对象”。这与文档中关于复制操作只有在“新对象被修改”时才会发生(惰性)的说法一致。 y 指向的地址与先前不同。

y <- c(y, -3)
print(lobstr::obj_addr(y))
# [1] "0x7ff25e825b48"

1

@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!

这在用 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


感谢提供这些有见地的示例!您知道当我们在将对象的子集用作函数参数时,是否也会复制该对象 - 比如矩阵? - pglpm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接