Likan Zhan

第 07 章、面向对象的编程

战立侃 · 2017-04-07

原文链接:OO field guide

R有一种基本的数据类型(base types)和三种面向对象的系统(object oriented systems)。

任何一个面向对象系统都有两个核心观念:类别(class)和 方法(method)。类别 通过描写一个对象的特征(attributes),以及该对象与其他类别的关系来定义一个对象的行为。方法的行为会因为输入类别的不同而不同。类别的存在是层级性的(hierachy)。如果不存在一个可应用于某个子类别方法,那么能够应用于其母类别方法则可应用于该子类别。即子类别能够继承(inherits)来自于母类别方法

R的三种面向对象系统定义了不同的类别方法

R中还有一种实际上不是面向对象系统的类别:

7.1. 基本类别

每一个R语言对象的底层都有一个C结构(C structure 或 struct)。C结构描述了对象在内存中的存储方式。C结构包含了对象的内容、内存控制的相关信息、和类别。该类别描述的是R对象基本类别(base type)。本质上来说,基本类别并不是真正意义上的对象系统,因为只有R核心团队才有权限创造一个新的基本类别。所以,R语言中的基本类型很少发生改变。R语言中最近一次基本类型的增加发生在2011年:NEWXPFREEXP。你通常不会在R中看到这两个类型,但是它们对诊断内存问题是非常重要的。在此之前的最后一次基本类别的增加是2005年:S4SXP。这是一个专门针对S4系统的基本类型。

数据结构这一章介绍了最常见的一些基本类别,如原子数组(atomic vectors)、列表(lists)等。基本类型还包括函数(function)、环境(environments)、和一些奇特的对象,如名称(names)、调用(calls)、和允诺(promises)等。你可以用typeof()来确定一个对象的基本类型。需要注意的是,R语言中基本类型的名称是不一致的。你也可以用is加不同名称的方式来确定一个对象的基本类型。

# 一个函数的类别是`closure'
f <- function() {}
typeof(f)
is.function(f)
## [1] "closure"
## [1] TRUE
# 一个原函数的类别是`builtin'
typeof(sum)
is.primitive(sum)
## [1] "builtin"
## [1] TRUE

R语言中还有与typeof()类似的两个函数mode()storage.mode()。但这两个函数实际上只是typeof()函数的别名,而且只是为了与S语言兼容而存在的。所以读者完全可以忽略这两个函数。

R语言中因为基本类型的不同,行为方式而有所不同的函数大多数是由C语言写成的。在这些函数中,转化命令(switch statements)可以导致派送的产生,例如switch(TYPEOF())。就算你从不需要写C语言代码,正确理解基本类型也是非常重要的,因为R中所有其他对象都是基于基本类型建构的:S3对象能够基于任何一种基本类型来建构;S4对象使用了一种特殊的基本类型;而RC对象则同时使用了S4对象和环境(environments,另外一种基本类型)。如果is.object(x)返回错误值FALSE,说明该对象是一个纯粹的基础类型,而不具有S3S4或者RC的行为方式。

is.object(f)
is.object(sum)
## [1] FALSE
## [1] FALSE

7.2. S3类别

S3是R中第一种最简单的面向对象系统。软件包basestats使用的唯一一种面向对象系统即S3系统。CRAN中软件包普遍使用的系统也是\(S3\)系统。S3系统不够正式,却又是最简化的:删除该系统的任何一部分以后,他仍然是一个有用的面向对象系统。

7.2.1. 识别对象范型函数方法

R中大多数对象都是S3对象。但基础R软件包中并没有确定一个对象是否为S3类型的简单方法。一种最接近的方式是:is.object(x) & !isS4(x),即它是一个对象,但不是S4类型的对象。借助 pryr 软件包判断一个对象是否是S3类型的方法是pryr::otype()

library(pryr, quietly = TRUE, warn.conflicts = FALSE)
df <- data.frame(x = 1:10, y = letters[1:10])
otype(df)   # 数据框(data.frame)属于S3类型
otype(df$x) # 数值型向量(numeric vector)不属于S3类型
otype(df$y) # 因子(factor)属于S3类型
## [1] "S3"
## [1] "base"
## [1] "S3"

在S3类型中,方法不是对象类别而是函数,叫做范型函数,或简称范型(generics)。这与其他大部分编程语言不同,但在面向对象系统中却是合法的。

要确定一个函数是否是S3范型,你可以用UseMethod()来查看该函数调用的源代码。UseMethod()能确定该函数调用的正确方法、即方法分派 (method dispatch) 过程。与函数otype类似,软件包pryr提供了另外一个命令ftype(),用以描述与该函数相关的对象系统

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

