检查对象类型
虽然 R 中的一切都是对象,但它们有不同的类型。
试想我们正在处理一个用户自定义的对象,那么,就要根据输入对象的类型来创建相
应的函数。例如,我们需要创建一个名为 take_it 的函数,如果输入对象是一个原子向量(例
如数值向量、字符向量或者逻辑向量),函数就返回输入对象的第 1 个元素;如果输入对象
是一个包含数据和索引的列表,函数就返回一个用户自定义的元素。
举个例子:如果输入一个数值向量,例如 c(1,2,3),函数返回第 1 个元素 1;同样,
若输入一个字符向量,例如 c("a", "b", "c"),函数就会返回 a。但是,若输入一个列
表 list(data=c("a","b","c"),index=3),函数就应该返回 data 的第 3 个元素
(index=3),也就是 c。
为了创建这样的函数,我们分析一下可能的逻辑流。首先,由于函数的输出取决于输
入对象的类型,需要使用 is.* 函数来判别输入对象是否属于某个类型;其次,输入对象
的类型不同,函数的处理方式也不同,那么,就需要使用条件表达式,例如使用 if else 来
对逻辑流进行分支;最后,函数会从输入对象中提取某个元素,因此,我们需要使用元素
提取操作符(element-extraction operator)。现在,函数的实现流程就变得相当清晰了:
take_it <- function(x) {
if (is.atomic(x)) {
x[[1]]
} else if (is.list(x)) {
x$data[[x$index]]
} else {
stop("Not supported input type")
}
}
上面这个函数的行为随 x 取值类型的不同而不同。当 x 为原子向量(例如数值向量)
时,函数提取该原子向量的第 1 个元素;当 x 为一个包含数据和索引的列表时,函数根据
索引 index 的取值来抽取数据 x$data 的相应元素:
take_ _it(c(1, 2, 3))
## [1] 1
take_ _it(list(data = c("a", "b", "c"), index = 3))
## [1] "c"
若输入不支持的类型,函数不会输出其他任何值,而是终止运行并返回错误信息。例
如,take_it 不能处理函数型输入。注意到,一般情况下,就像任何其他对象一样,我们
可以将函数传递给其他函数作为参数。然而,在这种情况下,如果均值函数作为函数传递
给 take_it,它将转向 else 条件并停止运行:
take_ _it(mean)
## Error in take_it(mean): Not supported input type
如果输入一个既不包含 data 又不包含 index 的列表会怎样呢?我们做个试验看看
将会发生什么?输入一个包含 input(而不是 data),不包含 index 的列表:
take_ _it(list(input = c("a", "b", "c")))
## NULL
你可能会感到惊讶,函数居然没有报错!因为 x$data 为 NULL,而从 NULL 中提取
任何值仍为 NULL,所以函数输出了 NULL:
NULL[[1]]
## NULL
NULL[[NULL]]
## NULL
然而,如果一个列表只有 data 没有 index,函数就会报错:
take_ _it(list(data = c("a", "b", "c")))
## Error in x$data[[x$index]]: attempt to select less than one element
报错是因为 x$index 为 NULL,根据 NULL 从一个向量中提取元素就产生了错误:
c("a", "b", "c")[[NULL]]
## Error in c("a", "b", "c")[[NULL]]: attempt to select less than one
element
第 3 种情况和第 1 种情况有点类似,从 NULL 提取任何值仍为 NULL:
take_ _it(list(index = 2))
## NULL
如果你对 NULL 参与计算的这些极端情况不熟悉的话,就会觉得上述异常情况的错误
信息并没有提供有价值的信息。对于更复杂的情况,即使产生了错误信息,你也难以在短
时间内找出确切原因。一个不错的解决办法是,在运行函数的时候检查输入,并通过参数
来反映函数的假设条件。
为了避免上述误用情况,下面这个函数对每个参数的类型都进行了检查:
take_it2 <- function(x) {
if (is.atomic(x)) {
x[[1]]
} else if (is.list(x)) {
if (!is.null(x$data) && is.atomic(x$data)) {
if (is.numeric(x$index) && length(x) ==1) {
x$data[[x$index]]
} else {
stop("Invalid index")
}
} else {
stop("Invalid data")
}
} else {
stop("Not supported input type")
}
}
当 x 是一个列表时,我们检查 x$data 是否不为空,同时是一个原子向量。如果确
实如此,再检查 x$index 是否被正确设定为一个单元素的数值向量(或标量)。其中任
何一个条件被违背,函数就会停止运行,并输出一条错误信息告知用户有关输入的错误
原因。
但是,内置的检查函数也会出现古怪行为。例如,is.atomic(NULL) 返回 TRUE。
因此,即使列表 x 不包含名为 data 的成分,分支 if(is.atomic(x$data)) 也会被触
发(返回 TRUE),但该行判断语句仍会返回 NULL。包含了这些参数检验条件之后,函数
会变得更加稳健,而且,当假设条件被违背时,也会输出有参考价值的错误信息:
take_ _it2(list(data = c("a", "b", "c")))
## Error in take_it2(list(data = c("a", "b", "c"))): Invalid index
take_ _it2(list(index = 2))
## Error in take_it2(list(index = 2)): Invalid data
这个函数的另一种实现方法是利用 S3 方法分派,我们将在第 10 章中进行介绍。
识别对象的类和类型
除了使用 is.* 函数,我们也可以用 class( ) 或者 typeof( ) 实现这个功能。在
直接判断对象的类型前,有必要了解一下这两个函数的区别。
接下来的例子展示了 class( )和 typeof( )作用在不同类型的对象上时,它们输
出信息的不同之处。
以下例子中,对于每种对象 x,class( )和 typeof( )都被调用,并使用 str( )来
展示对象的结构。
对于数值向量:
x <- c(1, 2, 3)
class(x)
## [1] "numeric"
typeof(x)
## [1] "double"
str(x)
## num [1:3] 1 2 3
对于整数向量:
x <- 1:3
class(x)
## [1] "integer"
typeof(x)
## [1] "integer"
str(x)
## int [1:3] 1 2 3
对于字符向量:
x <- c("a", "b", "c")
class(x)
## [1] "character"
typeof(x)
## [1] "character"
str(x)
## chr [1:3] "a" "b" "c"
对于列表:
x <- list(a = c(1, 2), b = c(TRUE, FALSE))
class(x)
## [1] "list"
typeof(x)
## [1] "list"
str(x)
## List of 2
## $ a: num [1:2] 1 2
## $ b: logi [1:2] TRUE FALSE
对于数据框:
x <- data.frame(a = c(1, 2), b = c(TRUE, FALSE))
class(x)
## [1] "data.frame"
typeof(x)
## [1] "list"
str(x)
## 'data.frame': 2 obs. of 2 variables:
## $ a: num 1 2
## $ b: logi TRUE FALSE
可以看到,typeof( )返回对象的低级内部类型,class( )返回对象的高级类。我
们之前提到过,data.frame 本质上就是具有等长成分的 list。因此,一个数据框的类
(class)就是 data.frame,但 typeof( )返回它的内部类型就是 list。
这个主题与 S3 面向对象编程机制有关,具体细节将在后续章节中介绍。但这里有必要
提一下 class( )和 typeof( )的区别。
从上面的输出结果中,你也可以清晰地认识 str( )的功能,它展示了一个对象的结
构。这一点我们在前面的章节也介绍过。对于对象中的向量,str( )会展示它们的内部类
型(typeof( ))。

浙公网安备 33010602011771号