C# 实战项目
学了 17 篇基础,是时候把它们串起来做点真东西了。这一章给三个完整、可运行的项目——学生成绩管理、简易 Web API、文件批量处理工具,分别覆盖 C# 三大主力场景:业务建模(OOP + LINQ + 序列化)、Web 后端(ASP.NET Core)、系统脚本(文件 IO + 异步)。每个项目都从需求出发,给出完整代码、运行结果和要点讲解。建议你亲手跑一遍,再改改需求,这才是真正"上手"。
实战 1:学生成绩管理(class + List<T> + LINQ + JSON)
需求:做一个学生成绩管理小程序——定义 Student 类,装入若干学生(每人多科成绩),算均分、按均分排名、筛优秀学生;能把数据序列化到 JSON 文件,下次启动能读回来。
涉及知识:类与属性、List<T> 集合、LINQ(OrderByDescending / Where / Select / 查询语法)、System.Text.Json 序列化、文件读写。无需第三方依赖,.NET 内置就够。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
// 学生类:姓名 + 多科成绩,带一个算均分的方法
public class Student
{
public string Name { get; set; } = "";
public List<double> Scores { get; set; } = new();
public double Average() => Scores.Count == 0 ? 0 : Scores.Average();
public override string ToString() => $"{Name} 均分 {Average():F1}";
}
public class Program
{
const string DataFile = "students.json";
// WriteIndented = true 让 JSON 缩进好看;JsonNamingPolicy 不指定则用 PascalCase 原样
static readonly JsonSerializerOptions Opt = new() { WriteIndented = true };
static void Main()
{
// 1. 造一批数据(List<T> 集合初始化器 + target-typed new)
var students = new List<Student>
{
new() { Name = "小明", Scores = new() { 90, 80, 85 } },
new() { Name = "小红", Scores = new() { 95, 88, 92 } },
new() { Name = "小刚", Scores = new() { 70, 100, 85 } },
};
// 2. 序列化到 JSON 文件
string json = JsonSerializer.Serialize(students, Opt);
File.WriteAllText(DataFile, json);
Console.WriteLine($"已保存 {students.Count} 条到 {DataFile}");
// 3. 从 JSON 文件反序列化回来(! 抑止 nullable 警告)
var loaded = JsonSerializer.Deserialize<List<Student>>(File.ReadAllText(DataFile))!;
// 4. LINQ 方法语法:按均分降序排名
var rank = loaded.OrderByDescending(s => s.Average()).ToList();
Console.WriteLine("成绩排名:");
int i = 1;
foreach (var s in rank)
Console.WriteLine($" {i++}. {s}");
// 5. LINQ 查询语法:挑出均分超 85 的姓名,按名字排序
var good = (from s in loaded
where s.Average() > 85
orderby s.Name
select s.Name).ToList();
Console.WriteLine($"均分超 85: {string.Join(", ", good)}");
}
}
// 输出:
// 已保存 3 条到 students.json
// 成绩排名:
// 1. 小红 均分 91.7
// 2. 小明 均分 85.0
// 3. 小刚 均分 85.0
// 均分超 85: 小红
//
// students.json 文件内容:
// [
// { "Name": "小明", "Scores": [ 90, 80, 85 ] },
// { "Name": "小红", "Scores": [ 95, 88, 92 ] },
// { "Name": "小刚", "Scores": [ 70, 100, 85 ] }
// ]要点讲解:
record/class:数据模型优先用record(值相等、不可变),需要可变属性才用class;这里为了能改成绩用了class。- 自动属性 + 初始化器:
public string Name { get; set; } = "";用""兜底消除 nullable 警告;new(){ ... }是集合初始化器加 target-typed new(C# 9+),非常简洁。 - LINQ 方法语法:
OrderByDescending(s => s.Average())链式调用,语义清晰、几乎读着像英语;ToList()立即执行,否则是延迟求值。 - LINQ 查询语法:
from ... where ... orderby ... select像 SQL,与方法语法等价,挑你顺眼的写。 System.Text.Json:高性能内置 JSON 库;WriteIndented = true让文件缩进可读;默认大小写敏感、用 PascalCase(也可配JsonNamingPolicy.CamelCase)。- 可空引用类型:反序列化返回
List<Student>?,加!(null 抑止运算符)告诉编译器"我知道它不是 null"。
实战 2:简易 Web API(ASP.NET Core 最小 API)
需求:用 ASP.NET Core 写一个 Todo REST 服务,支持增删改查——GET /todos 列全部、GET /todos/5 取一条、POST /todos 新增、PUT 更新、DELETE 删除。数据存内存即可。
涉及知识:最小 API(MapGet/MapPost/...)、依赖注入(AddSingleton)、路由绑定、Results 状态码、curl 测试。先建项目:dotnet new web -o TodoApi。
// TodoApi/Program.cs — 用 ASP.NET Core 最小 API 写一个 Todo REST 服务
// 创建项目:dotnet new web -o TodoApi
// 运行:dotnet run
// 测试:curl http://localhost:5000/todos
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Generic;
using System.Linq;
var builder = WebApplication.CreateBuilder(args);
// 把 List<Todo> 注册为单例,充当"内存数据库"
builder.Services.AddSingleton<List<Todo>>(_ => new()
{
new() { Id = 1, Title = "学 C#", Done = false },
new() { Id = 2, Title = "写 Web API", Done = false },
});
var app = builder.Build();
// GET /todos — 返回全部
app.MapGet("/todos", (List<Todo> db) => db);
// GET /todos/5 — 返回单条;找不到返回 404
app.MapGet("/todos/{id:int}", (int id, List<Todo> db) =>
db.FirstOrDefault(t => t.Id == id) is { } todo
? Results.Ok(todo)
: Results.NotFound(new { error = "未找到" }));
// POST /todos — 新增;参数从 body 自动绑定成 Todo 对象
app.MapPost("/todos", (Todo todo, List<Todo> db) =>
{
todo.Id = db.Max(t => t.Id) + 1;
db.Add(todo);
return Results.Created($"/todos/{todo.Id}", todo);
});
// PUT /todos/5 — 更新
app.MapPut("/todos/{id:int}", (int id, Todo input, List<Todo> db) =>
{
var todo = db.FirstOrDefault(t => t.Id == id);
if (todo is null) return Results.NotFound();
todo.Title = input.Title;
todo.Done = input.Done;
return Results.NoContent();
});
// DELETE /todos/5 — 删除
app.MapDelete("/todos/{id:int}", (int id, List<Todo> db) =>
{
var removed = db.RemoveAll(t => t.Id == id);
return removed > 0 ? Results.NoContent() : Results.NotFound();
});
app.Run();
// 模型类
class Todo
{
public int Id { get; set; }
public string Title { get; set; } = "";
public bool Done { get; set; }
}
// 测试(另开终端):
// $ curl http://localhost:5000/todos
// [{"id":1,"title":"学 C#","done":false},{"id":2,"title":"写 Web API","done":false}]
// $ curl -X POST http://localhost:5000/todos -H "Content-Type: application/json" \
// -d '{"title":"跑起来","done":false}'
// $ curl http://localhost:5000/todos/3
// {"id":3,"title":"跑起来","done":false}要点讲解:
- 最小 API:
MapGet("/todos", (List<Todo> db) => db)一行就是一个端点——参数List<Todo> db自动从 DI 容器取,返回值自动序列化成 JSON。无需 Controller,极简。 - 依赖注入:
AddSingleton<List<Todo>>把内存列表注册成单例,每个请求拿到的是同一个实例,新增的数据后续能查到。 - 路由绑定:
{id:int}是路由约束,限定为整数;body 自动反序列化成Todo对象(声明在方法参数即可)。 is { } todo:C# 模式匹配,非 null 时绑定到todo变量并进入 true 分支,比写两次!= null干净。Results:静态工厂返回正确的 HTTP 状态码(Ok=200、Created=201、NoContent=204、NotFound=404),REST 语义清晰。- 进阶方向:接 EF Core + PostgreSQL、加 JWT 鉴权、加 Swagger、用
record不可变 DTO、发布到 Docker。
实战 3:文件批量处理工具(Directory + LINQ + async/await)
需求:写一个命令行工具,扫描指定目录下所有文件,统计总大小、Top 3 最大文件、按扩展名分组汇总,最后把报告异步写入 report.txt。支持 dotnet run -- ./some/dir 传参。
涉及知识:Directory.EnumerateFiles 递归遍历、FileInfo、LINQ 分组聚合、async/await 异步文件写入、命令行参数 args。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
// 扫描指定目录所有文件,统计大小并按扩展名分组,异步写报告
class Program
{
static async Task Main(string[] args)
{
var dir = args.Length > 0 ? args[0] : ".";
if (!Directory.Exists(dir))
{
Console.WriteLine($"目录不存在: {dir}");
return;
}
// 1. 递归枚举所有文件;EnumerateFiles 惰性枚举,大目录不爆内存
var stats = new List<(string Path, long Size)>();
foreach (var f in Directory.EnumerateFiles(dir, "*.*", SearchOption.AllDirectories))
{
stats.Add((f, new FileInfo(f).Length));
}
// 2. LINQ 聚合:总大小、Top 3 最大、按扩展名分组
long total = stats.Sum(s => s.Size);
var top3 = stats.OrderByDescending(s => s.Size).Take(3).ToList();
var byExt = stats
.GroupBy(s => Path.GetExtension(s.Path).ToLowerInvariant())
.Select(g => new { Ext = g.Key, Count = g.Count(), Size = g.Sum(x => x.Size) })
.OrderByDescending(g => g.Size)
.ToList();
Console.WriteLine($"扫描 {stats.Count} 个文件,总大小 {total / 1024.0:F1} KB");
Console.WriteLine("Top 3 最大文件:");
foreach (var s in top3)
Console.WriteLine($" {Path.GetFileName(s.Path)} — {s.Size / 1024.0:F1} KB");
// 3. 异步写报告;string.Join 用
换行拼接多行文本
var lines = byExt.Select(g => $"{g.Ext}: {g.Count} 个, {g.Size / 1024.0:F1} KB");
await File.WriteAllTextAsync("report.txt", string.Join("
", lines));
Console.WriteLine("报告已写入 report.txt");
}
}
// 运行:dotnet run -- ./src
// 输出:
// 扫描 42 个文件,总大小 158.3 KB
// Top 3 最大文件:
// data.json — 28.5 KB
// styles.css — 14.2 KB
// app.js — 9.8 KB
// 报告已写入 report.txt要点讲解:
Directory.EnumerateFiles:比GetFiles好——它是惰性枚举,大目录不会一次性把所有路径加载进内存。- 元组:
List<(string Path, long Size)>用元组当轻量数据结构,省去专门定义类;字段直接s.Path/s.Size访问。 - LINQ 聚合:
Sum/OrderByDescending/Take(3)/GroupBy一气呵成——这正是一个"管道",每步输入是上一步的输出。 async/await:await File.WriteAllTextAsync(...)写文件时不阻塞线程;在 UI 程序或 Web 服务里,这能显著提升吞吐。static async Task Main是异步入口。- 插值字符串格式化:
$"...{size / 1024.0:F1} KB"冒号后跟格式说明符,F1表示保留 1 位小数。 - 并发进阶:文件很多时可用
Parallel.ForEach或await Task.WhenAll(tasks)并行采集;注意 IO 密集型任务并发太高反而拖慢磁盘,需要控制度。
小结
三个项目分别对应 C# 三大主力场景:业务建模(类 + LINQ + JSON)、Web 后端(ASP.NET Core 最小 API)、系统脚本(文件 + 异步)。它们都用到了前面学的基础——类、集合、LINQ、异常、文件 IO——只是组合起来解决真实问题。建议你至少把其中一个改造成自己的版本(给学生加"班级"字段、给 Todo API 加鉴权、给文件工具加"按修改时间过滤"),做过一遍比读十遍更扎实。
恭喜!到这里 C# 系列就真正完结了。接下来挑一个方向深入——Web 后端(ASP.NET Core + EF Core)、游戏开发(Unity)、跨平台桌面(.NET MAUI)、云原生(.NET Aspire)——边做边学,进步最快。
← 上一篇 C# 练习题
返回 C# 教程目录