Day 7 - 小项目:记账 CLI
建议用时:180-240 分钟
你将学会什么
- 如何把第一周知识组合成一个小项目
- CLI 是什么,文字菜单怎么设计
- 如何用
while让程序持续运行 - 如何用
List保存多条记账数据 - 如何用多个
List暂时保存一条记录的不同字段 - 如何把添加、查看、统计拆成方法
- 如何处理错误输入,让程序不崩溃
- 如何按步骤做项目,而不是一口气写完整代码
本页只使用第一周学过的 C# 基础:变量、类型、输入、判断、循环、方法、数组和 List。先不使用类、数据库、文件保存和界面。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
从 Day 6 走到 Day 7
Day 7 没有要求你突然学会新语法。它只是把前六天已经练过的零件装在一起:
| 项目需求 | 用到的旧知识 |
|---|---|
| 显示菜单 | Console.WriteLine |
| 根据选择执行功能 | if 或 switch |
| 菜单反复出现 | 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() | 读取用户输入 |
| 判断固定选项 | if 或 switch | 根据 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 | 输入 0 时 break |
今天写项目时按这个顺序
- 先做菜单,只支持退出。
- 再做添加记录。
- 再做查看记录。
- 再做统计合计。
- 再把功能拆成方法。
- 最后测试正常输入和错误输入。
每完成一步都要停下来运行。正确的学习过程应该像这样:
写菜单 -> 运行 -> 保存
加添加功能 -> 运行 -> 保存
加查看功能 -> 运行 -> 保存
加统计功能 -> 运行 -> 保存
最后才拆方法和整理名称如果新一步坏了,就退回上一个能运行的版本比较。不要继续往坏代码上叠功能。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个可以添加、查看、统计的记账程序。
最终程序效果
运行后显示菜单:
==== 记账 CLI ====
1. 添加记录
2. 查看记录
3. 查看合计
0. 退出
请选择:添加一条记录:
请选择: 1
金额: 20
分类: food
备注: lunch
记录已添加查看记录:
请选择: 2
1. 20.00 | food | lunch查看合计:
请选择: 3
共 1 笔,合计 20.00今天要理解的是:这个项目并没有新语法,它只是把前六天的基础串起来。
第一次阅读效果时,只回答三个问题:
- 用户可以选择哪些功能?
- 一条记录包含哪三个字段?
- 输入什么可以退出程序?
答出这三题后就可以继续,不需要此时知道完整代码怎么写。
第三部分:跟着敲代码
从这里开始动手。每一步都可以直接放进 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 没有 Add3. 选择遍历方式
查看记录时需要同时读取 amounts[i]、categories[i]、notes[i],为什么这里更适合 for 而不是 foreach?
4. 找空列表问题
统计合计前为什么建议先判断 amounts.Count == 0?
5. 排开发顺序
下面四个动作应该怎样排序?
完成所有功能后拆方法
只做菜单和退出
添加一条记录
查看并统计记录参考答案
- 菜单只执行一次,处理一个选择后程序就结束,用户无法继续操作。
- 三个列表数量会不一致。以后按同一个下标读取时,
notes[i]可能越界或对应错记录。 for提供同一个下标i,可以用它同时读取三个列表中对应的一项。- 空列表没有记录。先判断可以给用户明确提示,也避免后续代码错误地读取不存在的数据。
- 只做菜单和退出 -> 添加一条记录 -> 查看并统计记录 -> 完成所有功能后拆方法。
如果第 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,要求:
- 菜单包含添加记录、查看记录、查看合计、删除记录、退出。
- 添加记录时输入金额、分类、备注。
- 金额必须是数字,且不能小于 0。
- 分类不能为空。
- 查看记录时要显示序号。
- 删除记录时输入序号,序号要转换成
List下标。 - 查看合计时输出笔数和总金额。
作业拆解
按这个顺序写:
- 先创建三个
List。 - 写
PrintMenu。 - 写
AddRecord。 - 写
ListRecords。 - 写
PrintTotal。 - 写
DeleteRecord。 - 在主循环里根据选择调用方法。
完整代码
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 开始。 - 小项目要先跑通主流程,再补错误处理。