R语言与面向对象的编程(3):R6类


专注系列化、高质量的R语言教程

本号已支持快捷转载,无需白名单即可转载


本系列将介绍R语言中三个与面向对象的编程(Object-Oriented Programming,OOP)相关的工具包:protoR6和基础包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 克隆对象

1 引言

proto基于对象的编程不同,R6采用的是基于类的编程(class-based programming)。类(class)和对象(object)的关系是:对象是类的实例,类是对象的模板;定义对象之前需要先定义类[2]

R语言基础包中有S3类、S4类和reference class(R5类为其非正式称呼),R6是基于这个顺序命名的。

2 创建R6类和对象

2.1 创建类

创建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())}
  )
)

在上面的代码中,我们只使用了classnamepublic两个参数。前者无需多说。在public中,我们定义了两个属性:nameage;两个方法:introinitialize

要点如下:

  • public的数据结构形式为列表,元素(属性或方法)之间使用逗号隔开;

  • 可以使用self表示类本身,进而在方法中引用它的的属性和方法。

R6类的数据类型:

class(Person)
## [1] "R6ClassGenerator"

2.2 创建对象

从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"

3 私有属性和方法

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中,当我们在对象后输入$时,自动联想中也不会出现私有部分,因此可以将一些作为过程的属性和方法放到私有部分。如下对比了TomTom2对象:

R语言与面向对象的编程(3):R6类_第1张图片 R语言与面向对象的编程(3):R6类_第2张图片

4 类的继承

我们可以在一个类的基础上定义一个新类,这称为继承(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"

注意:调用的只是父类中方法的形式,参数值仍来自子类。

5 外部引用

如果把其他类的对象作为要定义类的属性,通常情况下应将赋值过程放在initialize方法中,否则该类的所有对象会共享这一属性值。

下面例子中,我们使用类SimpleClass的对象作为类Person5age属性。

不放在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"

6 新增类的元素

在类已被创建后,可以使用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

7 类的锁定和解锁

如果希望在类创建后,不允许对其进行修改,可以在创建时设置参数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

8 克隆对象

这一部分对应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的内容变化而变化;本质上,TomTom10是同一对象的两个名字。

克隆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

你可能感兴趣的:(java,类,多态,封装,python)