在函数中通过引用向 data.table 添加新列并不总是有效

Posted

技术标签:

【中文标题】在函数中通过引用向 data.table 添加新列并不总是有效【英文标题】:Adding new columns to a data.table by-reference within a function not always working 【发布时间】:2015-01-21 23:13:11 【问题描述】:

在编写依赖于data.table 的包时,我发现了一些奇怪的行为。我有一个按引用删除和重新排序某些列的函数,它工作得很好,这意味着我传入的data.table 被修改了,但没有分配函数输出。我有另一个添加 new 列的函数,但是这些更改并不总是保留在传入的 data.table 中。

这是一个小例子:

library(data.table)  # I'm using 1.9.4
test <- data.table(id = letters[1:2], val=1:2)
foobar <- function(dt, col) 
    dt[, (col) := 1]
    invisible(dt)


test
#  id val
#1: a   1
#2: b   2
saveRDS(test, "test.rds")
test2 <- readRDS("test.rds")
all.equal(test, test2)
#[1] TRUE
foobar(test, "new")
test
#  id val new
#1: a   1   1
#2: b   2   1
foobar(test2, "new")
test2
#  id val
#1: a   1
#2: b   2

发生了什么? test2 有什么不同?我可以就地修改现有列:

foobar(test, "val")
test
#  id val new
#1: a   1   1
#2: b   1   1
foobar(test2, "val")
test2
#  id val
#1: a   1
#2: b   1

但添加到test2 仍然不起作用:

foobar(test2, "someothercol")
.Last.value
#  id val someothercol
#1: a   1            1
#2: b   1            1
test2
#  id val
#1: a   1
#2: b   1

我无法确定我看到此行为的所有情况,但保存到 RDS 和从 RDS 读取是我可以可靠复制的第一个情况。写入和读取 CSV 似乎没有同样的问题。

这是一个指针问题ala this issue,就像序列化 data.table 会破坏过度分配的指针?有没有简单的方法来恢复它们?如何在我的函数中检查它们,以便在操作不起作用时恢复指针或错误?

我知道我可以将函数输出分配为一种解决方法,但这不是很data.table-y。那不是也会在内存中创建一个临时副本吗?

对 Arun 解决方案的回应

Arun 指示确实是指针问题,可以用truelength 诊断,用setDTalloc.col 修复。我遇到了一个问题,将他的解决方案封装在一个函数中(继续上面的代码):

func <- function(dt) if (!truelength(dt)) setDT(dt)
func2 <- function(dt) if (!truelength(dt)) alloc.col(dt)
test2 <- readRDS("test.rds")
truelength(test2)
#[1] 0
truelength(func(test2))
#[1] 100
truelength(test2)
#[1] 0
truelength(func2(test2))
#[1] 100
truelength(test2)
#[1] 0

所以看起来函数内的本地副本正在被正确修改,但参考版本不是。为什么不呢?

【问题讨论】:

绝妙的问题/解释/可重复的示例/动机。荣誉。我什至可以在开发版本上重现这一点。顺便说一句,您的函数 IMO 中不需要 invisible(dt) 部分。 @DavidArenburg 谢谢!我知道我可以忽略它,但出于某种原因,我想禁止打印。再想一想,显示函数输出和原始data.table的区别可能更清楚了。哦,好吧。 【参考方案1】:

这是一个指针问题还是这个问题,比如序列化 data.table 会破坏过度分配的指针?

是的,从磁盘加载会将外部指针设置为 NULL。我们将不得不再次过度分配。

有没有简单的方法来恢复它们?

是的。你可以测试data.table的truelength(),如果是0,就用setDT()或者alloc.col()就可以了。

truelength(test2) # [1] 0
if (!truelength(test2))
    setDT(test2)
truelength(test2) # [1] 100

foobar(test2, "new")
test2[]
#    id val new
# 1:  a   1   1
# 2:  b   2   1

这可能应该作为常见问题解答(不记得在那里看到过)。 已在警告消息部分的FAQ 中。

【讨论】:

是否有理由选择setDT 而不是alloc.col 没有理由。我只是选择了setDT(),因为alloc.col 不会以不可见的方式返回结果,所以我必须用invisible() 包装它。 setDT() 因此显得更短。 哦,好吧,这是有道理的。因为它会在一个函数中,所以这一步的不可见性并不重要,所以我认为alloc.col 可以更好地传达意图。感谢@Arun 的快速准确回复! 实际上,这似乎不适用于函数内部,setDTalloc.col。我可以看到块触发并使用browser 提升truelength,但是我传入的表仍然没有修改,并且它的truelength 在调用后仍然为0,例如func &lt;- function(dt) if (!truelength(dt)) setDT(dt)。有什么想法吗? 确实如此!它让我得出一个我希望避免的结论(必须在某个地方重新分配),但至少副本很浅。

以上是关于在函数中通过引用向 data.table 添加新列并不总是有效的主要内容,如果未能解决你的问题,请参考以下文章

如何向 v-data-table 添加点击事件?

将向量作为单独的新列附加到 data.table,向量回收单个值

在 C++ 中通过引用传递 char 的效率

为啥复制构造函数应该在 C++ 中通过引用来接受它的参数?

在 C 中通过引用传递时会发生啥?

在函数c ++中通过引用传递的指针参数