9  R中的控制流

9.1 本章学习目标

通过本章学习,你将能够:

  • 理解什么是控制流(control flow)
  • 掌握 if / ifelse / case_when 的使用
  • 理解 switch 的适用场景
  • 掌握 for 循环的基本写法
  • 理解 while 和 repeat 的区别
  • 知道什么时候应该使用循环,什么时候避免循环

9.2 什么是控制流?

在默认情况下,R 代码是按顺序执行的

1 + 1
2 + 2
3 + 3

👉 从上到下依次执行

但在实际问题中,我们往往需要:

  • 根据条件决定是否执行某段代码
  • 重复执行某段代码
  • 对不同情况采取不同处理方式

👉 这就需要 控制流(Control Flow)
控制流是指程序在运行过程中,根据条件或规则,决定“哪些代码执行、哪些跳过、执行多少次”的机制。


9.3 条件判断(if 结构)

  • 基本语法
if (condition) {
  执行内容
}

或:

if (condition) {
  条件为真执行
} else {
  条件为假执行
}

示例:成绩等级判断

Code
x <- 85
if (x > 90) {
  "A"
} else if (x > 80) {
  "B"
} else if (x > 50) {
  "C"
} else {
  "F"
}
[1] "B"

  • 重要特点

👉 if 只能处理 单个值(标量)

Code
x <- c(80, 90, 70)

# if (x > 80)  ❌ 会报错或只取第一个值

9.4 向量化判断(ifelse)

当数据是向量(多个值)时,应使用:

ifelse(condition, yes, no)

示例:奇偶判断

Code
x <- 1:10
ifelse(x %% 2 == 0, "even", "odd")
 [1] "odd"  "even" "odd"  "even" "odd"  "even" "odd"  "even" "odd"  "even"

核心理解

ifelse = 对每个元素逐个判断

👉 和 if 的区别:

结构 适用对象
if 单个值
ifelse 向量

9.5 switch 结构

基本语法: switch 结构:多选一的“菜单”

当你有几个明确的、具体的选项时,用 switch 最方便。它就像自动售货机:按 A1 出可乐,按 A2 出雪碧,按 B1 出薯片。


  • 基本语法
    switch(选择的按钮,
      A1 = 可乐,
      A2 = 雪碧,
      B1 = 薯片,
      默认结果 # 如果都不匹配,执行这个
    )

👉 根据“选项”返回不同结果(类似菜单)


Code
x <- "a"

switch(x,
  a = "option 1",
  b = "option 2",
  c = "option 3",
  stop("Invalid value")
)
[1] "option 1"

👉 stop() 就像是在代码里拉下紧急刹车

当 R 运行到 stop() 这行代码时,它会立刻停止运行,并且把括号里的文字作为红色报错信息打印在控制台上,提醒你出了什么问题。 如果不加 stop(),当用户输入了错误的选项时,switch 会悄悄返回一个 NULL(空值),代码不会报错,会继续往下跑。但这可能是一个隐患,因为后面的代码如果用到这个结果,遇到 NULL 可能会算出莫名其妙的数据,而且很难排查原因。

用了 stop(),就是“早发现,早解决”。遇到不对劲的情况,立刻大声报错停下来,防止错误的数据悄悄流传下去。

x <- "b"
switch(x,
  a = "苹果",
  b = "香蕉",
  c = "橘子",
  stop("没有这个选项哦!") # 如果输入的不是a/b/c,就会报错提示
)
# 结果会返回 "香蕉"

switch 主要用于“字符精确匹配”,不适合数值大小比较!比如:判断 score >= 90 是A等,这种范围比较不能用switch,只能用 if / else if。 switch 只能做:x 是 “a” 就给A,是 “b” 就给B 这种一对一的死匹配。


9.6 循环(for)

👉 当你需要明确知道要重复几次,或者要挨个处理一组数据时,就用 for 循环。就像老师点名:从1号同学开始,挨个念名字,直到念完最后一个同学。


  • 基本语法
for (变量 in 序列) {
  # 重复执行的代码
}

