R6包为R提供了封装的面向对象编程的实现(有时也称为经典面向对象编程)。它类似于R的引用类,但效率更高,不依赖于S4类和methods包。
1、R6类
R6类类似于R的引用类,但重量更轻,并避免了使用S4时出现的一些问题(R的引用类是基于S4的)。有关速度和内存占用的更多信息,请参阅性能文章。
与R中的许多对象不同,R6类的实例(对象)具有引用语义。R6类还支持:
- 公有和私有方法
- 主动绑定
- 跨包继承(超类)
为什么取R6这个名字?当R的引用类被引入时,一些用户按照R的现有类系统S3和S4的名称,开玩笑地称新的类系统R5。尽管引用类实际上并没有被称为R5,但这个包及其类的名称是从这个名称中获得灵感的。
名字R5也是Simon Urbanek启动的另一个对象系统所使用的代号,旨在解决S4在语法和性能方面的一些问题。然而,R5分支在经过一点开发后被搁置,并且从未发布。
2、基础
以下是如何创建一个简单的R6类。
library(R6)
Person <- R6Class("Person",
public = list(
name = NULL,
hair = NULL,
initialize = function(name = NA, hair = NA) {
self$name <- name
self$hair <- hair
self$greet()
},
set_hair = function(val) {
self$hair <- val
},
greet = function() {
cat(paste0("Hello, my name is ", self$name, ".\n"))
}
)
)
> Person
<Person> object generator
Public:
name: NULL
hair: NULL
initialize: function (name = NA, hair = NA)
set_hair: function (val)
greet: function ()
clone: function (deep = FALSE)
Parent env: <environment: R_GlobalEnv>
Locked objects: TRUE
Locked class: FALSE
Portable: TRUE
代码中,参数public
是一个项目列表,可以是函数和字段(非函数)。其中的函数将用作方法(methods)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mlygAehz-1679559807496)(vx_images/369825120248787.png)]
要实例化此类的对象,请使用$new():
> ann <- Person$new("Ann", "black")
Hello, my name is Ann.
> ann
<Person>
Public:
clone: function (deep = FALSE)
greet: function ()
hair: black
initialize: function (name = NA, hair = NA)
name: Ann
set_hair: function (val)
$new()
方法创建对象(分配内存,创建出一个真实的R对象)并调用initialize()
方法(如果存在的话)。initialize()
方法内部可以再次调用类中的方法,比如本例中initialize()
方法调用greet()
方法
在类的方法内部,方法中的参数self
引用对象自身(所在的内存地址)。对象的公有成员(到目前为止您看到的所有成员)使用self$x
进行访问,而赋值则使用self$x<-y
进行。
一旦对象被实例化,您就可以使用$
访问值和方法:
> ann$hair
[1] "black"
> ann$set_hair("red")
> ann$hair
[1] "red"
> ann$greet()
Hello, my name is Ann.
实现说明:R6对象的外部基本上是一个包含公有成员的环境。这也被称为公共环境。R6对象的方法有一个单独的封闭环境,粗略地说,就是它们“运行”的环境。这就是找到self
绑定的地方,它只是对公共环境的引用。
3、私有成员
在前面的示例中,所有成员都是公有的。还可以添加私有成员:
Queue <- R6Class("Queue",
public = list(
initialize = function(...) {
for (item in list(...)) {
self$add(item)
}
},
add = function(x) {
private$queue <- c(private$queue, list(x))
invisible(self)
},
remove = function() {
if (private$length() == 0) return(NULL)
# Can use private$queue for explicit access
head <- private$queue[[1]]
private$queue <- private$queue[-1]
head
}
),
private = list(
queue = list(),
length = function() base::length(private$queue)
)
)
q <- Queue$new(5, 6, "foo")
公有成员使用self
(如self$add()
)访问,而私有成员使用private
(如private$queue
)访问。
可以像往常一样访问公共成员:
q$add("something")
q$add("another thing")
q$add(17)
q$remove()
但是,私人成员不能直接访问:
> q$queue
NULL
> q$length
NULL
> q$length()
Error: attempt to apply non-function
一个有用的设计模式是methods to在可能的情况下返回self(不可见)(即对象的内存地址,而R6中内存$可以调用类中的方法),所以可以把类中的方法依次链接。例如,add()方法返回self,因此可以将它们链接在一起:
4、主动绑定
主动绑定看起来像字段,但每次访问它们时,它们都会调用一个函数。(通过调用函数得到值,赋值给具体属性,具体属性仍然是一个值,不是函数)他们总是共有可见。
Numbers <- R6Class("Numbers",
public = list(
x = 100
),
active = list(
x2 = function(value) {
if (missing(value)) return(self$x * 2)
else self$x <- value/2
},
rand = function() rnorm(1)
)
)
n <- Numbers$new()
n$x
#> [1] 100
# ——————————————————————————
> n$x
[1] 100
> n$x(2)
Error: attempt to apply non-function
> n$rand()
Error: attempt to apply non-function
> n$rand
[1] -0.6067032
当直接访问主动绑定时,就像直接调用一个函数,此时,函数的参数默认为缺失,但结果任然是值,不是函数:
> n$x2
[1] 200
> n$x2()
Error: attempt to apply non-function
> n$x2 %>% class()
[1] "numeric"
当手动赋值后再访问时,所赋值将作为参数传入函数:
> n$x2 <- 900
> n$x
[1] 450
如果函数不带参数,则无法赋值:
> n$rand
[1] -0.04218144
> n$rand <- 1
Error in (function () : unused argument (base::quote(1))
实现说明:主动绑定是在公有环境中绑定的。这些功能的封闭环境也是公有环境。
5、继承
一个R6类可以从另一个继承。换句话说,你可以有超类和子类。
子类可以有额外的方法,也可以有覆盖超类方法的方法。在这个保留其历史记录的queue示例中,我们将添加一个show()方法并覆盖remove()方法:
# Note that this isn't very efficient - it's just for illustrating inheritance.
HistoryQueue <- R6Class("HistoryQueue",
inherit = Queue,
public = list(
show = function() {
cat("Next item is at index", private$head_idx + 1, "\n")
for (i in seq_along(private$queue)) {
cat(i, ": ", private$queue[[i]], "\n", sep = "")
}
},
remove = function() {
if (private$length() - private$head_idx == 0) return(NULL)
private$head_idx <- private$head_idx + 1
private$queue[[private$head_idx]]
}
),
private = list(
head_idx = 0
)
)
hq <- HistoryQueue$new(5, 6, "foo")
hq$show()
#> Next item is at index 1
#> 1: 5
#> 2: 6
#> 3: foo
hq$remove()
#> [1] 5
hq$show()
#> Next item is at index 2
#> 1: 5
#> 2: 6
#> 3: foo
hq$remove()
#> [1] 6
在子类中可以直接使用super$xx()
调用超类方法。CountingQueue(下面的示例)记录已添加到队列中的对象总数。它通过重写add()方法来实现这一点——它增加一个计数器,然后用super$add(x)
调用超类的add()方法:
CountingQueue <- R6Class("CountingQueue",
inherit = Queue,
public = list(
add = function(x) {
private$total <- private$total + 1
super$add(x)
},
get_total = function() private$total
),
private = list(
total = 0
)
)
cq <- CountingQueue$new("x", "y")
cq$get_total()
#> [1] 2
cq$add("z")
cq$remove()
#> [1] "x"
cq$remove()
#> [1] "y"
cq$get_total()
#> [1] 3
6、包含引用对象的字段
如果R6类包含任何具有引用语义的字段(例如,其他R6对象和环境),那么这些字段应该在initialize方法中填充。如果字段直接在类定义中设置为引用对象,则该对象将在R6对象的所有实例中共享。下面是一个例子:
public = list(x = NULL)
)
SharedField <- R6Class("SharedField",
public = list(
e = SimpleClass$new()
)
)
s1 <- SharedField$new()
s1$e$x <- 1
s2 <- SharedField$new()
s2$e$x <- 2
# Changing s2$e$x has changed the value of s1$e$x
s1$e$x
#> [1] 2
(因为是引用对象,直接在地址上操作。而环境是包含变量名的列表,通过引用对环境进行修改,实际是修改了列表中变量的值,所有在另一处查询环境变量的值,是修改后的值)
如果想避免这种情况,可以在子类initialize方法中明示定义相关字段
NonSharedField <- R6Class("NonSharedField",
public = list(
e = NULL,
initialize = function() self$e <- SimpleClass$new()
)
)
n1 <- NonSharedField$new()
n1$e$x <- 1
n2 <- NonSharedField$new()
n2$e$x <- 2
# n2$e$x does not affect n1$e$x
n1$e$x
#> [1] 1
7、将成员添加到现有类
在类已经创建之后向该类添加成员有时是有用的。这可以使用generator对象上的$set()
方法来完成。
Simple <- R6Class("Simple",
public = list(
x = 1,
getx = function() self$x
)
)
Simple$set("public", "getx2", function() self$x*2)
# To replace an existing member, use overwrite=TRUE
Simple$set("public", "x", 10, overwrite = TRUE)
s <- Simple$new()
s$x
#> [1] 10
s$getx2()
#> [1] 20
注意:添加属性,必须在实例化对象之前,即新成员将只出现在调用$set()之后创建的实例中。
> Simple <- R6Class("Simple",
+ public = list(
+ x = 1,
+ getx = function() self$x
+ )
+ )
> s <- Simple$new()
> Simple$set("public", "getx2", function() self$x*2)
> #> [1] 10
> s$getx2()
Error: attempt to apply non-function
> s <- Simple$new()
> #> [1] 10
> s$getx2()
[1] 2
为了防止修改类,可以在创建类时使用lock_class=TRUE。您也可以按如下方式锁定和解锁类:
# Create a locked class
Simple <- R6Class("Simple",
public = list(
x = 1,
getx = function() self$x
),
lock_class = TRUE
)
# This would result in an error
# Simple$set("public", "y", 2)
# Unlock the class
Simple$unlock()
# Now it works
Simple$set("public", "y", 2)
# Lock the class again
Simple$lock()
8、克隆对象
默认情况下,R6对象具有名为clone的方法,用于制作对象的副本。
Simple <- R6Class("Simple",
public = list(
x = 1,
getx = function() self$x
)
)
s <- Simple$new()
# Create a clone
s1 <- s$clone()
# Modify it
s1$x <- 2
s1$getx()
#> [1] 2
# Original is unaffected by changes to the clone
s$getx()
#> [1] 1
如果不希望添加克隆方法,可以在创建类时使用cloneable=FALSE
。如果任何加载的R6对象都有一个克隆方法,那么该函数将使用83552 B,但对于每个附加的对象,克隆方法将花费少量的空间(112字节)
9、深度克隆
如果有任何字段是具有引用语义的对象(环境、R6对象、引用类对象),则副本将获得对同一对象的引用。这有时是可取的,但通常不是。
例如,我们将创建一个包含另一个R6对象s的对象c1,然后克隆它。因为原始的和克隆的s字段都指向同一个对象,所以从一个对象修改它会导致另一个对象的变化。
Simple <- R6Class("Simple", public = list(x = 1))
Cloneable <- R6Class("Cloneable",
public = list(
s = NULL,
initialize = function() self$s <- Simple$new()
)
)
c1 <- Cloneable$new()
c2 <- c1$clone()
# Change c1's `s` field
c1$s$x <- 2
# c2's `s` is the same object, so it reflects the change
c2$s$x
#> [1] 2
在类代码中定义的属性是没有分配内存的:
实例化后都有了地址
克隆的C2的地址是自己的,但S地址与C1中的S的地址相同
注意,上面两张图中,S的值都是1
通过c1$s$x <- 2
改变S的值,如下图,S地址不变,X值变成2
再看C2,同样S地址不变,X值变成2
在R中,R6显示为环境对象
要使克隆接收s的副本,我们可以使用deep=TRUE选项:
c3 <- c1$clone(deep = TRUE)
# Change c1's `s` field
c1$s$x <- 3
# c2's `s` is different
c3$s$x
#> [1] 2
此时,C3中的S地址已经变换
所以对C3的编辑,不会影响C1