前言linux
本文接上一篇文章 R語言基於S3的面向對象編程,本文繼續介紹R語言基於S4的面向對象編程。程序員
S4對象系統具備明顯的結構化特徵,更適合面向對象的程序設計。Bioconductor社區,以S4對象系統作爲基礎架構,只接受符合S4定義的R包。web
目錄編程
S4對象介紹數據結構
建立S4對象架構
訪問對象的屬性函數
S4的泛型函數工具
查看S4對象的函數this
S4對象的使用spa
S4對象系統是一種標準的R語言面向對象實現方式,S4對象有明確的類定義,參數定義,參數檢查,繼承關係,實例化等的面向對象系統的特徵。
本文的系統環境
Linux: Ubuntu Server 12.04.2 LTS 64bit
R: 3.0.1 x86_64-pc-linux-gnu
爲了方便咱們檢查對象的類型,引入pryr包做爲輔助工具。關於pryr包的介紹,請參考文章:[撬動R內核的高級工具包pryr](http://blog.fens.me/r-pryr/)
# 加載pryr包 > library(pryr)
2.1 如何建立S4對象?
因爲S4對象是標準的面向對象實現方式, 有專門的類定義函數 setClass() 和類的實例化函數new() ,咱們看一下setClass()和new()是如何動做的。
2.1.1 setClass()
查看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之後不建議使用
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"
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屬性 > slot(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] "primitive" # 檢查son@mother屬性類型 > otype(son@mother) [1] "S4" # 用isS4(),檢查S4對象的類型 > isS4(son) [1] TRUE > isS4(son@name) [1] FALSE > isS4(son@mother) [1] TRUE
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)) # 屬性age爲空 > b<-new("Person",name="b") # 屬性age的默認值是20 > b An object of class "Person" Slot "name": [1] "b" Slot "age": [1] 20
2.5 S4對象的類型檢查
> setClass("Person",slots=list(name="character",age="numeric")) # 傳入錯誤的age類型 > bad<-new("Person",name="bad",age="abc") Error in validObject(.Object) : invalid class 「Person」 object: 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.") + }) Class "Person" [in ".GlobalEnv"] Slots: Name: name age Class: character numeric # 修傳入小於0的年齡 > bad2<-new("Person",name="bad",age=-1) Error in validityMethod(object) : Age is negative.
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
在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 Error in a$name : $ operator not defined for this S4 class > a[1] Error in a[1] : object of type 'S4' is not subsettable > a[[1]] Error in a[[1]] : this S4 class is not subsettable
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對象系統,是一個結構化的,完整的面向對象實現。
當咱們使用S4對象進行面向對象封裝後,咱們還須要能查看到S4對象的定義和函數定義。
還以上節中Person和work的例子
# 檢查work的類型 > ftype(work) [1] "s4" "generic" # 直接查看work函數 > work standardGeneric for "work" defined from package ".GlobalEnv" function (object) standardGeneric("work") <environment: 0x2aa6b18> Methods may be defined for arguments: object Use showMethods("work") for currently available ones. # 查看work函數的現實定義 > showMethods(work) Function: work (package .GlobalEnv) object="Person" # 查看Person對象的work函數現實 > getMethod("work", "Person") Method Definition: function (object) cat(object@name, "is working") Signatures: object target "Person" defined "Person" > selectMethod("work", "Person") Method Definition: function (object) cat(object@name, "is working") Signatures: object target "Person" defined "Person" # 檢查Person對象有沒有work函數 > existsMethod("work", "Person") [1] TRUE > hasMethod("work", "Person") [1] TRUE
咱們接下用S4對象作一個例子,定義一組圖形函數的庫。
6.1 任務一:定義圖形庫的數據結構和計算函數
假設最Shape爲圖形的基類,包括圓形(Circle)和橢圓形(Ellipse),並計算出它們的面積(area)和周長(circum)。
定義圖形庫的數據結構
定義圓形的數據結構,並計算面積和周長
定義橢圓形的數據結構,並計算面積和周長
如圖所示結構:
定義基類Shape 和 圓形類Circle
# 定義基類Shape > setClass("Shape",slots=list(name="character")) # 定義圓形類Circle,繼承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 + }) # 分別計算c1和c2的兩個圓形的面積 [1] "circum" > 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 not 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
6.2 任務二:重構圓形和橢圓形的設計
上一步,咱們已經完成了 圓形和橢圓形 的數據結構定義,以及計算面積和周長的方法現實。不知你們有沒有發現,圓形是橢圓形的一個特例呢?
當橢圓形的長半徑和短半徑相等時,即radius的兩個值相等,造成的圖形爲圓形。利用這個特色,咱們就能夠從新設計 圓形和橢圓形 的關係。橢圓形是圓形的父類,而圓形是橢圓形的子類。
如圖所示結構:
# 基類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("Circle Circum :\n") + 2*pi*obj@radius + }) [1] "circum" # 建立實例 > e1<-new("Ellipse",name="e1",radius=c(2,5)) > c1<-new("Circle",name="c1",radius=2) # 計算橢圓形的面積和周長 > area(e1) Ellipse Area : [1] 31.41593 > circum(e1) Ellipse Circum : [1] 23.92566 # 計算圓形的面積和周長 > area(c1) Circle Area : [1] 12.56637 > circum(c1) Circle Circum : [1] 12.56637
咱們重構後的結構,是否是會更合理呢!!
6.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 > area(s1) Square Area : [1] 4 # 計算正方形的面積和周長 > circum(r1) Rectangle Circum : [1] 14 > circum(s1) Square Circum : [1] 8
這樣,咱們的圖形庫,已經支持了4種圖形了!用面向對象的結構來設計,是否是結構化思路很清晰呢!!
6.4 任務四:在基類Shape中,增長shape屬性和getShape方法
接下來,要對圖形庫的全部圖形,定義圖形類型的變量shape,而後再提供一個getShape函數能夠檢查實例中的是shape變量。
這個需求,若是沒有面向對象的結構,那麼你須要在全部圖形定義的代碼中,都增長一個參數和一個判斷,若是有100圖形,改起來仍是挺複雜的。而面向對象的程序設計,就很是容易解決這個需求。咱們只須要在基類上改動代碼就能夠實現了。
如圖所示結構:
# 從新定義基類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"
其實,這樣改動一個就能夠了,咱們只須要重實例化每一個圖形的對象就好了。
# 實例化一個Square對象,並給shape屬性賦值 > s1<-new("Square",name="s1",edges=2, shape="Square") # 調用基類的getShape()函數 > getShape(r1) Rectangle
是否是很容易的呢!在代碼只在基類裏修改了,全部的圖形就有了對應的屬性和方法。
若是咱們再多作一步,能夠修改每一個對象的定義,增長shape屬性的默認值。
setClass("Ellipse",contains="Shape",slots=list(radius="numeric"),prototype=list(radius=c(1,1),shape="Ellipse")) setClass("Circle",contains="Ellipse",slots=list(radius="numeric"),prototype=list(radius = 1,shape="Circle")) setClass("Rectangle",contains="Shape",slots=list(edges="numeric"),prototype=list(edges=c(1,1),shape="Rectangle")) setClass("Square",contains="Rectangle",slots=list(edges="numeric"),prototype=list(edges=1,shape="Square"))
再實例化對象時,屬性shape會被自動賦值
# 實例化一個Square對象 > s1<-new("Square",name="s1",edges=2) # 調用基類的getShape()函數 > getShape(r1) Rectangle
下面是完整的R語言的代碼實現:
setClass("Shape",slots=list(name="character",shape="character")) setClass("Ellipse",contains="Shape",slots=list(radius="numeric"),prototype=list(radius=c(1,1),shape="Ellipse")) setClass("Circle",contains="Ellipse",slots=list(radius="numeric"),prototype=list(radius = 1,shape="Circle")) setClass("Rectangle",contains="Shape",slots=list(edges="numeric"),prototype=list(edges=c(1,1),shape="Rectangle")) setClass("Square",contains="Rectangle",slots=list(edges="numeric"),prototype=list(edges=1,shape="Square")) setGeneric("getShape",function(obj,...) standardGeneric("getShape")) setMethod("getShape","Shape",function(obj,...){ cat(obj@shape,"\n") }) setGeneric("area",function(obj,...) standardGeneric("area")) setMethod("area","Ellipse",function(obj,...){ cat("Ellipse Area :\n") pi * prod(obj@radius) }) setMethod("area","Circle",function(obj,...){ cat("Circle Area :\n") pi*obj@radius^2 }) setMethod("area","Rectangle",function(obj,...){ cat("Rectangle Area :\n") prod(obj@edges) }) setMethod("area","Square",function(obj,...){ cat("Square Area :\n") obj@edges^2 }) setGeneric("circum",function(obj,...) standardGeneric("circum")) setMethod("circum","Ellipse",function(obj,...){ cat("Ellipse Circum :\n") 2*pi*sqrt((obj@radius[1]^2+obj@radius[2]^2)/2) }) setMethod("circum","Circle",function(obj,...){ cat("Circle Circum :\n") 2*pi*obj@radius }) setMethod("circum","Rectangle",function(obj,...){ cat("Rectangle Circum :\n") 2*sum(obj@edges) }) setMethod("circum","Square",function(obj,...){ cat("Square Circum :\n") 4*obj@edges }) e1<-new("Ellipse",name="e1",radius=c(2,5)) c1<-new("Circle",name="c1",radius=2) r1<-new("Rectangle",name="r1",edges=c(2,5)) s1<-new("Square",name="s1",edges=2) area(e1) area(c1) circum(e1) circum(c1) area(r1) area(s1) circum(r1) circum(s1)
經過這個例子,咱們全面地瞭解了R語言中面向對象的使用,和S4對象系統的面向對象程序設計!
在程序員的世界裏,世間萬物均可以抽象成對象。