例如:

  • 计算多个均值
  • 多次模拟
  • 批量处理数据

在 R 语言的 for 循环语法中,in 是一个关键字(保留字),用于定义循环变量的取值范围来源

它的具体含义和作用如下: in 的字面意思是 “在…之中”“取自…”

整行代码 for (变量 in 序列) 读起来就是:

“对于 序列 之中的 每一个 变量,执行以下操作…”

它在循环结构中起到了连接指定来源的作用:

  • 左边(变量):是你要定义的循环变量,每次循环它都会被重新赋值。
  • 右边(序列):是你要遍历的对象(如向量、列表、矩阵列等)。
  • in:告诉 R 解释器,左边的变量是从右边的序列里逐个取值的。

当 R 看到这个结构时,它的执行过程是:

  1. 检查 in 右边的序列。
  2. 取出序列中的第一个元素。
  3. 将该元素赋值给 in 左边的变量。
  4. 执行循环体内的代码。
  5. 回到开头,取出序列中的下一个元素,重复步骤 3-4,直到序列中的元素被取完。
Code
for (i in 1:3) {
  print(i)
}
[1] 1
[1] 2
[1] 3

Code
names <- c("小明", "小红", "小华")
 
for (name in names) {
  print(paste("你好,", name))
}
[1] "你好, 小明"
[1] "你好, 小红"
[1] "你好, 小华"
Code
# 结果:
# [1] "你好, 小明"
# [1] "你好, 小红"
# [1] "你好, 小华"
  • 控制循环
Code
for (i in 1:10) {
  
  if (i < 3) next   # 跳过本次
  
  print(i)
  
  if (i >= 5) break # 终止循环
}
[1] 3
[1] 4
[1] 5

  • 两个关键命令
语句 作用
next 跳过本次循环
break 结束整个循环

for 循环取值的重要原则

  • 提前分配空间(非常重要) ⚠️ for 循环最重要的一条规矩:提前分配空间!很多新手写循环时,喜欢让结果一点点“长”出来,这在 R 里是大忌,会非常慢!
b <- Sys.time()
results <- c() 
for (i in 1:100000) {
  results <- c(results, i * 2) 
}
e <- Sys.time()
e-b
# 先准备好一个长度为10000的空盒子(数值型)
b <- Sys.time()
results <- vector("numeric", length = 10000)
for (i in 1:100000) {
  # 每次只要把结果放进对应的位置即可,非常快
  results[i] <- i * 2 
}
e <- Sys.time()
e-b

为什么?

避免在循环中不断扩展对象(效率很低)

特殊情况处理

seq_along(x)

seq_along(x) 主要用于 编写安全的 for 循环,特别是当你不确定被循环的对象 x 是否为空(长度为 0)时。

它的核心作用是:生成一个与对象长度相等的整数序列(1, 2, 3, …),且当对象为空时,返回空序列(而不是 c(1, 0))

x <- c() # 即使向量是空的
for (i in seq_along(x)) { # seq_along(c()) 是空的,循环直接不执行,安全!
  print(x[i])
}

比:

1:length(x)

更安全(避免长度为0时报错)

x <- c() # 假设向量是空的,长度为0
for (i in 1:length(x)) { # 1:0 会变成 c(1, 0),循环会倒着跑两次,大错特错!
  print(x[i])
}
  • 1:length(x) 的致命缺陷是:当 x 为空时,它会变成 c(1, 0),导致循环反向执行两次,产生越界、NA污染和错位赋值,且不报错,是最隐蔽的Bug。
  • seq_along(x) 的安全机制是:当 x 为空时,它返回 integer(0),循环直接不执行,符合“无数据不处理”的安全逻辑。

9.7 while 与 repeat

9.7.1 while 循环:先判断,后执行

主要用途:用于不确定循环次数,但需要在每次循环开始前检查某个条件是否成立的场景。只要条件为 TRUE,循环就会继续。

