第 3 章 基于S3的面向对象编程

对于R语言的面向对象编程,不同于其他编程语言,R语言提供了3种底层对象类型,一种是S3类型,一种是S4类型,还有一种是RC类型。

S3对象简答,具有动态性,结构化特征不明显,S4对象结构化。功能强大,RC对象是R2.12版本后使用的新类型,用于解决S3,S4很难是想的对象。

本章主要介绍S3的面向对象编程的细节

3.1 S3对象的介绍

在R语言中,基于S3对象的面向对象编程,是一种基于泛型函数的实现方式。泛型函数是一种特殊的函数,根据传入对象的类型决定调用那个具体的方法。基于S3对象实现面向对象编程,不同其他语言的面型对象编程,是一种动态函数调用的模拟实现。S3对象被广泛应用于R的早期的开发包中。

3.2 创建S3对象

注意:本文会用到pryr,为了方便我们检查对象的类型,引入pryr包作为辅助工具。

library(pryr)

#通过变量创建S3对象

x <- 1
attr(x,'class') <- 'foo'
x
## [1] 1
## attr(,"class")
## [1] "foo"
attr(x,"class")
## [1] "foo"
class(x)
## [1] "foo"
#用pryr包的otype函数,检查x的类型
otype(x)
## [1] "S3"

通过structure()函数创建S3对象

y <- structure(2,class="foo")

y
## [1] 2
## attr(,"class")
## [1] "foo"
attr(y,"class")
## [1] "foo"
class(y)
## [1] "foo"
otype(y)
## [1] "S3"

创建一个多类型的S3对象,S3独享没有明确结构关系,一个S3对象可以有多个类型,S3对象的class属性可以是一个响亮,包括多种类型

x <- 1
attr(x,"class") <- c("foo","bar")
class(x)
## [1] "foo" "bar"
otype(x)
## [1] "S3"

3.3 泛型函数和方法调用

对于S3对象的使用,通常用UseMethod()函数来定义一个泛型函数的名称,通过传入参数的class属性,来确定方法调用。

定义一个teacher的泛型函数

  • 用UseMethod()定义teacher泛型函数

  • 用teacher.xxx的语法格式定义teacher对象的行为

  • 其中teacher.default是默认行为

# 用UseMethod()定义teacher泛型函数
teacher <- function(x,...) UseMethod("teacher")
# 用pryr包中ftype()函数,检查teacher类型
ftype(teacher)
[1] "s3" "generic"

# 定义teacher内部函数

teacher.lecture <- function(x,...) print("讲课")
teacher.assignment <- function(x,...) print("布置作业")
teacher.correcting <- function(x,...) print("批改作业")
teacher.default <- function(x,...) print("你不是teacher")

方法调用通过传入参数的class属性,来确定不同方法调用

  • 定义一个变量a,并设置a的class属性为lecture

  • 把变量a传入到teacher泛型函数中

  • 函数teacher.lecture()函数的行为被调用

a <- "teacher"
# 给老师变量设置行为
attr(a,"class") <- 'lecture'
# 执行老师的行为
teacher(a)
[1] “讲课”

当然我们可以直接调用teacher中定义的行为,如果这样做就失去了面向对象封装的意义

teacher.lecture()
[1] "讲课"
teacher.lecture(a)
[1] "讲课"
teacher()
[1] "你不是teacher"

3.4 查看S3对象的函数

当我们使用S3队形进行面向对象封装后,可以使用methods()函数来查看S3对象中的定义的内部行为函数。

# 查看teacher对象
> teacher
function(x,...) Usemethod("teacher")

# 查看teacher对象的内部函数
> methods(teacher)
[1] teacher.assignment teacher.correcting teacher.default teacher.lecture

#通过methods()的generic.function参数,来匹配泛型函数名字
> methods(generic.function = predict)
[1] predict.ar* ......

通过methods()的class参数,来匹配类的名字

> methods(class=lm)
[1]add1.lm* ......

用getAnywhere()函数,查看所有函数

#查看teacher.lecture函数

>getAnywhere(teacher.lecture)

使用getS3method()函数,也同样可以查看不可见的函数

# getS3method()函数查找predict.ppr

get时method("predict","ppr")

3.5 S3对象的继承关系

S3独享有一种非常简单的继承方式,用NextMethod()函数来实现。

定义一个node泛型函数

> node <- function(x) UseMethod("node",x)
> node.default <- function(x) "Default node"

#father函数
> node.father <- function(x) c("father")

# son函数,通过NextMethod()函数只想father函数
> node.son <- function(x) c('son',NextMethod())

#定义n1
> n1 <- structure(1,class=c("father"))
# 在node函数中传入n1,执行node.father()函数
> node(n1)
[1] "father"

# 定义n2,设置class属性为两个
> n2 <- structure(1,class=c("son","father"))
# 在node函数中传入n2,执行node.son()函数和node.father()函数
> node(n2)
[1] "son" "father"

通过对node()函数传入n2的参数,node.son()先被执行,然后通过NextMethod()函数继续执行了node.father()函数。这样其实就模拟了,子函数调用父函数的过程,实现了面向对象编程中的继承。

3.6 S3对象的缺点

从上面S3对象的介绍上来看,S3对象并不是完全的面向对象实现,而是一种通过泛型函数模拟的面向对象的实现。

  • S3用起来简单,但在实际的面向对象编程的过程中,当对象关系有一定的复杂度,S3对象所表达的意义就变得不太清楚

  • S3封装的内部函数,可以绕过泛型函数的检查,以直接被调用

  • S3参数的class属性,可以被任意设置,没有预处理的检查

  • S3参数,只能通过调用class属性进行函数调用,其他属性则不会被class()函数执行

  • S3参数的class属性有多个值时,调用时会被按照程序赋值顺序来调用第一个合法的函数

所以,S3只是R语言面向对象的一种简单的实现。

3.7 S3对象的使用

S3对象系统,被广泛的应用于R语言早期的开发中。在base包中,就有很多S3对象

base包的S3对象

# mean 函数
mean
## function (x, ...) 
## UseMethod("mean")
## <bytecode: 0x000000001b953ed8>
## <environment: namespace:base>
ftype(mean)
## [1] "s3"      "generic"
# t函数
ftype(t)
## [1] "s3"      "generic"
# plot函数
ftype(plot)
## [1] "s3"      "generic"

自定义S3对象

# 定义数字变量a

a <- 1
# 变量a的class为number
class(a)
## [1] "numeric"
# 定义泛型函数f1
f1 <- function(x){
  a <-2
  UseMethod("f1")
}
# 定义f1的内部函数
f1.numeric <- function(x) a

# 给f1()传入变量a
f1(a)
## [1] 2
# 给f1()传入99
f1(99)
## [1] 2
# 定义f1内部函数
f1.character <- function(x) paste("char",x)

# 给f1()传入字符a
f1("a")
## [1] "char a"

这样,我们就对S3对象系统有了一个全面认识,开始R语言的面向对象编程之路。