导论
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