3

我在实现访问附加到引用类对象的属性的一致行为时遇到了一些麻烦。例如,

testClass <- setRefClass('testClass',
  methods = list(print_attribute = function(name) print(attr(.self, name))))
testInstance <- testClass$new()
attr(testInstance, 'testAttribute') <- 1
testInstance$print_attribute('testAttribute')

R 控制台愉快地打印NULL。但是,如果我们尝试另一种方法,

testClass <- setRefClass('testClass',
  methods = list(initialize = function() attr(.self, 'testAttribute') <<- 1,
                 print_attribute = function(name) print(attr(.self, name))))
testInstance <- testClass$new()
testInstance$print_attribute('testAttribute')

现在我们1有如预期的那样。请注意,<<-运算符是必需的,可能是因为分配给与.self分配给引用类字段具有相同的限制。请注意,如果我们试图在构造函数之外进行分配,比如说

testClass <- setRefClass('testClass',
  methods = list(set_attribute = function(name, value) attr(.self, name) <<- value,
                 print_attribute = function(name) print(attr(.self, name))))
testInstance <- testClass$new()
testInstance$set_attribute('testAttribute', 1)

我们会被打耳光

Error in attr(.self, name) <<- value :
 cannot change value of locked binding for '.self'

事实上,文档?setRefClass解释说

整个对象可以通过保留名称在方法中引用.self......这些字段是只读的(修改这些引用没有意义),但有一个例外。原则上, .self可以在$initialize方法中修改该字段,因为在此阶段仍在创建对象。

我对这一切都很满意,并同意作者的决定。但是,我担心的是以下内容。回到上面的第一个例子,如果我们尝试请求attr(testInstance, 'testAttribute'),我们从全局环境中看到它是1

据推测,.self在引用类对象的方法中使用的那个存储在相同的内存位置testInstance——它是同一个对象。因此,通过在全局环境中成功设置属性testInstance,而不是作为.self参考(如第一个示例所示),我们是否无意中触发了全局环境中整个对象的副本?还是以某种方式存储属性的方式“有趣”,即对象可以驻留在同一内存中,但其属性因调用环境而异?

attr(.self, 'testAttribute')我看不出为什么is NULLbut attr(testInstance, 'testAttribute')is的其他解释1绑定 .self被一劳永逸地锁定,但这并不意味着它引用的对象不能更改。如果这是所需的行为,那似乎是一个陷阱。

最后一个问题是,是否attr<-应该在引用类对象上避免上述结果,至少在对象方法中使用结果属性的情况下是这样。

4

1 回答 1

2

我想我可能已经想通了。我首先深入研究引用类的实现,以获取对.self.

 bodies <- Filter(function(x) !is.na(x),
   structure(sapply(ls(getNamespace('methods'), all.names = TRUE), function(x) {
     fn <- get(x, envir = getNamespace('methods'))
     if (is.function(fn)) paste(deparse(body(fn)), collapse = "\n") else NA
   }), .Names = ls(getNamespace('methods'), all.names = TRUE))
 )

现在bodies保存methods包中所有函数的命名字符向量。我们现在寻找.self

goods <- bodies[grepl("\\.self", bodies)]
length(goods) # 4
names(goods) # [1] ".checkFieldsInMethod" ".initForEnvRefClass"  ".makeDefaultBinding"  ".shallowCopy"

所以methods包中有四个包含字符串的函数.self。检查它们表明这.initForEnvRefClass是我们的罪魁祸首。我们有声明selfEnv$.self <- .Object。但什么是selfEnv?好吧,在同一个函数的前面,我们有.Object@.xData <- selfEnv. testInstance确实,从我们的示例一中查看属性

$.xData
<environment: 0x10ae21470>

$class
[1] "testClass"
attr(,"package")
[1] ".GlobalEnv"

窥视attributes(attr(testInstance, '.xData')$.self)表明我们确实可以.self使用这种方法直接访问。请注意,在执行示例一的前两行(即设置testInstance)之后,我们有

identical(attributes(testInstance)$.xData$.self, testInstance)
# [1] TRUE

是的!他们是平等的。现在,如果我们执行

attr(testInstance, 'testAttribute') <- 1
identical(attributes(testInstance)$.xData$.self, testInstance)
# [1] FALSE

因此,将属性添加到引用类对象会强制创建副本,并且.self不再与对象相同。但是,如果我们检查

identical(attr(testInstance, '.xData'), attr(attr(testInstance, '.xData')$.self, '.xData'))
# [1] TRUE

我们看到附加到引用类对象的环境保持不变。因此,就内存占用而言,复制并不是很重要。

这次尝试的最终结果是最终答案是肯定的,如果您打算在该对象的方法中使用它们,则应避免在引用类上设置属性。这样做的原因是,在.self对象初始化后,引用类对象环境中的对象应该被认为是一劳永逸的——这包括创建附加属性。

由于.self对象存储在作为属性附加到引用类对象的环境中,因此如果不使用指针瑜伽似乎无法避免这个问题——而且 R 没有指针。

编辑

看来,如果你疯了,你可以做到

unlockBinding('.self', attr(testInstance, '.xData'))
attr(attr(testInstance, '.xData')$.self, 'testAttribute') <- 1
lockBinding('.self', attr(testInstance, '.xData'))

上面的问题神奇地消失了。

于 2014-03-31T01:45:55.193 回答