news 2026/7/22 8:34:34

C#停车场管理系统实战:面向对象编程与文件操作综合应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#停车场管理系统实战:面向对象编程与文件操作综合应用

1. 项目概述:从零到一构建一个C#停车场游戏

最近在带新人,发现很多朋友在学完C#基础语法后,面对“做一个完整项目”这个任务时,常常会感到无从下手。理论学了一堆,但不知道怎么把它们串起来,变成一个有血有肉的程序。正好,我手头有一个非常适合新手练手的项目思路——一个控制台版本的停车场管理系统,或者说,一个“停车场游戏”。

这个项目麻雀虽小,五脏俱全。它不涉及复杂的图形界面(暂时用控制台),但完整涵盖了面向对象编程的核心思想、集合的灵活运用、文件读写、简单的逻辑判断和用户交互。你可以把它想象成一个模拟经营类游戏的极简原型:你管理一个停车场,车辆进来、出去、缴费、找车位,你需要设计一套规则来让这一切有序运转。通过实现它,你能把类与对象List<T>集合、Dictionary<TKey, TValue>字典、DateTime时间处理、StreamReader/StreamWriter文件操作这些知识点,像拼乐高一样组合成一个可运行、可扩展的完整应用。这比你单独去刷十道算法题或者背二十个设计模式的定义,理解要深刻得多。

2. 核心需求与游戏规则设计

在动手写代码之前,我们必须先把游戏规则,也就是业务逻辑,想清楚。这就像盖房子先画图纸,能避免后期代码的反复重构。

2.1 实体定义:停车场里都有什么?

首先,我们需要抽象出几个核心的“实体类”。这是面向对象分析的第一步。

  1. 车辆 (Car类):这是停车场管理的基本单位。每辆车至少应该包含以下属性:

    • PlateNumber(车牌号):string类型,唯一标识一辆车。
    • Type(车辆类型):可以用enum枚举来定义,比如CarType.Small(小型车)、CarType.Medium(中型车)、CarType.Large(大型车)。不同类型的车可能占用不同数量的车位,或者收费标准不同。
    • EntryTime(入场时间):DateTime类型,记录车辆进入停车场的时间,用于计算停车时长和费用。
    • ParkedSpaceId(停放车位编号):stringint类型,记录这辆车具体停在了哪个车位上。
  2. 停车位 (ParkingSpace类):停车场由一个个停车位组成。

    • Id(车位编号):唯一标识,如A-01,B-12
    • Type(车位类型):同样可以用枚举,与车辆类型对应,例如SpaceType.Small,SpaceType.Large。一个大型车位可以停小型车,但一个小型车位不能停大型车,这里就涉及到匹配规则。
    • IsOccupied(是否被占用):bool类型,最简单直接的状态标识。
    • OccupiedCarPlate(当前停放车辆车牌号):当车位被占用时,记录车牌号,方便查询。
  3. 停车场 (ParkingLot类):这是我们的核心管理类,相当于游戏的主控制器。

    • 它应该包含一个List<ParkingSpace>,用来管理场内所有车位。
    • 包含一个Dictionary<string, Car>,以车牌号为Key,快速查找在场车辆信息。
    • 还需要记录一些全局属性,如TotalSpaces(总车位数)、AvailableSpaces(可用车位数)、HourlyRate(每小时费率)等。

2.2 核心业务流程与规则

