第 2 章 R语言面向对象编程指南

面向对象是一种对世界理解和抽象的方法,当代码复杂度增加难以维护的时候,面向对象就会显得很重要,我经历过Java和Python两种语言从面向过程到面向对象的改造,对R的面向对象的编程也早有些研究但开始的时候并不是那么的透彻。随着大数据时代AI时代的来临,R将走向大规模的企业级应用,因此面向对象的编程方式也会将成为R语言的一种非常重要的趋势,并且多位R语言大神,像Hadley Wickham等在R包开发中早就引入了面向对象的编程方式,R语言的发展也势必会面向对象。

2.1 什么是面向对象

学过计算机编程基础的人都知道,计算机是通过接受一些逻辑指令,然后翻译成机器码,进而控制CPU的电路,从而实现我们能看到的所有操作。无论是何种计算机编程语言,都可以认为是计算机和人类沟通的翻译,将一般人能懂的计算机语言翻译成计算机能懂的机器语言。

简单的理解,有的语言比较接近机器的习惯,机器执行起来会更有效率;有的语言比较接近人的习惯,人类设计起来会更容易。面向对象的思想就属于第二种情况。

人的思维方式和计算机是不同的,计算机习惯按照顺序执行不同的指令,依据严格的逻辑进行不同的行为,而人类处理问题的方式通常是先对问题进行分析,然后调动不同的资源做不同的事情。

喜欢历史故事的朋友都知道诸葛亮打仗和刘邦打仗的区别。诸葛亮会命令某人埋伏,某人放火,某人举旗,某人不战而退,某人斜刺里杀出,每个人听到命令都不知道最后会发生什么。直到最后敌军被一条龙的歼灭后,得胜归来的将军们对诸葛亮佩服的五体投地。而刘邦打仗的故事远没有那么精彩,事情来了该让韩信做的交给韩信,该让萧何做的交给萧何。

如何像刘邦一样的写程序,这就是面向对象的程序设计。韩信,萧何这些人都可以认为是对象,我们只需要知道他们有什么特点,根据问题的不同派不同的人去做就可以了,而诸葛亮关注的是具体流程,至于是关羽去放火还是张飞去放火反而不重要了,这就是过程式的编程思想。

面向对象的程序设计在运行效率上可能没有优势,但是节约了开发者和设计者的时间,在R语言和S语言上这一点完全一致,S语言很重要的设计理念是“人的时间远比机器的时间宝贵”。对各种模型和算法的封装及重用与面向对象的编程目的是相同的。

在面向对象的程序设计中,对象(object)是最基本的元素,不过对象指的是具体的实例,在对象之上还有一个类(class)的概念。这里的类和R中的类的概念没有任何不同,都是指某一种抽象对象的类型(和R中的type不同,type指的是在内存存储方面的类型)。

比如说“马”就是一个类,随便牵来一匹白马或红马都属于马这个范畴,但都和马这个东西不一样,如果牵来的白马是刘备的的卢马,红马是关羽的赤兔马,那么这两匹马就是对象。所以类是抽象的概念,对象是类的具体实例。我们直接操作的是对象,但是需要定义的是类。

用面向对象的专业术语来说,马就是一个“类”,白马和红马是马的“子类”,的卢马是白马实例化的对象,也是马实例化的对象。

一般来说类包含属性和方法,属性指的是类具有的某些信息,在计算机程序中通常是变量,方法指的是类进行的操作,在计算机程序中相当于函数。

并不是具有了类和对象的概念后就成了面向对象的程序设计,一般来说还得具备三个特性:封装、继承和多态。

封装指的是隐藏对象的实现细节,仅对外公开接口,每个对象都可以独立的完成一定的功能,不需要和其他对象有过多的交互,所有的数据交换都通过接口来处理,专业术语是降低系统的“耦合度”。

比如说马这种交通工具就是一个封装的很好的类,具有颜色、体重等属性,具有载人、奔跑等方法。用的时候把马牵出来,通过缰绳和马鞭这几个接口来控制动作。

继承是一个类可以继承另一个类的各种属性及方法,重写或增加某些属性和方法,被继承的类称为“父类”,继承了父类的类的称为“子类”。通过继承可以重写父类方法或增加额外功能,注意R语言像Python一样支持多重继承,Java不支持多继承,但是有其他办法实现。当然除了继承也可以组合类。

多态可以说是面向对象的程序设计中最关键的特性,如果某种语言支持以上所有特性但是不支持多态,我们称其为“基于对象”而不是面向对象,所谓多态简单来说就是希望能用相同的命令作用于不同的类,根据类的不同产生不同的结果。