特点:

  • 入口控制:在执行循环体之前先测试条件。

  • 可能一次都不执行:如果初始条件就是 FALSE,循环体内的代码直接跳过。 典型场景:

  • 迭代算法(如牛顿法求根),直到误差足够小为止。

  • 寻找满足特定条件的第一个元素。

  • 模拟过程,直到达到特定状态。

while (condition) {
  执行内容
}

示例:

Code
i <- 0

while (i < 5) {
  print("I'm happy")
  i <- i + 1
}
[1] "I'm happy"
[1] "I'm happy"
[1] "I'm happy"
[1] "I'm happy"
[1] "I'm happy"

9.7.2 repeat 循环:先执行,后判断(无限循环)

主要用途: 用于需要无限循环直到内部明确触发退出指令的场景。它本质上是一个死循环,必须在循环体内部使用 break 语句来退出,否则会一直运行下去。

特点:

  • 出口控制:先执行循环体,再由内部逻辑决定是否退出。
  • 至少执行一次:循环体一定会被执行至少一次,因为开头没有条件检查。
  • 必须配合 break:如果没有 break,程序将陷入死循环。

典型场景:

  • 用户交互(例如:不断提示用户输入,直到输入有效值或输入 “exit”)。
  • 复杂的迭代逻辑,其中退出条件不是简单的布尔表达式,或者退出条件在循环体执行过程中才生成。
  • 某些需要“先做动作,再检查结果”的算法。
repeat {
  执行内容
  if (条件) break
}

示例:

Code
i <- 0

repeat {
  print("Hi")
  i <- i + 1
  
  if (i > 4) break
}
[1] "Hi"
[1] "Hi"
[1] "Hi"
[1] "Hi"
[1] "Hi"

9.7.3 区别总结

类型 特点
while 条件控制
repeat 无限循环 + break 退出

9.8 什么时候用循环?什么时候不用?

👉 很重要的一点:

R 更推荐“向量化”,而不是循环

例如:

Code
# 不推荐
out <- c()
for (i in 1:10) {
  out[i] <- i^2
}

# 推荐
(1:10)^2
 [1]   1   4   9  16  25  36  49  64  81 100

9.9 本章总结

  • 控制流用于控制代码执行逻辑
  • if 适用于单个条件判断
  • ifelse 适用于向量
  • switch 用于选项匹配
  • for 用于重复执行
  • while 和 repeat 用于条件循环
  • 尽量使用向量化替代循环

9.10 课堂思考

  1. 为什么 if 不能处理向量?
  2. 为什么 R 不推荐大量使用 for 循环?

9.11 练习:成绩等级判断(if / else)

9.11.1 if 训练

给定一个变量 score,请编写 R 代码实现成绩等级划分:

  • score ≥ 90 :A
  • 80 ≤ score < 90 :B
  • 60 ≤ score < 80 :C
  • score < 60 :F

要求

  • 使用 if / else if / else
  • 输入一个 score 值并输出对应等级

示例数据

score <- 85

9.11.2 switch 训练

给定一个操作指令 action 和两个数字 ab,请使用 switch 实现一个简单的计算器:

  • 如果指令是 "add",就执行加法 a + b

  • 如果指令是 "subtract",就执行减法 a - b

  • 如果指令是 "multiply",就执行乘法 a * b

  • 如果指令是 "divide",就执行除法 a / b

  • 如果输入了其他乱七八糟的指令,使用 stop("不支持该操作!") 报错停止


9.11.3 for循环 训练

请使用 for 循环,计算从 1 加到 100 的总和。


library(shiny)
library(shinythemes)
library(shinyjs)
library(DT)
library(dplyr)

