R语言高级编程系列之面向对象的类型系统--S3对象

导论

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

原文地址:https://www.cnblogs.com/HeYanjie/p/6292536.html