在R语言的循环家族里,for和while的出镜率一直很高,repeat却常年坐冷板凳。不少写R的人直到遇到迭代次数事先定不下来的任务,才回头翻文档找它。repeat的结构其实是三个循环里最简单的一个:不检查进入条件,不检查退出条件,循环体执行完就回到开头继续跑,除非碰到break语句才停下来。这种极简设计让它在数值收敛、请求重试、抽样到满足条件为止这类场景里特别顺手。这篇文章从语法、循环对比、实战代码和避坑四个角度,把repeat循环一次讲透。

Repeat循环是什么:语法结构与执行流程
repeat的语法形式朴素到几乎没有存在感:关键字repeat后面跟一对花括号,花括号里就是循环体,没有条件表达式,没有迭代变量,连圆括号都不需要。它的执行逻辑只有一条,把循环体从头到尾执行一遍,执行完立刻回到循环体开头再来一遍,如此往复,直到某次执行中碰到了break才彻底跳出。也就是说,repeat把退出时机的决定权完全交给了循环体内部,语法本身不做任何干预。
正因为repeat自己不带条件,一个能正常结束的repeat循环体几乎必然包含两块内容:一块是让循环状态发生变化的语句,比如累加计数器、更新变量值;另一块是能触发break的if判断。少了前者,退出条件永远达不到;少了后者,循环就成了死循环。下面是repeat的最小可用结构:
i <- 1
repeat {
cat("当前计数:", i, "\n")
i <- i + 1
if (i > 5) {
break
}
}
这段代码从1开始输出计数,每轮让i自增,当i超过5时跳出。细看会发现,break的判断放在了循环体末尾,这意味着无论初始状态如何,循环体至少会完整执行一次。这种先执行后判断的语义,和C语言、Java里的do-while如出一辙,而R语言恰恰没有提供do-while这个语法,repeat实际上就是它在R里的官方替身。
Repeat、while、for三种循环怎么选
三种循环各有各的地盘。for适合遍历一个已经存在的序列,元素个数在循环开始前就确定了;while适合在循环开始前就能明确写出继续条件的场景;repeat则适合退出条件要靠循环过程中的计算结果动态判断的情况,比如迭代到相邻两次结果足够接近才停、请求成功才停。用一句话概括:知道循环多少次用for,知道什么时候继续用while,只知道什么时候该停就用repeat。
while和repeat最核心的差别在判断时机上。while是先判断后执行,初始条件不满足时循环体一次都不会跑;repeat是先执行后判断,循环体至少跑一次。看下面两段输出完全相同的代码,体会一下判断位置的区别:
# while 写法:先判断,条件不满足时一次都不执行
i <- 1
while (i <= 5) {
cat("当前计数:", i, "\n")
i <- i + 1
}
# repeat 写法:先执行,至少跑一次,再决定是否退出
i <- 1
repeat {
cat("当前计数:", i, "\n")
if (i >= 5) {
break
}
i <- i + 1
}
两段代码输出一模一样,但内部节奏不同。当业务逻辑天然要求至少执行一次时,比如先做一次计算再根据结果决定要不要继续,用repeat写出来的代码更直接,不需要在循环外面预先写一遍判断或者人为构造初始条件。反过来,如果循环体在条件不满足时完全不执行才符合预期,while更合适,硬用repeat反而要在循环体开头加判断,代码绕了一圈。
顺带说一下性能。网上流传repeat比while更底层所以更快的说法,实际测试下来差距完全可以忽略,真正决定速度的是循环体里干了什么。如果在循环里频繁做向量拼接、反复调用低效函数,用什么循环都救不回来。数据量大的场合,优先考虑向量化操作或者apply家族,循环只留给逻辑复杂、无法向量化的部分。
Repeat循环的典型应用场景与代码实例
第一个场景是数值迭代直到收敛。这类任务的特点是事先不知道要迭代多少轮,只能算一步看一步,repeat的先执行后判断正好匹配这种节奏。以牛顿迭代法求平方根为例:
# 用牛顿迭代法求 value 的平方根
value <- 10
x <- value / 2 # 任取一个正数作为初始值
repeat {
x_new <- (x + value / x) / 2
if (abs(x_new - x) < 1e-8) {
break # 相邻两次结果足够接近,认为已收敛
}
x <- x_new
}
cat("10 的平方根约为:", x_new, "\n")
这段代码的退出条件是相邻两次迭代结果的差小于1e-8,这个条件只有在算完一轮之后才有资格判断,硬用while写的话反而要先把判断逻辑在循环外预演一遍。注意收敛判断要在变量更新之前完成,这样break发生时x_new就是满足精度要求的结果,直接拿来用即可。
第二个场景是请求重试。调用外部接口或读取远程数据时,偶尔失败是常态,常见的处理策略是失败后等一会儿再试,试到成功或者达到次数上限为止。退出条件涉及两个动态因素,本次是否成功、已试了几次,用repeat表达最贴切:
fetch_data <- function() {
# 模拟一个可能失败的数据请求
if (runif(1) > 0.7) {
return(rnorm(5))
}
return(NULL)
}
attempts <- 0
result <- NULL
repeat {
attempts <- attempts + 1
result <- fetch_data()
if (!is.null(result) || attempts >= 5) {
break
}
Sys.sleep(1) # 失败后等待 1 秒再重试
}
if (is.null(result)) {
cat("重试 5 次仍然失败\n")
} else {
cat("第", attempts, "次尝试成功,取到", length(result), "条数据\n")
}
这段代码把成功和次数上限两个退出条件合并在一个if里,逻辑一目了然。attempts计数器每轮都会先更新,保证了循环状态始终在变化,这是repeat循环能安全退出的前提,后面讲误区时还会回到这一点。
常见误区提醒:这些坑千万别踩
第一个坑也是最致命的坑:忘记写break。repeat语法上完全允许一个不含break的循环体存在,R不会在解析阶段给出任何警告,代码一跑起来就是死循环,控制台疯狂输出直到你手动按Esc中断。看这个错误示范:
# 错误示范:循环体里没有 break,程序永远停不下来
total <- 0
repeat {
total <- total + 1
cat("total =", total, "\n")
}
写repeat的时候可以给自己立个规矩:先写break和它的判断条件,再往循环体里填其他逻辑。判断条件里引用的那个状态变量,必须保证每轮循环都有机会变化,否则判断永远不成立,效果等同于没写break。
第二个坑是break判断的位置放错,导致多执行或少执行一轮,也就是经典的差一错误。下面的两段代码意图都是计算1加到5,正确答案是15,但写法二把判断放在了累加之后,多跑了一轮:
# 意图:计算 1 加到 5 的和,正确答案是 15
# 写法一:先判断再累加,结果正确
total <- 0
i <- 0
repeat {
i <- i + 1
if (i > 5) {
break
}
total <- total + i
}
cat("写法一结果:", total, "\n") # 输出 15
# 写法二:先累加再判断,多执行了一轮,结果出错
total <- 0
i <- 0
repeat {
i <- i + 1
total <- total + i
if (i > 5) {
break
}
}
cat("写法二结果:", total, "\n") # 输出 21
写法二在i等于6时已经把6加进了total才触发break,结果变成21。排查这类问题时,最有效的办法是在循环体里临时加一行cat输出关键变量的值,手动跑两三轮就能看清判断和更新的先后顺序哪里不对。养成在注释里先写清楚每轮循环状态变化的习惯,比事后调试省事得多。
第三个坑和next有关。next的作用是跳过本轮循环剩余部分,直接回到循环体开头。在repeat里用它时要特别小心:如果next把计数器更新或break判断一起跳过了,循环状态就不再变化,直接演变成死循环:
# 错误示范:next 跳过了自增和判断,i 卡在 2 上,陷入死循环
i <- 1
repeat {
if (i %% 2 == 0) {
next # 偶数时跳到下一轮,但自增也被跳过了
}
cat(i, "是奇数\n")
if (i >= 9) {
break
}
i <- i + 1
}
这段代码在i等于2时触发next,跳过了后面的自增,下一轮i还是2,永远出不来。正确做法是把状态更新放在next之前,保证不管走哪条路径,每轮循环的状态都在往前走:
# 正确写法:计数器更新放在 next 之前,循环状态每轮都在变化
i <- 0
repeat {
i <- i + 1
if (i > 9) {
break
}
if (i %% 2 == 0) {
next # 偶数跳过输出,但计数器已经更新
}
cat(i, "是奇数\n")
}
还有一个概念层面的误区值得提一句:有人试图给repeat后面直接加条件,写成类似其他语言do-while那种带尾部条件的语法,R里没有这种写法,repeat后面只能跟循环体,条件判断必须以if加break的形式写在循环体内部。另外,repeat和花括号之间是否换行不影响语法,但保持循环体用花括号包裹是好习惯,哪怕只有一行,避免后续加语句时出错。
把这几个点记住,repeat循环基本就踩不到什么坑了:循环体里必须有break,break依赖的状态变量每轮都要变化,判断位置要和业务语义对齐,next别跳过关键更新。在迭代次数未知的任务里,repeat配合break写出来的代码干净直接,值得放进你的R语言工具箱。