Menu

R for 循环:语法、seq_along、break、next 及何时不用循环

R 中 for 循环的工作方式:遍历向量和索引,收集结果时避开向量增长的陷阱,next 和 break,以及什么时候向量化调用比循环更好。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

基本的 for 循环

R 中的 for 循环对向量或列表的每个元素各运行一次代码块。循环变量依次取每个值:

三个元素,运行三次。不需要设置计数器,也不需要维护条件:for (fruit in fruits) 读作“对 fruits 中的每个 fruit”,它做的正是这件事。同样的写法可以遍历列表的元素、一个数字序列(for (i in 1:10)),或者通过 names() 遍历任何东西的名称。

在循环中打印需要 print() 或 cat()

在控制台输入 x 会打印它的值。但在循环体内不会:自动打印只在顶层发生,循环里单独的表达式会被求值然后丢掉:

所以当一个循环“什么都没做”时,首先要检查的就是这一点。想快速查看任意值用 print(),想自己组织一行输出用 cat()(记得加上 "\n",cat() 不会自动换行)。

用 seq_along() 遍历索引

有时你除了值还需要位置,比如要在另一个向量的同一索引处写入,或者打印一个带编号的列表。这时用 seq_along() 遍历索引:

在较早的代码里你会看到 for (i in 1:length(prices)),它藏着一个真正的 bug。当向量为空时,length() 是 0,而 1:0 并不表示“不循环”,冒号会倒着数:

遍历 1:length(empty) 的循环会运行两次,去索引并不存在的元素(empty[1] 是 NA,赋值还会凭空创建条目)。seq_along() 对空向量返回空序列,所以循环体根本不会运行。养成用 seq_along() 的习惯;当你手上是个数而不是向量时,它的兄弟 seq_len(n) 能做同样的事。

收集结果:预分配,不要让向量增长

收集结果时,人们的第一反应通常是这样写:

squares <- c()
for (i in 1:10000) {
  squares <- c(squares, i^2)   # copies the ENTIRE vector every pass
}

它能运行,但每次 c(squares, ...) 都会分配一个全新的向量,把所有旧元素复制进去。到最后,为了得到 10,000 个数,你大约复制了 5000 万个数。这种让向量不断增长的反模式,是人们说 R 循环慢的最大原因。

解决方法:先按完整大小创建结果,再按索引赋值:

如果结果不是数字,同样的思路也适用:用 character(n)、logical(n),或者用 vector("list", n) 创建一个可以装任何东西的列表。预分配的循环速度完全没问题。

next 和 break

有两个关键字可以在循环体内控制循环:next 放弃当前这一轮,跳到下一个元素;break 直接退出整个循环。

这段代码打印 2 4 6 8:奇数被 next 跳过,当 10 到来时,break 在打印之前就结束了循环。用 next 提前过滤掉某些情况(这样主体代码不用多缩进一层),当循环已经找到要找的东西、没必要继续时,用 break。

嵌套循环

循环体内可以再包含一个循环。外层循环每运行一轮,内层循环都会完整地跑一遍,经典的例子是乘法表:

对每一行 i,内层循环遍历所有列 j,内层循环结束后再打印这一行的换行符。两层嵌套没问题;到了三层或更多,循环体通常就该改写成一个函数,或者整个计算该改用 outer() 或矩阵运算。

嵌套也可以用来遍历一个由向量组成的列表:外层循环遍历列表,内层循环(或者更好的做法,一个向量化调用)处理每个元素:

注意这里其实根本没有内层循环:mean() 一次调用就处理了整个内层向量。这个发现可以推广开来,这就引出了接下来要坦白讲的部分。

什么时候不该用循环

R 是一门向量化的语言:它的基本运算本来就能一次作用于整个向量。一个逐个变换元素的循环,往往只是把一行代码写长了:

只要有向量化的写法就优先用它:更短、更不容易出错,也更快。要对列表的每个元素应用一个函数,apply 函数族(sapply、lapply、vapply)能起到同样的作用。

但不要盲目地把这变成“循环不好”。当每次迭代依赖于之前的结果(累积状态、模拟),当你要做写文件这类有副作用的事情,或者只是当循环版本是你和读者一眼就能看懂的写法时,循环就是正确的工具。清晰的循环胜过没人看得懂的巧妙一行代码。

要点总结

  • for (x in v) { ... } 对每个元素运行一次循环体,不用管理计数器。
  • 在循环内要用 print() 或 cat() 显式打印;单独的表达式会被丢弃。
  • 用 seq_along(v) 遍历位置,永远不要用 1:length(v),后者遇到空向量会运行两次。
  • 用 numeric(n) / vector("list", n) 预分配结果,再按索引赋值;用 c() 让向量增长,每一轮都会复制全部内容。
  • next 跳过一轮,break 退出循环;有向量化写法时优先使用,但也不必害怕可读的循环。

下一篇:不是对每个元素各运行一次、而是一直运行到某件事发生为止的循环:while 和 repeat。

常见问题

在 R 中怎么写 for 循环?

for (x in v) { ... }:循环变量 x 依次取向量(或列表)v 中的每个元素,循环体对每个元素运行一次。如果要遍历位置而不是值,就用 for (i in seq_along(v)),再用 v[i] 取值。

R 中为什么要用 seq_along 而不是 1:length(v)?

当 v 为空时,length(v) 是 0,所以 1:length(v) 变成 1:0,也就是有两个元素的向量 c(1, 0),循环体会针对不存在的元素运行两次。seq_along(v) 对空向量返回空序列,所以循环会正确地运行零次。

为什么我的 R for 循环什么都不打印?

R 的自动打印只对在控制台顶层输入的表达式生效。在循环体内,像 x 这样单独的表达式会被求值然后丢弃。用 print(x) 包起来,或者在想自己控制输出格式时用 cat(...)。

R 中的 for 循环慢吗?

循环本身没问题,慢的是在循环里用 result <- c(result, ...) 让向量不断增长,这样每次都会复制整个向量。用 numeric(n) 或 vector("list", n) 预分配,再按索引赋值,循环的性能就完全没问题。话虽如此,如果向量化运算或 apply 函数族能表达同样的意思,它们通常既更快又更清晰。

Coddy 编程语言插图

和 Coddy 一起学编程

开始