R语言面向对象编程包R6 介绍

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值