# ==================== 问题数据 ====================
questions <- list(
  # 1-10 if / ifelse / case_when
  list(id=1, question="在R中,哪一种语句最适合进行单个条件判断?",
       options=c("for", "if", "while", "repeat"), correct=2,
       explanation="if 语句用于单个条件判断。"),
  
  list(id=2, question="下面哪一项是 if 语句的正确基本结构?",
       options=c("if 条件 {代码}", "if (条件) {代码}", "if [条件] {代码}", "if <条件> {代码}"), correct=2,
       explanation="R 中 if 的标准写法是 if (条件) {代码}。"),
  
  list(id=3, question="x <- 5;执行 if (x > 3) {'yes'} 会返回什么?",
       options=c("\"no\"", "\"yes\"", "TRUE", "FALSE"), correct=2,
       explanation="因为 5 > 3 条件成立,所以返回 'yes'。"),
  
  list(id=4, question="ifelse() 最适合处理哪类问题?",
       options=c("多个语句循环执行", "函数定义", "向量化条件判断", "文件读取"), correct=3,
       explanation="ifelse() 常用于向量化条件判断。"),
  
  list(id=5, question="x <- c(2, 5, 8);ifelse(x > 4, 'A', 'B') 的结果是什么?",
       options=c("c('A','A','A')", "c('B','A','A')", "c('B','B','A')", "c('A','B','A')"), correct=2,
       explanation="2 不大于 4,所以是 B;5 和 8 大于 4,所以是 A。"),
  
  list(id=6, question="在 dplyr 中,哪个函数常用于多条件分类?",
       options=c("ifelse()", "switch()", "case_when()", "repeat()"), correct=3,
       explanation="case_when() 适合多条件分类判断。"),
  
  list(id=7, question="下面关于 case_when() 的说法正确的是?",
       options=c("只能处理一个条件", "常用于向量化多条件判断", "只能用于循环", "不能返回字符"), correct=2,
       explanation="case_when() 是 dplyr 中常见的多条件向量化判断函数。"),
  
  list(id=8, question="x <- 80,若将成绩分为:>=90优秀,>=60及格,否则不及格,最适合用什么?",
       options=c("repeat", "switch", "case_when 或 if...else if...else", "for"), correct=3,
       explanation="这种多分支判断适合 case_when() 或 if...else if...else。"),
  
  list(id=9, question="if 后面的条件结果通常应是什么类型?",
       options=c("numeric", "character", "logical", "list"), correct=3,
       explanation="if 的条件一般应为逻辑值 TRUE/FALSE。"),
  
  list(id=10, question="下面哪句代码表示“如果 x 大于 0,则输出 positive,否则输出 non-positive”?",
       options=c("if (x > 0) 'positive' else 'non-positive'",
                 "if x > 0 then 'positive' else 'non-positive'",
                 "if [x > 0] 'positive' else 'non-positive'",
                 "ifelse x > 0 'positive' 'non-positive'"), correct=1,
       explanation="R 中 if...else 的标准写法为 if (条件) 表达式 else 表达式。"),
  
  # 11-16 switch
  list(id=11, question="switch() 语句通常适合哪种场景?",
       options=c("重复执行代码", "根据一个值匹配多个分支", "读取数据框", "绘图"), correct=2,
       explanation="switch() 适用于根据一个值选择不同分支。"),
  
  list(id=12, question="在R中,switch('a', a='苹果', b='香蕉') 的结果是什么?",
       options=c("'苹果'", "'香蕉'", "NULL", "报错"), correct=1,
       explanation="因为匹配到了 a,所以返回 '苹果'。"),
  
  list(id=13, question="下面关于 switch() 的说法,哪项正确?",
       options=c("特别适合连续数值比较", "适合有限个固定选项匹配", "只能处理逻辑值", "只能在循环中使用"), correct=2,
       explanation="switch() 更适合固定值匹配,而不是复杂区间判断。"),
  
  list(id=14, question="若 choice <- 'B',switch(choice, A=1, B=2, C=3) 的结果是?",
       options=c("1", "2", "3", "NULL"), correct=2,
       explanation="choice 为 B,因此返回 2。"),
  
  list(id=15, question="下列哪种任务最适合 switch()?",
       options=c("判断年龄是否大于18", "根据月份编号返回月份名称", "计算1到100的和", "重复直到条件满足"), correct=2,
       explanation="switch() 很适合做固定选项映射,如编号到名称。"),
  
  list(id=16, question="如果 switch() 没有匹配到选项,通常返回什么?",
       options=c("FALSE", "0", "NULL", "NA"), correct=3,
       explanation="switch() 在无匹配时通常返回 NULL。"),
  
  # 17-25 for 循环
  list(id=17, question="for 循环最适合做什么?",
       options=c("单次条件判断", "重复执行一组代码", "定义数据框", "输出图形"), correct=2,
       explanation="for 循环用于按顺序重复执行代码。"),
  
  list(id=18, question="下面哪一个是正确的 for 循环结构?",
       options=c("for i in 1:5 { }", "for (i in 1:5) { }", "for [i in 1:5] { }", "for (1:5 in i) { }"), correct=2,
       explanation="R 中 for 循环标准格式是 for (变量 in 序列) {代码}。"),
  
  list(id=19, question="for (i in 1:3) print(i) 会依次输出什么?",
       options=c("0 1 2", "1 2 3", "1 2", "3 2 1"), correct=2,
       explanation="for 会按顺序遍历 1, 2, 3。"),
  
  list(id=20, question="下面代码的作用是什么? sum <- 0; for (i in 1:4) sum <- sum + i",
       options=c("计算1到4的乘积", "计算1到4的和", "计算4次随机数", "创建长度为4的向量"), correct=2,
       explanation="该循环逐步累加 i,结果是 1+2+3+4。"),
  
  list(id=21, question="若 x <- c(10, 20, 30),for (i in x) print(i) 会遍历什么?",
       options=c("1 2 3", "10 20 30", "x 的列名", "报错"), correct=2,
       explanation="for 会依次遍历向量 x 中的元素。"),
  
  list(id=22, question="在 for 循环中,常用于存储每次结果的做法是?",
       options=c("提前创建向量或列表", "必须使用 data.frame", "只能 print()", "不需要对象"), correct=1,
       explanation="通常会预先创建向量/列表来保存循环结果。"),
  
  list(id=23, question="以下哪段代码可以创建平方数向量 1,4,9,16,25?",
       options=c(
         "for (i in 1:5) x <- i^2",
         "x <- c(); for (i in 1:5) x[i] <- i^2",
         "x <- 1:5^2",
         "square(1:5)"
       ), correct=2,
       explanation="需要逐个把 i^2 存入向量 x。"),
  
  list(id=24, question="for 循环中的 i 通常表示什么?",
       options=c("固定常数", "循环变量", "函数名", "数据框"), correct=2,
       explanation="i 是循环变量,用于依次取序列中的值。"),
  
  list(id=25, question="for (i in seq_along(x)) 的优点是什么?",
       options=c("比 1:length(x) 更稳妥", "一定更快", "只能用于列表", "可以替代 if"), correct=1,
       explanation="seq_along(x) 在 x 为空时更安全。"),
  
  # 26-31 while / repeat
  list(id=26, question="while 循环的特点是什么?",
       options=c("固定次数执行", "条件满足时持续执行", "只能执行一次", "必须与 switch 配合"), correct=2,
       explanation="while 会在条件为 TRUE 时持续执行。"),
  
  list(id=27, question="下面哪一个是正确的 while 结构?",
       options=c("while 条件 {代码}", "while (条件) {代码}", "while [条件] {代码}", "while <条件> {代码}"), correct=2,
       explanation="R 中 while 的标准格式是 while (条件) {代码}。"),
  
  list(id=28, question="x <- 1;while (x < 4) {x <- x + 1} 结束后 x 等于多少?",
       options=c("3", "4", "5", "1"), correct=2,
       explanation="x 依次变为 2、3、4,当 x<4 不成立时停止。"),
  
  list(id=29, question="repeat 循环通常需要搭配什么来终止?",
       options=c("next", "break", "return", "stopifnot"), correct=2,
       explanation="repeat 是无限循环结构,通常要用 break 跳出。"),
  
  list(id=30, question="下列关于 while 和 repeat 的说法,正确的是?",
       options=c("while 不需要条件", "repeat 自带终止条件", "repeat 通常需手动 break", "两者完全一样"), correct=3,
       explanation="repeat 本身没有终止条件,一般靠 break 退出。"),
  
  list(id=31, question="哪种情形更适合用 while?",
       options=c("已经明确循环次数", "不知道次数,直到条件满足为止", "只做一次判断", "做固定映射"), correct=2,
       explanation="while 适合未知循环次数、依条件结束的任务。"),
  
  # 32-36 break / next
  list(id=32, question="break 在循环中的作用是什么?",
       options=c("跳过本次,进入下一次", "立即终止整个循环", "重新开始循环", "暂停程序"), correct=2,
       explanation="break 用于直接退出整个循环。"),
  
  list(id=33, question="next 在循环中的作用是什么?",
       options=c("终止整个循环", "跳过当前这一次,进入下一次循环", "返回函数值", "删除对象"), correct=2,
       explanation="next 会跳过本轮后续代码,直接进入下一轮。"),
  
  list(id=34, question="for (i in 1:5) { if (i == 3) next; print(i) } 会输出什么?",
       options=c("1 2 3 4 5", "1 2 4 5", "3", "1 2"), correct=2,
       explanation="当 i=3 时执行 next,跳过 print(3)。"),
  
  list(id=35, question="for (i in 1:5) { if (i == 3) break; print(i) } 会输出什么?",
       options=c("1 2", "1 2 3", "1 2 4 5", "3 4 5"), correct=1,
       explanation="当 i=3 时 break,循环立即终止,所以只输出 1 和 2。"),
  
  list(id=36, question="break 和 next 的主要区别是?",
       options=c("break 跳过一次,next 终止循环",
                 "break 终止循环,next 跳过当前一次",
                 "两者完全一样",
                 "break 用于if,next用于for"), correct=2,
       explanation="break 直接结束循环,next 只跳过当前这轮。"),
  
  # 37-40 综合
  list(id=37, question="下面哪种任务最适合 ifelse() 而不是 for?",
       options=c("逐个打印1到100", "对一个向量中的元素进行奇偶分类", "重复读取文件", "无限循环直到输入正确"), correct=2,
       explanation="向量化分类任务更适合 ifelse()。"),
  
  list(id=38, question="下面哪种任务最适合 for 循环?",
       options=c("判断单个成绩是否及格", "把一个向量所有元素逐个平方后保存", "对固定选项做菜单映射", "多条件分组"), correct=2,
       explanation="逐个处理元素并保存结果,是 for 的典型场景。"),
  
  list(id=39, question="若要根据数字1、2、3分别返回“春”“夏”“秋”,更适合用?",
       options=c("switch()", "while()", "repeat()", "ifelse()"), correct=1,
       explanation="固定编号映射最适合 switch()。"),
  
  list(id=40, question="控制流(control flow)的核心作用是什么?",
       options=c("定义数据类型", "决定代码按什么条件和顺序执行", "创建图形", "读取CSV文件"), correct=2,
       explanation="控制流决定程序执行路径,包括判断、分支和循环。")
)

