班级制度
查看 R、S3、S4 和 Reference 类中的三个类系统。
## S3 methods, Section 5 of
RShowDoc("R-lang")
## S4 classes
?Classes
?Methods
## Reference classes
?ReferenceClasses
具有 Java 背景的您会很想使用引用类,但它们具有“引用语义”和远距离操作(更改一个对象会更改另一个引用相同数据的对象),而大多数 R 用户希望“更改时复制” ' 语义。一个人可以在 S3 课程上取得很大进步,但在我看来,更严格的方法会采用 S4。S4 的特性会让你大吃一惊,部分原因是类系统更接近于普通的 lisp 对象系统而不是 java。
还有其他意见和选择。
基本实现
我不太确定您使用“ProcessData”的设计目标是什么;我会将您的两个类实现为一个类、一个泛型和一个在 MyClass 类上运行的泛型方法。
## definition and 'low-level' constructor
.MyClass <- setClass("MyClass", representation(word="character"))
## definition of a generic
setGeneric("processData", function(x, ...) standardGeneric("processData"))
setMethod("processData", "MyClass", function(x, ...) {
cat("processData(MyClass) =", x@word, "\n")
})
这是完整且功能齐全的
> myClass <- .MyClass(word="hello world")
> processData(myClass)
processData(MyClass) = hello world
这三个代码行可能放在两个文件中,“AllGenerics.R”和“MyClass.R”(包括方法)或三个文件“AllGenerics.R”、“AllClasses.R”、“processData-methods.R”(请注意,方法与泛型相关联,并在类上分派)。
附加实施
通常会添加一个对用户更友好的构造函数,例如,向用户提供有关预期数据类型的提示或执行复杂的参数初始化步骤
MyClass <- function(word=character(), ...)
{
.MyClass(word=word, ...)
}
通常,人们想要一个插槽访问器,而不是直接插槽访问。这可以是一个简单的函数(如图所示)或一个通用的 + 方法。
word <- function(x, ...) x@word
如果要更新槽,则编写替换函数或方法。函数或方法通常具有三个参数,要更新的对象、可能的附加参数以及更新对象的值。这是一个通用的+方法实现
setGeneric("word<-", function(x, ..., value) standardGeneric("word<-"))
setReplaceMethod("word", c("MyClass", "character"), function(x, ..., value) {
## note double dispatch on x=MyClass, value=character
x@word <- value
x
})
一个有点棘手的替代实现是
setReplaceMethod("word", c("MyClass", "character"), function(x, ..., value) {
initialize(x, word=value)
})
它使用initialize
泛型和默认方法作为复制构造函数;如果同时更新多个插槽,这可能会很有效。
因为用户可以看到该类,所以希望使用“show”方法以用户友好的方式显示它,为此getGeneric("show")
已经存在泛型 ( )
setMethod("show", "MyClass", function(object) {
cat("class:", class(object), "\n")
cat("word:", word(object), "\n")
})
现在我们的用户会话看起来像
> myClass
class: MyClass
word: hello world
> word(myClass)
[1] "hello world"
> word(myClass) <- "goodbye world"
> processData(myClass)
processData(MyClass) = goodbye world
效率
R 在向量上有效地工作;S4课程也不例外。所以设计是一个类的每个槽代表一个跨越多行的列,而不是单行的元素。我们期望 slot 'word' 通常包含一个长度远大于 1 的向量,并且操作要作用于向量的所有元素。因此,人们会考虑到这一点来编写方法,例如,将 show 方法修改为
setMethod("show", "MyClass", function(object) {
cat("class:", class(object), "\n")
cat("word() length:", length(word(object)), "\n")
})
这是更大的数据对象(使用我的 Linux 系统上的文件)
> amer <- MyClass(readLines("/usr/share/dict/american-english"))
> brit <- MyClass(readLines("/usr/share/dict/british-english"))
> amer
class: MyClass
word() length: 99171
> brit
class: MyClass
word() length: 99156
> sum(word(amer) %in% word(brit))
[1] 97423
> amer_uc <- amer ## no copy, but marked to be copied if either changed
> word(amer_uc) <- toupper(word(amer_uc)) ## two distinct objects
所有这些都非常高效。
参考类“远距离行动”的危害
让我们回到 S4 类的一个更简单的实现,它具有直接的插槽访问,没有花哨的构造函数。这是美国字典和一个副本,已转换为大写
.MyClass <- setClass("MyClass", representation(word="character"))
amer <- .MyClass(word=readLines("/usr/share/dict/american-english"))
amer_uc <- amer
amer_uc@word <- toupper(amer_uc@word)
请注意,我们已经大写amer_uc
但不是amer
:
> amer@word[99 + 1:10]
[1] "Adana" "Adar" "Adar's" "Addams" "Adderley"
[6] "Adderley's" "Addie" "Addie's" "Addison" "Adela"
> amer_uc@word[99 + 1:10]
[1] "ADANA" "ADAR" "ADAR'S" "ADDAMS" "ADDERLEY"
[6] "ADDERLEY'S" "ADDIE" "ADDIE'S" "ADDISON" "ADELA"
这确实是 R 用户所期待的——我创建了一个单独的对象并对其进行了修改;原始对象未修改。这是我的一个断言;也许我不知道 R 用户期望什么。我假设 R 用户并没有真正注意这是一个引用类这一事实,而是认为它只是另一个 R 对象,例如integer()
向量或 adata.frame
或lm()
.
相比之下,这里是一个参考类的最小实现,以及类似的操作
.MyRefClass <- setRefClass("MyRefClass", fields = list(word="character"))
amer <- .MyRefClass(word=readLines("/usr/share/dict/american-english"))
amer_uc <- amer
amer_uc$word <- toupper(amer_uc$word)
但是现在我们已经改变了amer
和amer_uc
!C 或 Java 程序员完全期望,但 R 用户不期望。
> amer$word[99 + 1:10]
[1] "ADANA" "ADAR" "ADAR'S" "ADDAMS" "ADDERLEY"
[6] "ADDERLEY'S" "ADDIE" "ADDIE'S" "ADDISON" "ADELA"
> amer_uc$word[99 + 1:10]
[1] "ADANA" "ADAR" "ADAR'S" "ADDAMS" "ADDERLEY"
[6] "ADDERLEY'S" "ADDIE" "ADDIE'S" "ADDISON" "ADELA"