# 基础语法 {#base}
“程序 = 算法 + 数据结构”,**数据结构**是信息的载体,而**算法**是完成任务所需要的步骤。两者的构造和使用方法形成了编程语言独特的语法。本章先介绍 R 的基本数据结构,然后介绍条件和循环控制,接着介绍函数的创建与拓展包的使用,最后通过编程实战来实践和掌握本章涉及的知识点。
## 基本数据结构
为了表示现实世界的信息,各类编程语言常包含 3 种基本的数据类型:**数值型**,包括整数和浮点数;**字符型**,表示文本信息;**逻辑型**,也常称为布尔值,表示是非判断,如对与错,是与否。在 R 中,除了这些基本数据类型的实现,为了方便计算工作,R 本身还包含了矩阵、数据框和列表等复杂的数据类型,以支持表示各类常用的数据。
### 向量
在 R 中,数据运算常通过向量的形式进行。**向量**是一组同质的信息,如 20 个数字、30 个字符串(与数学术语中的向量类似,但不等同)。单一的信息在此被称为**元素**。**标量**可以看作元素数量为 1 的向量。
接下来我们通过向量元素的数据类型来实际地了解和操作它。
#### 数值
数值应该可以说是最常用的信息表现形式,如人的身高、年龄。在 R 中使用小学学到的阿拉伯表示法即可创建数值,如圆周率 $\pi$:
```{r}
3.14
```
> 此处 `#>` 后显示 R 运行代码后的返回结果,`[1]` 是结果的索引,以辅助用户观测,这里表示结果的第 1 个值是 3.14。
`typeof()` 与 `class()` 是两个对于初学者非常有用的函数,它们可以返回数据的类型信息。
```{r}
typeof(3.14)
class(3.14)
```
在 R 中不需要像其他语言一样区分数值的精度信息,`typeof()` 返回结果为 `double` 提示该值是一个浮点数。
在 R 中,任何所见的事物皆为**对象**,`class()` 返回对象的类信息,此处是 `numeric`(数值)。
我们再来看看如何在 R 中表示整数。借助上述两个工具函数,我们不难发现下面的代码与想象不同。
```{r}
3
typeof(3)
class(3)
```
`typeof()` 与 `class()` 对于 3 的返回结果与 3.14 完全相同!这是因为即便只输入 3,R 也将其作为浮点数对待。
我们可以利用 `identical()` 函数或 `is.integer()` 函数进行检查:
```{r}
identical(3, 3.0)
is.integer(3)
```
返回的结果是后面将介绍的逻辑值,`TRUE` 表示对、`FALSE` 表示错。因此可以判断 `3` 并不是整数。
正确的整数表示方法需要在数字后加 `L` 后缀,如 `3L`。
```{r}
is.integer(3L)
identical(3L, 3)
```
`is.integer()` 函数隶属于 `is.xxx()` 家族,该函数家族用于辅助判断对象是否属于某一类型。读者在 RStudio 中输入 `is.` 后 RStudio 将智能提示有哪些函数的名字以 `is.` 开头。
浮点数和整数都是数值,所以下面的代码都会返回 `TRUE`:
```{r}
is.numeric(3.14)
is.numeric(3L)
```
现实中的数据常成组出现,例如,一组学生的身高。R 使用 `c()` 函数(`c` 为 `combine` 的缩写)对数据进行组合:
```{r}
c(1.70, 1.72, 1.80, 1.66, 1.65, 1.88)
```
这样我们就有了一组身高数据。
利用 R 自带的 `mean()` 和 `sd()` 还是我们可以轻易求取这组数据的均值和标准差:
```{r}
# 均值
mean(c(1.70, 1.72, 1.80, 1.66, 1.65, 1.88))
# 标准差
sd(c(1.70, 1.72, 1.80, 1.66, 1.65, 1.88))
```
上面我们计算时我们重复输入了身高数据,如果在输入时发生了小小的意外,如计算标准差时将 `1.65` 写成了 `1.66`,那么我们分析得就不是同一组数据了!虽然说在上述的简单计算不太可能发生这种情况,但如果存在 100 甚至 1000 个数据的重复输入,依靠人眼判断几乎是必然出错的。
一个解决办法是依赖系统自带的复制粘贴机制,但如果一组数据被上百次重复使用,这种办法也不实际。
正确的解决办法是引入一个符号(Symbol),用该符号**指代**一组数据,然后每次需要使用该数据时,使用符号代替即可。符号在编程语言中也常被称为**变量**,后面我们统一使用该术语。
上述代码块改写为:
```{r}
heights ` 的写法也是有效的:
```{r}
c(1.70, 1.72, 1.80, 1.66, 1.65, 1.88) -> heights2
heights2
```
但通常以 ` `sd` 的计算中使用的是 `n-1` 而不是 `n` 的原因是我们计算的是**样本**标准差。
实际操作如下:
```{r}
# 先计算均值
heightsMean 1.7)
# 然后组合取子集操作提取子集数据
heights[which(heights > 1.7)]
```
实际上,我们完全没有必要引入 `which()` 函数用来返回数据的整数索引,`heights > 1.7` 比较的结果是一个逻辑值向量,它本身就可以作为索引用于提取子集。
```{r}
heights > 1.7
heights[heights > 1.7]
```
`TRUE` 对应的元素被保留,而 `FALSE` 对应的元素被去除。请读者记住,逻辑索引是首选的取子集方式,它更加高效。
#### 深入向量
向量除了保存数据,还可以保存与之相关的属性。例如,为了更好展示 `heights` 信息,我们可以增加名字属性。
```{r}
names(heights) 1.8] 5, ]
```
### 数据框
数据框(`data.frame`)是 R 中非常独特的一种数据结构,它可以非常好存储和展示常见的表格数据。从外形上看,它与矩阵非常相似,但与矩阵不同的是,数据框的列可以是不同的数据类型。
例如,创建一个数据框存储性别,年龄和身高数据。
```{r}
df 18) {
# 如果条件判断结果为 TRUE
# 该代码块中的语句会执行
message("你是个成年人啦!")
}
```
条件判断语句结果必须返回一个逻辑值,即 `TRUE` 或 `FALSE`。如果返回为 `TRUE`,随后以 `{}` 包裹的代码块会被执行。如果我们要处理为 `FALSE` 的情况,增加一个可选的 else 语句块。
```{r, message=T}
age 18) {
# 为 TRUE 时执行
message("你是个成年人啦!")
} else {
# 为 FALSE 时执行
message("你还是个小孩子哟!")
}
```
代码块中可以包含任意代码,所以 if-else 语句是支持内部嵌套的,结构如下:
```{r, eval=FALSE}
if () {
if () {
} else {
}
} else {
}
```
如果需要处理的情况是多种,if-else 语句可以连用。例如:
```{r, message=TRUE}
age 18) {
message("你是个成年人啦!")
} else if (age < 17) {
message("你还是个小孩子哟!")
} else {
message("恭喜你,快要成年了!")
}
```
#### switch 语句
swtich 语句在 R 中存在,但读者会极少见到和使用它。结构如下:
```{r, eval=FALSE}
switch(EXPR, ...)
```
这里 `EXPR` 指代表达式,而 `...` 说明可以输入任意参数。
这里只举一个简单的例子:
```{r, message=TRUE}
ch 在此强调一下,无论是程序的编写还是科研分析工作,**完成**永远比**高效**重要。
#### for 语句
for 语句需要配合迭代变量、in 关键字一起使用,结构如下:
```{r, eval=FALSE}
for (i in obj) {
# 这里输入任意条语句
}
```
这里 `i` 指代迭代变量,它可以是索引,也可以是子数据集。`obj` 指代一个可迭代对象。
针对循环打印变量 `heights` 的信息,可以有以下 2 种方式:
```{r}
# 第一种方式
# 直接循环迭代对象本身
for (i in heights) {
print(i)
}
# 第二种方式
# 通过索引进行迭代
for (i in 1:length(heights)) {
print(heights[i])
}
```
第二种方式写法看起来更为复杂,但如果针对一些复杂的程序,它则显得更加逻辑分明。
初学者容易犯的一个错误是将 in 后面的可迭代对象写成一个标量,如下:
```{r, error=TRUE}
for (i in length(heights)) {
print(heights[i])
}
```
需要注意下面两者的区别:
```{r}
length(heights)
1:length(heights)
```
一种更好的写法是使用 `seq_along(heights)` 替代 `1:length(heights)`:
```{r}
for (i in seq_along(heights)) {
print(heights[i])
}
```
`seq_along()` 会自动返回可迭代对象的索引序列:
```{r}
seq_along(heights)
```
#### while 语句
for 语句已经能满足一般场景的使用,while 语句则特别适合于算法的设计中:
- 不知道要运行多少次循环。
- 知道要退出循环的条件。
下面举一个简单的例子:
```{r}
v 2) {
print(v)
v 100) break
}
```
break 语句执行后将跳出当前的循环,另有 next 语句,它可以跳过后续代码的运行进入下一次循环。
基于上面的例子我们再构造一个示例:
```{r}
i 200) break()
if (i > 100) next()
print("Can you see me?")
}
```
当 `i > 100` 后,最后一条输出语句就不再运行。
## 函数与函数式编程
**函数是代码模板**。
前面我们使用符号(Symbol)来对数据抽象形成我们所谓的变量,变量名解释了所指向数据的内含但遮掩了底层的结构。类似地,我们也利用符号来对代码块所运行的操作集合进行抽象,并将其称为**函数**。
- 变量 10) {
message(
"Mean of sequence ",
paste(c(x[1:5], "...", x[(l-4):l]), collapse = ","),
" is ",
mu
)
} else {
message(
"Mean of sequence ",
paste(x, collapse = ","),
" is ",
mu
)
}
}
return(mu)
}
```
我们用这个函数试一下输入少或多的情况。
```{r, message=TRUE}
customMean_v3(x = 1:10)
customMean_v3(x = 1:100)
```
除此之外,我们在新的版本中引入了一个默认参数 `verbose`,我们可以选择不打印信息:
```{r, message=TRUE}
customMean_v3(x = 1:100, verbose = FALSE)
```
当按顺序输入函数参数时,参数的名称是可以不输入的,下面的结果一致:
```{r, message=TRUE}
customMean_v3(1:100, FALSE)
```
以上的输入都是基于函数使用者很清楚的知道输入是一个数值型向量,有时候这一点很难做到。例如,你将代码发送给一位不懂编程的人员使用。此时,添加参数检查和注释是有必要的,我们由此创建一个新的函数版本:
```{r}
# @title 计算均值
# @param x 输入数据,一个数值向量
# @param verbose 逻辑值,控制是否打印
customMean_v4 10) {
message(
"Mean of sequence ",
paste(c(x[1:5], "...", x[(l-4):l]), collapse = ","),
" is ",
mu
)
} else {
message(
"Mean of sequence ",
paste(x, collapse = ","),
" is ",
mu
)
}
}
return(mu)
}
```
以`#` 开始的文本被 R 认为是一个代码注释,后续 `@title` 和 `@param` 是注释标签,这些是**非必需**的,它只是用来更好地描述注释的内容。
> 代码标签符合 **roxygen2** 包的定义,有兴趣的读者可以看一看这个包文档。
```{r, message=TRUE, error=TRUE}
customMean_v4(c("1", "2", "3"))
```
最后,我们来了解一下函数的计算效率。这里我们将创建的 `customMean()` 函数与 R 内置的 `mean()` 函数进行对比。`system.time()` 函数用来判断函数执行消耗的时间。
```{r}
system.time(customMean(1:1e7))
system.time(mean(1:1e7))
```
`elapsed` 项给出了计算机执行函数消耗的总时间(以秒为单位),可以看出,内置的函数还是要快很多的。当然,这并不是一个严格的性能测评,但它已经能清楚地表明两者的差距。
### 作用域
每个函数都有它的领地,更专业地说,当一个函数被创建后,R 中存在一个让这个函数发挥作用的环境。举一个比较形象的例子,冬天我们在购物商场外常常感到寒冷,而进去之后会感到暖和,这是因为商场空调的作用范围只是整个商场。
R 中所有的对象都处于各自的环境之中,我们可以把环境想象成城市里各种不同房子,而对象是处于其中的物品。当然这只是一些形象的比喻,实际上 R 的工作原理可能远不仅如此,但它已经能够很好地帮助理解我们这个概念了。
在启动 R 之后,我们就进去了一个全局环境之中(Global Environment),我们创建的各自变量、函数都会处于其中。这一点我们可以轻易地从 RStudio 右上角的环境窗口中观察到,如图 \@ref(fig:rstudio-env-panel) 所示:
```{r rstudio-env-panel, echo=FALSE, fig.cap="RStudio 的环境窗口"}
knitr::include_graphics("fig/ch02-rstudio-env-panel.PNG")
```
一个函数(如 `customMean()`)与全局环境的关系可以简单用下面两个嵌套的矩形表示:
```{r fun-env, echo=FALSE, fig.cap="全局环境与在其中创建的一个函数"}
knitr::include_graphics("fig/ch02-fun-env.png")
```
我使用了蓝色箭头来表示两者的从属关系,先有全局环境,再有函数环境。我使用绿色箭头表示函数查询变量的方向,先从自己内部查找,如果找不到,再从外部查找。
前面我们创建的函数内部都是自给自足的,下面我们创建一个不一样的。
```{r}
a