導論
R語言的類型系統相對於一般語言而言要復雜很多,一般來說,官方制定的類型系統有四種:基礎類型、S3類型、S4類型和RC類型。在本文中主要給大家介紹一下R3類型。
為什么需要S3類型
在正式介紹S3類型之前,有個問題本人認為最需要想清楚,那就是為什么需要有S3類型。我相信對於許多有面向對象編程經驗而言,應該多多少少能感到R3對象的設計有些“反直覺”,很難理解。原因在於,S3類型於大多數面向對象的語言(C#、JAVA、C++等)都不一樣。
存在即合理
R語言當中有不少和其他常用語言不一致的設定,最常被人吐槽的就是賦值符號不是等號而是<-。但是我覺得比起吐槽更重要的是,要去理解為什么設計這個語言的人要這樣設計。對於S3系統的設計而言,一個很重要的目的就是,設計者希望打造一種“可拓展的函數”。
對初學者友好的編程方式
這里指的“初學者”並不代表其水平低,相反,其中不乏許多統計、金融等領域的大牛,只是術業有專攻,他們在計算機領域可以算是初學者。而R語言的很大的一批用戶是這一群人。那么什么樣的編程方式更容易讓初學者理解呢?自然是面向過程的編程方式。
對於初學者而言:
result <- mean(v1)
要比
result = v1.mean()
更加容易理解,雖然對於面向對象的開發者而言后者可讀性更高,原因在於,在面向過程的編程語言(如C語言)里,很多時候寫起來其實是這樣的……
result = vector_mean(v1)
甚至是這樣的……
result = vector_double_mean(v1)
但是不管怎么樣吧,面向過程依舊是對初學者而言比較容易理解的方式。因此,R語言的常用功能往往是以一個個函數的形式提供給用戶,如plot,summary,mean等等。
“可拓展”的函數
一般的實現思路
試想一下這種情況,如果說要編寫一個concat的函數,對有序容器進行連接。我們希望對於這個的函數而言,不僅可以連接數組,還可以連接列表,那我們可以寫出這樣的代碼:
array_concat <- function(x,y) {
array(c(x, y))
}
list_concat <- function(x,y) {
list(c(unlist(x), unlist(y)))
}
concat <- function(x, y) {
if (is.array(x) && is.array(y)){
return(array_concat(x, y))
} else if (is.list(x) && is.list(y)) {
return(list_concat(x, y))
} else {
stop("not supported type")
}
}
在concat函數中,利用條件語句對輸入類型進行判斷,然后調用相應的函數。這個方法在這里的可行的,問題在於,假如用戶自己添加了一種新類型呢?那就玩不轉了,只能對concat函數進行修改。設想一下,全世界那么對R語言開發者,假如每個人在添加新的類型時,對於其所需要的內置函數(如summary,mean等)都需要進行修改,那樣容易造成混亂,可行度非常低。假如不能進行修改的話,那就會出現一堆諸如array_concat, list_concat之類的函數,對於初學者而言顯然不夠友好。
泛型函數(Generic Function)
現在就輪到泛型函數登場了。首先要指出的是,這里的泛型跟C#里的泛型完全不是一個概念,請不要混淆。
創建的方法其實非常簡單,需要用到UseMethod函數。
concat.array <- function(x, y) {
if (is.array(y)) {
return(array(c(x, y)))
} else {
stop("not supported type")
}
}
concat.list <- function(x, y) {
if (is.list(y)) {
return(list(c(unlist(x), unlist(y))))
} else {
stop("not supported type")
}
}
concat.default <- function(x, y) {
stop("not supported type")
}
concat <- function(x, y) {
UseMethod("concat")
}
此時concat函數的功能與前文中concat的功能是一樣的。
從代碼中可見,函數的可拓展性大大提升了,用戶可以在不修改內置函數的情況下使得內置函數支持新類型。
創建S3對象
S3對象神奇的地方在於,它的類是沒有顯示聲明的,也就是說沒有“類作用域”這么一個玩意。更坑的是,並沒有一個簡單通用的辦法檢查一個對象是不是S3對象(我當時在書上看到這里的時候簡直想摔書)。但是不管怎么樣,S3對象依舊是R語言里面最常見的對象,所以還是有它的價值的。
創建S3對象的語法
有兩種創建方式,見代碼:
myClass <- structure(list(), class = "myClass")
myClass <- list()
class(myClass) <- "myClass"
兩種創建方式並沒有什么不同。
編寫構造函數
如果有個構造函數的話,代碼看起來會清晰很多,使用起來也更加方便。
下面的代碼演示了如何利用構造函數來創建復數類。
Complex <- function(real, imaginary) {
structure(list(real,imaginary), class = "Complex")
}
print.Complex <- function(x) {
print(paste(x[1],"+",x[2],"i",sep = ""))
}
c1 = Complex(10,20)
方法分派
如果前文的內容讀者能夠完全理解的話,這快內容的理解其實是順理成章的。S3對象可以多重繼承,即一個對象可以繼承多個類。在使用UseMethod調用時,會依次從對象的各個類中尋找相應的函數,如果都沒有找到,則會調用default方法。
f <- function(x){
UseMethod("f")
}
f.a <- function(x) {
return("Class a")
}
f.default <- function(x) {
return("Unknown class")
}
x <- structure(list(), class = "a")
f(x)
y <- structure(list(), class = c("b", "a"))
f(y)
z <- structure(list(), class = "c")
f(z)
進一步的探討
關於S3對象的本質,我個人認為其實是每個對象都遵循着一個接口約束,然后由一個方法來調用這些遵守接口約束的對象的方法。其實這種編程方式在強類型的編程語言里也能很好地實現,以下面的代碼為例,由於參數遵守\(ICollection<T>\)接口,因此對於任何符合該接口的對象都有CopyTo方法和Count字段,都可以作為該CopyToArray方法的參數。
public static T[] CopyToArray<T>(ICollection<T> collection)
{
var arr = new T[collection.Count];
collection.CopyTo(arr, 0);
return arr;
}
轉載聲明
文章為本人原創,轉載請注明作者名稱,謝謝!
參考文獻
1.Hadley, Wickham. 高級R語言編程指南(Advanced R)[M]. 北京:機械工業出版社, 2016. 66-70