第 2 周 Day 5-6:综合小游戏 —— 学生成绩管理系统

第 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(),打印对象时能直接看到内容,方便调试
  • totalaverageisPassed 是计算属性,随成绩变化自动更新
  • average 使用 total / 3.0 而非 total / 33.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 也可能是 null
  • searchfilter 做模糊匹配,containsignoreCase = true 表示忽略大小写
  • showStatsstudents.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 设置默认值为 0
  • when 表达式根据用户输入的数字分发到不同操作,比多个 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,放在类体内而不是构造函数中。

七、练习任务

基础任务(必做)

  1. 手敲完整代码并运行

    新建一个 Kotlin 项目,把第四节的三个模块代码完整输入,运行并测试所有 6 个菜单功能

  2. 添加"修改成绩"功能

    在菜单中新增第 7 个选项:输入学生姓名,然后输入新的三科成绩,更新该学生的成绩

    (提示:给 StudentManager 增加一个 updateScore 方法)

  3. 添加按总分排序功能

    在菜单中新增第 8 个选项:按总分从高到低显示所有学生

    (提示:使用 students.sortedByDescending { it.total }

进阶任务(选做)

  1. 成绩验证

    在添加和修改学生时,检查每科成绩是否在 0~100 之间,超出范围给出提示并拒绝操作

  2. 导出成绩报告

    新增一个功能:将所有学生信息和统计结果写入一个 .txt 文件

    (提示:用 File("report.txt").writeText(content)

  3. 用 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、成绩属性
计算属性 totalaverageisPassed 由 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 进阶主题:继承、接口、抽象类、泛型、集合操作等。基础打扎实了,进阶的路会顺畅很多。

相关推荐
AxureMost2 小时前
ActivePresenter Pro v10.1.2 屏幕录制软件
windows
多年小白3 小时前
2026年5月5日
大数据·人工智能·深度学习·microsoft·机器学习·ai·自动驾驶
坚果派·白晓明3 小时前
【开发者必备工具】Windows 11 安装 Git 完整指南
windows·git·项目开发必备工具·参与开源项目必备工具
- J°雾3 小时前
GitNexus 安装配置 + 网页版 GUI 使用教程(Windows 环境)
windows·开源·github·知识图谱
装杯让你飞起来啊4 小时前
Kotlin List / Array 与 for 循环
开发语言·kotlin·list
练习时长一年4 小时前
分页插件冲突问题
服务器·前端·windows
xier_ran5 小时前
【BUG问题】5060Ti显卡Windows配置Anaconda中的CUDA及Pytorch,sm_120问题
人工智能·pytorch·windows
前端之虎陈随易5 小时前
为什么今天还会有新语言?MoonBit 想解决什么问题?
大数据·linux·javascript·人工智能·算法·microsoft·typescript