#作用在数值数据
summary(rnorm(10))
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
## -2.69364 -0.65721 -0.02727 -0.13618  0.58467  1.68234
#作用在Model上
summary(lm(rnorm(10)~rnorm(10)))
## 
## Call:
## lm(formula = rnorm(10) ~ rnorm(10))
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -1.6121 -0.8625  0.1777  0.8150  1.7043 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)
## (Intercept) -0.08898    0.33865  -0.263    0.799
## 
## Residual standard error: 1.071 on 9 degrees of freedom

2.2 R为什么要进行面向对象的编程

R主要面向统计计算,而且代码量一般不会很大,几十行,几百行,使用面向过程的编程方式就可以很好的完成编程任务。在R中流传着一个深入人心的说法“万物皆对象”,另一方面,R是一种函数式的语言,与面向对象的程序设计存在着天然的差异,实际上这两个对象的描述的含义是不同的。“万物皆对象”是指R的基本数据结构的地位都是相同的,任何东西包括函数都可以认为是对象,都能作为参数传入到函数中。而“面向对象”指的是一种编程泛型,已经成为一个专有的名词。

但是随着R语言在工业界的火热,伴随着越来越多的工程背景的人的加入,R语言开始向更多领域发展,会有越来越难以维护的海量代码项目,所以必须使用面向对象的编程思想,此外如果你开发一些R包需要对特殊的对象重定义S3(下一章会讲)的方法,需要对大量程序代码最大化的重用和封装(S4(后面会讲到)),那么这时候你同样需要用到R语言的面向对象的编程。

一句话,随着R语言的发展,R面向对象编程一定是一个大的趋势。

2.3 R的面向对象编程

R的面向对象是基于泛型函数(generic function)的,而不是基于类层次结构,接下来我们从面向对象的3个特征入手,分别用R语言进行实现。

  • 封装
# 定义teacher对象和行为
teacher <- function(x,...) UseMethod("teacher")
teacher.lecture <- function(x,...) print("上课")
teacher.assignment <- function(x,...) print("布置作业")
teacher.correcting <- function(x,...) print("批改作业")
teacher.default <- function(x,...) print("你不是teacher")

# 定义同学对象和行为

student <- function(x,...) UseMethod("student")
student.attend <- function(x,...) print("听课")
student.homework <- function(x,...) print("写作业")
student.exam <- function(x,...) print("考试")
student.default <- function(x,...) print("你不是student")

#定义两个变量,a老师和b同学

a <- 'teacher'
b <- 'student'

# 给老师变量设置行为

attr(a,"class") <- 'lecture'
# 执行老师的行为

teacher(a)
## [1] "上课"
attr(b,'class') <- 'attend'
student(b)
## [1] "听课"
attr(a,'class') <- 'assignment'
teacher(a)
## [1] "布置作业"
  • 继承
# 给同学对象增加新的行为
student.correcting <- function(x) print("帮助老师批改作业")

# 辅助变量用于设置初始值
char0 = character(0)
#实现继承关系
create <- function(classes=char0,parents=char0){
  mro <- c(classes)
  for(name in parents){
    mro <- c(mro,name)
    ancestors <- attr(get(name),'type')
    mro <- c(mro,ancestors(ancestors != name))
    
  }
  
  return(mro)
}

# 定义构造函数,创建对象
NewInstance <- function(value=0,classes=char0,parents=char0) {
  obj <- value
  attr(obj,'type') <- create(classes,parents)
  attr(obj,"class") <- c('homework','correcting','exam')
  return(obj)
}

# 创建对象实例
StudentObj <- NewInstance()
# 创建子对象实例
s1 <- NewInstance("普通同学",classes = 'normal',parents = "StudentObj")
s2 <- NewInstance('课代表',classes='leader',parents='StudentObj')

# 给课代表,增加批改作业行为
attr(s2,'class') <- c(attr(s2,'class'),'correcting')
s1
s2
  • 多态
# 创建优等生和次等生,两个实例
e1 <- NewInstance("优等生",classes='excellent',parents='StudentObj')

e2 <- NewInstance("次等生",classes='poor',parents='StudentObj')

student.exam <- function(x,score){
  p <- '考试'
  if(score>85) print(paste(p,"优秀"))
  if(score<70) print(paste(p,"及格"))

}

# 执行优等生的考试行为,并输入分数为90
attr(e1,'class') <- 'exam'
stuent(e1,90)
[1] “考试优秀"

attr(e2,'class') <- 'exam'

student(e2,66)
[1] ”考试及格"

通过R语言的面向对象的反省函数,我们就可以实现面向对象的编程。