# install.packages("R6")
library(R6)
14 R6
Introduction
本章介绍R6 OOP系统,它有两大特点:
R6使用了封装的OOP范式,意味着方法(method)属于对象(object)而不是泛型函数(generic),调用方法的范式为
object$method()
。R6对象是可改变的,意味着它们可以原地修改并具有引用语义。当你将一个R6对象赋值给另一个变量时,实际上是将指向该R6对象的引用来赋值给新变量。这样,任何对该对象所做的更改都会反映在所有引用它的变量中。
虽然R6 OOP系统与其他语言中的OOP范式相同,使用起来更容易上手,但它缺点就是不符合R的使用习惯,我们将在第16章中讨论它们。
Outline
- 14.2节:介绍使用
R6::R6Class()
创建R6类,使用构造器$new()
创建新的R6对象。 - 14.3节:讨论R6的访问机制:私有域和主动域。
- 14.4节:探讨R6的引用语义的影响。学习如何使用终结器自动清理初始化器中执行的任何操作,以及如何在另一个R6对象中将一个R6对象作为字段使用。
- 14.5节:对比R6系统和RC系统。
Prerequisites
Classes and methods
R6::R6Class()
函数可以同时构建类(class)和方法(method),同时也是R6包中唯一需要使用的函数。
R6Class()
函数有两个极其重要的参数:
classname
:类名,它不是必须的,但它改进了错误消息,并使得R6对象可以与S3类的泛型函数结合使用。R6 class名称通常使用UpperCamelCase
命名法。public
:一个列表,包含类的属性(field)和方法,可以通过self$
的方法获取。属性和方法通常使用snake_case
命名法。
<- R6Class(
Accumulator classname = "Accumulator",
public = list(
sum = 0,
add = function(x = 1) {
$sum <- self$sum + x
selfinvisible(self)
}
) )
在使用R6Class()
创建对象时,需要始终将创建的结果赋值给与类名相同的变量。
Accumulator#> <Accumulator> object generator
#> Public:
#> sum: 0
#> add: function (x = 1)
#> clone: function (deep = FALSE)
#> Parent env: <environment: R_GlobalEnv>
#> Locked objects: TRUE
#> Locked class: FALSE
#> Portable: TRUE
可以使用object$new()
的方法创建新对象。
<- Accumulator$new() x
同样地,使用$
获取对的属性和方法。
$add(4)
x$sum
x#> [1] 4
后续我们以()
区分$
获取的时属性还是方法——$add()
表示方法,$sum
表示属性。
Method chaining
当$add()
方法返回的是self
而不是$sum
时,我们就可以使用方法链(method chaining),类似管道符。通常我们使用return()
来返回,但鉴于self
的隐私性,这里使用invisible()
。
$add(10)$add(10)$sum
x#> [1] 24
$
xadd(10)$
add(10)$
sum#> [1] 44
Important methods
对大多数R6对象,有两个重要的方法需要定义——$initialize()
和$print()
。它们非必须,但会提升对象的使用性。
$initialize()
方法会覆盖默认的$new()
方法。例如下面的“Person”类,我在$initialize()
方法中判断了$name
属性只能是单一的字符串,$age
属性只能是单一的数字。如果你有更多对输入的检查,将它们放在$validate()
方法中更合适。
<- R6Class("Person", list(
Person name = NULL,
age = NA,
initialize = function(name, age = NA) {
stopifnot(is.character(name), length(name) == 1)
stopifnot(is.numeric(age), length(age) == 1)
$name <- name
self$age <- age
self
}
))
<- Person$new("Hadley", age = "thirty-eight")
hadley #> Error in initialize(...): is.numeric(age) is not TRUE
<- Person$new("Hadley", age = 38) hadley
$print()
方法会覆盖默认的print()
方法,允许你自定义对象的打印输出。和其他R6对象的方法一样,最终使用invisible()
来返回。
<- R6Class("Person", list(
Person name = NULL,
age = NA,
initialize = function(name, age = NA) {
$name <- name
self$age <- age
self
},print = function(...) {
cat("Person: \n")
cat(" Name: ", self$name, "\n", sep = "")
cat(" Age: ", self$age, "\n", sep = "")
invisible(self)
}
))
<- Person$new("Hadley")
hadley2
hadley2#> Person:
#> Name: Hadley
#> Age: NA
Adding methods after creation
可以使用$set()
修改R6对象的属性和方法。
<- R6Class("Accumulator")
Accumulator $set("public", "sum", 0)
Accumulator$set("public", "add", function(x = 1) {
Accumulator$sum <- self$sum + x
selfinvisible(self)
})
需要注意:对象添加新的属性和方法后,只有用它创建新的对象时才会添加,已经创建好的对象不会添加新的属性和方法。
Inheritance
参数inherit
允许创建继承关系。
<- R6Class(
AccumulatorChatty "AccumulatorChatty",
inherit = Accumulator,
public = list(
add = function(x = 1) {
cat("Adding ", x, "\n", sep = "")
$add(x = x)
super
}
)
)
<- AccumulatorChatty$new()
x2 $add(10)$add(1)$sum
x2#> Adding 10
#> Adding 1
#> [1] 11
拥有继承关系的子类可以使用父类的方法,但时如何名称相同发生覆盖,则需要使用suppe$
来方法父类方法,这与上一章中的NextMethod()
函数类似。
Introspection
每一个R6对象中都含有一个S3类。这意味着我们可以对R6对象使用一些S3类常用的函数,上述提到的$print()
方法,本质上是print.R6()
函数。
class()
可以确定是否属于R6类。
class(hadley2)
#> [1] "Person" "R6"
names()
可以查看R6类的所有属性和方法名。下面的.__enclos_env__
是R6内部的实现细节(R6 = S3 + env)。
names(hadley2)
#> [1] ".__enclos_env__" "age" "name" "clone"
#> [5] "print" "initialize"
Exercises
- Create a bank account R6 class that stores a balance and allows you to deposit and withdraw money. Create a subclass that throws an error if you attempt to go into overdraft. Create another subclass that allows you to go into overdraft, but charges you a fee.
solution
<- R6Class("Bank", list(
Bank name = "",
balance = 0,
initialize = function(name, balance = 0) {
stopifnot(is.character(name), length(name) == 1)
stopifnot(is.numeric(balance), length(balance) == 1)
$name <- name
self$balance <- balance
self
},print = function(...) {
cat("Bank: \n")
cat(" Name: ", self$name, "\n", sep = "")
cat(" Balance: ", self$balance, "\n", sep = "")
invisible(self)
},deposit = function(x) {
$balance <- self$balance + x
selfinvisible(self)
},withdraw = function(x) {
$balance <- self$balance - x
selfinvisible(self)
}
))
<- Bank$new(name = "a", balance = 1000)
a $deposit(500)$withdraw(2000)
a
a#> Bank:
#> Name: a
#> Balance: -500
<- R6Class("Bank2", inherit = Bank, public = list(
Bank2 withdraw = function(x) {
if (self$balance - x < 0) {
stop("Insufficient funds", call. = FALSE)
}
}
))
<- Bank2$new(name = "b", balance = 1000)
b $deposit(500)$withdraw(2000)
b#> Error: Insufficient funds
b#> Bank:
#> Name: b
#> Balance: 1500
<- R6Class("Bank3", inherit = Bank, public = list(
Bank3 withdraw = function(x) {
if (self$balance - x < 0) {
message("charge of $5 applied")
$balance <- self$balance - x - 5
self
}
}
))
<- Bank3$new(name = "c", balance = 1000)
c $deposit(500)$withdraw(2000)
c#> charge of $5 applied
c#> Bank:
#> Name: c
#> Balance: -505
- Create an R6 class that represents a shuffled deck of cards. You should be able to draw cards from the deck with $draw(n), and return all cards to the deck and reshuffle with $reshuffle(). Use the following code to make a vector of cards.
<- c("♠", "♥", "♦", "♣")
suit <- c("A", 2:10, "J", "Q", "K")
value <- paste0(rep(value, 4), suit) cards
solution
<- R6Class(
ShuffledDeck classname = "ShuffledDeck",
public = list(
deck = NULL,
initialize = function(deck = cards) {
$deck <- sample(deck)
self
},reshuffle = function() {
$deck <- sample(cards)
selfinvisible(self)
},n = function() {
length(self$deck)
},draw = function(n = 1) {
if (n > self$n()) {
stop("Only ", self$n(), " cards remaining.", call. = FALSE)
}
<- self$deck[seq_len(n)]
output $deck <- self$deck[-seq_len(n)]
self
output
}
)
)
<- ShuffledDeck$new()
my_deck $draw(52)
my_deck#> [1] "Q♦" "5♥" "K♦" "6♣" "K♠" "2♠" "Q♥" "J♣" "9♣" "J♦" "6♦" "5♠"
#> [13] "K♣" "2♦" "8♥" "A♥" "10♠" "4♦" "10♥" "7♦" "9♦" "3♠" "3♥" "7♥"
#> [25] "K♥" "5♦" "7♣" "8♠" "A♣" "10♣" "9♠" "6♥" "J♠" "2♥" "9♥" "A♦"
#> [37] "8♣" "A♠" "3♦" "8♦" "Q♠" "4♠" "4♣" "3♣" "4♥" "6♠" "J♥" "Q♣"
#> [49] "5♣" "7♠" "10♦" "2♣"
$draw(10)
my_deck#> Error: Only 0 cards remaining.
$reshuffle()$draw(5)
my_deck#> [1] "10♣" "5♦" "8♥" "K♦" "2♠"
$reshuffle()$draw(5)
my_deck#> [1] "K♦" "4♥" "9♣" "J♠" "K♥"
Controlling access
R6Class()
函数有两个与public
参数类似的参数:
private
:创建的R6对象的私有属性和方法,只允许对象内部访问。active
:创建的R6对象的动态属性,通过 accessor 函数访问。
Privacy
private
参数创建的私有属性和方法有两个特点:
- 创建方式与
public
参数一样,都是一个带有name的list。 - 在对象内部调用时,需要使用
private$
前缀。
下面是一个私有属性示例:
<- R6Class("Person",
Person public = list(
initialize = function(name, age = NA) {
$name <- name
private$age <- age
private
},print = function(...) {
cat("Person: \n")
cat(" Name: ", private$name, "\n", sep = "")
cat(" Age: ", private$age, "\n", sep = "")
}
),private = list(
age = NA,
name = NULL
)
)
<- Person$new("Hadley")
hadley3
hadley3#> Person:
#> Name: Hadley
#> Age: NA
$name
hadley3#> NULL
相交于其他语言,私有方法在R语言中通常不是很重要。
Active fields
动态属性看起来像是公共属性,但实际是由一个active binding函数定义。active binding函数只有一个参数value
,如果参数missing()
, 则检索该值;否则,将对其进行修改。
下例定义了动态属性random
,每次访问时,会返回一个随机数。
<- R6::R6Class("Rando", active = list(
Rando random = function(value) {
if (missing(value)) {
runif(1)
else {
} stop("Can't set `$random`", call. = FALSE)
}
}
))<- Rando$new()
x $random(3)
x#> Error: attempt to apply non-function
$random
x#> [1] 0.197574
$random <- 31
x#> Error: Can't set `$random`
动态属性可以使静态属性看起来像公共属性。例如下例中,我们创建了只读的属性age
和能确保字符串长度为1的属性name
。
<- R6Class("Person",
Person private = list(
.age = NA,
.name = NULL
),active = list(
age = function(value) {
if (missing(value)) {
$.age
privateelse {
} stop("`$age` is read only", call. = FALSE)
}
},name = function(value) {
if (missing(value)) {
$.name
privateelse {
} stopifnot(is.character(value), length(value) == 1)
$.name <- value
private
self
}
}
),public = list(
initialize = function(name, age = NA) {
$.name <- name
private$.age <- age
private
}
)
)
<- Person$new("Hadley", age = 38)
hadley4 $name
hadley4#> [1] "Hadley"
$name <- "Hadley2"
hadley4$name <- 10
hadley4#> Error in (function (value) : is.character(value) is not TRUE
$age <- 20
hadley4#> Error: `$age` is read only
子类无法访问到父类的私有属性,但是可以访问到父类的私有方法:
<- R6Class(
A classname = "A",
private = list(
field = "foo",
method = function() {
"bar"
}
)
)
<- R6Class(
B classname = "B",
inherit = A,
public = list(
test = function() {
cat("Field: ", super$field, "\n", sep = "")
cat("Method: ", super$method(), "\n", sep = "")
}
)
)
$new()$test()
B#> Field:
#> Method: bar
Reference semantics
R6 OOP系统与其他系统的最大不同就是它的引用语义。引用语义意味着对象被修改时不会被复制。
<- Accumulator$new()
y1 <- y1
y2
$add(10)
y1c(y1 = y1$sum, y2 = y2$sum)
#> y1 y2
#> 10 10
如果你想要复制对象,你需要使用$clone()
方法,添加参数deep = TRUE
可以克隆嵌套的对象。
<- Accumulator$new()
y1 <- y1$clone()
y2
$add(10)
y1c(y1 = y1$sum, y2 = y2$sum)
#> y1 y2
#> 10 0
引用语义的使用同样会带来其他结果:
- 需要更多的上下文才能理解R6对象。
- 考虑何时删除 R6对象是有意义的,你可以编写
$finalize()
来补充$initialize()
,。 - 如果某个属性是R6对象,则必须在
$initialize()
中创建它,而不是在R6Class()
中。
Reasoning
通常,参考语义会导致代码更难推理。考虑下面的例子:
<- list(a = 1)
x <- list(b = 2)
y
<- f(x, y) z
因为函数f
内部无法修改外部的x
,y
,所以我们知道函数f
只修改了z
。
但是想象x
,y
是一个R6对象:
<- List$new(a = 1)
x <- List$new(b = 2)
y
<- f(x, y) z
函数f
内部可以调用x
和y
内部的属性或方法,并对它们进行修改。我们无法仅从z <- f(x, y)
判断函数f
是否修改了x
,y
,我们需要查看函数f
内部的代码。
Finalizer
因为R6对象具有引用语义,所以删除一次就会完全删除对象(不发生修改即拷贝)。这意味着我们可以在R6对象被删除时,使用$finalize()
执行某些清理工作(类似on.exit()
),来补充$initialize()
。如下例中,我们实例化一个创建临时文件对象,然后删除该实例,就会删除临时文件。
<- R6Class(
TemporaryFile "TemporaryFile",
public = list(
path = NULL,
initialize = function() {
$path <- tempfile()
self
}
),private = list(
finalize = function() {
message("Cleaning up ", self$path)
unlink(self$path)
}
)
)
<- TemporaryFile$new()
tf rm(tf)
gc() # 使用gc()才会触发,书中好像是rm(tf)就会触发。
#> used (Mb) gc trigger (Mb) max used (Mb)
#> Ncells 949190 50.7 1892770 101.1 1892770 101.1
#> Vcells 2058420 15.8 8388608 64.0 3451412 26.4
R6 fields
当使用R6类作为另外一个R6类的属性时,必须在$initialize
方法中初始化属性。因为在外部定义的属性,表示该属性在定义R6类时已经创建,后续的所有实例都会继承这个属性。例如下面案例:我们想每次创建临时数据库时都创建一个临时文件,如果在外部定义属性file
,实例db_a
和db_b
都会继承这个属性,这样db_a
和db_b
的属性file
都指向同一个文件。
<- R6Class(
TemporaryDatabase "TemporaryDatabase",
public = list(
con = NULL,
file = TemporaryFile$new(),
initialize = function() {
$con <- DBI::dbConnect(RSQLite::SQLite(), path = file$path)
self
}
),private = list(
finalize = function() {
::dbDisconnect(self$con)
DBI
}
)
)
<- TemporaryDatabase$new()
db_a <- TemporaryDatabase$new()
db_b
$file$path == db_b$file$path
db_a#> [1] TRUE
相反,使用$initialize()
方法,在创建实例时,始终会重新创建属性file
。
<- R6Class(
TemporaryDatabase "TemporaryDatabase",
public = list(
con = NULL,
file = NULL,
initialize = function() {
$file <- TemporaryFile$new()
self$con <- DBI::dbConnect(RSQLite::SQLite(), path = file$path)
self
}
),private = list(
finalize = function() {
::dbDisconnect(self$con)
DBI
}
)
)
<- TemporaryDatabase$new()
db_a <- TemporaryDatabase$new()
db_b
$file$path == db_b$file$path
db_a#> [1] FALSE
Why R6?
R6 OOP系统相较于 RC OOP系统的一些优势:
- R6 更简单。R6 基于S3,RC 基于S4。
- R6 有全面的文档。https://r6.r-lib.org/
- R6 提供了一种更简单的跨包子类化机制,这种机制无需思考就能正常工作。
- R6 对属性方法的管理更加明确。
- R6 更快。
- RC 与 base R 绑定,意味着你需要修改不同R版本的bug。