1. 项目概述从零到一构建一个C#停车场游戏最近在带新人发现很多朋友在学完C#基础语法后面对“做一个完整项目”这个任务时常常会感到无从下手。理论学了一堆但不知道怎么把它们串起来变成一个有血有肉的程序。正好我手头有一个非常适合新手练手的项目思路——一个控制台版本的停车场管理系统或者说一个“停车场游戏”。这个项目麻雀虽小五脏俱全。它不涉及复杂的图形界面暂时用控制台但完整涵盖了面向对象编程的核心思想、集合的灵活运用、文件读写、简单的逻辑判断和用户交互。你可以把它想象成一个模拟经营类游戏的极简原型你管理一个停车场车辆进来、出去、缴费、找车位你需要设计一套规则来让这一切有序运转。通过实现它你能把类与对象、ListT集合、DictionaryTKey, TValue字典、DateTime时间处理、StreamReader/StreamWriter文件操作这些知识点像拼乐高一样组合成一个可运行、可扩展的完整应用。这比你单独去刷十道算法题或者背二十个设计模式的定义理解要深刻得多。2. 核心需求与游戏规则设计在动手写代码之前我们必须先把游戏规则也就是业务逻辑想清楚。这就像盖房子先画图纸能避免后期代码的反复重构。2.1 实体定义停车场里都有什么首先我们需要抽象出几个核心的“实体类”。这是面向对象分析的第一步。车辆 (Car类)这是停车场管理的基本单位。每辆车至少应该包含以下属性PlateNumber(车牌号)string类型唯一标识一辆车。Type(车辆类型)可以用enum枚举来定义比如CarType.Small小型车、CarType.Medium中型车、CarType.Large大型车。不同类型的车可能占用不同数量的车位或者收费标准不同。EntryTime(入场时间)DateTime类型记录车辆进入停车场的时间用于计算停车时长和费用。ParkedSpaceId(停放车位编号)string或int类型记录这辆车具体停在了哪个车位上。停车位 (ParkingSpace类)停车场由一个个停车位组成。Id(车位编号)唯一标识如A-01,B-12。Type(车位类型)同样可以用枚举与车辆类型对应例如SpaceType.Small,SpaceType.Large。一个大型车位可以停小型车但一个小型车位不能停大型车这里就涉及到匹配规则。IsOccupied(是否被占用)bool类型最简单直接的状态标识。OccupiedCarPlate(当前停放车辆车牌号)当车位被占用时记录车牌号方便查询。停车场 (ParkingLot类)这是我们的核心管理类相当于游戏的主控制器。它应该包含一个ListParkingSpace用来管理场内所有车位。包含一个Dictionarystring, Car以车牌号为Key快速查找在场车辆信息。还需要记录一些全局属性如TotalSpaces总车位数、AvailableSpaces可用车位数、HourlyRate每小时费率等。2.2 核心业务流程与规则定义了实体接下来要规定它们如何互动也就是游戏的主要玩法。车辆入场 (Park In)输入车牌号、车辆类型。逻辑系统需要根据车辆类型寻找一个合适的、空闲的车位类型匹配且IsOccupied false。如果找到则记录入场时间更新车位状态为占用并将车辆信息存入字典。如果找不到则提示“车位已满”或“无合适车位”。输出分配的车位编号和入场时间。车辆出场 (Park Out)输入车牌号。逻辑系统根据车牌号从字典中查找车辆信息。找到后计算停车时长当前时间 -EntryTime根据时长和费率计算停车费用。然后释放对应的车位IsOccupied false,OccupiedCarPlate null并从在场车辆字典中移除该车。输出显示停车时长、应付费用并确认出场。查询功能查看空余车位遍历ListParkingSpace统计并列出所有IsOccupied false的车位。查看在场车辆遍历Dictionarystring, Car或根据车位列表反查列出所有车辆信息及停放位置。根据车牌号查询车辆直接使用字典的TryGetValue方法高效查询。计费规则这是游戏可玩性的关键扩展点。最初可以设定为每小时X元不足1小时按1小时计算。后续可以扩展为首小时X元之后每小时Y元或者每日封顶费用甚至针对不同车辆类型如大型车费率更高设置不同标准。设计心得在初期设计时我建议把“车辆”和“车位”的关联设计得松耦合一些。比如Car类里记录ParkedSpaceIdParkingSpace类里记录OccupiedCarPlate。这样无论是通过车找车位还是通过车位找车都非常方便避免了在集合里进行耗时的循环查找。这是一种用空间换时间的设计思路在数据量不大时优势不明显但养成这种思维习惯很重要。3. 项目架构与核心类实现详解有了清晰的设计图我们就可以开始搭建代码的“骨架”了。我会用一个清晰的、分层的方式来组织代码虽然项目小但好的结构能让它更容易理解和扩展。3.1 定义枚举与常量首先在项目根部创建一个Enums.cs文件定义一些枚举和常量让代码更可读。// Enums.cs namespace ParkingLotGame { // 车辆类型枚举 public enum CarType { Small, // 小型车如轿车 Medium, // 中型车如SUV Large // 大型车如货车、大巴 } // 车位类型枚举 public enum SpaceType { Small, Medium, Large } // 可以在这里定义一些常量比如费率 public static class ParkingConstants { public const decimal HourlyRate 5.0m; // 每小时5元 public const int FreeMinutes 15; // 15分钟内免费 } }3.2 实现实体类接着我们创建实体类。注意属性的封装我习惯使用自动属性Auto-Property并在构造函数中进行必要的初始化。// Car.cs using System; namespace ParkingLotGame { public class Car { // 自动属性 public string PlateNumber { get; set; } public CarType Type { get; set; } public DateTime EntryTime { get; set; } public string ParkedSpaceId { get; set; } // 停靠的车位ID // 构造函数 public Car(string plateNumber, CarType type) { PlateNumber plateNumber; Type type; EntryTime DateTime.Now; // 默认入场时间为当前时间 ParkedSpaceId string.Empty; } // 一个方便的方法用于输出车辆信息 public override string ToString() { return $车牌[{PlateNumber}]类型{Type}入场时间{EntryTime:yyyy-MM-dd HH:mm:ss}车位{ParkedSpaceId ?? \未停放\}; } } }// ParkingSpace.cs namespace ParkingLotGame { public class ParkingSpace { public string Id { get; set; } public SpaceType Type { get; set; } public bool IsOccupied { get; set; } public string OccupiedCarPlate { get; set; } // 当前停放车辆的车牌 public ParkingSpace(string id, SpaceType type) { Id id; Type type; IsOccupied false; OccupiedCarPlate null; } // 停车方法 public bool ParkCar(Car car) { if (IsOccupied) return false; // 这里可以添加车位与车辆类型的匹配逻辑 // 例如大型车位可以停所有车中型车位不能停大型车等 if (!CanFitCar(car.Type)) return false; IsOccupied true; OccupiedCarPlate car.PlateNumber; car.ParkedSpaceId this.Id; // 建立双向关联 return true; } // 取车方法 public Car UnparkCar() { if (!IsOccupied) return null; // 在实际项目中这里应该通过某个车辆管理器来获取Car对象 // 我们暂时返回一个“标记”表示车已开出 IsOccupied false; OccupiedCarPlate null; return null; // 注意这里简化了实际需要返回对应的Car对象 } // 判断车位是否能停下该类型车 private bool CanFitCar(CarType carType) { // 简单的匹配规则车位类型必须大于等于车辆类型 // 假设枚举值 Small0, Medium1, Large2 return (int)this.Type (int)carType; } public override string ToString() { string status IsOccupied ? $已占用 (车牌{OccupiedCarPlate}) : 空闲; return $车位[{Id}] - 类型{Type} - 状态{status}; } } }3.3 实现核心管理类ParkingLot这是最核心的类它负责协调所有操作。我们将业务逻辑集中在这里。// ParkingLot.cs using System; using System.Collections.Generic; using System.Linq; namespace ParkingLotGame { public class ParkingLot { private ListParkingSpace _allSpaces; private Dictionarystring, Car _parkedCars; // 车牌号 - 车辆对象 private decimal _hourlyRate; public int TotalSpaces _allSpaces.Count; public int AvailableSpaces _allSpaces.Count(s !s.IsOccupied); public decimal HourlyRate { get _hourlyRate; set _hourlyRate value; } // 构造函数初始化停车场 public ParkingLot(decimal hourlyRate 5.0m) { _allSpaces new ListParkingSpace(); _parkedCars new Dictionarystring, Car(); _hourlyRate hourlyRate; InitializeSpaces(); // 初始化一些车位 } private void InitializeSpaces() { // 示例创建20个车位编号A-01到A-05为小型B-01到B-10为中型C-01到C-05为大型 for (int i 1; i 5; i) { _allSpaces.Add(new ParkingSpace($A-{i:D2}, SpaceType.Small)); } for (int i 1; i 10; i) { _allSpaces.Add(new ParkingSpace($B-{i:D2}, SpaceType.Medium)); } for (int i 1; i 5; i) { _allSpaces.Add(new ParkingSpace($C-{i:D2}, SpaceType.Large)); } Console.WriteLine($停车场初始化完成共有{TotalSpaces}个车位。); } // 核心方法1车辆入场 public (bool success, string message, string spaceId) ParkCar(string plateNumber, CarType carType) { // 1. 检查车辆是否已在场 if (_parkedCars.ContainsKey(plateNumber)) { return (false, $车牌号 {plateNumber} 的车辆已在停车场内。, null); } // 2. 创建车辆对象 Car newCar new Car(plateNumber, carType); // 3. 寻找合适的空闲车位 ParkingSpace targetSpace _allSpaces.FirstOrDefault(s !s.IsOccupied s.CanFitCar(carType)); if (targetSpace null) { return (false, 抱歉当前没有合适的空闲车位。, null); } // 4. 执行停车操作 bool parkResult targetSpace.ParkCar(newCar); if (!parkResult) { return (false, 停车失败未知原因。, null); } // 5. 登记车辆信息 _parkedCars.Add(plateNumber, newCar); newCar.ParkedSpaceId targetSpace.Id; // 确保关联 return (true, $车辆 [{plateNumber}] 停放成功车位{targetSpace.Id}入场时间{newCar.EntryTime:HH:mm:ss}, targetSpace.Id); } // 核心方法2车辆出场并计费 public (bool success, string message, decimal fee) UnparkCar(string plateNumber) { // 1. 查找车辆 if (!_parkedCars.TryGetValue(plateNumber, out Car car)) { return (false, $未找到车牌号 {plateNumber} 的在场车辆。, 0); } // 2. 找到对应的车位 ParkingSpace space _allSpaces.FirstOrDefault(s s.Id car.ParkedSpaceId); if (space null || !space.IsOccupied) { // 数据不一致这是一个错误状态需要处理 _parkedCars.Remove(plateNumber); return (false, 车辆数据异常已强制出场。, 0); } // 3. 计算停车费用 DateTime exitTime DateTime.Now; TimeSpan parkingDuration exitTime - car.EntryTime; decimal fee CalculateFee(parkingDuration); // 4. 释放车位 space.UnparkCar(); // 5. 从登记簿中移除车辆 _parkedCars.Remove(plateNumber); return (true, $车辆 [{plateNumber}] 出场成功。停车时长{parkingDuration:hh\\:mm} 应付费用{fee:C2}。, fee); } // 计费方法 private decimal CalculateFee(TimeSpan duration) { // 免费时段判断 if (duration.TotalMinutes ParkingConstants.FreeMinutes) { return 0.0m; } // 计算完整的小时数不足1小时按1小时算 int totalHours (int)Math.Ceiling(duration.TotalHours); return totalHours * _hourlyRate; } // 查询方法1查看所有车位状态 public ListParkingSpace GetAllSpaces() { // 返回副本以避免外部修改内部数据 return new ListParkingSpace(_allSpaces); } // 查询方法2查看所有在场车辆 public ListCar GetAllParkedCars() { return new ListCar(_parkedCars.Values); } // 查询方法3查找特定车辆 public Car FindCarByPlate(string plateNumber) { _parkedCars.TryGetValue(plateNumber, out Car car); return car; } } }实操心得在ParkingLot类中我使用了C#的元组(bool success, string message, ...)作为多个方法的返回值。这在C# 7.0之后是非常方便的特性可以让我们在不定义新类的情况下返回多个相关的值特别适合这种需要返回操作结果和附带信息的场景。比起用out参数或者单独封装一个Result类代码更简洁直观。4. 控制台交互与游戏主循环实现有了后台引擎我们需要一个前台界面来和玩家用户交互。这里我们用控制台应用程序来实现一个简单的菜单驱动界面。4.1 主程序入口与菜单设计创建一个Program.cs文件作为应用程序的入口点。// Program.cs using System; using System.Linq; namespace ParkingLotGame { class Program { static ParkingLot parkingLot; static void Main(string[] args) { Console.WriteLine( C# 基础停车场管理模拟游戏 ); Console.WriteLine(作者一位乐于分享的老码农); Console.WriteLine(-----------------------------------); // 初始化停车场可以在这里设置费率 parkingLot new ParkingLot(ParkingConstants.HourlyRate); Console.WriteLine($当前费率{ParkingConstants.HourlyRate:C2}/小时{ParkingConstants.FreeMinutes}分钟内免费。\n); bool isRunning true; while (isRunning) { ShowMainMenu(); string input Console.ReadLine(); Console.WriteLine(); switch (input) { case 1: HandleParkIn(); break; case 2: HandleParkOut(); break; case 3: ShowParkingStatus(); break; case 4: SearchCar(); break; case 5: ShowAllCars(); break; case 6: // 扩展功能手动模拟时间流逝用于测试计费 SimulateTimePass(); break; case 0: isRunning false; Console.WriteLine(感谢使用停车场模拟系统再见); break; default: Console.WriteLine(输入无效请重新选择。); break; } Console.WriteLine(\n按任意键继续...); Console.ReadKey(); Console.Clear(); } } static void ShowMainMenu() { Console.WriteLine( 主菜单 ); Console.WriteLine(1. 车辆入场); Console.WriteLine(2. 车辆出场); Console.WriteLine(3. 查看车位状态); Console.WriteLine(4. 查询车辆信息); Console.WriteLine(5. 查看所有在场车辆); Console.WriteLine(6. [测试] 模拟时间流逝); Console.WriteLine(0. 退出系统); Console.WriteLine(); Console.Write(请选择操作 (0-6): ); } } }4.2 核心交互功能实现接下来我们在Program.cs中补充各个菜单选项对应的具体处理方法。// 在 Program.cs 类中继续添加以下方法 static void HandleParkIn() { Console.WriteLine(--- 车辆入场 ---); Console.Write(请输入车牌号: ); string plate Console.ReadLine()?.Trim().ToUpper(); // 统一转为大写便于比较 if (string.IsNullOrEmpty(plate)) { Console.WriteLine(车牌号不能为空。); return; } Console.WriteLine(请选择车辆类型: ); Console.WriteLine(1. 小型车); Console.WriteLine(2. 中型车); Console.WriteLine(3. 大型车); Console.Write(请选择 (1-3): ); string typeInput Console.ReadLine(); CarType carType; switch (typeInput) { case 1: carType CarType.Small; break; case 2: carType CarType.Medium; break; case 3: carType CarType.Large; break; default: Console.WriteLine(类型选择无效。); return; } var result parkingLot.ParkCar(plate, carType); Console.WriteLine(result.message); } static void HandleParkOut() { Console.WriteLine(--- 车辆出场 ---); Console.Write(请输入出场车辆的车牌号: ); string plate Console.ReadLine()?.Trim().ToUpper(); if (string.IsNullOrEmpty(plate)) { Console.WriteLine(车牌号不能为空。); return; } var result parkingLot.UnparkCar(plate); Console.WriteLine(result.message); if (result.success result.fee 0) { Console.WriteLine($请支付 {result.fee:C2}。); } } static void ShowParkingStatus() { Console.WriteLine(--- 车位状态概览 ---); var spaces parkingLot.GetAllSpaces(); int total parkingLot.TotalSpaces; int available parkingLot.AvailableSpaces; Console.WriteLine($车位总数: {total} 空闲车位: {available} 占用率: {((total - available) / (double)total):P0}\n); // 按车位类型分组显示 var groupedSpaces spaces.GroupBy(s s.Type).OrderBy(g g.Key); foreach (var group in groupedSpaces) { Console.WriteLine($[{group.Key}型车位]); foreach (var space in group) { Console.WriteLine($ {space}); } Console.WriteLine(); } } static void SearchCar() { Console.WriteLine(--- 查询车辆 ---); Console.Write(请输入要查询的车牌号: ); string plate Console.ReadLine()?.Trim().ToUpper(); var car parkingLot.FindCarByPlate(plate); if (car ! null) { Console.WriteLine(找到车辆信息); Console.WriteLine(car); // 计算如果现在出场需要多少费用 TimeSpan duration DateTime.Now - car.EntryTime; decimal potentialFee parkingLot.HourlyRate * (int)Math.Ceiling(duration.TotalHours); if (duration.TotalMinutes ParkingConstants.FreeMinutes) potentialFee 0; Console.WriteLine($如现在出场预估费用{potentialFee:C2}); } else { Console.WriteLine(未找到该车辆可能已出场或从未入场。); } } static void ShowAllCars() { Console.WriteLine(--- 所有在场车辆 ---); var cars parkingLot.GetAllParkedCars(); if (cars.Count 0) { Console.WriteLine(停车场内暂无车辆。); return; } foreach (var car in cars.OrderBy(c c.EntryTime)) // 按入场时间排序 { Console.WriteLine(car); } Console.WriteLine($共计 {cars.Count} 辆车。); } static void SimulateTimePass() { // 这是一个用于测试的调试功能可以快速验证计费逻辑 Console.WriteLine(--- 模拟时间流逝 ---); Console.Write(请输入要向前推进的小时数 (例如 1.5): ); if (double.TryParse(Console.ReadLine(), out double hours) hours 0) { // 注意这个操作会修改所有在场车辆的入场时间仅用于演示。 // 在实际项目中我们不会直接修改历史数据而是记录一个“模拟当前时间”的变量。 Console.WriteLine($警告此操作将修改系统内所有车辆的入场时间向前推进 {hours} 小时。仅用于测试(Y/N)); if (Console.ReadLine()?.ToUpper() Y) { var cars parkingLot.GetAllParkedCars(); foreach (var car in cars) { // 这里通过反射或其他方式修改私有字段仅作演示。 // 更规范的做法是在Car类中提供一个用于测试的“时间旅行”方法或者使用一个独立的模拟时钟服务。 Console.WriteLine($车辆 {car.PlateNumber} 的入场时间从 {car.EntryTime} 被修改。); // 实际修改代码需要调整Car类的EntryTime的setter或使用测试专用接口此处省略具体实现。 } Console.WriteLine(时间模拟完成。); } } else { Console.WriteLine(输入无效。); } }避坑指南在HandleParkIn和HandleParkOut方法中我对用户输入的车牌号进行了.Trim().ToUpper()处理。这是一个非常实用的小技巧。.Trim()可以去掉用户无意中输入的首尾空格.ToUpper()将车牌号统一为大写。这样做可以避免因为大小写或空格不一致导致的查询失败比如用户输入“京A12345”和“京a12345”会被系统视为同一个车牌。在实际业务中这种数据清洗和规范化是保证系统健壮性的重要一环。5. 数据持久化让游戏状态得以保存目前我们的停车场数据都保存在内存中程序一关闭就全没了。对于一个完整的应用来说数据持久化是必须的。我们可以选择最简单的方式文本文件存储。5.1 设计数据存储格式我们需要保存两类核心数据车位状态和在场车辆信息。为了简单我们可以用CSV逗号分隔值格式。车位状态文件 (spaces.csv)每行记录一个车位的状态。Id,Type,IsOccupied,OccupiedCarPlate A-01,Small,False, B-01,Medium,True,京A12345车辆信息文件 (cars.csv)每行记录一辆在场车辆的信息。PlateNumber,Type,EntryTime,ParkedSpaceId 京A12345,Medium,2023-10-27 14:30:00,B-015.2 实现保存与加载功能我们在ParkingLot类中增加两个方法。// 在 ParkingLot.cs 类中添加以下方法 using System.IO; using System.Text; public class ParkingLot { // ... 之前的代码不变 ... private readonly string _spacesFilePath spaces.csv; private readonly string _carsFilePath cars.csv; // 保存数据到文件 public void SaveData() { try { // 保存车位信息 using (StreamWriter writer new StreamWriter(_spacesFilePath, false, Encoding.UTF8)) { writer.WriteLine(Id,Type,IsOccupied,OccupiedCarPlate); // 表头 foreach (var space in _allSpaces) { writer.WriteLine(${space.Id},{space.Type},{space.IsOccupied},{space.OccupiedCarPlate ?? }); } } // 保存车辆信息 using (StreamWriter writer new StreamWriter(_carsFilePath, false, Encoding.UTF8)) { writer.WriteLine(PlateNumber,Type,EntryTime,ParkedSpaceId); foreach (var car in _parkedCars.Values) { // 注意DateTime需要转换为可稳定解析的字符串格式 writer.WriteLine(${car.PlateNumber},{car.Type},{car.EntryTime:O},{car.ParkedSpaceId}); } } Console.WriteLine( 停车场数据已保存。); } catch (Exception ex) { Console.WriteLine($ 保存数据时出错: {ex.Message}); } } // 从文件加载数据 public void LoadData() { try { _allSpaces.Clear(); _parkedCars.Clear(); // 加载车位信息 if (File.Exists(_spacesFilePath)) { var lines File.ReadAllLines(_spacesFilePath, Encoding.UTF8); // 跳过表头行 for (int i 1; i lines.Length; i) { var parts lines[i].Split(,); if (parts.Length 4) { var space new ParkingSpace(parts[0], (SpaceType)Enum.Parse(typeof(SpaceType), parts[1])); space.IsOccupied bool.Parse(parts[2]); space.OccupiedCarPlate string.IsNullOrEmpty(parts[3]) ? null : parts[3]; _allSpaces.Add(space); } } } else { // 文件不存在使用默认初始化 InitializeSpaces(); } // 加载车辆信息 if (File.Exists(_carsFilePath)) { var lines File.ReadAllLines(_carsFilePath, Encoding.UTF8); for (int i 1; i lines.Length; i) { var parts lines[i].Split(,); if (parts.Length 4) { var car new Car(parts[0], (CarType)Enum.Parse(typeof(CarType), parts[1])); // 使用 DateTime.ParseExact 或 TryParse 来安全解析 ISO 8601 格式 if (DateTime.TryParse(parts[2], out DateTime entryTime)) car.EntryTime entryTime; car.ParkedSpaceId parts[3]; // 将车辆添加到字典 _parkedCars[car.PlateNumber] car; // 需要将车辆与车位关联起来因为车位加载时已经记录了车牌 // 这里主要是为了同步 Car 对象中的 ParkedSpaceId实际上在 Load Spaces 时车位状态已经恢复。 // 如果车位状态显示被占用但这里没有对应的车说明数据不一致需要处理。 var correspondingSpace _allSpaces.FirstOrDefault(s s.Id car.ParkedSpaceId); if (correspondingSpace ! null correspondingSpace.OccupiedCarPlate car.PlateNumber) { // 关联正确无需额外操作 } else { // 数据不一致这是一个需要修复的错误状态 Console.WriteLine($ 警告车辆 {car.PlateNumber} 与车位 {car.ParkedSpaceId} 的关联数据不一致。); } } } } Console.WriteLine($ 停车场数据加载完成。共有{_allSpaces.Count}个车位{_parkedCars.Count}辆在场车。); } catch (Exception ex) { Console.WriteLine($ 加载数据时出错: {ex.Message}将使用默认配置。); // 出错时清空数据并重新初始化 _allSpaces.Clear(); _parkedCars.Clear(); InitializeSpaces(); } } }然后我们需要修改Program.cs中的Main方法在程序启动时加载数据在程序退出时保存数据。// 修改 Program.cs 中的 Main 方法 static void Main(string[] args) { Console.WriteLine( C# 基础停车场管理模拟游戏 ); // ... 其他初始化 ... // 初始化停车场 parkingLot new ParkingLot(ParkingConstants.HourlyRate); // 尝试加载之前保存的数据 parkingLot.LoadData(); Console.WriteLine($当前费率{ParkingConstants.HourlyRate:C2}/小时{ParkingConstants.FreeMinutes}分钟内免费。\n); bool isRunning true; while (isRunning) { // ... 菜单循环 ... } // 在退出循环后程序结束前保存数据 parkingLot.SaveData(); Console.WriteLine(感谢使用停车场模拟系统再见); }注意事项文件读写是I/O操作可能会因为文件被占用、权限不足、磁盘已满等原因失败。因此务必使用try-catch块包裹并给用户友好的提示。另外我们使用了Encoding.UTF8来确保中文车牌号能正确保存和读取。DateTime的存储格式我选择了O往返日期/时间模式这是一种标准的ISO 8601字符串可以被DateTime.Parse稳定地解析回来避免了不同文化区域设置带来的问题。6. 功能扩展与优化思路一个基础版本完成后我们可以思考如何让它变得更像一个“游戏”或者更实用的“工具”。这里提供几个扩展方向你可以选择一两个来实现作为进阶练习。6.1 引入更多游戏化元素收入与支出系统在ParkingLot类中增加一个decimal TotalRevenue属性每次成功收费后累加。增加支出项如每日固定成本、按车位数的维护费等。在菜单中增加“查看财务报表”选项显示当日/当月的收入、支出和利润。车位升级与扩建将ParkingLot的InitializeSpaces方法改造为动态的。初始只有少量车位。玩家可以用赚取的利润来“升级”车位将小型车位升级为中型或者“购买”新的地块来扩建停车场增加新的车位列表。这需要引入一个“游戏状态”类来管理资金、已解锁的地块等。随机事件使用Random类在每次车辆入场或出场时有一定概率触发事件。例如“VIP客户本次停车免费”收入为0、“车辆发生轻微刮蹭支付赔偿金200元”支出增加、“举办活动临时车位利用率提升50%”接下来一小时收入增加。这能极大增加游戏的可玩性和不确定性。6.2 提升代码质量与架构使用接口与依赖注入定义一个IParkingChargeCalculator接口包含一个CalculateFee(TimeSpan duration)方法。创建不同的计费策略类来实现它比如StandardHourlyCalculator标准计时、FirstHourPremiumCalculator首小时高价等。在ParkingLot的构造函数中注入这个计算器。这样要改变计费规则只需要换一个计算器对象无需修改ParkingLot的内部逻辑。这是策略模式的典型应用。引入日志记录使用像NLog或Serilog这样的日志库或者自己写一个简单的文件日志工具。将重要的操作入场、出场、收费、错误都记录下来格式为[时间][级别] 操作详情。这对于调试和后期分析用户行为非常有帮助。数据存储升级将文件存储改为使用轻量级数据库如SQLite。你可以使用Microsoft.Data.Sqlite这个NuGet包。定义Car和ParkingSpace的表结构使用SQL语句或简单的ORM如Dapper进行增删改查。数据库能更可靠地处理并发和数据关系也是从“玩具项目”迈向“正经项目”的关键一步。异常处理的完善目前我们只在文件读写处有简单的try-catch。应该对更多可能出错的地方进行防御性编程。比如在ParkCar时如果传入的plateNumber为null或空字符串应该抛出ArgumentException。在UnparkCar时如果计算出的停车时长是负数系统时间被篡改应该记录一个警告或抛出InvalidOperationException。6.3 迈向图形化界面控制台界面毕竟简陋。当你熟悉了核心逻辑后可以尝试为其添加图形界面。Windows Forms / WPF这是C#传统的桌面开发技术。你可以用拖拽控件的方式快速做出一个有按钮、文本框、数据表格的窗口程序。将我们Program.cs中的控制台交互逻辑搬到按钮的Click事件处理程序中即可。Avalonia UI这是一个跨平台的.NET UI框架。如果你想让程序能在Windows、macOS、Linux上运行Avalonia是一个很好的选择。你搜索的热词里就有“c#创建avalonia项目在linux环境运行”这说明你已经关注到跨平台了。用Avalonia重写UI层业务逻辑的ParkingLot类几乎可以原封不动地复用这就是分层架构的好处。Blazor如果你想尝试Web技术可以用Blazor。它允许你使用C#来编写前端交互逻辑并运行在浏览器中。你可以做一个Web版的停车场管理页面。我个人在实际带新人的过程中发现从控制台到图形界面这一步最大的挑战不在于技术而在于事件驱动编程思维的转变。控制台是“你问我答”的线性流程而图形界面是“用户点了哪里我就响应哪里”的事件流。把核心逻辑封装好界面只是换一种方式调用它们而已。这个停车场游戏项目就像一块很好的敲门砖。它用到的技术点都是C#开发中最基础、最常用的部分。通过实现它你不仅能巩固语法更能建立起“分析需求-设计类-实现逻辑-处理交互-持久化数据”的完整开发思维链条。当你能够独立完成它并且实现上面提到的一两个扩展功能时你对C#基础的理解就已经超过很多只停留在理论层面的初学者了。接下来无论是去学习更高级的异步编程、LINQ、Entity Framework还是尝试WPF、ASP.NET Core你都会有更扎实的底气。