第 2 周 Day 5-6:综合小游戏 ------ 学生成绩管理系统
学习主题:综合运用函数、类、属性与方法,完成一个命令行交互小项目
建议时长:4~5 小时(分两天完成)
学习目标:能把数据建模为类,用 List 管理多个对象,通过 while 循环实现菜单交互,独立完成一个可运行的控制台程序
一、适合读者与学习目标
本文适合正在学习 Kotlin 的初学者,已经完成第 2 周 Day 1-4 的内容(函数、默认参数、lambda、class、属性与方法)。
通过本文,你将:
- 把"函数"和"类"的知识整合到一个实际项目中
- 学会用
while循环 +when分支构建命令行菜单 - 学会用
MutableList管理多个对象的增删改查 - 体验从零搭建一个完整控制台程序的全过程
- 复习前两周的核心语法:变量、条件判断、循环、函数、类
阅读前需要你已经:
- 能定义类并声明属性和方法
- 了解 List 和 MutableList 的基本用法
- 理解 if / when 条件判断和 for / while 循环
二、为什么学这个
单学语法容易忘。当你把 val / var、if / when、List、函数、class 写进同一个程序时,这些知识点才会真正"串"起来,变成你能随时调用的技能。
这个小游戏麻雀虽小但五脏俱全------有数据结构设计、有用户交互、有增删改查------是很多真实应用(后台管理系统、终端工具、信息管理 App)的微缩版。
学完今天的内容,你就有能力独立写一个命令行工具了。
三、项目设计
3.1 功能清单
我们要做一个"学生成绩管理系统",在命令行中运行,支持以下操作:
| 编号 | 功能 | 说明 |
|---|---|---|
| 1 | 添加学生 | 输入姓名和三科成绩 |
| 2 | 查看全部学生 | 列表展示所有学生信息 |
| 3 | 查找学生 | 按姓名搜索某个学生 |
| 4 | 删除学生 | 按姓名删除一个学生 |
| 5 | 统计信息 | 显示平均分、最高分、最低分 |
| 6 | 退出系统 | 结束程序 |
3.2 类的设计
整个程序只需要一个核心类------Student:
kotlin
data class Student(
val name: String, // 姓名
val math: Int, // 数学成绩
val english: Int, // 英语成绩
val kotlin: Int // Kotlin 成绩
) {
// 计算属性:总分
val total: Int
get() = math + english + kotlin
// 计算属性:平均分(保留一位小数)
val average: Double
get() = total / 3.0
// 判断是否及格(三科都 ≥ 60)
val isPassed: Boolean
get() = math >= 60 && english >= 60 && kotlin >= 60
}
说明:
- 使用
data class而不是普通class,因为 data class 自动生成toString(),打印对象时能直接看到内容,方便调试 total、average、isPassed是计算属性,随成绩变化自动更新average使用total / 3.0而非total / 3,3.0确保结果是 Double 类型
四、完整代码(分模块讲解)
4.1 数据层 ------ Student 类
kotlin
// Student.kt --- 数据模型
data class Student(
val name: String,
val math: Int,
val english: Int,
val kotlin: Int
) {
val total: Int
get() = math + english + kotlin
val average: Double
get() = total / 3.0
val isPassed: Boolean
get() = math >= 60 && english >= 60 && kotlin >= 60
// 格式化输出单行信息
fun toRow(): String {
val status = if (isPassed) "✅ 通过" else "❌ 未通过"
return "%-8s | 数学:%-3d | 英语:%-3d | Kotlin:%-3d | 总分:%-3d | 均分:%-5.1f | %s"
.format(name, math, english, kotlin, total, average, status)
}
}
说明:
toRow()用String.format()对齐输出,%-8s表示左对齐占 8 个字符宽度,%-3d表示左对齐占 3 位整数%-5.1f表示占 5 位宽度、保留 1 位小数的浮点数format是 Kotlin 中字符串的格式化方法,和 Java 的String.format用法一致
4.2 业务层 ------ 学生管理逻辑
kotlin
// StudentManager.kt --- 管理学生列表的所有操作
class StudentManager {
private val students = mutableListOf<Student>()
// 添加学生
fun addStudent(name: String, math: Int, english: Int, kotlin: Int) {
if (name.isBlank()) {
println("⚠️ 姓名不能为空")
return
}
if (students.any { it.name == name }) {
println("⚠️ 学生 $name 已存在,请勿重复添加")
return
}
students.add(Student(name, math, english, kotlin))
println("✅ 学生 $name 添加成功")
}
// 查看全部学生
fun listAll() {
if (students.isEmpty()) {
println("📭 暂无学生数据")
return
}
println("\n========== 学生列表(共 ${students.size} 人)==========")
students.forEach { println(it.toRow()) }
println("=".repeat(60))
}
// 按姓名查找
fun findByName(name: String): Student? {
return students.find { it.name == name }
}
// 搜索(支持模糊匹配)
fun search(keyword: String) {
val result = students.filter { it.name.contains(keyword, ignoreCase = true) }
if (result.isEmpty()) {
println("🔍 未找到姓名包含「$keyword」的学生")
return
}
println("\n🔍 搜索「$keyword」的结果(共 ${result.size} 人):")
result.forEach { println(it.toRow()) }
}
// 删除学生
fun removeStudent(name: String) {
val removed = students.removeAll { it.name == name }
if (removed) {
println("✅ 学生 $name 已删除")
} else {
println("⚠️ 未找到学生 $name")
}
}
// 统计信息
fun showStats() {
if (students.isEmpty()) {
println("📭 暂无学生数据,无法统计")
return
}
val avgTotal = students.map { it.total }.average()
val maxStudent = students.maxByOrNull { it.total }
val minStudent = students.minByOrNull { it.total }
val passedCount = students.count { it.isPassed }
val failCount = students.size - passedCount
println("\n========== 成绩统计 ==========")
println("学生总数:${students.size}")
println("通过人数:$passedCount 人")
println("未通过人数:$failCount 人")
println("全班总分平均:%.1f".format(avgTotal))
maxStudent?.let {
println("最高分:${it.name}(总分 ${it.total})")
}
minStudent?.let {
println("最低分:${it.name}(总分 ${it.total})")
}
println("=".repeat(30))
}
// 预置示例数据(方便测试)
fun initSampleData() {
addStudent("张三", 85, 72, 90)
addStudent("李四", 58, 60, 45)
addStudent("王五", 92, 88, 95)
addStudent("赵六", 70, 65, 68)
}
}
说明:
MutableList<Student>用private修饰,外部不能直接操作列表,只能通过提供的方法操作------这是封装的体现students.any { it.name == name }用 lambda 表达式检查是否已存在同名学生,it代表列表中的每个元素findByName返回Student?,可空类型。返回值可能是Student也可能是nullsearch用filter做模糊匹配,contains的ignoreCase = true表示忽略大小写showStats中students.map { it.total }把学生列表转换成总分列表,再用.average()求均值maxByOrNull/minByOrNull找到总分最高/最低的学生,结果可能为 null(列表为空时)
4.3 交互层 ------ 命令行菜单
kotlin
// Main.kt --- 程序入口 + 菜单交互
fun main() {
val manager = StudentManager()
manager.initSampleData() // 预置几条测试数据
var running = true
while (running) {
printMenu()
print("请输入操作编号:")
val choice = readlnOrNull()?.trim() ?: ""
when (choice) {
"1" -> {
// 添加学生
print("姓名:")
val name = readlnOrNull()?.trim() ?: ""
print("数学成绩:")
val math = readlnOrNull()?.toIntOrNull() ?: 0
print("英语成绩:")
val english = readlnOrNull()?.toIntOrNull() ?: 0
print("Kotlin 成绩:")
val kotlin = readlnOrNull()?.toIntOrNull() ?: 0
manager.addStudent(name, math, english, kotlin)
}
"2" -> manager.listAll()
"3" -> {
print("请输入要查找的关键词:")
val keyword = readlnOrNull()?.trim() ?: ""
manager.search(keyword)
}
"4" -> {
print("请输入要删除的学生姓名:")
val name = readlnOrNull()?.trim() ?: ""
manager.removeStudent(name)
}
"5" -> manager.showStats()
"6" -> {
println("👋 感谢使用,再见!")
running = false
}
else -> println("⚠️ 无效操作,请输入 1~6 之间的数字")
}
if (running) {
println("\n按回车键继续......")
readlnOrNull()
}
}
}
fun printMenu() {
println(
"""
╔══════════════════════════╗
║ 学生成绩管理系统 v1.0 ║
╠══════════════════════════╣
║ 1. 添加学生 ║
║ 2. 查看全部学生 ║
║ 3. 查找学生 ║
║ 4. 删除学生 ║
║ 5. 统计信息 ║
║ 6. 退出系统 ║
╚══════════════════════════╝
""".trimIndent()
)
}
说明:
while (running)是程序的主循环,只要running为 true 就不断显示菜单并处理用户输入readlnOrNull()安全地读取用户输入,当输入流结束时返回 null。用?: ""确保空输入不会导致程序崩溃toIntOrNull()把字符串转为 Int,转换失败时返回 null。用?: 0设置默认值为 0when表达式根据用户输入的数字分发到不同操作,比多个 if-else 更清晰printMenu()用trimIndent()去除多行字符串的前导空格,让菜单排版整齐- 每次操作后
按回车键继续暂停一下,防止屏幕滚动太快看不清结果
4.4 运行效果
╔══════════════════════════╗
║ 学生成绩管理系统 v1.0 ║
╠══════════════════════════╣
║ 1. 添加学生 ║
║ 2. 查看全部学生 ║
║ 3. 查找学生 ║
║ 4. 删除学生 ║
║ 5. 统计信息 ║
║ 6. 退出系统 ║
╚══════════════════════════╝
请输入操作编号:2
========== 学生列表(共 4 人)==========
张三 | 数学:85 | 英语:72 | Kotlin:90 | 总分:247 | 均分:82.3 | ✅ 通过
李四 | 数学:58 | 英语:60 | Kotlin:45 | 总分:163 | 均分:54.3 | ❌ 未通过
王五 | 数学:92 | 英语:88 | Kotlin:95 | 总分:275 | 均分:91.7 | ✅ 通过
赵六 | 数学:70 | 英语:65 | Kotlin:68 | 总分:203 | 均分:67.7 | ✅ 通过
============================================================
请输入操作编号:5
========== 成绩统计 ==========
学生总数:4
通过人数:3 人
未通过人数:1 人
全班总分平均:222.0
最高分:王五(总分 275)
最低分:李四(总分 163)
==============================
五、代码结构总览
项目文件结构(三个文件在同一个包下):
StudentManager.kt ← 数据模型 Student(data class)
← 管理逻辑 StudentManager(增删改查)
Main.kt ← main 函数 + 菜单打印 + 交互循环
实际编写时,可以把 Student 和 StudentManager 放在同一个文件里,也可以分两个文件。Main.kt 单独一个文件。
六、常见错误
6.1 直接操作列表而不是通过方法
kotlin
val manager = StudentManager()
manager.students.add(Student("测试", 100, 100, 100)) // ❌ students 是 private,外部不可访问
解决: 使用 manager.addStudent("测试", 100, 100, 100)。封装要求外部通过公开方法操作数据,而不是直接碰内部列表。
6.2 读取输入时忘记处理空值
kotlin
val name = readln() // ❌ 输入流结束时可能抛异常
解决: 使用 readlnOrNull()?.trim() ?: "",安全兜底。
6.3 成绩输入后忘记转 Int
kotlin
val math = readlnOrNull() // ❌ 类型是 String?,不能直接赋给 Int 参数
manager.addStudent(name, math, english, kotlin)
解决: 用 readlnOrNull()?.toIntOrNull() ?: 0,读取 → 转换 → 兜底,一条链搞定。
6.4 while 循环写成死循环
kotlin
while (true) { // ❌ 没有出口
// ...
}
解决: 用 var running = true + while (running),退出时设置 running = false。
6.5 data class 中把计算属性放到构造函数里
kotlin
data class Student(
val name: String,
val math: Int,
val total: Int = math + english + kotlin // ❌ 构造函数参数之间不能相互引用
)
解决: 把 total 定义为计算属性 val total get() = math + english + kotlin,放在类体内而不是构造函数中。
七、练习任务
基础任务(必做)
-
手敲完整代码并运行
新建一个 Kotlin 项目,把第四节的三个模块代码完整输入,运行并测试所有 6 个菜单功能
-
添加"修改成绩"功能
在菜单中新增第 7 个选项:输入学生姓名,然后输入新的三科成绩,更新该学生的成绩
(提示:给 StudentManager 增加一个
updateScore方法) -
添加按总分排序功能
在菜单中新增第 8 个选项:按总分从高到低显示所有学生
(提示:使用
students.sortedByDescending { it.total })
进阶任务(选做)
-
成绩验证
在添加和修改学生时,检查每科成绩是否在 0~100 之间,超出范围给出提示并拒绝操作
-
导出成绩报告
新增一个功能:将所有学生信息和统计结果写入一个
.txt文件(提示:用
File("report.txt").writeText(content)) -
用 enum 表示等级
增加一个枚举类
Grade,根据平均分判定等级:- A:≥ 90
- B:≥ 80
- C:≥ 70
- D:≥ 60
- F:< 60
在 Student 类中增加val grade计算属性
八、阶段小项目
不满足于成绩管理系统?Try 下面两个方向,任意选一个实现:
方向 A:简单银行账户系统
text
需求:
- Account 类:账户名、余额、账户类型(储蓄/信用)
- 功能:存款、取款(不能透支)、转账、查看流水
- 菜单交互与成绩管理系统类似
方向 B:图书借阅管理
text
需求:
- Book 类:书名、作者、是否已借出
- 功能:添加图书、借书、还书、查看可借图书列表、按书名搜索
- 菜单交互与成绩管理系统类似
两个方向的核心骨架和成绩管理系统完全一致------都是 数据类 + MutableList + while + when。你能完成成绩管理系统,就一定能完成这两个。
九、今日总结
| 知识点 | 在项目中的体现 |
|---|---|
| 类与属性 | Student data class,包含 name、成绩属性 |
| 计算属性 | total、average、isPassed 由 getter 动态计算 |
| 方法 | StudentManager 中的 add / remove / search / showStats |
| 封装 | students 列表设为 private,通过公开方法访问 |
| Lambda 表达式 | students.any { }、students.filter { }、students.map { } |
| 空安全 | findByName 返回 Student?,readlnOrNull() 处理空输入 |
| while 循环 | 主菜单循环 while (running) |
| when 分支 | 菜单选项分发 when (choice) |
| MutableList | students.add()、students.removeAll() 动态增删 |
自测清单:
- 完整输入三段代码并成功运行
- 测试了全部 6 个菜单功能,结果正确
- 能解释
data class和普通class在使用上的区别 - 理解为什么
students用 private 修饰 - 知道
readlnOrNull()?.toIntOrNull() ?: 0每一步的作用 - 能解释
students.map { it.total }.average()的执行过程 - 至少完成了 1 个进阶任务或 1 个阶段小项目方向
- 代码运行无编译错误
本次项目你用到的前两周知识清单:
✅ val / var ------ Student 属性声明
✅ if / when ------ 菜单分发、成绩判断
✅ List / forEach ------ 学生列表遍历
✅ String? / 空安全 ------ readlnOrNull、findByName 返回类型
✅ fun / 默认参数 ------ printMenu、addStudent
✅ lambda ------ filter { }、any { }、map { }
✅ class / 属性 / 方法 ------ Student、StudentManager
✅ init 块概念 ------ (在 Manager 的 initSampleData 中有体现)
十、下一步建议
第 2 周课程到此结束!你已经完成了 Kotlin 语言基础中最核心的"函数与类"阶段。
Day 7 是复习日,建议:
- 把这两周写的代码从头回顾一遍(变量 → 条件 → 循环 → 列表 → 函数 → 类 → 综合项目)
- 找一个小需求,尝试自己不查资料写出来(比如"一个简单的待办事项列表")
- 把综合小游戏 Stage 上的项目推送到 GitHub,作为学习记录
下一阶段(第 3~4 周)将进入 Kotlin 进阶主题:继承、接口、抽象类、泛型、集合操作等。基础打扎实了,进阶的路会顺畅很多。