# ==================== UI ====================
ui <- fluidPage(
  theme = shinytheme("cerulean"),
  useShinyjs(),
  titlePanel("R控制流测验 (黄利东设计)"),
  
  sidebarLayout(
    sidebarPanel(
      wellPanel(
        h4("学生身份认证"),
        textInput("student_name", "姓名:", ""),
        textInput("student_id", "学号:", ""),
        helpText("提示:提交后成绩将自动记录。")
      ),
      hr(),
      actionButton("submit", "提交答案", class = "btn-primary", style="width:100%"),
      br(), br(),
      actionButton("reset", "重新开始", style="width:100%"),
      br(), br(),
      h4("说明"),
      p("1. 必须填写姓名和学号。"),
      p("2. 学号需为13位。"),
      p("3. 提交后下方会显示得分和错题解析。")
    ),
    
    mainPanel(
      uiOutput("auth_check_ui"),
      uiOutput("question_ui"),
      hr(),
      uiOutput("result_ui"),
      DTOutput("explanation_table")
    )
  )
)

# ==================== SERVER ====================
server <- function(input, output, session) {
  
  submitted <- reactiveVal(FALSE)
  
  is_identified <- reactive({
    name_ok <- nzchar(trimws(input$student_name))
    id_ok <- nchar(trimws(input$student_id)) == 13
    name_ok && id_ok
  })
  
  output$auth_check_ui <- renderUI({
    sid <- trimws(input$student_id)
    sname <- trimws(input$student_name)
    
    if (!nzchar(sname)) {
      h4("请输入姓名", style="color:#f0ad4e; text-align:center;")
    } else if (nchar(sid) < 13) {
      h4(paste0("学号位数不足(当前 ", nchar(sid), "/13 位)"),
         style="color:#f0ad4e; text-align:center;")
    } else if (nchar(sid) > 13) {
      h4("警告:学号超过13位,请检查是否输入错误",
         style="color:#d9534f; text-align:center;")
    } else {
      h4("✅ 身份验证成功,请开始答题", style="color:#5cb85c; text-align:center;")
    }
  })
  
  output$question_ui <- renderUI({
    if (!is_identified()) return(NULL)
    
    tagList(
      h4("--- 答题区 ---", style="text-align:center; color:#999;"),
      lapply(questions, function(q) {
        radioButtons(
          inputId = paste0("q", q$id),
          label = paste0(q$id, ". ", q$question),
          choices = setNames(seq_along(q$options), q$options),
          selected = character(0)
        )
      })
    )
  })
  
  observeEvent(input$submit, {
    if (!is_identified()) {
      showModal(modalDialog("请先填写完整信息!", title = "提醒", easyClose = TRUE))
      return()
    }
    
    if (submitted()) {
      showModal(modalDialog("您已经提交过了,请刷新页面或点击重新开始。", title = "提醒", easyClose = TRUE))
      return()
    }
    
    score <- 0
    wrong_ids <- c()
    
    for (q in questions) {
      ans <- input[[paste0("q", q$id)]]
      user_ans <- if (!is.null(ans)) as.numeric(ans) else NA
      
      if (!is.na(user_ans) && user_ans == q$correct) {
        score <- score + 1
      } else {
        wrong_ids <- c(wrong_ids, q$id)
      }
    }
    
    wrong_str <- paste(wrong_ids, collapse = ", ")
    
    res_data <- data.frame(
      提交时间 = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
      姓名 = input$student_name,
      学号 = input$student_id,
      得分 = score,
      总题数 = length(questions),
      错题题号 = wrong_str,
      stringsAsFactors = FALSE
    )
    
    log_file <- "control_flow_quiz_results.csv"
    
    if (!file.exists(log_file)) {
      write.csv(res_data, log_file, row.names = FALSE, fileEncoding = "GBK")
    } else {
      write.table(
        res_data, log_file,
        sep = ",",
        row.names = FALSE,
        col.names = FALSE,
        append = TRUE,
        fileEncoding = "GBK"
      )
    }
    
    submitted(TRUE)
    showNotification("成绩已存档!", type = "message")
  })
  
  observeEvent(input$reset, {
    session$reload()
  })
  
  output$result_ui <- renderUI({
    if (!submitted()) return(NULL)
    
    explanations <- data.frame(
      题号 = sapply(questions, function(x) x$id),
      状态 = sapply(questions, function(q) {
        ans <- input[[paste0("q", q$id)]]
        if (!is.null(ans) && as.numeric(ans) == q$correct) "✅ 正确" else "❌ 错误"
      }),
      正确答案 = sapply(questions, function(x) x$options[x$correct]),
      解析 = sapply(questions, function(x) x$explanation),
      stringsAsFactors = FALSE
    )
    
    output$explanation_table <- renderDT({
      datatable(
        explanations,
        options = list(pageLength = 6, scrollX = TRUE),
        rownames = FALSE
      ) %>%
        formatStyle(
          "状态",
          color = styleEqual(c("✅ 正确", "❌ 错误"), c("green", "red"))
        )
    })
    
    h3(
      paste0("测试结束!得分:", sum(explanations$状态 == "✅ 正确"), " / ", length(questions)),
      style = "text-align:center; color:#2c3e50;"
    )
  })
}

shinyApp(ui, server)