R语言基于R6的面向对象编程

R的极客理想系列文章,涵盖了R的思想,使用,工具,创新等的一系列要点,以我个人的学习和体验去诠释R的强大。

R语言作为统计学一门语言,一直在小众领域闪耀着光芒。直到大数据的爆发,R语言变成了一门炙手可热的数据分析的利器。随着越来越多的工程背景的人的加入,R语言的社区在迅速扩大成长。现在已不仅仅是统计领域,教育,银行,电商,互联网….都在使用R语言。

要成为有理想的极客,我们不能停留在语法上,要掌握牢固的数学,概率,统计知识,同时还要有创新精神,把R语言发挥到各个领域。让我们一起动起来吧,开始R的极客理想。

关于作者:

  • 张丹(Conan), 程序员Java,R,PHP,Javascript
  • weibo:@Conan_Z
  • blog: http://blog.fens.me
  • email: bsspirit@gmail.com

转载请注明出处:
http://blog.fens.me/r-class-r6/

r-class-r6

前言

R6是什么?听说过S3、S4和RC(R5)的面向对象类型 ,R6难道是一种新的类型吗?

其实,我也是在无意中发现R6的。R6是R语言的一个面向对象的R包,R6类型非常接近于RC类型(Reference classes),但比RC类型更轻,由于R6不依赖于S4的对象系统,所以用R6的构建面向对象系统会更加有效率。

目录

  1. 初识R6
  2. 创建R6类和实例化对象
  3. R6类的主动绑定
  4. R6类的继承关系
  5. R6类的对象的静态属性
  6. R6类的可移植类型
  7. R6类的动态绑定
  8. R6类的打印函数
  9. 实例化对象的存储
  10. R6面向对象系统的案例

1. 初识R6

R6是一个单独的R包,与我们熟悉的原生的面向对象系统类型S3,S4RC类型不一样。在R语言的面向对象系统中,R6类型与RC类型是比较相似的,但R6并不基于S4的对象系统,因此我们在用R6类型开发R包的时候,不用依赖于methods包,而用RC类型开发R包的时候则必须设置methods包的依赖,在发布gridgame游戏包 文章中,就会出现RC依赖于methods包的使用情况。

R6类型比RC类型更符合其他编程对于面向对象的设置,支持类的公有成员和私有成员,支持函数的主动绑定,并支持跨包的继承关系。由于RC类型的面向对象系统设计并不彻底,所以才会有R6这样的包出现。下面就让我们体会一下,基于R6面向对象系统编程吧。

2. 创建R6类和实例化对象

本文的系统环境

  • Win7 64bit
  • R: 3.1.1 x86_64-w64-mingw32/x64 (64-bit)

我们先安装R6包,同时为了方便我们检查对象的类型,引入pryr包作为辅助工具。关于pryr包的介绍,请参看撬动R内核的高级工具包pryr一文。


~ R                         # 启动R程序
> install.packages("R6")    # 安装R6包
> library(R6)               # 加载R6包
> library(pryr)             # 加载pryr包

注:R6同时支持Win7环境和Linux环境

2.1 如何创建R6类?

R6对象系统是以类为基本类型, 有专门的类的定义函数 R6Class() 和 实例化对象的生成方法,下面我们用R6对象系统创建一个类。

先查看R6的类创建函数R6Class()函数的定义。


> R6Class
function (classname = NULL, public = list(), private = NULL,
    active = NULL, inherit = NULL, lock = TRUE, class = TRUE,
    portable = TRUE, parent_env = parent.frame())

参数列表:

  • classname 定义类名。
  • public 定义公有成员,包括公有方法和属性。
  • private 定义私有成员,包括私有方法和属性。
  • active 主动绑定的函数列表。
  • inherit 定义父类,继承关系。
  • lock 是否上锁,如果上锁则用于类变量存储的环境空间被锁定,不能修改。
  • class 是否把属性封装成对象,默认是封装,如果选择不封装,类中属性存存在一个环境空间中。
  • portable 是否为可移植类型,默认是可移植型类,类中成员访问需要用调用self和private对象。
  • parent_env 定义对象的父环境空间。

从R6Class()函数的定义来看,参数比RC类定义函数的setRefClass()函数有更多的面对对象特征。

2.2 创建R6的类和实例化对象

我们先创建一个最简单的R6的类,只包括一个公有方法。