定义了实体,接下来要规定它们如何互动,也就是游戏的主要玩法。

  1. 车辆入场 (Park In)

    • 输入:车牌号、车辆类型。
    • 逻辑:系统需要根据车辆类型,寻找一个合适的、空闲的车位(类型匹配且IsOccupied == false)。如果找到,则记录入场时间,更新车位状态为占用,并将车辆信息存入字典。如果找不到,则提示“车位已满”或“无合适车位”。
    • 输出:分配的车位编号和入场时间。
  2. 车辆出场 (Park Out)

    • 输入:车牌号。
    • 逻辑:系统根据车牌号从字典中查找车辆信息。找到后,计算停车时长(当前时间 -EntryTime),根据时长和费率计算停车费用。然后,释放对应的车位(IsOccupied = false,OccupiedCarPlate = null),并从在场车辆字典中移除该车。
    • 输出:显示停车时长、应付费用,并确认出场。
  3. 查询功能

    • 查看空余车位:遍历List<ParkingSpace>,统计并列出所有IsOccupied == false的车位。
    • 查看在场车辆:遍历Dictionary<string, Car>或根据车位列表反查,列出所有车辆信息及停放位置。
    • 根据车牌号查询车辆:直接使用字典的TryGetValue方法,高效查询。
  4. 计费规则

    • 这是游戏可玩性的关键扩展点。最初可以设定为每小时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) { // 简单的匹配规则:车位类型必须大于等于车辆类型 // 假设枚举值 Small=0, Medium=1, Large=2 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 List<ParkingSpace> _allSpaces; private Dictionary<string, 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 List<ParkingSpace>(); _parkedCars = new Dictionary<string, 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 List<ParkingSpace> GetAllSpaces() { // 返回副本以避免外部修改内部数据 return new List<ParkingSpace>(_allSpaces); } // 查询方法2:查看所有在场车辆 public List<Car> GetAllParkedCars() { return new List<Car>(_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("输入无效。"); } }

避坑指南:在HandleParkInHandleParkOut方法中,我对用户输入的车牌号进行了.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-01

5.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 引入更多游戏化元素

  1. 收入与支出系统

    • ParkingLot类中增加一个decimal TotalRevenue属性,每次成功收费后累加。
    • 增加支出项,如每日固定成本按车位数的维护费等。
    • 在菜单中增加“查看财务报表”选项,显示当日/当月的收入、支出和利润。
  2. 车位升级与扩建

    • ParkingLotInitializeSpaces方法改造为动态的。初始只有少量车位。
    • 玩家可以用赚取的利润来“升级”车位(将小型车位升级为中型),或者“购买”新的地块来扩建停车场(增加新的车位列表)。
    • 这需要引入一个“游戏状态”类,来管理资金、已解锁的地块等。
  3. 随机事件

    • 使用Random类在每次车辆入场或出场时,有一定概率触发事件。
    • 例如:“VIP客户,本次停车免费!”(收入为0)、“车辆发生轻微刮蹭,支付赔偿金200元”(支出增加)、“举办活动,临时车位利用率提升50%”(接下来一小时收入增加)。
    • 这能极大增加游戏的可玩性和不确定性。

6.2 提升代码质量与架构

  1. 使用接口与依赖注入

    • 定义一个IParkingChargeCalculator接口,包含一个CalculateFee(TimeSpan duration)方法。
    • 创建不同的计费策略类来实现它,比如StandardHourlyCalculator(标准计时)、FirstHourPremiumCalculator(首小时高价)等。
    • ParkingLot的构造函数中注入这个计算器。这样,要改变计费规则,只需要换一个计算器对象,无需修改ParkingLot的内部逻辑。这是策略模式的典型应用。
  2. 引入日志记录

    • 使用像NLogSerilog这样的日志库,或者自己写一个简单的文件日志工具。
    • 将重要的操作(入场、出场、收费、错误)都记录下来,格式为[时间][级别] 操作详情
    • 这对于调试和后期分析用户行为非常有帮助。
  3. 数据存储升级

    • 将文件存储改为使用轻量级数据库,如SQLite。你可以使用Microsoft.Data.Sqlite这个NuGet包。
    • 定义CarParkingSpace的表结构,使用SQL语句或简单的ORM(如Dapper)进行增删改查。
    • 数据库能更可靠地处理并发和数据关系,也是从“玩具项目”迈向“正经项目”的关键一步。
  4. 异常处理的完善

    • 目前我们只在文件读写处有简单的try-catch。应该对更多可能出错的地方进行防御性编程。
    • 比如,在ParkCar时,如果传入的plateNumbernull或空字符串,应该抛出ArgumentException
    • UnparkCar时,如果计算出的停车时长是负数(系统时间被篡改),应该记录一个警告或抛出InvalidOperationException

6.3 迈向图形化界面

控制台界面毕竟简陋。当你熟悉了核心逻辑后,可以尝试为其添加图形界面。

  1. Windows Forms / WPF:这是C#传统的桌面开发技术。你可以用拖拽控件的方式,快速做出一个有按钮、文本框、数据表格的窗口程序。将我们Program.cs中的控制台交互逻辑,搬到按钮的Click事件处理程序中即可。
  2. Avalonia UI:这是一个跨平台的.NET UI框架。如果你想让程序能在Windows、macOS、Linux上运行,Avalonia是一个很好的选择。你搜索的热词里就有“c#创建avalonia项目在linux环境运行”,这说明你已经关注到跨平台了。用Avalonia重写UI层,业务逻辑的ParkingLot类几乎可以原封不动地复用,这就是分层架构的好处。
  3. Blazor:如果你想尝试Web技术,可以用Blazor。它允许你使用C#来编写前端交互逻辑,并运行在浏览器中。你可以做一个Web版的停车场管理页面。

我个人在实际带新人的过程中发现,从控制台到图形界面这一步,最大的挑战不在于技术,而在于事件驱动编程思维的转变。控制台是“你问我答”的线性流程,而图形界面是“用户点了哪里,我就响应哪里”的事件流。把核心逻辑封装好,界面只是换一种方式调用它们而已。

这个停车场游戏项目,就像一块很好的敲门砖。它用到的技术点都是C#开发中最基础、最常用的部分。通过实现它,你不仅能巩固语法,更能建立起“分析需求-设计类-实现逻辑-处理交互-持久化数据”的完整开发思维链条。当你能够独立完成它,并且实现上面提到的一两个扩展功能时,你对C#基础的理解就已经超过很多只停留在理论层面的初学者了。接下来,无论是去学习更高级的异步编程、LINQ、Entity Framework,还是尝试WPF、ASP.NET Core,你都会有更扎实的底气。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/22 8:33:39

苹果M芯片如何重塑电脑耐用性与行业生态

1. 苹果M芯片Mac的耐用性革命 去年帮朋友检修一台2015款MacBook Pro时&#xff0c;我惊讶地发现这台服役7年的老机器依然能流畅运行Final Cut Pro。这种耐用性在采用Intel芯片的时代已是惊人&#xff0c;而搭载M系列芯片的新Mac更是将硬件寿命推向了新高度。我的M1 Max MacBook…

作者头像 李华
网站建设 2026/7/22 8:33:23

2026 全域营销服务商甄选完整研判体系:避开外包低效损耗

2026 年生成式 AI 全面改变用户信息检索路径&#xff0c;全域整合营销成为企业数字化标配&#xff0c;但市场各类外包机构能力差距悬殊。大量企业拆分对接短视频、软文、SEO 多类服务商&#xff0c;多渠道品牌表述互相冲突&#xff0c;AI 抓取矛盾信息后品牌推荐权重持续走低&a…

作者头像 李华
网站建设 2026/7/22 8:33:03

开发者隐形负债清理:信息过载、上下文切换与完美主义的能量管理

在技术项目管理和个人效率提升领域&#xff0c;我们常常关注代码质量、架构设计和工具链优化&#xff0c;却容易忽略那些无形中消耗我们精力和注意力的“技术债”之外的负担。这些负担并非代码中的坏味道或过时的依赖&#xff0c;而是工作习惯、信息环境和心理模式中积累的“隐…

作者头像 李华
网站建设 2026/7/22 8:31:49

TRELLIS.2:3秒照片转3D模型的技术突破

1. 项目概述&#xff1a;TRELLIS.2如何颠覆3D生成领域 微软亚洲研究院最新发布的TRELLIS.2技术&#xff0c;彻底改变了传统3D模型生成的游戏规则。这项技术能够在短短3秒内将普通照片转换为高保真3D模型&#xff0c;其核心突破在于解决了当前3D生成领域的三大痛点&#xff1a;材…

作者头像 李华
网站建设 2026/7/22 8:31:43

2026国内VR安全体验馆厂家推荐 7家主流厂商完整选型指南

选型指南速览本文梳理了VR安全体验馆厂家选型的四大核心维度&#xff0c;对比7家主流厂商的综合能力&#xff0c;适用于工地、校园、消防、公共场馆等领域的采购方参考&#xff0c;其中南京纳仕嘉凭借齐全的资质与强落地能力位列推荐序列前列。本次选型标准兼顾资质合规、落地能…

作者头像 李华
网站建设 2026/7/22 8:28:18

Cocos Creator游戏字体特效实战:从TTF导入到Shader流光与性能优化

1. 项目概述&#xff1a;为什么游戏字体值得你投入精力&#xff1f; 做游戏开发这些年&#xff0c;我越来越觉得&#xff0c;字体是游戏里最容易被忽视&#xff0c;但效果提升最立竿见影的“性价比之王”。你想想&#xff0c;一个玩家进入游戏&#xff0c;最先接触的往往是UI界…

作者头像 李华