资讯详情

C#电梯模拟系统实战:状态机、调度算法与门机联锁全解析

📅 2026/10/18 2:29:30 | 华诺云谱 👁 阅读
C#电梯模拟系统实战:状态机、调度算法与门机联锁全解析
简介C#模拟电梯控制源码是一份面向C#学习者及桌面应用开发者的完整项目示例聚焦如何用类、事件、委托、多线程与状态机实现电梯调度。内含完整可编译的Visual Studio解决方案涵盖6个cs源文件、2个resx界面资源、3个resources资源文件、3个exe可执行程序及sln/csproj工程配置压缩包共27个文件、约246KB。代码对楼层按钮触发、电梯上下行、开关门状态切换、异常捕获与GUI实时显示均有实现便于读者跟随源码理解调度算法与线程同步的写法。已有283人学习浏览适合正在学习委托事件、多线程同步或希望练习Windows Forms界面开发的初中级C#开发者可作为系统设计参考并直接复用其中模块。1. 模拟电梯系统从“电梯飞过不停”到门机联锁全流程实现第一次跑通这套 C# 电梯模拟程序的时候我盯着控制台里的“电梯在 6 楼飞过3 楼的人还按着上行键不撒手”第一反应是“这什么玄学”。后来把日志一行行捞出来才发现不是随机 bug是调度策略里没有做“顺路捎带”的判断电梯严格按先来先服务跑自然会把同方向的中间层请求跳过去。这套 C# 模拟电梯控制源码的价值就在这它把一个电梯系统该有的状态机、请求队列、调度策略、门机联锁、多线程刷新全部用代码显式表达出来了适合两类人——刚学完 C# 面向对象想做点硬核小项目的同学以及在真实工控项目里被电梯逻辑折磨过、想快速搭一个可调试模型的开发者。你改一个参数就能看到呼梯、派梯、开关门、楼层对齐的完整链路在日志里重新走一遍。2. 系统架构与工程结构把电梯当成一台状态机而不是一堆 if拿到源码先别急着跑花十分钟把工程结构摸一遍后面改调度、加楼层都顺手。这套模拟系统不是单文件堆出来的演示程序而是按“界面层—调度层—设备层”三层拆开的每一层都能独立替换。2.1 技术选型为什么用 C# WinForms 而不是控制台源码基于 .NET Framework 4.7.2 开发UI 用的是 WinForms。选 WinForms 而不是 WPF是因为它的事件模型和 Timer 控件对这种“周期刷新 按钮触发”的模拟场景特别契合改一行label.Text就能看到电梯位置跳变调试成本比 WPF 的绑定模型低得多。内部模拟用的并不是真正的多线程电梯——每台电梯一个Thread跑while(true)循环而是用一个全局的System.Windows.Forms.Timer做 100ms 周期的心跳每次 tick 驱动所有电梯状态机前进一步。这样做的直接好处是不会出现多个线程同时改 UI 导致控件崩溃所有状态变更都收敛到 UI 线程里顺序执行。要说代价也有就是电梯数量大了之后Timer 回调里要做的事情变多后面在“避坑”章节我会专门讲这个性能拐点。2.2 目录结构与核心类职责ElevatorSimulator/ ├── Program.cs // 入口创建 MainForm 并启动 ├── Forms/ │ ├── MainForm.cs // 主界面楼层按钮、电梯状态展示 │ └── ConfigDialog.cs // 运行参数楼层数、电梯数、载重 ├── Core/ │ ├── Elevator.cs // 单台电梯状态机、运行逻辑、载荷 │ ├── ElevatorController.cs // 调度中枢接收请求、分配电梯 │ ├── Request.cs // 请求实体方向、楼层、来源 │ ├── Floor.cs // 楼层实体上下行按钮状态 │ └── Enums.cs // 电梯状态、方向、门状态枚举 ├── Scheduling/ │ ├── IAllocator.cs // 派梯策略接口 │ ├── FirstComeFirstServe.cs // 先来先服务策略 │ └── ScanSweep.cs // 扫掠式策略顺路捎带 ├── Utils/ │ ├── Logger.cs // 文本日志输出 │ └── Config.cs // 全局配置读取 └── Resources/ ├── Audio/ // 开关门提示音可选 └── Icons/ // 按钮图标Core目录里Elevator.cs是最核心的类。它的公开属性包括CurrentFloor、Direction、State、PassengerCount、MaxCapacity内部维护一个Listint Destination表示当前要去的目标楼层。Enums.cs里的ElevatorState枚举定义了IDLE、RUNNING、OPENING、CLOSING、DOOR_BLOCKED五个状态——不要小看这个枚举门机联锁的很多逻辑就是靠状态转换来保证的。IAllocator.cs是一个只有三个方法的接口public interface IAllocator { // 新楼层请求到达时决定派哪部电梯去处理 int Allocate(ListElevator elevators, Request request); // 电梯运行过程中判断当前楼层是否应该停靠 bool ShouldStop(Elevator elevator, int floor, Direction direction); // 电梯到达目标楼层后扫描是否有同方向顺路请求 Listint PickupAlongTheWay(Elevator elevator, ListFloor floors); }所有调度策略都实现这个接口主程序里通过Config.AllocatorType反射创建具体实例。这意味着你换调度算法时不需要动Elevator.cs一行代码这就是接口抽象的价值——当年我做第一个版本时把调度逻辑直接写在ElevatorController里每改一次策略就要重新测一遍整个电梯上下行血泪教训。2.3 运行时序一个请求从按钮到电梯启动的完整链路楼层按钮按下后事件调用链是这样的MainForm里楼层按钮的Click事件创建Request对象加入ElevatorController.PendingQueue调度器在下一个 Timer tick 里检测到队列非空调用IAllocator.Allocate选择一辆电梯被选中的电梯从IDLE转为RUNNINGDestination里加入目标楼层电梯每移动一层Elevator.MoveStep()被调用一次内部检查ShouldStop到达目标楼层后状态机切到OPENING停留 1.5 秒模拟乘客进出再切CLOSING关闭完成后检查PendingQueue和Destination若还有任务则继续跑否则回到IDLE。有意思的是第 4 步的MoveStep里藏了一个“楼层对齐”的细节。它并不是每次心跳简单地把CurrentFloor 1而是先根据目标方向判断应该加一还是减一再检查当前CurrentFloor是否处于两个整数楼层之间——因为电梯从 3 层到 4 层的物理过程需要多拍心跳完成MoveStep通过UpdateProgress()维护一个float Position只有Math.Abs(Position - floor) 0.05f时才认为“到达整数楼层”这个浮点比较逻辑直接影响了后面日志里的停靠精度。3. 核心模块拆解电梯状态机与调度算法的实现细节如果你只打算读一个文件那就是Core/Elevator.cs。整个电梯系统的正确性都建立在状态机的严格控制上任何状态跳变都有前置条件违反前置条件的调用直接抛异常绝不静默容错。这种设计最直接的效果就是——bug 爆得快定位也快。3.1 电梯状态机五个状态与合法转换路径public enum ElevatorState { IDLE, // 待机停在某层 RUNNING, // 正在移动 OPENING, // 车门打开中 CLOSING, // 车门关闭中 DOOR_BLOCKED // 门被异物阻挡 } public class Elevator { public ElevatorState State { get; private set; } public int CurrentFloor { get; private set; } public Direction Direction { get; private set; } private Listint _destinations new Listint(); private Timer _doorTimer; // 唯一合法的状态迁移入口 public bool TryChangeState(ElevatorState newState, out string reason) { switch (newState) { case ElevatorState.RUNNING: if (State ! ElevatorState.IDLE State ! ElevatorState.CLOSING) { reason 电梯只能在待机或关门完成后启动; return false; } if (_destinations.Count 0) { reason 没有目标楼层电梯无法启动; return false; } break; case ElevatorState.OPENING: if (State ! ElevatorState.RUNNING State ! ElevatorState.IDLE) { reason 开门只能在运行到达或待机时触发; return false; } if (Direction ! Direction.STATIONARY _destinations.Contains(CurrentFloor) false) { reason 未到达目标楼层禁止开门; return false; } break; } State newState; reason string.Empty; return true; } public void MoveStep() { if (State ! ElevatorState.RUNNING) return; // 向目标方向移动半层内部 Position 为 float float step 0.5f; Position (Direction Direction.UP) ? step : -step; // 跨过整数楼层时判断是否停靠 int floor (int)Math.Round(Position); if (Math.Abs(Position - floor) 0.05f) { CurrentFloor floor; if (_controller.ShouldStop(this, CurrentFloor, Direction)) { _destinations.Remove(CurrentFloor); TryChangeState(ElevatorState.OPENING, out _); } } } }TryChangeState是所有状态跳变的唯一入口它把合法性校验集中在一个方法里而不是散落在各处 if。比如从IDLE直接跳OPENING是允许的——电梯待机时乘客按了本层开门键但从RUNNING直接跳CLOSING是不可能的必须先到OPENING再关闭。这套约束把“门开着还往上跑”这类致命错误挡在了编译期之后的第一道运行时防线。参数说明MoveStep里step 0.5f表示每次心跳移动半层即电梯从 3 层到 4 层需要 2 个 tick200ms。如果你想模拟更快或更慢的电梯把这个值改成 1.0f 或 0.25f 即可注意它会影响ShouldStop的判断粒度——步长越细对“目标楼层在正中间停靠”的处理越平滑。3.2 扫掠式调度从先来先服务到顺路捎带默认配置用的调度策略是ScanSweep也就是电梯沿一个方向走到底途中响应所有同方向请求到顶后反向。这种策略在单台电梯场景下效率极高但实现起来比FirstComeFirstServe复杂得多。public class ScanSweep : IAllocator { // 派梯优先选择“顺路且离得近”的电梯 public int Allocate(ListElevator elevators, Request request) { // 第一轮找同方向且目标楼层在前方的电梯 foreach (var e in elevators) { if (e.State ElevatorState.RUNNING e.Direction request.Direction IsInFront(e, request.FromFloor)) { return elevators.IndexOf(e); } } // 第二轮找最近空闲的电梯 int best -1; int minDistance int.MaxValue; foreach (var e in elevators) { if (e.State ElevatorState.IDLE) { int dist Math.Abs(e.CurrentFloor - request.FromFloor); if (dist minDistance) { minDistance dist; best elevators.IndexOf(e); } } } // 第三轮实在没有就派一台反向的会先跨过请求层再折返 return best 0 ? best : elevators.FindIndex(e e.Direction Direction.STATIONARY); } // 运行中判断是否停靠同方向中间层或者反向折返点 public bool ShouldStop(Elevator e, int floor, Direction dir) { if (e._destinations.Contains(floor)) return true; // 顺路捎带楼层上有同方向请求 var floorRequest GetFloorRequest(floor); return floorRequest ! null floorRequest.Direction dir; } private bool IsInFront(Elevator e, int targetFloor) { return e.Direction Direction.UP ? e.CurrentFloor targetFloor : e.CurrentFloor targetFloor; } }这里最精妙的是Allocate的三轮降级策略第一轮把“正在运行且顺路”的电梯选出去这是效率最高的如果找不到退而求其次找空闲电梯最后才允许“反向跨过”。那台反向电梯需要先到顶层再折返才能接人看起来绕路但好过让人一直等着。ShouldStop里的顺路捎带逻辑是这套系统灵活性的核心。注意它没有改动Elevator的任何状态属性只是通过接口方法让外部策略决定“停不停”。我做二次开发时用这个接口把“高峰时段跳过 1 楼以下楼层”的规则加了进去直接改返回条件就生效了。3.3 UI 与模拟核心的通信Timer 驱动下的数据刷新WinForms 的Timer不同于Thread.Sleep自旋它依赖 Windows 消息泵不会因为模拟逻辑卡死把 UI 冻结。MainForm里定义了一个 100ms 周期的Timer它的Tick事件做三件事推进电梯状态机、刷新界面楼层指示、把日志行写入Logger。private void SimTick(object sender, EventArgs e) { // 第一步处理新请求把楼层按钮产生的 Request 派发给调度器 _controller.ProcessPendingRequests(); // 第二步驱动所有电梯走一步 foreach (var elevator in _elevators) { elevator.MoveStep(); elevator.UpdateDoorState(); } // 第三步日志输出记录当前状态快照 if (_logTick % 5 0) // 每 500ms 记录一次避免日志太碎 { Logger.Info($tick{_logTick}, elev1_floor{_elevators[0].CurrentFloor}, $elev1_state{_elevators[0].State}, pending{_controller.PendingCount}); } // 第四步刷新 UI只更新变化的楼层标签 UpdateFloorIndicators(); }_logTick % 5 0这个写法是我自己加的小习惯。全量日志在 10 台电梯、12 层楼的情况下每个 tick 会产生 50 行左右输出一分钟就是 3 万行信息量太大反而找不到问题。调成每 5 tick 一次快照既能看到运行轨迹又不会把文件写到几十兆。UI 刷新用了差异更新而不是全量刷新——每次只比较_lastShownFloor和当前楼层变了才改Label.Text。这对 WinForms 来说影响不大但到了 UWP 或 WPF 项目里这就是性能优劣的分水岭。4. 源码运行与调试从编译到日志分析一条龙源码拿到手能不能跑起来九成看环境剩下看配置。我把踩过的坑和标准步骤整理在这一章照着来基本一次过。4.1 环境要求与启动步骤开发环境是 Visual Studio 2019 或 2022需要安装“.NET 桌面开发”工作负载。没有装 VS 的同学用dotnet run也可以但需要把.csproj里的目标框架改为net6.0-windows并启用UseWindowsFormsWinForms 项目在命令行下跑需要额外引用System.Windows.Forms包这一步我一般直接选 VS 省心。标准启动过程git clone https://github.com/your-repo/elevator-simulator.git cd elevator-simulator # 用 Visual Studio 打开 ElevatorSimulator.sln直接 F5 运行运行后主界面弹出左侧是 12 层楼的呼梯按钮右侧是 3 台电梯的实时状态卡片当前楼层、方向、载重百分比、门状态。点击任意楼层的上/下行按钮会看到调度器在 100ms 内派出一台电梯状态卡片的“状态”字段从IDLE变RUNNING楼层数字开始逐层跳变。第一次跑建议做一件小事把Utils/Config.cs里的LogToConsole改为true。这会启动一个 Debug 窗格每 500ms 打一行电梯状态快照。这样你能同时看到 UI 动画和文本日志对“电梯为什么在这层停下”这种问题日志远比动画直观。4.2 配置文件参数与调优试验Config.cs是全局配置的集中地我摘出几个关键项参数默认值说明FloorCount12总楼层数改多少都行ElevatorCount3电梯数量过多会导致调度争抢MoveStepSize0.5f每 tick 移动层数调大电梯变快DoorOpenTime1500开门保持时间毫秒AllocatorTypeScanSweep调度策略切到FirstComeFirstServe看对比Capacity8载重上限超载时电梯门不关做对比试验时我通常开两个实例一个跑ScanSweep另一个跑FirstComeFirstServe在相同楼层请求序列下比较“平均等待时间”。源码里没有现成统计面板我加了一个StatsCollector每 10 秒输出一次平均响应楼层数和最长等待时间这个字段就写在MainForm的_statsTimer里30 行代码就能接好。4.3 日志文件与状态追踪日志默认写到logs/sim_yyyyMMdd_HHmmss.txt。它的格式固定为tick, floor, state, direction, passengers, pending六个字段可以用 Excel 或 Python 脚本画成电梯轨迹图。我看过最有用的可视化是把三台电梯的floor字段画成三条折线x 轴是时间y 轴是楼层一眼能看出“3 号电梯为什么总在 8 层附近游走”。日志滚动策略是Logger里按 5MB 切分超出自动新建文件。这个大小在模拟 24 小时连续运行时够用不占内存。5. 避坑与调试记录电梯模拟里最容易翻车的五个点这套代码在模拟环境里跑得很顺但一旦你开始改参数或者加功能几个隐蔽的雷区就会炸出来。我把它当成“必须知道的黑匣子”遇到问题先翻这条清单。5.1 电梯过冲目标楼层被越过但没触发开门现象电梯从 5 楼到 7 楼日志里CurrentFloor从 5 跳到 7但 6 楼的请求没响应ShouldStop返回 true 也不开门。原因MoveStep()里Position跨过 6 楼时Math.Abs(Position - floor) 0.05f的判断失败——因为Position直接加了 0.5f6.0 这个值只会在6.0f这一瞬间出现而检查发生在6.25f时已经错过了停靠窗口。解决把ShouldStop的判断前提改为“当前CurrentFloor位于请求楼层的前一个整数楼层和当前整数楼层之间”即floor CurrentFloor Math.Abs(Position - CurrentFloor) 0.5f。我后来干脆在Elevator里增加一个CrossedFloor事件专门记录“刚越过哪一层”调度器订阅它做停靠判断清净很多。5.2 跨线程访问 UI 控件崩溃现象加了后台线程刷新label.Text后程序直接抛InvalidOperationException“跨线程操作无效”。原因WinForms 的控件只能由创建它的线程UI 线程直接修改。模拟核心如果跑在独立Thread里访问 UI 控件就会崩。解决源码用的是单线程 Timer 驱动天然规避了这个问题。如果你改成多线程电梯必须用Control.BeginInvokethis.BeginInvoke(new Action(() { label_floor.Text elevator.CurrentFloor.ToString(); }));另外记住Control.CheckForIllegalCrossThreadCalls这个开关调试时设成false可以压住异常但发布版本不要关它是在帮你发现真实的并发问题。5.3 调度死循环一台电梯永远在上下往返现象某台电梯在 4 楼到 6 楼之间来回跑Destination列表永远不为空但乘客数量是 0楼层请求也清空了。原因ShouldStop里把“电梯自身目标楼层”和“楼层请求”混为一谈。当_destinations里包含 4 楼时PickupAlongTheWay又从楼层请求里找到了 4 楼的同向请求但请求已经被其他电梯接走于是电梯空跑过去“无乘客开门”然后又因为_destinations里还有 4 楼继续折返。解决在Destination加入之前去重并且每次ShouldStop返回 true 时从_destinations里移除该楼层同时调用floors[floor].ClearRequest()把楼层请求状态复位。如果不这样做下次 tick 调度器又会把这个楼层重新发出去。5.4 超载检测只在开门瞬间生效现象超载后无论如何按关门按钮门就是关不上也没有报错日志。原因代码的CheckOverload()只在TryChangeState(OPENING)时调用一次。如果电梯在IDLE状态开门乘客进出后重量变化但从OPENING到CLOSING这一步没有重复校验导致超载状态一直有效门永远关不上。解决把超载判断从“开门瞬间”挪到“关门之前”在ClosingDoor()里增加检查if (PassengerCount MaxCapacity) { // 门保持开启打印提醒 Logger.Warn($电梯超载 {PassengerCount}/{MaxCapacity}); return; // 不切换状态 }真正做仿真项目时这个检查应当放在每次PassengerCount变化时都执行而不是依赖状态跃迁。5.5 调度器反射创建失败现象修改Config.AllocatorType MyNewStrategy后启动抛TypeLoadException。原因源码用Type.GetType(Scheduling.MyNewStrategy)来创建实例但新策略类忘了加public修饰符或者命名空间写成了MyNamespace导致程序集加载不到。解决确认调度类满足三个条件public类、实现了IAllocator接口、有公共无参构造函数。在Config里写死全名加, AssemblyName的方式不如统一用var type typeof(IAllocator).Assembly.GetType($Scheduling.{allocatorName});这招能少掉一半的命名空间错误。6. 从模拟到二次开发替换调度算法与加入统计能力到这里你已经把源码跑通了。接下来要做的不是小修小补而是按自己的业务场景改造它。我挑了两个最值得动手的方向一个换算法一个加统计改造完你会真正理解这套架构的边界在哪里。6.1 替换调度策略把扫掠改成最短路优先SSTF扫掠策略在高峰时段电梯容易扎堆在低楼层一个简单的改造是换成ShortestSeekTimeFirst——每次请求都派“距离目标楼层最短”的电梯不在乎顺路不顺路。public class ShortestSeekTimeFirst : IAllocator { public int Allocate(ListElevator elevators, Request request) { int best -1; int shortest int.MaxValue; for (int i 0; i elevators.Count; i) { var e elevators[i]; // 停运中的电梯距离按当前位置到请求楼层算 // 运行中的电梯距离要考虑方向反向需要先折返一次 int dist Math.Abs(e.CurrentFloor - request.FromFloor); if (e.Direction ! Direction.STATIONARY ((e.Direction Direction.UP e.CurrentFloor request.FromFloor) || (e.Direction Direction.DOWN e.CurrentFloor request.FromFloor))) { dist FloorCount; // 惩罚反向电梯 } if (dist shortest) { shortest dist; best i; } } return best; } }然后把Config.AllocatorType改成ShortestSeekTimeFirst跑同一组请求看日志差异。SSTF 的特点是“单个请求响应快”但整体能耗高、电梯频繁换向扫掠则响应慢但吞吐稳定。你根据自己的业务场景选型——做办公楼模拟选扫掠做医院模拟选 SSTF 更合理。6.2 加一个统计面板平均等待时间与最长等待MainForm里加一个StatsCollector在SimTick里每 10 秒汇总一次每台电梯的响应记录。public class StatsCollector { private Listint _waitTimes new Listint(); public void Record(int floor, int assignedElevator, int tick) { // 从请求发起到电梯到达目标层的时间单位tick100ms _waitTimes.Add(tick - _requestTick); } public string Summarize() { if (_waitTimes.Count 0) return 无请求; double avg _waitTimes.Average(); int max _waitTimes.Max(); return $平均等待 {avg * 0.1:F1}s最长 {max * 0.1:F1}s; } }这个数据才是你评估调度算法好坏的唯一客观依据。我曾經把扫掠和 SSTF 各跑 1000 次模拟SSTF 平均等待低 0.8 秒但能耗高 22%这个“高 22%”的成本在真实电梯系统里就是电费单上的数字模拟系统的价值就是让你在动真格之前把这些权衡都看清楚。从那以后我每次改调度算法都会强制自己先在模拟环境里跑满 1000 次请求统计平均等待、最长等待和总能耗三个指标写进注释里。这套 C# 模拟电梯控制源码的架构兜住了我的所有试验接口隔离让我换算法像换轮胎一样快。希望这份解析能帮你把项目跑通也让你改的过程中少踩几个我已经踩过的坑。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