【r<-高级|理论】R-面向对象编程(续)

接R-面向对象编程

下面演示如何基于TimeSeries类实现一个WeightHistory类以记录个人的历史体重信息。

> setClass("TimeSeries",
+   representation(
+     data="numeric",
+     start="POSIXct",
+     end="POSIXct"
+ 
+     )
+ )
> setValidity("TimeSeries",
+   function(object) {
+     object@start <= object@end &&
+     length(object@start)==1 &&
+     length(object@end)==1
+   }
+ )
Class "TimeSeries" [in ".GlobalEnv"]

Slots:
                              
Name:     data   start     end
Class: numeric POSIXct POSIXct

创建子类:

> setClass(
+   "WeightHistory",
+   representation(
+     height = "numeric",
+     name = "character"
+  ),
+   contains = "TimeSeries"
+ )

添加实例对象:

> john.doe <- new("WeightHistory", 
+   data=c(170,169,171,168,170,169),
+   start=as.POSIXct("02/14/2019 0:00:00", tz="GMT",
+     format="%m/%d/%Y %H:%M:%S"),
+   end=as.POSIXct("03/28/2019 0:00:00", tz="GMT",
+     format="%m/%d/%Y %H:%M:%S"),
+   height=72,
+   name="John Doe")
> john.doe
An object of class "WeightHistory"
Slot "height":
[1] 72

Slot "name":
[1] "John Doe"

Slot "data":
[1] 170 169 171 168 170 169

Slot "start":
[1] "2019-02-14 GMT"

Slot "end":
[1] "2019-03-28 GMT"

我们还可以通过另外一种方式构建一个体重记录。假设我们已经创建好了一个包含人名和体重的Person类。

> setClass("Person",
+   representation(
+     height = "numeric",
+     name = "character")
+ )

我们可以创建一个基于TimeSeries类和Person类的体重记录类。

> setClass(
+   "AltWeightHistory",
+   contains = c("TimeSeries", "Person")
+ )

可以发现,如果我们已经有了先期的开发经验或者相关类的代码,对新任务进行重构是非常方便的。短短几行代码就搞定了,充分利用了代码的可重复性。这也是OOP在高级语言中如此普遍的一个原因吧。

S4类

我们接下来更深入地探讨构造类的函数。

类的定义

R中使用setClass函数来创建一个新类,格式如下:

setClass(Class, representation, prototype, contains=character(), validity, access, where, version, sealed, package, S3methods=FALSE)

描述

  • Class - 字符串,用来指定新类的名字(这是唯一必需的参数)
  • representation - 列表,列表的每一个元素代表不同的槽的类型,元素名为槽名(可以用"ANY"来指定类型为任意)
  • prototype - 包含各个槽的默认值的对象
  • contains - 字符向量,包含该类继承的父类名
  • validity - 验证该类的对象有效性的函数(默认没有检查),可以后续使用setValidity函数来设置
  • access - 无作用,为了和S-PLUS兼容
  • where - 存储该对象定义的环境
  • version - 无作用,为了和S-PLUS兼容
  • sealed - 逻辑值,表示该类是否还能被setClass按照原来的类名重新定义
  • package - 字符串,指定该类所在的R包名
  • S3methods - 逻辑值,表示是否使用了S3类写这个类

为了简化类的创建,methods包提供了representation以及protype函数。它们在将其他类继承为数据部分、拥有多个父类、或者组合继承类和槽的时候非常有用。

值得注意的是,有些名字是属性的保留字因而不能作为槽名使用,包括"class","comment","dim","dimnames","names","rownames"和"tsp"。

可以使用setIs函数来显式地定义继承关系。

setIs(class1, class2, test=NULL, coerce=NULL, replace=NULL,
     by=character(), where=topenv(parent.frame()), classDef=, extensionObject=NULL, doComplete=TRUE)

可以使用setValidity函数来显式地设置类的验证函数:

setValidity(Class, method, where=topenv(parent.frame()))

R可以定义一个虚类作为多个其他类的父类。如果一个虚类本身不包含任何数据,但是如果你想要创建一批函数用于一批类中,这种方式非常有用。可以通过setClassUnion函数实现:

setClassUnion(name, members, where)
  • name - 新的父类的名字
  • members - 字符向量,指定所有子类的名字
  • where - 新类所在的环境

对象的新建

我们可以通过调用类的new方法新建一个对象。专业术语中称为构造函数。

new(c, ...)

在调用new的时候,我们可以通过指定参数将数据填充到槽中。如果c中存在名为initialize的方法,那么当新的对象被创建后,会立刻调用initialize函数进行初始化。

槽的存取

我们可以使用slot函数或者简化符号@来访问存储对象某个槽中的值,当然也可以用它来赋值。

> john.doe@name
[1] "John Doe"
> slot(john.doe, "name")
[1] "John Doe"

对象的操作

使用is(o, c)函数测试对象o是否是类c的成员。使用函数extend(c1, c2)测试类c1是否继承于类c2

如果要得到对象o包含的所有槽的名称,使用slotNames(o),如果要得到槽的类型,使用getSlots(o)。这两个函数也可以对类使用。

> getSlots("WeightHistory")
     height        name        data       start         end 
  "numeric" "character"   "numeric"   "POSIXct"   "POSIXct" 

