第 4 章 基于S4的面向对象编程
S4对象系统具有明显的结构化特征,更适合面向对象的程序设计。Bioconductor社区以S4对象作为基础框架,只接受S4定义的R包。
4.1 S4对象介绍
S4对象系统是一种标准的R语言面向对象实现方式,S4对象有明确的类定义,参数定义,参数检查,继承关系,实例化等的面向对象系统的特征。
4.2 创建S4对象
使用辅助的pryr包
# 加载pryr包
library(pryr)
4.2.1 如何创建S4对象?
由于S4对象是标准的面向对象实现方式,有专门的类定义函数setClass()和类的实例化函数new(),我们看一下setClass()和new()是如何工作的。
- setClass()
setClass(class,representation,prototype,contains=character(),
validity,access,where,version,sealed,package,
S3methods=FALSE,slots)
参数列表:
Class:定义类名
slots:定义属性和属性类型
prototype:定义属性的默认值
contains=character():定义父类,继承关系
validity:定义属性的类型检查
where:定义存储空间
sealed:如果设置TRUE,则同名类不能再次定义
package:定义所属的包
S3methods:R3.0.0后不建议使用
representation:R3.0.0后不建议使用
access:R3.0.0后不建议使用
version:R3.0.0后不建议使用
4.2.2 创建一个S4对象实例
# 定义一个S4对象
setClass("Person",slots=list(name="character",age="numeric"))
# 实例化一个Person对象
father <- new("Person",name="F",age=44)
# 查看father对象,有两个属性name和age
father
## An object of class "Person"
## Slot "name":
## [1] "F"
##
## Slot "age":
## [1] 44
# 查看father对象类型为Person
class(father)
## [1] "Person"
## attr(,"package")
## [1] ".GlobalEnv"
# 查看father对象为S4的对象
otype(father)
## [1] "S4"
4.2.3 创建一个有继承关系的S4对象
# 创建一个S4对象Person
setClass("Person",slots=list(name="character",age="numeric"))
# 创建Person的子类
setClass("Son",slots=list(father="Person",mother="Person"),contains = "Person")
# 实例化Person对象
father <- new("Person",name="F",age=44)
mother <- new("Person",name="M",age=39)
# 实例化一个Son对象
son <- new("Son",name="S",age=16,father=father,mother=mother)
# 查看son对象的name属性
son@name
## [1] "S"
# 查看son对象的age属性
son@age
## [1] 16
# 查看son对象的father属性
son@father
## An object of class "Person"
## Slot "name":
## [1] "F"
##
## Slot "age":
## [1] 44
# 查看son对象的mother属性
son@mother
## An object of class "Person"
## Slot "name":
## [1] "M"
##
## Slot "age":
## [1] 39
# 查看son类型
otype(son)
## [1] "S4"
# 查看son@name的属性
otype(son@name)
## [1] "base"
# 查看son@mother的属性
otype(son@mother)
## [1] "S4"
# 用isS4()检查S4对象的类型
isS4(son)
## [1] TRUE
isS4(son@name)
## [1] FALSE
isS4(son@mother)
## [1] TRUE
4.2.4 S4对象的默认值
setClass("Person",slots=list(name="character",age="numeric"))
# 属性age为空
a <- new("Person",name="a")
a
## An object of class "Person"
## Slot "name":
## [1] "a"
##
## Slot "age":
## numeric(0)
# 设置属性age的默认值为20
setClass("Person",slots=list(name="character",age="numeric"),prototype=list(age=20))
# 初始化b对象
b <- new("Person",name="b")
# 属性age的默认值是20
b
## An object of class "Person"
## Slot "name":
## [1] "b"
##
## Slot "age":
## [1] 20
4.2.5 S4对象的类型检查
setClass("Person",slots=list(name="character",age="numeric"))
#传入错误age类型
bad <- new("Person",name="bad",age="abc")
###
Error in validObject(.Object) : 类别为“Person”的对象不对: invalid object for slot "age" in class "Person": got class "character", should be or extend class "numeric"
# 设置age的非负检查
setValidity("Person",function(object){
if(object@age <= 0) stop("Age is negative.")
})
# 传入小于0的年龄
bad2 <- new("Person",name="bad",age=-1)
###
Error in validityMethod(object) : Age is negative.
4.2.6 从一个已经实例化的对象中创建新对象
S4对象,还支持从一个已经实例化的对象中创建新对象,创建时可以覆盖旧对象的值
setClass("Person",slots=list(name="character",age="numeric"))
# 创建一个对象实例n1
n1 <- new("Person",name="n1",age=19)
n1
## An object of class "Person"
## Slot "name":
## [1] "n1"
##
## Slot "age":
## [1] 19
# 从实例n1中,创建实例n2,并修改name的属性值
n2 <- initialize(n1,name="n2")
n2
## An object of class "Person"
## Slot "name":
## [1] "n2"
##
## Slot "age":
## [1] 19
4.3 访问对象的属性
在S3对象中,一般我使用$来访问一个对象的属性,但在S4对象中,我们只能用@来访问一个对象的属性。
setClass("Person",slots=list(name="character",age="numeric"))
a <- new("Person",name="a")
# 访问S4对象的属性
a@name
## [1] "a"
slot(a,"name")
## [1] "a"
# 错误的访问
#a$name
#a[1]
4.4 S4的泛型函数
S4的泛型函数实现有别于S3的实现,S4分离了方法的定义和实现,如在其他语言中我们常说的接口和实现分离。通过setGeneric()来定义接口,通过setMethod()来定义现实类。这样可以让S4对象系统,更符合面向对象的特征。
普通函数的定义和调用
work <- function(x) cat(x,"is working")
work("Conan")
## Conan is working
让我们看看如何用R分离接口和实现
# 定义Person对象
setClass("Person",slots=list(name="character",age="numeric"))
# 定义泛型函数work即接口
setGeneric("work",function(object) standardGeneric("work"))
## [1] "work"
# 定义work的实现,并指定参数类型为Person对象
setMethod("work",signature(object="Person"),function(object) cat(object@name,"is working"))
## [1] "work"
# 创建一个Person对象a
a <- new("Person",name="Conan",age=16)
# 把对象a传入work函数
work(a)
## Conan is working
通过S4对象系统,把原来的函数定义和调用2步完成的分成4步。
定义数据对象类型
定义接口函数
定义实现函数
把数据对象以参数传入到接口函数,执行实现函数
通过S4对象系统,是一个结构化的,完整的面向对象的实现。
4.5 查看S4对象的函数
当我们使用S4对象进行面向对象封装后,我们还需要能查看到S4对象的定义和函数定义,还是以上街中Person和work的例子
library(pryr)
# 检查work的类型
ftype(work)
# 直接查看work函数
work
# 查看work函数的显示定义
showMethod(work)
# 查看Person对象的work函数现实
getMethod("work","Person")
# 检查Person对象有没有work函数
existMethod("work","Person")
hasMethod("work","Person")
4.6 S4对象的使用
下面我们用S4对象那个实现一个具体的例子。
4.6.1 任务1:定义一个图形库的数据结构和计算函数
假设Shape为图形的基类,包括圆形(Circle)和椭圆形(Ellipse),并计算出他们的面积(area)和周长(circum)
定义图形库的数据结构
定义圆形的数据结构,并计算面积和周长
定义椭圆形的数据结构,并计算面积和周长
定义基类Shape和圆形类Circle
# 定义基类Shape
setClass("Shape",slots=list(name="character"))
# 定义圆形类,并继承shape,属性radius默认为1
setClass("Circle",contains = "Shape",slots=list(radius="numeric"),prototype=list(radius=1))
# 验正radius属性值要大于等于0
setValidity("Circle",function(object){
if(object@radius <= 0) stop("Radius is negative")
})
## Class "Circle" [in ".GlobalEnv"]
##
## Slots:
##
## Name: radius name
## Class: numeric character
##
## Extends: "Shape"
# 创建两个圆形实例
c1 <- new("Circle",name="c1")
c2 <- new ("Circle",name="c2",radius=5)
定义计算面积的接口和实现
setGeneric("area",function(obj,...){
standardGeneric("area")
})
## [1] "area"
# 计算面积的函数实现
setMethod("area","Circle",function(obj,...){
print("Area Circle Method")
pi*obj@radius^2
})
## [1] "area"
# 分别计算c1和c2的两个圆形的面积
area(c1)
## [1] "Area Circle Method"
## [1] 3.141593
area(c2)
## [1] "Area Circle Method"
## [1] 78.53982
定义计算周长的接口和实现
# 计算周长泛型函数接口
setGeneric("circum",function(obj,...){
standardGeneric("circum")
})
## [1] "circum"
# 计算周长的函数实现
setMethod("circum","Circle",function(obj,...){
2*pi*obj@radius
})
## [1] "circum"
# 分别计算c1和c2的周长
circum(c1)
## [1] 6.283185
circum(c2)
## [1] 31.41593
上面代码,我们实现了圆形的定义,下面我们实现椭圆形
# 定义椭圆形的类,继承Shape,radius参数默认值为c(1,1)# 分别表示椭圆形的常半径和短半径
setClass("Ellipse",contains = "Shape",slots=list(radius="numeric"),prototype = list(radius=c(1,1)))
# 验证radius参数
setValidity("Ellipse",function(object){
if(length(object@radius)!=2) stop("It's note Ellipse")
if(length(which(object@radius<=0))>0) stop("Radius is negative")
})
## Class "Ellipse" [in ".GlobalEnv"]
##
## Slots:
##
## Name: radius name
## Class: numeric character
##
## Extends: "Shape"
# 创建两个椭圆形实例e1,e2
e1 <- new("Ellipse",name="e1")
e2 <- new("Ellipse",name="e2",radius=c(5,1))
# 计算椭圆形面积的函数的实现
setMethod("area",'Ellipse',function(obj,...){
print("Area Ellipse Method")
pi*prod(obj@radius)
})
## [1] "area"
# 计算e1,e2的面积
area(e1)
## [1] "Area Ellipse Method"
## [1] 3.141593
area(e2)
## [1] "Area Ellipse Method"
## [1] 15.70796
# 计算椭圆形周长的函数实现
setMethod("circum","Ellipse",function(obj,...){
cat("Ellipse Circum:\n")
2*pi*sqrt((obj@radius[1]^2+obj@radius[2]^2)/2)
})
## [1] "circum"
# 计算e1,e2周长
circum(e1)
## Ellipse Circum:
## [1] 6.283185
circum(e2)
## Ellipse Circum:
## [1] 22.65435
4.6.2 任务2:重构圆形和椭圆形的设计
上一步,我们已经完成了圆形和椭圆形的数据结构定义,以及计算面积和周长的方法实现。不知大家有没有发现,圆形是椭圆的一个特例吗?当椭圆形的长轴和短轴相等时,形成的图形为圆形。椭圆是圆形的父类,而圆形是椭圆形的子类。
# 基类Shape
setClass("Shape",slots=list(name="character",shape="character"))
# Ellipse继承Shape
setClass("Ellipse",contains = "Shape",slots=list(radius="numeric"),prototype = list(radius=c(1,1),shape="Ellipse"))
# Circle继承Ellipse
setClass("Circle",contains = "Ellipse",slots=list(radius="numeric"),prototype=list(radius=1,shape="Circle"))
# 定义area接口
setGeneric("area",function(obj,...) standardGeneric("area"))
## [1] "area"
# 定义area的Ellipse实现
setMethod("area","Ellipse",function(obj,...){
cat("Ellipse Area: \n")
pi*prod(obj@radius)
})
## [1] "area"
# 定义area的Circle实现
setMethod("area","Circle",function(obj,...){
cat("Circle Area:\n")
pi*obj@radius^2
})
## [1] "area"
# 定义circum接口
setGeneric("circum",function(obj,...) standardGeneric("circum"))
## [1] "circum"
# 定义circum的ellipse实现
setMethod("circum","Ellipse",function(obj,...){
cat("Ellipse circum:\n")
2*pi*sqrt((obj@radius[1]^2+obj@radius[2]^2)/2)
})
## [1] "circum"
# 定义circum的circle实现
setMethod("circum","Circle",function(obj,...){
cat("Ellipse circum:\n")
2*pi*obj@radius
})
## [1] "circum"
# 创建实例
e1 <- new("Ellipse",name="e1",radius=c(2,5))
e2 <- new("Circle",name="e2",radius=2)
#计算面积和周长
area(e1)
## Ellipse Area:
## [1] 31.41593
circum(e1)
## Ellipse circum:
## [1] 23.92566
area(e2)
## Circle Area:
## [1] 12.56637
circum(e2)
## Ellipse circum:
## [1] 12.56637
这样是不是显得更合理?
4.6.3 任务3:增加矩形的图形处理
进一步扩充图形库需要加入矩形和正方形
定义矩形的数据结构,计算面积和周长
定义长方形的数据结构,计算面积和周长
矩形是正方形的父类,正方形是矩形的子类
# 定义矩形Rectangle,继承Shape
setClass("Rectangle",contains = "Shape",slots=list(edges="numeric"),prototype=list(edges=c(1,1),shape="Rectangle"))
# 定义正方形Square继承Rectangle
setClass("Square",contains = "Rectangle",slots=list(edges="numeric"),prototype = list(edges=1,shape="Square"))
# 定义area的Rectangle实现
setMethod("area","Rectangle",function(obj,...){
cat("Rectangle Area:\n")
prod(obj@edges)
})
## [1] "area"
# 定义area的Square
setMethod("area","Square",function(obj,...){
cat("Square Area: \n")
obj@edges^2
})
## [1] "area"
# 定义circum的Rectangle实现
setMethod("circum","Rectangle",function(obj,...){
cat("Rectangle Circum:\n")
2*sum(obj@edges)
})
## [1] "circum"
# 定义circum的Square实现
setMethod("circum","Square",function(obj,...){
cat("Square circum:\n")
4*obj@edges
})
## [1] "circum"
# 创建实例
r1 <- new("Rectangle",name="r1",edges=c(2,5))
s1 <- new("Square",name='s1',edges=2)
# 计算矩形的面积和周长
area(r1)
## Rectangle Area:
## [1] 10
circum(r1)
## Rectangle Circum:
## [1] 14
area(s1)
## Square Area:
## [1] 4
circum(s1)
## Square circum:
## [1] 8
这样,图形库就支持4种图形了,用面向对象的结构去设计,就会非常清晰。
4.6.4 任务4:在基类Shape中增加shape属性和getShape方法
对图形库的所有图形定义图形类型的变量shape,然后在提供一个getShape函数来检查实例中的shape变量。
# 重新定义基类Shape,增加shape属性
setClass("Shape",slots=list(name="character",shape="character"))
# 定义getShape接口
setGeneric("getShape",function(obj,...){
standardGeneric("getShape")
})
## [1] "getShape"
# 定义getShape实现
setMethod("getShape","Shape",function(obj,...){
cat(obj@shape,"\n")
})
## [1] "getShape"
其实,这样改动一下就OK了,我们只需要重新实例化每个图形的对象就好了。
# 实例化一个Square对象,并给shape属性赋值
s1 <- new("Square",name='s1',edges=2,shape='Square')
# 调用基类的getShape()函数
getShape(s1)
## Square
如果再多做一步,可以修改每个对象的定义,增加shape属性的默认值。
setClass("Ellipse",contains = 'Shape',slots=list(radius="numeric"),prototype = list(radius=c(1,1),shape="Ellipse"))
通过这节的例子,我们全面的了解了R语言的面向的使用和S4对象系统的面向对象的程序设计。
在程序猿的世界里,世间万物都可以抽象成对象!