R语言OOP(3):S4的实现方法

来源:互联网 发布:windows查看本机端口 编辑:程序博客网 时间:2024/06/05 14:45

搁置了一些时日,原本写好的提纲很多都忘了要写些什么内容了。一些标题先留空,以后有时间再补充吧。

1 闲话

《R Language Definition》中只有S3 OOP的介绍,找不到S4 OOP的相关说明,因为S4 OOP还不够完善,没有定型。但很多人已经大量使用它了,尤其是BioC们,前些年用S3写的一些包都在逐渐更新为S4系统,他们可能是尝到了某些甜头。不过苦头肯定也不少,R core们宣布自R 3.0版后弃用类定义setClass函数的representation参数,改用slots和contains参数。BioC们又有得折腾了。

话虽如此,好的系统还是很有生命力的,能吸引用户。同样是序列处理的软件包,seqinr出现的时间也不短了,没用S4,和BioC格格不入,功能虽然很齐全但响应者似乎寥寥。我看seqinr说明书的第一感觉就一个字:“乱”,心情也是一个字:“烦”。不过应该肯定的是seqinr还是非常不错的,小巧、函数间的依赖关系简单、容易移植,适合小量序列的分析。闲话少说,入题。

2 S4类

2.1 S4类定义

S4 OOP系统有比较完善的CLASS定义方法,用于类定义的函数是setClass。

## NOT RUN (非运行代码)setClass(Class, representation, prototype, contains = character(), validity,    access, where, version, sealed, package, S3methods = FALSE, slots)

2.1.1 最基本的设置:Class和slots

先看例子再解释:

Somebody <- setClass(Class = "Somebody", slots = c(name = "character", gender = "factor"))someone <- Somebody(name = "Adam", gender = factor("M"))someone
## An object of class "Somebody"## Slot "name":## [1] "Adam"## ## Slot "gender":## [1] M## Levels: M

上面第一行代码定义了一个类的最基本两个内容:类名称和类对象存储的数据。参数说明如下:

  • Class 用于表示类名称的字符串
  • slots 很多人称它为“接口”,用于存储对象的具体数据。既然是数据就得有名称和数据类型,slots的作用就是指定这两者。它的内容是有名的列表或有名向量,列表或向量的名称(如上面的name和gender)表示类数据的名称,而它们的值(字符串)表示数据的类型(如上面的character和factor)。

setClass函数结果可以不赋值,但赋值运算会在创建类的同时获得一个类构造函数。构造函数的名称一般和类名称相同,但这不是硬性规定:

diets <- setClass(Class = "Diet", slots = c(food = "character", drink = "character"))diets(food = "rice", drink = "water")
## An object of class "Diet"## Slot "food":## [1] "rice"## ## Slot "drink":## [1] "water"

如果setClass函数结果不赋值,构造函数不会产生,如果要产生此类对象就得使用系统的new函数了。

setClass(Class = "Anybody", slots = c(name = "character", gender = "factor"))

## 错,类定义并没有赋值,没有产生Anybody构造函数Anybody(name = "Adam", gender = factor("M"))
## Error: 没有"Anybody"这个函数
new(Class = "Anybody", name = "Adam", gender = factor("M"))
## An object of class "Anybody"## Slot "name":## [1] "Adam"## ## Slot "gender":## [1] M## Levels: M
## setClass赋值的作用事实上就是产生了一个调用new函数的函数:Somebody
## class generator function for class "Somebody" from package '.GlobalEnv'## function (...) ## new("Somebody", ...)

从R语言的根本上来看(不涉及任何OOP概念),slots和CLASS一样,都是数据的属性而已,只是换了个名称:

str(attributes(someone))
## List of 3##  $ name  : chr "Adam"##  $ gender: Factor w/ 1 level "M": 1##  $ class : atomic [1:1] Somebody##   ..- attr(*, "package")= chr ".GlobalEnv"

2.1.2 类继承:contains

  • contains 用于设置新类中包含(即继承)的其他已定义类(父类),其作用是把父类中的slots包含进来:

showClass("Somebody")
## Class "Somebody" [in ".GlobalEnv"]## ## Slots:##                           ## Name:       name    gender## Class: character    factor
Anybody <- setClass("Anybody", contains = "Somebody", slots = c(skin = "character"))showClass("Anybody")
## Class "Anybody" [in ".GlobalEnv"]## ## Slots:##                                     ## Name:       skin      name    gender## Class: character character    factor## ## Extends: "Somebody"

contains可以包含多个类,继承多个父类的slots:

Anybody <- setClass("Anybody", contains = c("Somebody", "Diet"), slots = c(skin = "character"))showClass("Anybody")
## Class "Anybody" [in ".GlobalEnv"]## ## Slots:##                                                         ## Name:       skin      name    gender      food     drink## Class: character character    factor character character## ## Extends: "Somebody", "Diet"

2.1.3 原型设置:prototype

  • prototype 原型,即类对象初始化(新建对象)时slots中的默认数据,相当于函数的默认参数。这是一个有名列表,不能用有名向量。

Somebody <- setClass(Class = "Somebody", slots = c(name = "character", gender = "factor"),    prototype = list(name = "Adam", gender = factor("M")))str(Somebody())
## Formal class 'Somebody' [package ".GlobalEnv"] with 2 slots##   ..@ name  : chr "Adam"##   ..@ gender: Factor w/ 1 level "M": 1
diets <- setClass(Class = "Diet", slots = c(food = "character", drink = "character"),    prototype = list(food = "rice", drink = "water"))str(diets())
## Formal class 'Diet' [package ".GlobalEnv"] with 2 slots##   ..@ food : chr "rice"##   ..@ drink: chr "water"