有些S3范型,如[sum()、和cbind,无法调用UseMethod(),因为这些范型是基于C语言代码编写的。相对的,它们可以调用C语言函数DispatchGroup()或者DispatchOrEval()。C语言代码中调用方法分派的函数叫做内在范型(internal generics)。这些函数的说明文档可以通过?"internal generic"函数来调用。函数ftype()也能识别这些特定函数。

ftype("[")
## [1] "function"

给定一个类型,S3范型的作用是调用正确的S3方法。你可以通过其名称来识别该类型调用的S3方法,形式如下generic.class()。例如范型函数mean()的日期(Date)方法是通过mean.Date()来调用的;范型函数print()因子(factor)方法是通过print.factor()调用的。

X <- 1:6
mean(X)
mean.Date(X)
print(X)
print.factor(X)
## Warning in print.factor(X): factor levels must be "character"
## [1] 3.5
## [1] "1970-01-04"
## [1] 1 2 3 4 5 6
## [1] 1 2 3 4 5 6
## Levels:

这也就是当代大多数编码风格指导手册不鼓励在函数中用点.的原因:使用点.会让它看起来像S3方法。例如,t.test()对象testt方法吗?类似的在类型中使用点.也会产生歧义:类型print.data.frame()表示的是数据框data.frameprint()方法,还是表示框frameprint.data()方法?pryr::ftype()能够识别这些例外,所以你可以用该函数来确定一个函数是S3方法还是范型函数。

ftype(t.data.frame) # 数据框data frame的t()方法
ftype(t.test) # t检验的范型函数
## [1] "s3"     "method"
## [1] "s3"      "generic"

你可以用methods()查看属于某个范型函数的所有方法:

methods("mean")
methods("t.test")
## [1] mean.Date     mean.default  mean.difftime mean.POSIXct  mean.POSIXlt 
## see '?methods' for accessing help and source code
## [1] t.test.default* t.test.formula*
## see '?methods' for accessing help and source code

(除了基本包中定义的方法,大多数S3方法是无法看到的,我们可以用getS3method()来查看它们的源代码.)

我们也可以列出与某一个类型相关的拥有方法的所有范型函数:

methods(class = "ts")
##  [1] [             [<-           aggregate     as.data.frame cbind        
##  [6] coerce        cycle         diff          diffinv       initialize   
## [11] kernapply     lines         Math          Math2         monthplot    
## [16] na.omit       Ops           plot          print         show         
## [21] slotsFromS3   t             time          window        window<-     
## see '?methods' for accessing help and source code

但是,如我们下一节将要学到的,我们没有办法列出所有的S3类型。

7.2.2. 定义类型和创造对象

S3是一个简单的和特设(ad hoc)的系统。S3系统中没有对类型的正式定义。要把一个对象定义为某一种类型,方法是给该基本对象( base object)定义一个类型特征(class attribute)。我们可以用如下两种方式定义类型特征:

# 一步到位,定义类型
foo <- structure(list(), class = "foo")
# 两步走,定义类型
foo <- list()
class(foo) <- "foo"

S3对象通常是在列表(lists)、或有特征原子数组基础上建构。你也可以把函数转化成S3对象。其他的基础类型或者在R中很少出现,或者其语义系统使得其与特征不能很好的工作。

你可以用class()来确定一个对象的类别,或用inherits(x, "classname")来确定一个对象是否继承自一个特定的类别。

class(foo)
inherits(foo, "foo")
## [1] "foo"
## [1] TRUE

一个S3对象的类别可以是一个向量(vector),它描述了从最特殊性的行为到最一般化的行为。例如,对象glm()类别c("glm", "lm"),说明一般化的线性模型的行为继承自线性模型的行为。类别名称通常用小写字母表示,并且避免使用点.。否则的化,命名可能会出现用下划线my_class还是大小写MyClass来区分同一类别名称中不同单词的混乱情况。

大多数S3类别都提供了一个建构函数(constructor function):

foo <- function(x){
  if(!is.numeric(x)) stop("X must be numeric")
  structure(list(x), class = "foo")
}

如果情况允许(如factor()data.frame()),你应该尽量使用建构函数。这将保证你用正确的成分创造了该类别。建构函数通常与类别名称相同。

除了代码编写着提供的建构函数,S3是核查函数正确性的。所以你能够随意修改一个已有对象的类别:

# 构建一个线性模型
mod <- lm(log(mpg) ~ log(disp), data = mtcars)
class(mod)
print(mod)
## [1] "lm"
## 
## Call:
## lm(formula = log(mpg) ~ log(disp), data = mtcars)
## 
## Coefficients:
## (Intercept)    log(disp)  
##      5.3810      -0.4586
# 把mod编程一个数据框 (?!)
class(mod) <- "data.frame"
# 很明显,这会导致print函数无法正常显示
print(mod)
# 但是,数据仍然还在
mod $ coefficients
##  [1] coefficients  residuals     effects       rank          fitted.values
##  [6] assign        qr            df.residual   xlevels       call         
## [11] terms         model        
## <0 rows> (or 0-length row.names)
## (Intercept)   log(disp) 
##   5.3809725  -0.4585683

如果你是用过其他面向对象的语言,这会让你难以忍受。但令人惊讶的是,在R语言中,该灵活性并没有导致太多的问题:虽然你能够改变一个对象的类别,但是你绝对不应该改变。

7.2.3. 创造新的方法范型函数

要添加一个新的范型,你要调用UseMethod()来建立一个函数UseMethod()有两个论元:一个论元是范型函数的名称,另一个论元用于方法的指派(method dispatch)。 如果第二个论元缺失,那么该UseMethod()将把第一个论元指派给该函数。你没有必要也不应该把范型函数的所有论元传递到UseMethod()函数的论元。UseMethod()能够自己找到它们。

f <- function(x) UseMethod("f")

没有方法的范型函数是没有用的。要给函数添加一个方法,就要用正确的名称(即generic.class)定义一个普通的函数:

f.a <- function(x) "Class a"
a <- structure(list(), class = "a")
class(a)
## [1] "a"

To be continued ~

7.2.4. 方法指派

7.3. S4类别

7.4. RC类别

7.5. 类别选择

7.6. 测验问题