> Person <- R6Class("Person",    # 定义一个R6类
+  public=list(
+    hello = function(){         # 定义公有方法hello
+      print(paste("Hello"))
+    }
+  )
+)

> Person                   # 查看Person的定义
<Person> object generator
  Public:
    hello: function
  Parent env: 
  Lock: TRUE
  Portable: TRUE

> class(Person)             # 检查Person的类型
[1] "R6ClassGenerator"

接下来,实例化Person对象,使用$new()函数完成。


> u1<-Person$new()   # 实例化一个Person对象u1
> u1                 #查看u1对象
<Person>
  Public:
    hello: function
> class(u1)           # 检查u1的类型
[1] "Person" "R6"

通过pryr包的otype检查Person类的类型和u1对象的实例化类型。


> otype(Person)   # 查看Person类型
[1] "S3"
> otype(u1)       # 查看u1类型
[1] "S3"

完全没有想到的结果,Person和u1都是S3类型的。如果R6是基于S3系统构建的,那么其实就可以解释R6类型与RC类型的不同,并且R6在传值和继承上会更有效率。

2.3 公有成员和私有成员

类的成员,包括属性和方法2部分。R6类定义中,可以分开设置公有成员和私有成员。我们设置类的公有成员,修改Person类的定义,在public参数中增加公有属性name,并通过hello()方法打印name的属性值,让这个R6的类更像是Java语言的JavaBean。在类中访问公有成员时,需要使用self对象进行调用。


> Person <- R6Class("Person",
+  public=list(
+    name=NA,                           # 公有属性
+    initialize = function(name){       # 构建函数方法
+      self$name <- name
+    },
+    hello = function(){                # 公有方法
+      print(paste("Hello",self$name))
+    }
+  )
+)

> conan <- Person$new('Conan')          # 实例化对象
> conan$hello()                         # 调用用hello()方法
[1] "Hello Conan"

接下来再设置类的私有成员,给Person类中增加private参数,并在公有函数有调用私有成员变量,调用私有成员变量时,要通过private对象进行访问。


> Person <- R6Class("Person",
+   public=list(                       # 公有成员
+     name=NA,
+     initialize = function(name,gender){
+       self$name <- name
+       private$gender<- gender        # 给私有属性赋值
+     },
+     hello = function(){
+       print(paste("Hello",self$name))
+       private$myGender()             # 调用私有方法
+     }
+   ),
+   private=list(                      # 私有成员
+     gender=NA,
+     myGender=function(){
+       print(paste(self$name,"is",private$gender))
+     }
+   )
+ )
> conan <- Person$new('Conan','Male')         # 实例化对象
> conan$hello()                               # 调用用hello()方法
[1] "Hello Conan"
[1] "Conan is Male"

在给Person类中增加私有成员时,通过private参数定义gender的私有属性和myGender()的私有方法。之得注意的是在类的内部,要访问私有成员时,需要用private对象进行调用。

那我直接访问公有属性和私有属性时,公有属性返回正确,而私有属性就是NULL值,并且访问私有方法不可见。


> conan$name            # 公有属性
[1] "Conan"
> conan$gender          # 私有属性
NULL
> conan$myGender()      # 私有方法
Error: attempt to apply non-function

进一步地,我们看看self对象和private对象,具体是什么。在Person类中,增加公有方法member(),在member()方法中分别打印self对象和private对象。


> Person <- R6Class("Person",
+   public=list(
+     name=NA,
+     initialize = function(name,gender){
+       self$name <- name
+       private$gender<- gender
+     },
+     hello = function(){
+       print(paste("Hello",self$name))
+       private$myGender()
+     },
+     member = function(){              # 用于测试的方法
+       print(self)
+       print(private)
+       print(ls(envir=private))
+     }
+   ),
+   private=list(
+     gender=NA,
+     myGender=function(){
+       print(paste(self$name,"is",private$gender))
+     }
+   )
+ )
>
> conan <- Person$new('Conan','Male')
> conan$member()                            # 执行member()方法
<Person>                                    # print(self)的输出
  Public:
    hello: function
    initialize: function
    member: function
    name: Conan

<environment: 0x0000000008cfc918>          # print(private)的输出
[1] "gender"   "myGender"                  # print(ls(envir=private))的输出