> slotNames("WeightHistory")
[1] "height" "name"   "data"   "start"  "end"   
> slotNames("john.doe")
character(0)
> slotNames(john.doe)
[1] "height" "name"   "data"   "start"  "end"  

注意一些差别,有引号和没引号结果是不同的。

方法

泛型函数允许使用同一个函数名来代表很多不同的函数,针对不同的类,调用不同的参数。

设定方法的第一步是创建一个合适的泛型函数,如果该函数还不存在,可以使用setGeneric函数来创建这个泛型方法:

setGeneric(name, def=, group=list(), valueClass=character(),
          where=, package=, signature=, useAsDefault=,
          genericFUnction=, simpleInheritanceOnly=)

要把一个方法关联到某个类(具体而言就是指定泛型函数的signature参数),可以使用setMethod函数:

setMethod(f, signature=character(), definition,
         where = topenv(parent.frame()),
         valueClass=NULL, sealed=FALSE)

方法的管理

methods包包含了很多管理泛型方法的函数。

函数 描述
isGeneric 检查指定的泛型函数是否存在
isGroup 检查指定的分组泛型函数是否存在
removeGeneric 删除某个泛型函数关联的所有方法以及该泛型函数本身
dumpMethod 转存储某个方法到文件
findFunction 根据函数名查找函数对象,返回搜寻列表中的位置或当前顶层环境
dumpMethods 转存储一个泛型函数关联的所有方法
signature 返回在某个指定路径下定义了方法的泛型函数的名称
removeMethods 删除某个泛型函数关联的所有方法
setGeneric 根据指定的函数名创建新的泛型函数

methods包同样包含了很多管理方法的函数。

函数 描述
getMethod, selectMethod 返回某个特定泛型函数和类型标记的方法
existsMethod, hasMethod 检查某个方法(指定了泛型函数名和类型标记)是否存在
findMethod 返回包含了某个方法的包
showMethods 显示关联了某个S4泛型的所有方法

更多的帮助通过library(help="methods")命令获取。

守旧派OOP: S3

如果我们想要用R实现复杂的工程,应该使用S4的类和对象。不幸的是,我们在R中是很难避免S3对象的。比如统计包中的大部分建模工具都是用S3对象实现的。为了能够对这些软件包进行更好地理解、修改和扩展。我们必须了解S3类是如何实现的。

S3的类

S3对象只是原始的R对象加上一些额外的属性(包括一个类名)而已。它没有正式的定义,我们可以手工修改属性甚至类。

之前我们使用了时间序列作为S4的例子,其实在R中已经存在了表示它的S3类,称为ts对象。我们这里创建简单的时间序列对象,查看它的属性以及一些底层对象。

> my.ts <- ts(data=c(1,2,3,4,5), start=c(2009,2), frequency=12)
> my.ts
     Feb Mar Apr May Jun
2009   1   2   3   4   5
> attributes(my.ts)
$tsp
[1] 2009.083 2009.417   12.000

$class
[1] "ts"

> typeof(my.ts)
[1] "double"
> unclass(my.ts)
[1] 1 2 3 4 5
attr(,"tsp")
[1] 2009.083 2009.417   12.000
> attributes(my.ts)
$tsp
[1] 2009.083 2009.417   12.000

$class
[1] "ts"

可以发现ts对象只不过是一个数值向量加上classtsp这两个属性。class属性起始只是ts对象的类名。我们无法像S4对象中操作槽来提取S3对象的属性。

> my.ts@tsp
错误: 非S4类别的对象(类别为"ts")没有"tsp"这样的槽

S3方法

S3的泛型函数是通过命名约定来实现的。以下是步骤:

  1. 为泛型函数挑选一个名字,这里我们命名为gname
  2. 新建一个名为gname的函数,在gname的函数体中,调用UseMethod("gname")
  3. 为每一个想要使用gname的类创建一个名为gname.classname的函数,该函数的第一个参数必须是该对象的类名classname

一个现成的例子是plot函数:

> plot
function (x, y, ...) 
UseMethod("plot")



在调用plot的时候,plot将会调用UseMethod("plot")UseMethod会查看x对象的类,然后查找名为plot.class的函数,然后调用该函数。

比如给我们之前定义的TimeSeries类添加一个plot方法。

> plot.TimeSeries <- function(object, ...) {
+   plot(object@data, ...)
+ }

在S4的类中使用S3的类

我们不能直接指定S3的类到S4的槽。如果想要做到,我们需要基于S3的类创建一个S4的类。一个简单的方式是使用setOldClass函数:

setOldClass(Classes, prototype, where, test=FALSE, S4Class)

查找隐藏的S3方法

有时候我们会发现一些包的作者会选择隐藏单个方法,而把方法的实现封装在包中。这样可以鼓励用户去使用泛型函数。

> library(lattice)
> methods(histogram)
[1] histogram.factor*  histogram.formula* histogram.numeric*
see '?methods' for accessing help and source code

有时候我们可能需要找回这些隐藏的方法(想要查看源代码),这时候可以使用getS3method函数。例如,想要取到histgram.formula中的代码,可以使用以下命令:

> getS3method(f="histogram", class="formula")

或者使用getAnywhere函数:

> getAnywhere(histogram.formula)

学习整理自《R核心技术手册》

【r<-高级|理论】R-面向对象编程(续)_第1张图片

你可能感兴趣的:(【r<-高级|理论】R-面向对象编程(续))