Skip to Content
Week 01Day 7 - 小项目:记账 CLI

Day 7 - 小项目:记账 CLI

建议用时:180-240 分钟

你将学会什么

  • 如何把第一周知识组合成一个小项目
  • CLI 是什么,文字菜单怎么设计
  • 如何用 while 让程序持续运行
  • 如何用 List 保存多条记账数据
  • 如何用多个 List 暂时保存一条记录的不同字段
  • 如何把添加、查看、统计拆成方法
  • 如何处理错误输入,让程序不崩溃
  • 如何按步骤做项目,而不是一口气写完整代码

本页只使用第一周学过的 C# 基础:变量、类型、输入、判断、循环、方法、数组和 List。先不使用类、数据库、文件保存和界面。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

从 Day 6 走到 Day 7

Day 7 没有要求你突然学会新语法。它只是把前六天已经练过的零件装在一起:

项目需求用到的旧知识
显示菜单Console.WriteLine
根据选择执行功能ifswitch
菜单反复出现while
读取金额ReadLine + TryParse
保存多条记录List + Add
查看所有记录for
计算总额累加变量 + foreach
整理代码方法

看到项目代码很长,不代表每一行都是新知识。项目难在“组合”,不是难在“新语法”。

今天的最低通过线

今天不要求一次做完整。按下面三个等级学习:

合格:菜单能显示,输入 0 能退出 良好:能添加一条记录并查看 完成:能添加多条记录、查看全部并统计合计

只做到“合格”也不是失败。先保存能运行的版本,第二次再继续。

不要从第四部分复制完整答案开始。项目代码一旦同时出现多个错误,初学者很难判断是哪一步造成的。必须按第三部分逐步运行。

第一部分:先学原理和最小知识

这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。

今天先抓住什么

记账程序的主线是:

显示菜单 -> 读取选择 -> 根据选择执行不同功能 -> 添加时读取金额、分类、备注 -> 用 List 保存数据 -> 查看时遍历 List -> 统计时累加金额 -> 回到菜单

这就是一个小项目的闭环。

名词先讲清楚

名词直接解释本页例子
CLI通过文字输入输出操作的程序终端菜单
菜单给用户选择功能的文字列表添加、查看、统计、退出
主循环让程序反复显示菜单while (true)
记录一条业务数据一笔账
字段一条记录里的某个数据金额、分类、备注
状态程序运行中保存的数据三个 List
校验检查输入是否合法金额必须是数字
统计把多条数据算成结果合计金额

为什么先不用类

一笔账其实有三个字段:

金额:20 分类:food 备注:lunch

更正式的写法会把这三个字段放进一个类或记录类型里。但第一周还没系统学习类,所以今天先用三个 List

amounts 保存所有金额 categories 保存所有分类 notes 保存所有备注

它们靠同一个下标对应同一笔记录:

amounts[0] -> 第 1 笔金额 categories[0] -> 第 1 笔分类 notes[0] -> 第 1 笔备注

这不是最终大型项目写法,但很适合用来串第一周基础。

菜单程序为什么需要 while

如果没有循环,程序执行一次菜单就结束了。

记账程序需要反复操作:

添加一笔 再添加一笔 查看记录 查看合计 退出

所以外层要用:

while (true) { // 显示菜单 // 读取选择 // 执行动作 }

输入 0 时,用 break 结束循环。

数据怎么保存

今天用三个列表保存数据:

var amounts = new List<decimal>(); var categories = new List<string>(); var notes = new List<string>();

添加记录时,三个列表要一起添加:

var amounts = new List<decimal>(); var categories = new List<string>(); var notes = new List<string>(); amounts.Add(20m); categories.Add("food"); notes.Add("lunch"); Console.WriteLine("已添加一笔记录");

查看记录时,用同一个下标读取:

var amounts = new List<decimal> { 20m }; var categories = new List<string> { "food" }; var notes = new List<string> { "lunch" }; Console.WriteLine($"{amounts[0]} | {categories[0]} | {notes[0]}");

功能怎么拆成方法

主流程只负责分发菜单,不要把所有细节都塞进去。

可以拆成这些方法:

方法职责
PrintMenu打印菜单
AddRecord添加一笔记录
ListRecords查看所有记录
PrintTotal统计合计
TryReadDecimal读取并转换金额

方法拆清楚后,主流程会更像一张目录。

本项目用到的常用操作清单

这个项目没有新语法,但把第一周常用操作全部串起来了。

需求用到的写法作用
持续显示菜单while (true)程序不立刻结束
退出菜单break输入 0 时结束循环
读取选择Console.ReadLine()读取用户输入
判断固定选项ifswitch根据 1/2/3/0 执行动作
读取金额decimal.TryParse防止输入 abc 崩溃
清理分类category.Trim()去掉前后空格
判断空白string.IsNullOrWhiteSpace分类或备注不能为空时使用
保存金额amounts.Add(amount)添加到金额列表
保存分类categories.Add(category)添加到分类列表
保存备注notes.Add(note)添加到备注列表
判断有没有数据amounts.Count == 0空列表不要直接遍历输出
按下标读取amounts[i]同一笔记录的三个字段靠下标对应
遍历记录for (int i = 0; i < amounts.Count; i++)同时读取三个列表
累计合计total += amount统计总金额
金额格式{amount:F2}保留两位小数
拆方法void AddRecord(...)让主流程更清楚

这张表要能反过来读:

我要保存多条金额 -> 用 List<decimal> 我要新增一笔 -> 三个 List 都 Add 我要查看第 1 笔 -> 三个 List 都读 [0] 我要统计总额 -> foreach 或 for 里 total += amount 我要处理错误金额 -> decimal.TryParse

常见错误和修法

错误原因修法
输入金额 abc 程序崩溃直接转换,没有校验decimal.TryParse
查看记录时没有内容列表是空的先判断 Count == 0
三个列表数量不一致添加时漏了某个字段添加记录时三个 Add 必须一起写
输入未知选项没有反应没写 else增加“未知选项”提示
程序无法退出循环里没有 break输入 0break

今天写项目时按这个顺序

  1. 先做菜单,只支持退出。
  2. 再做添加记录。
  3. 再做查看记录。
  4. 再做统计合计。
  5. 再把功能拆成方法。
  6. 最后测试正常输入和错误输入。

每完成一步都要停下来运行。正确的学习过程应该像这样:

写菜单 -> 运行 -> 保存 加添加功能 -> 运行 -> 保存 加查看功能 -> 运行 -> 保存 加统计功能 -> 运行 -> 保存 最后才拆方法和整理名称

如果新一步坏了,就退回上一个能运行的版本比较。不要继续往坏代码上叠功能。

第二部分:把知识组合成完整例子

前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个可以添加、查看、统计的记账程序。

最终程序效果

运行后显示菜单:

==== 记账 CLI ==== 1. 添加记录 2. 查看记录 3. 查看合计 0. 退出 请选择:

添加一条记录:

请选择: 1 金额: 20 分类: food 备注: lunch 记录已添加

查看记录:

请选择: 2 1. 20.00 | food | lunch

查看合计:

请选择: 3 共 1 笔,合计 20.00

今天要理解的是:这个项目并没有新语法,它只是把前六天的基础串起来。

第一次阅读效果时,只回答三个问题:

  1. 用户可以选择哪些功能?
  2. 一条记录包含哪三个字段?
  3. 输入什么可以退出程序?

答出这三题后就可以继续,不需要此时知道完整代码怎么写。

第三部分:跟着敲代码

从这里开始动手。每一步都可以直接放进 Program.cs 运行。不要跳到最终答案,先把每一步跑通。

项目按三轮完成:第一轮只做到菜单退出;第二轮做到新增和查看;第三轮再加统计并拆方法。每一轮都保留一个能运行的版本。

第 1 步:只做菜单和退出

while (true) { Console.WriteLine("==== 记账 CLI ===="); Console.WriteLine("1. 添加记录"); Console.WriteLine("2. 查看记录"); Console.WriteLine("3. 查看合计"); Console.WriteLine("0. 退出"); Console.Write("请选择: "); string? choice = Console.ReadLine(); if (choice == "0") { Console.WriteLine("程序结束"); break; } Console.WriteLine($"你选择了: {choice}"); }

先确认输入 0 能退出。

第 2 步:准备三个 List

var amounts = new List<decimal>(); var categories = new List<string>(); var notes = new List<string>(); amounts.Add(20m); categories.Add("food"); notes.Add("lunch"); Console.WriteLine($"{amounts[0]:F2} | {categories[0]} | {notes[0]}");

预期输出:

20.00 | food | lunch

这一步要理解:三个列表的下标 0 对应同一笔记录。

第 3 步:添加一笔记录

var amounts = new List<decimal>(); var categories = new List<string>(); var notes = new List<string>(); Console.Write("金额: "); string? amountText = Console.ReadLine(); if (!decimal.TryParse(amountText, out decimal amount)) { Console.WriteLine("金额必须是数字"); return; } Console.Write("分类: "); string? category = Console.ReadLine(); Console.Write("备注: "); string? note = Console.ReadLine(); if (string.IsNullOrWhiteSpace(category)) { Console.WriteLine("分类不能为空"); return; } amounts.Add(amount); categories.Add(category.Trim()); notes.Add(string.IsNullOrWhiteSpace(note) ? "" : note.Trim()); Console.WriteLine("记录已添加"); Console.WriteLine($"{amounts[0]:F2} | {categories[0]} | {notes[0]}");

输入金额错误时,程序会提前结束,不会添加记录。

第 4 步:查看多条记录

var amounts = new List<decimal> { 20m, 35.5m }; var categories = new List<string> { "food", "book" }; var notes = new List<string> { "lunch", "csharp" }; if (amounts.Count == 0) { Console.WriteLine("暂无记录"); return; } for (int i = 0; i < amounts.Count; i++) { Console.WriteLine($"{i + 1}. {amounts[i]:F2} | {categories[i]} | {notes[i]}"); }

预期输出:

1. 20.00 | food | lunch 2. 35.50 | book | csharp

for 是因为要显示序号,同时还要用下标读取三个列表。

第 5 步:统计合计

var amounts = new List<decimal> { 20m, 35.5m, 10m }; decimal total = 0m; foreach (decimal amount in amounts) { total += amount; } Console.WriteLine($"共 {amounts.Count} 笔"); Console.WriteLine($"合计: {total:F2}");

预期输出:

共 3 笔 合计: 65.50

第 6 步:把菜单和方法组合起来

var amounts = new List<decimal>(); var categories = new List<string>(); var notes = new List<string>(); while (true) { void PrintMenu() { Console.WriteLine("==== 记账 CLI ===="); Console.WriteLine("1. 添加记录"); Console.WriteLine("2. 查看记录"); Console.WriteLine("3. 查看合计"); Console.WriteLine("0. 退出"); Console.Write("请选择: "); } void AddRecord(List<decimal> amounts, List<string> categories, List<string> notes) { Console.Write("金额: "); string? amountText = Console.ReadLine(); if (!decimal.TryParse(amountText, out decimal amount)) { Console.WriteLine("金额必须是数字"); return; } Console.Write("分类: "); string? category = Console.ReadLine(); Console.Write("备注: "); string? note = Console.ReadLine(); if (string.IsNullOrWhiteSpace(category)) { Console.WriteLine("分类不能为空"); return; } amounts.Add(amount); categories.Add(category.Trim()); notes.Add(string.IsNullOrWhiteSpace(note) ? "" : note.Trim()); Console.WriteLine("记录已添加"); } void ListRecords(List<decimal> amounts, List<string> categories, List<string> notes) { if (amounts.Count == 0) { Console.WriteLine("暂无记录"); return; } for (int i = 0; i < amounts.Count; i++) { Console.WriteLine($"{i + 1}. {amounts[i]:F2} | {categories[i]} | {notes[i]}"); } } void PrintTotal(List<decimal> amounts) { decimal total = 0m; foreach (decimal amount in amounts) { total += amount; } Console.WriteLine($"共 {amounts.Count} 笔,合计 {total:F2}"); } PrintMenu(); string? choice = Console.ReadLine(); if (choice == "1") { AddRecord(amounts, categories, notes); } else if (choice == "2") { ListRecords(amounts, categories, notes); } else if (choice == "3") { PrintTotal(amounts); } else if (choice == "0") { Console.WriteLine("程序结束"); break; } else { Console.WriteLine("未知选项"); } }

这一步已经是完整项目的主体。第四部分会给带删除功能的完整答案。

每日小测

先独立回答,再看参考答案。

1. 为什么需要主循环

如果删掉最外层的 while (true),菜单程序会发生什么?

2. 为什么三个列表必须一起添加

下面代码漏掉了什么风险?

amounts.Add(20m); categories.Add("food"); // notes 没有 Add

3. 选择遍历方式

查看记录时需要同时读取 amounts[i]categories[i]notes[i],为什么这里更适合 for 而不是 foreach

4. 找空列表问题

统计合计前为什么建议先判断 amounts.Count == 0

5. 排开发顺序

下面四个动作应该怎样排序?

完成所有功能后拆方法 只做菜单和退出 添加一条记录 查看并统计记录

参考答案

  1. 菜单只执行一次,处理一个选择后程序就结束,用户无法继续操作。
  2. 三个列表数量会不一致。以后按同一个下标读取时,notes[i] 可能越界或对应错记录。
  3. for 提供同一个下标 i,可以用它同时读取三个列表中对应的一项。
  4. 空列表没有记录。先判断可以给用户明确提示,也避免后续代码错误地读取不存在的数据。
  5. 只做菜单和退出 -> 添加一条记录 -> 查看并统计记录 -> 完成所有功能后拆方法。

如果第 2、3 题答错,回看“为什么先不用类”和“数据怎么保存”,画出三个列表下标的对应关系再继续。

上位机专项练习

本周项目不再只做记账思维,而是把输入、判断、循环、方法和 List 组合成最小设备采集控制台。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:显示设备监控菜单

Console.WriteLine("1. 录入采集值"); Console.WriteLine("2. 查看全部数据"); Console.WriteLine("3. 查看报警数量"); Console.WriteLine("0. 退出");

运行结果或界面效果:

1. 录入采集值 2. 查看全部数据 3. 查看报警数量 0. 退出

改动任务: 增加“4. 清空数据”菜单项。

专项例子 2:保存并显示采集值

var readings = new List<double>(); readings.Add(72.5); readings.Add(81.2); foreach (double reading in readings) { Console.WriteLine($"温度: {reading:F1} ℃"); }

运行结果或界面效果:

温度: 72.5 ℃ 温度: 81.2 ℃

改动任务: 再添加 79.0。

专项例子 3:统计报警数量

var readings = new List<double> { 72.5, 81.2, 79.0, 90.1 }; int alarmCount = 0; foreach (double reading in readings) { if (reading >= 80) { alarmCount++; } } Console.WriteLine($"报警数量: {alarmCount}");

运行结果或界面效果:

报警数量: 2

改动任务: 把报警线从 80 改成 75。

第四部分:作业完整答案

这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。

作业要求

完成一个记账 CLI,要求:

  1. 菜单包含添加记录、查看记录、查看合计、删除记录、退出。
  2. 添加记录时输入金额、分类、备注。
  3. 金额必须是数字,且不能小于 0。
  4. 分类不能为空。
  5. 查看记录时要显示序号。
  6. 删除记录时输入序号,序号要转换成 List 下标。
  7. 查看合计时输出笔数和总金额。

作业拆解

按这个顺序写:

  1. 先创建三个 List
  2. PrintMenu
  3. AddRecord
  4. ListRecords
  5. PrintTotal
  6. DeleteRecord
  7. 在主循环里根据选择调用方法。

完整代码

var amounts = new List<decimal>(); var categories = new List<string>(); var notes = new List<string>(); while (true) { void PrintMenu() { Console.WriteLine(); Console.WriteLine("==== 记账 CLI ===="); Console.WriteLine("1. 添加记录"); Console.WriteLine("2. 查看记录"); Console.WriteLine("3. 查看合计"); Console.WriteLine("4. 删除记录"); Console.WriteLine("0. 退出"); Console.Write("请选择: "); } void AddRecord(List<decimal> amounts, List<string> categories, List<string> notes) { Console.Write("金额: "); string? amountText = Console.ReadLine(); if (!decimal.TryParse(amountText, out decimal amount)) { Console.WriteLine("金额必须是数字"); return; } if (amount < 0) { Console.WriteLine("金额不能小于 0"); return; } Console.Write("分类: "); string? category = Console.ReadLine(); if (string.IsNullOrWhiteSpace(category)) { Console.WriteLine("分类不能为空"); return; } Console.Write("备注: "); string? note = Console.ReadLine(); amounts.Add(amount); categories.Add(category.Trim()); notes.Add(string.IsNullOrWhiteSpace(note) ? "" : note.Trim()); Console.WriteLine("记录已添加"); } void ListRecords(List<decimal> amounts, List<string> categories, List<string> notes) { if (amounts.Count == 0) { Console.WriteLine("暂无记录"); return; } for (int i = 0; i < amounts.Count; i++) { Console.WriteLine($"{i + 1}. {amounts[i]:F2} | {categories[i]} | {notes[i]}"); } } void PrintTotal(List<decimal> amounts) { decimal total = 0m; foreach (decimal amount in amounts) { total += amount; } Console.WriteLine($"共 {amounts.Count} 笔,合计 {total:F2}"); } void DeleteRecord(List<decimal> amounts, List<string> categories, List<string> notes) { if (amounts.Count == 0) { Console.WriteLine("暂无记录,不能删除"); return; } ListRecords(amounts, categories, notes); Console.Write("请输入要删除的序号: "); string? indexText = Console.ReadLine(); if (!int.TryParse(indexText, out int number)) { Console.WriteLine("序号必须是整数"); return; } int index = number - 1; if (index < 0 || index >= amounts.Count) { Console.WriteLine("序号不存在"); return; } amounts.RemoveAt(index); categories.RemoveAt(index); notes.RemoveAt(index); Console.WriteLine("记录已删除"); } PrintMenu(); string? choice = Console.ReadLine(); if (choice == "1") { AddRecord(amounts, categories, notes); } else if (choice == "2") { ListRecords(amounts, categories, notes); } else if (choice == "3") { PrintTotal(amounts); } else if (choice == "4") { DeleteRecord(amounts, categories, notes); } else if (choice == "0") { Console.WriteLine("程序结束"); break; } else { Console.WriteLine("未知选项"); } }

正确运行结果

添加两笔后查看:

1. 20.00 | food | lunch 2. 35.50 | book | csharp

查看合计:

共 2 笔,合计 55.50

删除第 1 笔后查看:

1. 35.50 | book | csharp

错误输入要分别测试:

金额输入 abc:提示 金额必须是数字 分类直接回车:提示 分类不能为空 删除序号输入 abc:提示 序号必须是整数 删除序号输入 99:提示 序号不存在

你要能说清楚这 8 件事

  • CLI 是用文字输入输出操作的程序。
  • while (true) 让菜单持续显示。
  • 输入 0 时用 break 退出。
  • 三个 List 用同一个下标表示同一笔记录。
  • 添加记录时三个 List 要一起 Add
  • 删除记录时三个 List 要一起 RemoveAt
  • 用户看到的序号从 1 开始,List 下标从 0 开始。
  • 小项目要先跑通主流程,再补错误处理。