从测试结果,我们可以看出self对象,就是实例化的对象本身。private对象则是一个的环境空间,是self对象所在环境空间的中的一个子环境空间,所以私有成员只能在当前的类中被调用,外部访问私有成员时,就会找不到。在环境空间中保存私有成员的属性和方法,通过环境空间的访问控制让外部调用无法使用私有属性和方法,这种方式是经常被用在R包开发上的技巧。

3. R6类的主动绑定

主动绑定(Active bindings)是R6中一种特殊的函数调用方式,把对函数的访问表现为对属性的访问,主动绑定是属于公有成员。在类定义中,通过设置active参数实现主动绑定的功能,给Person类增加两个主动绑定的函数active和rand。


> Person <- R6Class("Person",
+   public = list(
+     num = 100
+   ),
+   active = list(                      # 主动绑定
+     active  = function(value) {
+       if (missing(value)) return(self$num +10 )
+       else self$num <- value/2
+     },
+     rand = function() rnorm(1)
+   )
+)

> conan <- Person$new()
> conan$num                   # 查看公有属性
[1] 100
> conan$active                # 调用主动绑定的active()函数,结果为 num +10= 100+10=100
[1] 110

给主动绑定的active函数传参数,这里传参数要用赋值符号”<-",而不能是方法调用"()"。


> conan$active<-100    # 传参数
> conan$num            # 查看公有属性num
[1] 50
> conan$active         # 调用主动绑定的active()函数,结果为 num+10=50+10=60
[1] 60
> conan$active(100)    # 如果进行方法调用,其实会提示没有这个函数的
Error: attempt to apply non-function

我们再来调用rand函数,看看执行情况。


