专注系列化、高质量的R语言教程
(本号已支持快捷转载,无需白名单即可转载)
本系列将介绍R语言中三个与面向对象的编程(Object-Oriented Programming,OOP)相关的工具包:proto
、R6
和基础包methods
。这是一个承上启下的系列,上承《自定义ggplot2绘图系统函数》系列,下启《基于mlr3
工具包的机器学习》系列(该系列将在本系列之后推出)。这两个系列的编程风格都属于OOP,前者基于proto
包的proto对象,后者基于R6
包的R6类。
本篇介绍R6
工具包,主要参考资料[1]:
[https://r6.r-lib.org/articles/Introduction.html]。
1 引言
2 创建R6类和对象
2.1 创建类
2.2 创建对象
3 私有属性和方法
4 类的继承
5 外部引用
6 新增类的元素
7 类的锁定和解锁
8 克隆对象
与proto
基于对象的编程不同,R6
采用的是基于类的编程(class-based programming)。类(class)和对象(object)的关系是:对象是类的实例,类是对象的模板;定义对象之前需要先定义类[2]
R语言基础包中有S3类、S4类和reference class(R5类为其非正式称呼),R6是基于这个顺序命名的。
创建R6类的函数是R6
工具包中的R6Class()
,它的语法结构如下:
R6Class(
classname = NULL,
public = list(),
private = NULL,
active = NULL,
inherit = NULL,
lock_objects = TRUE,
class = TRUE,
portable = TRUE,
lock_class = FALSE,
cloneable = TRUE,
parent_env = parent.frame()
)
这里先看前两个参数:
classname:类的名称;
public:类的公用属性和方法。
由“类”到“对象”,需要使用基础包methods
中的new()
函数,它要求“类”里必须包含initialize
方法。
如下是一个示例:
library(R6)
Person <- R6Class(
classname = "Person",
public = list(
## 属性
name = NULL,
age = NULL,
## 方法
intro = function() {
print(paste("Hello, I am", self$name, "and",
self$age, "years old"))},
initialize = function(name, age) {
self$name <- name
self$age <- age
return(self$intro())}
)
)
在上面的代码中,我们只使用了classname
和public
两个参数。前者无需多说。在public
中,我们定义了两个属性:name
、age
;两个方法:intro
、initialize
。
要点如下:
public
的数据结构形式为列表,元素(属性或方法)之间使用逗号隔开;可以使用
self
表示类本身,进而在方法中引用它的的属性和方法。
R6类的数据类型:
class(Person)
## [1] "R6ClassGenerator"
从R6类创建对象的代码形式为object <- class$new(...)
,其中new()
函数来自基础包methods
,它的参数为类的initialize
方法的参数。
根据类Person
创建对象:
Tom <- Person$new("Tom", 20)
## [1] "Hello, I am Tom and 20 years old"
Jane <- Person$new("Jane", 18)
## [1] "Hello, I am Jane and 18 years old"
Tom$age <- 30
Tom$intro()
## [1] "Hello, I am Tom and 30 years old"
除public
参数外,还可以使用R6Class()
函数的private
参数定义私有属性和方法。
要点如下:
private
的数据结构形式为列表,元素(属性或方法)之间使用逗号隔开;私有属性和方法可以使用
private$x
的形式在其他方法中进行引用。
如下代码,我们把intro
方法放到private
参数中:
Person2 <- R6Class(
classname = "Person2",
public = list(
name = NULL,
age = NULL,
initialize = function(name, age) {
self$name <- name
self$age <- age
return(private$intro())}
),
## 私有属性和方法
private = list(
intro = function() {
print(paste("Hello, I am", self$name, "and", self$age, "years old"))}
)
)
根据类Person2
创建对象:
Tom2 <- Person2$new("Tom", 20)
## [1] "Hello, I am Tom and 20 years old"
属性和方法的私有部分与公用部分的区别是:前者不能使用object$x
的形式进行访问。如下代码会报错:
Tom2$intro()
## Error: attempt to apply non-function
在RStudio中,当我们在对象后输入$
时,自动联想中也不会出现私有部分,因此可以将一些作为过程的属性和方法放到私有部分。如下对比了Tom
和Tom2
对象:
我们可以在一个类的基础上定义一个新类,这称为继承(inheritance);前者是后者的父类(super-class),后者是前者的子类(sub-class)。
子类除了继承或修改父类的属性和方法外,还可以定义新的属性和方法。下面代码中,我们在Person2
的基础上修改intro
方法形成Person3
:
library(lubridate)
Person3 <- R6Class(
## 继承
inherit = Person2,
private = list(
## 修改intro方法
intro = function() {
born = year(Sys.Date()) - self$age
print(paste("Hello, I am", self$name, "and borned in", born))}
)
)
Tom3 <- Person3$new("Tom", 20)
## [1] "Hello, I am Tom and borned in 2002"
在子类中可以以super$method(...)
的形式调用父类的方法:
Person4 <- R6Class(
inherit = Person2,
public = list(
initialize = function(name, age) {
self$name <- name
self$age <- age
return(super$intro())}
),
private = list(
intro = function() {
born = year(Sys.Date()) - self$age
print(paste("Hello, I am", self$name, "and borned in", born))}
)
)
Tom4 <- Person4$new("Tom", 20)
## [1] "Hello, I am Tom and 20 years old"
注意:调用的只是父类中方法的形式,参数值仍来自子类。
如果把其他类的对象作为要定义类的属性,通常情况下应将赋值过程放在initialize
方法中,否则该类的所有对象会共享这一属性值。
下面例子中,我们使用类SimpleClass
的对象作为类Person5
的age
属性。
不放在initialize
方法中:
SimpleClass <- R6Class("SimpleClass",
public = list(x = NULL)
)
Person5 <- R6Class(
classname = "Person5",
public = list(
name = NULL,
## 外部引用
age = SimpleClass$new(),
intro = function() {
print(paste("Hello, I am", self$name, "and",
self$age$x, "years old"))},
initialize = function(name) {
self$name <- name
return(self$intro())}
)
)
如下根据Person5
创建两个对象,因为他们会共享age
属性,所以年龄始终保持一致:
Tom5 <- Person5$new("Tom")
Jane5 <- Person5$new("Jane")
Tom5$age$x <- 20
Tom5$intro()
## [1] "Hello, I am Tom and 20 years old"
Jane5$intro()
## [1] "Hello, I am Jane and 20 years old"
Tom5$age$x <- 30
Tom5$intro()
## [1] "Hello, I am Tom and 30 years old"
Jane5$intro()
## [1] "Hello, I am Jane and 30 years old"
放在initialize
方法中:
Person6 <- R6Class(
classname = "Person6",
public = list(
name = NULL,
age = NULL,
intro = function() {
print(paste("Hello, I am", self$name, "and",
self$age$x, "years old"))},
initialize = function(name) {
self$name <- name
## 在initialize方法内进行外部引用
self$age <- SimpleClass$new()
return(self$intro())}
)
)
Tom6<- Person6$new("Tom")
Jane6 <- Person6$new("Jane")
Tom6$age$x <- 20
Jane6$age$x <- 30
Tom6$intro()
## [1] "Hello, I am Tom and 20 years old"
Jane6$intro()
## [1] "Hello, I am Jane and 30 years old"
在类已被创建后,可以使用set()
函数增加属性和方法。
如下,先生成类Person7
,再在它的基础上新增一个公用方法born
:
Person7 <- R6Class(inherit = Person)
Person7$set("public", "born", function() year(Sys.Date()) - self$age)
Tom7 <- Person7$new("Tom", 20)
Tom7$born()
## [1] 2002
如果希望在类创建后,不允许对其进行修改,可以在创建时设置参数lock_class = TRUE
。如下,此时再想新增元素就会报错:
Person8 <- R6Class(
inherit = Person,
lock_class = T
)
Person8$set("public", "born", function() year(Sys.Date()) - self$age)
## Error in Person7$set("public", "born2", function() year(Sys.Date()) - :
## Can't modify a locked R6 class.
锁定类后,可以使用unlock()
函数对其进行解锁:
Person9 <- R6Class(
inherit = Person,
lock_class = T
)
## 解锁
Person9$unlock()
Person9$set("public", "born", function() year(Sys.Date()) - self$age)
Tom9 <- Person9$new("Tom", 20)
Tom9$born()
## [1] 2002
这一部分对应proto对象的备份(详见proto对象)。在那篇推文中,我们已经知道使用赋值符号不能克隆环境对象,而只是给对象起了一个新名字:
Tom10 <- Tom
Tom$age <- 20
Tom10$intro()
## [1] "Hello, I am Tom and 20 years old"
Tom$age <- 30
Tom10$intro()
## [1] "Hello, I am Tom and 30 years old"
从本例可以看出,
Tom10
的内容会随着Tom
的内容变化而变化;本质上,Tom
和Tom10
是同一对象的两个名字。
克隆R6对象可以使用clone()
函数:
Tom11 <- Tom$clone()
Tom11$age <- 40
Tom11$intro()
## [1] "Hello, I am Tom and 40 years old"
Tom$intro()
## [1] "Hello, I am Tom and 30 years old"
如果想不允许对象被克隆,在创建类时可以设置参数cloneable = FALSE
。如果类中包含有外部引用,克隆时应设置参数deep = TRUE
。
## 未设置参数deep = T
Tom12 <- Tom6$clone()
Tom6$intro()
## [1] "Hello, I am Tom and 20 years old"
Tom12$age$x <- 20
Tom12$intro()
## [1] "Hello, I am Tom and 20 years old"
Tom6$intro()
## [1] "Hello, I am Tom and 20 years old"
## 设置参数deep = T
Tom13 <- Tom6$clone(deep = T)
Tom6$intro()
## [1] "Hello, I am Tom and 20 years old"
Tom13$age$x <- 30
Tom13$intro()
## [1] "Hello, I am Tom and 30 years old"
Tom6$intro()
## [1] "Hello, I am Tom and 20 years old"
更复杂的情况请参见参考资料[1]或“阅读原文”。
[1]
Introduction to R6: https://r6.r-lib.org/articles/Introduction.html
[2]类和对象: https://baike.baidu.com/item/%E7%B1%BB%E5%92%8C%E5%AF%B9%E8%B1%A1/1394902