创建新类时如果使用contains,新类的构造函数将自动父类的原型设置

Anybody <- setClass("Anybody", contains = c("Somebody", "Diet"), slots = c(skin = "character"),    prototype = list(skin = character()))str(Anybody())
## Formal class 'Anybody' [package ".GlobalEnv"] with 5 slots##   ..@ skin  : chr(0) ##   ..@ name  : chr "Adam"##   ..@ gender: Factor w/ 1 level "M": 1##   ..@ food  : chr "rice"##   ..@ drink : chr "water"

2.1.4 类型检查:validity

定义S4新类时本身就已经具备类型检查的能力,如果给对象提供的数据类型不正确就直接出错而不是警告:

## 下面的skin不是字符型,不能通过类型检查Anybody(skin = 1)
## Error: invalid class "Anybody" object: invalid object for slot "skin" in## class "Anybody": got class "numeric", should be or extend class## "character"

然而,这种检查能力还是相当有限的,编程过程中往往需要对数据进行更严格的审查,比如对数据进行进行数量和取值的限定。这可以通过validity参数设置类型检查函数来实现。

Anybody <- setClass("Anybody", contains = c("Somebody", "Diet"), slots = c(skin = "character"),    prototype = list(skin = character()), validity = function(object) {        if (length(object@skin) != 1)            return("\"skin\" must be length of 1.")        if (!object@skin %in% c("W", "Y", "B"))            return("Invalid \"skin\" type.")        return(TRUE)    })

## 正确设置可以通过类型检查(xx <- Anybody(skin = "Y"))
## An object of class "Anybody"## Slot "skin":## [1] "Y"## ## Slot "name":## [1] "Adam"## ## Slot "gender":## [1] M## Levels: M## ## Slot "food":## [1] "rice"## ## Slot "drink":## [1] "water"
## validity设置了skin只能是‘W’,‘Y’,‘B’中的一个,设置其他值或长度不为1都出错Anybody(skin = c("Y", "W"))
## Error: invalid class "Anybody" object: "skin" must be length of 1.
Anybody(skin = "Black")
## Error: invalid class "Anybody" object: Invalid "skin" type.

2.1.5 setClass函数的其他参数

除以上参数外,类定义函数setClass还可以设置的参数有:

  • where 用于指定类定义存储的环境(命名空间),如果在软件包里面定义类,类默认存储到软件包的命名空间
  • package 此参数极少使用,作用和where类似
  • sealed 是否封装(TRUE/FALSE)。如果设为TRUE,已经用setClass定义过的类(名称)就不能用setClass再定义,防止误操作

下面的参数在R 3.0.0版以后都被弃用了,虽仍可用但应尽量避免:

  • S3methods
  • representation
  • access
  • version

2.2 通过读取数据产生S4类对象

这是考虑用户层次和使用体验的内容,对R软件开发者来说也相当简单:编写几个函数,通过读取不同类型的文件返回S4类对象。一般软件包都会替用户考虑这点很起码的要求。比如Affy包,用ReadAffy函数就获得了AffyBatch类对象,不用关心AffyBatch类是个什么东西。很多人可能根本就不想知道S3或S4是什么玩意,伤脑筋。

3 虚拟类

4 类联合

5 S4泛型函数和方法

5.1 定义的一般过程

S4的泛型函数通过调用setGeneric函数产生,该函数的用法如下:

## NOT RUNsetGeneric(name, def = , group = list(), valueClass = character(), where = ,    package = , signature = , useAsDefault = , genericFunction = , simpleInheritanceOnly = )

参数虽然很多,但常用两个:

  • name: 表示泛型函数名称的字符串。这个函数必需已经定义,它将被转成泛型函数(如果它还不是泛型函数),而且该函数将被设为默认方法
  • def: 这是可选项。如果name参数没有对应的已有函数,这一项必需提供。如果name已经有对应的函数,使用def项可以指定其他的函数作为泛型函数(偷梁换柱)。

泛型函数的方法使用setMethod函数进行定义:

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

  • f:泛型函数名称
  • signature:识别指纹,即对象的类名称

在R终端输入变量名后回车会得到变量的内容,其实质是调用show函数显示变量。下面通过设置泛型函数及其方法重定义show函数输出Anybody类对象的内容:

## 重定义前show函数输出的内容xx
## An object of class "Anybody"## Slot "skin":## [1] "Y"## ## Slot "name":## [1] "Adam"## ## Slot "gender":## [1] M## Levels: M## ## Slot "food":## [1] "rice"## ## Slot "drink":## [1] "water"
setGeneric(name = "show")
## [1] "show"
setMethod("show", signature = "Anybody", function(object) show(object@drink))
## [1] "show"
## 重定义后show函数输出的内容xx
## [1] "water"

5.2 设置获取类对象内容的辅助函数

5.3 设置S4类对象赋值(内容替换)方法

5.4 类转换

用于S4类转换的函数是as。如果定义新类时使用了contains获得了继承关系,那么就可以用as函数将一个对象强制转换为其父类对象,对象的内容是父类定应的相应部分内容:

class(xx)
## [1] "Anybody"## attr(,"package")## [1] ".GlobalEnv"
as(xx, "Diet")
## An object of class "Diet"## Slot "food":## [1] "rice"## ## Slot "drink":## [1] "water"
as(xx, "Somebody")
## An object of class "Somebody"## Slot "name":## [1] "Adam"## ## Slot "gender":## [1] M## Levels: M

Author: ZGUANG@LZU

Created: 2014-03-04 二 13:42

Emacs 24.3.1 (Org mode 8.2.1)

Validate

0 0