> conan$rand           # 调用rand函数
[1] -0.4767338
> conan$rand
[1] 0.1063623
> conan$rand<-99       # 传参出错
Error in (function ()  : unused argument (quote(99))

我们直接使用rand()函数完全没有问题,但给rand()函数传参数的时候就出现了错误,由于rand()函数没有定义参数,所以这个操作是不允许的。

通过主动绑定,可以把函数的行为转换成属性的行为,让类中的函数操作更加灵活。

4. R6类的继承关系

继承是面向对象的基本特征,R6的面向对象系统也是支持继承的。当你创建一个类时,可以继承另一个类做为父类存在。

先创建一个父类Person,包括公有成员和私有成员。


> Person <- R6Class("Person",
+   public=list(                            # 公有成员
+     name=NA,
+     initialize = function(name,gender){
+       self$name <- name
+       private$gender <- gender
+     },
+     hello = function(){
+       print(paste("Hello",self$name))
+       private$myGender()
+     }
+   ),
+   private=list(                           # 私有成员
+     gender=NA,
+     myGender=function(){
+       print(paste(self$name,"is",private$gender))
+     }
+   )
+ )

创建子类Worker继承父类Person,并在子类增加bye()公有方法。


> Worker <- R6Class("Worker",
+   inherit = Person,                # 继承,指向父类
+   public=list(
+     bye = function(){
+       print(paste("bye",self$name))
+     }
+   )
+ )

实例化父类和子类,看看继承关系是不是生效了。


> u1<-Person$new("Conan","Male")        # 实例化父类
> u1$hello()
[1] "Hello Conan"
[1] "Conan is Male"

> u2<-Worker$new("Conan","Male")        # 实例化子类
> u2$hello()
[1] "Hello Conan"
[1] "Conan is Male"
> u2$bye()
[1] "bye Conan"

我们看到继承确实生效了,在子类中我们并没有定义hello()方法,子类实例u2可以直接使用hello()方法。同时,子类u2的bye()方法,到了在父类中定义的name属性,输出结果完全正确的。

接下来,我们在子类中定义父类的同名方法,然后再查看方法的调用,看看是否会出现继承中函数重写的特征。修改Worker类,在子类定义private的属性和方法。


> Worker <- R6Class("Worker",
+   inherit = Person,
+   public=list(
+     bye = function(){
+       print(paste("bye",self$name))
+     }
+   ),
+   private=list(
+     gender=NA,
+     myGender=function(){
+       print(paste("worker",self$name,"is",private$gender))
+     }
+   )
+ )

实例化子类,调用hello()方法。


> u2<-Worker$new("Conan","Male")
> u2$hello()                    # 调用hello()方法
[1] "Hello Conan"
[1] "worker Conan is Male"

由于子类中的myGender()私有方法,覆盖了父类的myGender()私有方法,所以在调用hello()方法时,hello()方法中会调用子类中的myGender()方法实现,而忽略了父类中的myGender()方法。

如果在子类中想调用父类的方法,有一个办法是使用super对象,通过super$xx()的语法进行调用。


> Worker <- R6Class("Worker",
+   inherit = Person,
+   public=list(
+     bye = function(){
+       print(paste("bye",self$name))
+     }
+   ),
+   private=list(
+     gender=NA,
+     myGender=function(){
+       super$myGender()                                      # 调用父类的方法
+       print(paste("worker",self$name,"is",private$gender))
+     }
+   )
+ )

> u2<-Worker$new("Conan","Male")
> u2$hello()
[1] "Hello Conan"
[1] "Conan is Male"
[1] "worker Conan is Male"

在子类myGender()方法中,用super对象调用父类的myGender()方法,从输出可以看出,父类的同名方法也同时被调用了。

5. R6类的对象的静态属性

用面向对象的方法进行编程,那么所有变量其实都是对象,我们可以把一个实例化的对象定义成另一个类的属性,这样就形成了对象的引用关系链。

需要注意的一点是,当属性赋值为另一个R6的对象时,属性的值保存了对象的引用,而非对象实例本身。利用这个规则就可以实现对象的静态属性,也就是可以在多种不同的实例中是共享对象属性,类似于Java中的static属性一样。

下面用代码描述一下,就能很容易的理解。定义两个类A和B,A类中有一个公有属性x,B类中有一个公有属性a,a为A类的实例化对象。


> A <- R6Class("A",
+  public=list(
+    x = NULL
+  )
+ )
>
> B <- R6Class("B",
+  public = list(
+    a = A$new()
+  )
+ )

运行程序,实现B实例化对象对A实例化对象的调用,并给x变量赋值。


> b <- B$new()         # 实例化B对象
> b$a$x <- 1           # 给x变量赋值
> b$a$x                # 查看x变量的值
[1] 1

> b2 <- B$new()        # 实例化b2对象
> b2$a$x <- 2          # 给x变量赋值
> b2$a$x               # 查看x变量的值
[1] 2

> b$a$x                # b实例的a对象的x值也发生改变
[1] 2

从输出结果可以看到,a对象实现了在多个b实例的共享,当b2实例修改a对象x值的时候,b实例的a对象的x值也发生了变化。

这里有一种写法,我们是应该要避免的,就是通过initialize()方法赋值。


> C <- R6Class("C",
+  public = list(
+    a = NULL,
+    initialize = function() {
+      a <<- A$new()
+    }
+  )
+ )

> cc <- C$new()
> cc$a$x <- 1
> cc$a$x
[1] 1

> cc2 <- C$new()
> cc2$a$x <- 2
> cc2$a$x
[1] 2

> cc$a$x        # x值未发生改变
[1] 1

通过initialize()构建是的a对象,是对单独的环境空间中的引用,所以不能实现引用对象的共享。

6. R6类的可移植类型

在R6类的定义中,portable参数可以设置R6类的类型为可移植类型和不可移植类型。可移植类型和不可移植类型主要有2个明显的特征。

  • 可移植类型支持跨R包的继承;不可移植类型,在跨R包继承的时候,兼容性不太好。
  • 可移植类型必须要用self对象和private对象来访问类中的成员,如self$x,private$y;不可移植类型,可以直接使用变量x,y,并通过<<-实现赋值。

本文中使用的是R6的最新版本2.0,所以默认创建的是可移植类型。所以,当我们要考虑是否有跨包继承的需求时,可以在可移植类型和不可移植类型之间进行选择。

我们比较一下RC类型,R6的可移植类型和R6的不可移植类型三者的区别,定义一个简单的类,包括一个属性x和两个方法getx(),setx()。


> RC <- setRefClass("RC",                  # RC类型的定义
+   fields = list(x = 'Hello'),
+   methods = list(
+     getx = function() x,
+     setx = function(value) x <<- value
+   )
+ )
> rc <- RC$new()
> rc$setx(10)
> rc$getx()
[1] 10

创建一个行为完全一样的不可移植类型的R6类。


> NR6 <- R6Class("NR6",                # R6不可移植类型
+   portable = FALSE,
+   public = list(
+     x = NA,
+     getx = function() x,
+     setx = function(value) x <<- value
+   )
+ )
> np6 <- NR6$new()
> np6$setx(10)
> np6$getx()
[1] 10

再创建一个行为完全一样的可移植类型的R6类。


> PR6 <- R6Class("PR6",
+   portable = TRUE,            # R6可移植类型
+   public = list(
+    x = NA,
+    getx = function() self$x,
+    setx = function(value) self$x <- value
+   )
+ )
> pr6 <- PR6$new()
> pr6$setx(10)
> pr6$getx()
[1] 10

从这个例子中,可移植类型的R6类和不可移植类型的区别在,就在于self对象的使用。

7. R6类的动态绑定

对于静态类型的编程语言来说,一旦类定义后,就不能再修改类中的属性和方法,像反射这样的高开销的特殊操作除外。 而对于动态类型的编程语言来说,通常不存在这样的限制,可以任意修改其类的结构或者已实例化的对象的结构。 R作为动态语言来说,同样是支持动态变量修改的,基于S3类型和S4类型可以通过泛型函数动态地增加函数定义,但RC类型是不支持的,再次感觉到了R语言中面向对象系统设计的奇葩了。

R6包已考虑这个情况,提供了一种动态设置成员变量的方法用$set()函数。


> A <- R6Class("A",
+   public = list(
+     x = 1,
+     getx = function() x
+   )
+ )
> A$set("public", "getx2", function() self$x*2)     # 动态增加getx2()方法
> s <- A$new()
> s                     # 查看实例化对象的结构
<A>
  Public:
    getx: function
    getx2: function
    x: 1
> s$getx2()             # 调用getx2()方法
[1] 20

同样地,属性也可以动态修改,动态改变x属性的值。


> A$set("public", "x", 10, overwrite = TRUE)     # 动态改变x属性
> s <- A$new()
> s$x                                            # 查看x属性
[1] 10
> s$getx()                                       # 调用getx()方法,可移植类型x变量丢失
Error in s$getx() : object 'x' not found

由于A类默认是可移植类型的,所以在使用x变量时应该通过self对象来访问,否则动态成员修改的时候,就会出现错误,我们把getx中的x改为self$x。


> A <- R6Class("A",
+  public = list(
+    x = 1,
+    getx = function() self$x     # 修改为self$x
+  )
+ )
> A$set("public", "x", 10, overwrite = TRUE)
> s <- A$new()
> s$x
[1] 10
> s$getx()                        # 调用getx()方法
[1] 10

对于可移植类型和不可移植类型,建议大家养成习惯都使用self和private对象进行访问。

8. R6类的打印函数

R6提供了用于打印的默认方法print(),每当要打印实例化对象时,都会调用这个默认的print()方法,有点类似于Java类中默认的toString()方法。

我们可以覆盖print()方法,使用自定义的打印提示。


> A <- R6Class("A",
+  public = list(
+    x = 1,
+    getx = function() self$x
+  )
+ )
> a <- A$new()
> print(a)             # 使用默认的打印方法
<A>
  Public:
    getx: function
    x: 1

自定义打印方法,覆盖print()方法。


> A <- R6Class("A",
+    public = list(
+      x = 1,
+      getx = function() self$x,
+      print = function(...) {
+        cat("Class <A> of public ", ls(self), " :", sep="")
+        cat(ls(self), sep=",")
+        invisible(self)
+      }
+    )
+ )
> a <- A$new()
> print(a)
Class <A> of public getxprintx :getx,print,x

通过自定义的方法,就可以覆盖系统默认的方法,从而输出我们想显示的文字。

9. 实例化对象的存储

R6是基于S3面向对象系统的构建,而S3类型又是一种比较松散的类型,会造成用户环境空间的变量泛滥的问题。R6提供了一种方式,设置R6Class()的class参数,把类中定义的属性和方法统一存储到一个S3对象中,这种方式是默认的。另一种方式为,把把类中定义的属性和方法统一存储到一个单独的环境空间中。

我们看查看一下默认的情况,class=TRUE,实例化后的a对象,就是一个S3的类。


> A <- R6Class("A",
+  class=TRUE,
+  public = list(
+    x = 1,
+    getx = function() self$x
+  )
+ )
> a <- A$new()
> class(a)
[1] "A"  "R6"
> a
<A>
  Public:
    getx: function
    x: 1

当class=FALSE时,实例化后的a对象,是一个环境空间,在环境空间中存储了类的变量数据。


> B <- R6Class("B",
+   class=FALSE,
+   public = list(
+     x = 1,
+     getx = function() self$x
+   )
+ )
> b <- B$new()
> class(b)
[1] "environment"
> b
<environment: 0x000000000d83c970>
> ls(envir=b)
[1] "getx" "x"

实例化对象的存储还有另外一方面的考虑,由于类中的变量都是存在于一个环境空间中的,我们也可以通过手动的方式找到这个环境空间,从而进行变量的增加或修改。 如果随意地对环境空间中的变量进行修改,那么会给我们的程序带来一些安全上的风险,所以为了预防安全上的问题,可以通过R6Class()的lock参数所定环境空间,不允许动态修改,默认值为锁定状态不能修改。


> A <- R6Class("A",
+   lock=TRUE,       # 锁定环境空间
+   public = list(
+     x = 1
+   )
+ )
> s<-A$new()
> ls(s)         # 查看s环境空间的变量
[1] "x"
> s$aa<-11      # 增加新变量,错误
Error in s$aa <- 11 : cannot add bindings to a locked environment
> rm("x",envir=s)       # 删除原有变量,错误
Error in rm("x", envir = s) :
  cannot remove bindings from a locked environment

如果不锁定环境空间,让lock=FALSE,则环境空间处于完全开放的状态,可以任意进行变量的修改。


> A <- R6Class("A",
+  lock=FALSE,         # 不锁定环境空间
+  public = list(
+    x = 1
+  )
+ )
> s<-A$new()
> ls(s)         # 查看s环境空间的变量
[1] "x"
> s$aa<-11      # 增加变量
> ls(s)
[1] "aa" "x"
> rm("x",envir=s)    # 删除变量
> ls(s)
[1] "aa"

通过上面对R6的介绍,我就基本掌握R6面向对象系统的知识。接下来,我们做一个简单的例子,应用一下R6的面向对象编程。

10. R6面向对象系统的案例

我们用R6的面向对象系统,来构建一个图书分类的使用案例。

任务一:定义图书的静态结构

以图书(book)为父类,包括R,Java,PHP 的3个分类,在book类中定义私有属性及公有方法。

r6-class

定义图书系统的数据结构,包括父类的结构 和 3种型类的图书。


> Book <- R6Class("Book",            # 父类
+    private = list(
+      title=NA,
+      price=NA,
+      category=NA
+    ),
+   public = list(
+     initialize = function(title,price,category){
+       private$title <- title
+       private$price <- price
+       private$category <- category
+     },
+     getPrice=function(){
+       private$price
+     }
+   )
+ )

> R <- R6Class("R",     # 子类R图书
+    inherit = Book
+ )
> Java <- R6Class("JAVA",  # 子类JAVA图书
+   inherit = Book
+ )
> Php <- R6Class("PHP",    # 子类PHP图书
+   inherit = Book
+ )

创建3个实例化对象,R语言图书《R的极客理想-工具篇》,JAVA语言图书《Java编程思想》,PHP语言图书《Head First PHP & MySQL》,并获得图书的定价。


> r1<-R$new("R的极客理想-工具篇",59,"R")
> r1$getPrice()
[1] 59

> j1<-Java$new("Java编程思想",108,"JAVA")
> j1$getPrice()
[1] 108

> p1<-Java$new("Head First PHP & MySQL",98,"PHP")
> p1$getPrice()
[1] 98

任务二:正逢双11对各类图书打折促销

我们设计一种打折规则,用来促进图书的销售,这个规则纯属虚构。

  • 所有图书9折
  • JAVA图书7折,不支持重复打折
  • 为了推动R图书的销售,R语言图书7折,并支持重复打折
  • PHP图书无特别优惠

根据打折规则,图书都可以被打折,那么打折就可以作为图书对象的一个行为,然后R, Java, PHP的3类图书,分别还有自己的打折规则,所以是一种多态的表现。

我们修改父类的定义,增加打折的方法discount(),默认设置为9折,满足第一条规则。


> Book <- R6Class("Book",
+   private = list(
+     title=NA,
+     price=NA,
+     category=NA
+   ),
+   public = list(
+     initialize = function(title,price,category){
+       private$title <- title
+       private$price <- price
+       private$category <- category
+     },
+     getPrice=function(){
+       p<-private$price*self$discount()
+       print(paste("Price:",private$price,", Sell out:",p,sep=""))
+     },
+     discount=function(){
+       0.9
+     }
+   )
+ )

3个子类,分别对应自己的打折规则,分别进行修改。

  • 给JAVA子类增加 discount()方法,用于覆盖父类的discount()方法,让JAVA图书7折,不支持重复打折,从而满足第二条规则。
  • 给R子类增加 discount()方法,在子类的discount()方法中调用父类的discount()方法,让支持 R图书7折和9折的折上折,从而满足第三条规则。
  • PHP子类,没有修改,完全遵循第一条规则的。
    • 
      > Java <- R6Class("JAVA",
      +   inherit = Book,
      +   public = list(
      +     discount=function(){
      +       0.7
      +     }
      +   )    
      + )
      > 
      > R <- R6Class("R",
      +   inherit = Book,
      +   public = list(
      +     discount=function(){
      +       super$discount()*0.7
      +     }
      +   )             
      + )
      > 
      > Php <- R6Class("PHP",
      +   inherit = Book       
      + )
      

      分别查看3本图书的折后价格。

      
      > r1<-R$new("R的极客理想-工具篇",59,"R")
      > r1$getPrice()
      [1] "Price:59, Sell out:37.17"   # 59 * 0.9 *0.7= 37.17
      >
      > j1<-Java$new("Java编程思想",108,"JAVA")
      > j1$getPrice()
      [1] "Price:108, Sell out:75.6"    # 108 *0.7= 75.6
      >
      > p1<-Php$new("Head First PHP & MySQL",98,"PHP")
      > p1$getPrice()
      [1] "Price:98, Sell out:88.2"      # 98 *0.9= 88.2
      

      R图书打折最多,享受7折和9折的折上折优惠, 59 * 0.9 * 0.7= 37.17;Java图书享受7折优惠,108 *0.7= 75.6;PHP图书享受9折优惠 98 *0.9= 88.2。

      通过这个实例,我们用R6的方法实现了面向对象编程中的封装、继承和多态的3个特征,证明R6是一种完全的面向对象的实现。R6类对象系统,提供了一种可兼容的面向对象实现方式,更接近于其他的编程语言上的面向对象的定义,由于R6底层基于S3来实现的,所以比RC的类更加有效果。

      我们一共介绍了4种R语言的面向对象体系结构,选择自己理解的,总有一种会适合你。

      转载请注明出处:
      http://blog.fens.me/r-class-r6/

      打赏作者

      This entry was posted in R语言实践

0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

12 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
dj123jary

一直有个疑问,R的长处在于统计和算法设计,让R去实现面向对象,进行面向对象编程,是不是有点舍长取短啊,问下楼主,R语言实现面向对象,主要是不是想用面向对象的思想去设计算法啊

Conan Zhang

面向对象主要由于解决复杂问题的设计,当你的R程序超过1万行时,用面向函数的编程方法就不可维护了。

淡忘~浅思

嘿嘿 觉得贵站不错 已经将贵站加入 http://www.ido321.com/daohang/daohang.php 综合资讯类 如有错误请指正

Conan Zhang

谢谢!

ppl

animal = R6Class(“animal”, list(mother = NA, foreign = NA, say = NA))

cat = animal$new()

cat$mother = “喵喵”

cat$foreign = “汪汪”

athome = TRUE

if (athome) {

cat$say = function() print(self$mother)

} else {

cat$say = function() print(self$foreign)

}

cat$say() # error

ppl

实例里面的函数好像不能用self,或者得用其他办法搞…不太方便实现单例模式的样子…或者还是把cat定义成class会好点么…

Conan Zhang

你的代码有问题,self只能用在R6Class的定义里面。

你单独做了函数空间,这里根本没有self变量。
function() print(self$mother)

先按规范写程序,开始不要想当然。

ppl

r里面函数用到上层的变量很自然啊
f = function(){
x=10
ff = function(){
print(x)
}
ff()
}

f()

ppl

run的时候是global的env么…好像前面这种需求是比较麻烦…

Conan Zhang

用到上层变量没问题,你之前的程序self不是上层变量。

chilly

笔误:7. R6类的动态绑定
> s$getx2() # 调用getx2()方法
[1] 20
应该为
[1] 2

Xiaotong Yao

比绝大多数英文的介绍写得都好!!!太有用了

12
0
Would love your thoughts, please comment.x
()
x