2026-05-06
C#
0

做过一个温控系统的维护工作,接手的时候差点没绷住——Form1.cs 足足 2300 行,btnStart_ClickbtnStop_ClickbtnExport_Click 密密麻麻,每个按钮里头都塞着一坨业务逻辑,改一个功能要翻半天,生怕动了哪根线把别的东西带崩。

这种代码,不是写出来的,是"堆"出来的。

后来我把这个项目用 [RelayCommand] 重构了一遍,Form 从 2300 行缩到不到 300 行,测试覆盖率从零提到 74%。今天就把这套东西拆开讲清楚,从原理到工业落地,一次说透。


🤔 事件驱动到底哪里"坏了"

先说清楚问题在哪。传统 WinForms 的写法,大概长这个样子:

csharp
private void btnStart_Click(object sender, EventArgs e) { if (!_isRunning) { _timer.Interval = (int)nudInterval.Value; _timer.Start(); _isRunning = true; btnStart.Enabled = false; btnStop.Enabled = true; lblStatus.Text = "采集中..."; } }

看起来没什么问题对吧?但麻烦就藏在这几行里。业务状态_isRunning)、UI 操作btnStart.Enabled)、服务调用_timer.Start())全部揉在一起,Form 既是界面,又是控制器,还是状态机。

想单元测试?没法测,因为逻辑依赖 UI 控件。想复用逻辑?没法复用,因为它跟 Form 死死绑着。想换个界面框架?——那就重写吧。

这不是某个人的问题,是这种写法天然的局限。


💡 命令模式:把"做什么"和"谁来触发"拆开

ICommand 接口其实挺老了,WPF 时代就有,但 WinForms 开发者用得少。它的核心思路就一句话:把操作封装成对象,让 UI 只负责触发,不负责实现

按钮点击 → Execute(command) → ViewModel 里的方法 ↑ CanExecute() 决定按钮灰不灰

UI 不再需要知道"点了之后干什么",只需要知道"有没有权限点"。这个权限——也就是 CanExecute——由 ViewModel 自己管,UI 监听结果就好。

干净。彻底。


🔧 [RelayCommand] 是怎么工作的

CommunityToolkit.Mvvm 把这套东西做到了极致简洁。你只需要在方法上贴一个特性:

csharp
[RelayCommand(CanExecute = nameof(CanStartSampling))] private void StartSampling() { _timer.Interval = Interval; _timer.Start(); IsRunning = true; } private bool CanStartSampling() => !IsRunning;

编译器(Source Generator)在后台帮你生成了这些:

csharp
// 这段代码你不用写,编译器自动生成在 .g.cs 里 private RelayCommand? _startSamplingCommand; public IRelayCommand StartSamplingCommand => _startSamplingCommand ??= new RelayCommand(StartSampling, CanStartSampling);

零样板代码。不是"少写一点",是一个字都不用写

更妙的是 [NotifyCanExecuteChangedFor],把它贴在属性上:

csharp
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(StartSamplingCommand))] [NotifyCanExecuteChangedFor(nameof(StopSamplingCommand))] private bool _isRunning;

IsRunning 一变,两个命令的 CanExecuteChanged 自动触发,按钮的 Enabled 状态跟着联动——整个过程,Form 里一行判断代码都不需要。


👨‍💻 先看一下效果

image.png

image.png

image.png

🏭 工业场景落地:温度监控面板

光说概念没用,来看实际项目怎么组织。我用的是一个工业温度采集面板,场景包括:周期采样、停止、清除历史、导出 CSV 日志、报警检查。

ViewModel 骨架

csharp
public sealed partial class SensorViewModel : ObservableObject { private readonly SensorService _sensor = new(); private readonly System.Windows.Forms.Timer _timer = new(); [ObservableProperty] [NotifyPropertyChangedFor(nameof(StatusText))] [NotifyCanExecuteChangedFor(nameof(StartSamplingCommand))] [NotifyCanExecuteChangedFor(nameof(StopSamplingCommand))] private bool _isRunning; [ObservableProperty] private double _currentTemp; [ObservableProperty] private double _maxTemp; [ObservableProperty] private double _minTemp = 999; [ObservableProperty] private double _avgTemp; public string StatusText => IsRunning ? "● 采集中" : "○ 已停止"; public ObservableCollection<SensorReading> Readings { get; } = []; }
2026-05-06
C#
0

你有没有遇到过这种情况:

从 PLC 读回来一个温度值,明明是 "85.6",存的是字符串。你想把它和报警阈值 90.0 比大小,结果编译器直接给你报红——"无法将 string 隐式转换为 double"。

你改了半天,改出了一个新问题:数值截断了,85.6 变成了 85,精度没了。

这种情况,不是你代码写得差,是你还没搞清楚 C# 的类型转换规则。今天这篇,把三种转换方式讲透,工厂场景全覆盖。


📌 上节回顾

「上一节我们学了 constenum,掌握了用常量锁定报警阈值、用枚举定义设备状态的方法。

今天在这个基础上,我们进一步学习如何在不同数据类型之间安全地"搬运"数值——类型转换。」


💡 核心知识讲解


为什么工厂程序特别需要类型转换?

工业现场的数据来源极其复杂。

PLC 给你的是 int,数据库存的是 string,界面控件绑定的是 double,通信协议传来的是 byte[]

这些数据要在一起"工作",就必须先统一"语言"。类型转换,就是让不同格式的数据能互相理解的翻译官。

C# 里的类型转换,主要分三种:隐式转换、显式转换(强制转换)、Convert 类转换。


第一种:隐式转换(系统自动帮你转)

隐式转换(Implicit Conversion):不需要写任何额外代码,编译器自动完成,且100%安全,不会丢失数据。

类比工厂:就像把一个 500ml 的量杯里的水倒进 1000ml 的量杯,绝对装得下,不会溢出,不用你操心。

什么情况下可以隐式转换? 简单记:小范围 → 大范围,整数 → 浮点数。

从(小)到(大)是否安全
intlong✅ 安全
intdouble✅ 安全
floatdouble✅ 安全
byteint✅ 安全

举个工厂例子:设备编号是 int,统计报表需要 long 类型存储,直接赋值就行,编译器不报错。


第二种:显式转换(你亲自动手,风险自负)

显式转换(Explicit Conversion),也叫强制转换(Cast),需要你用括号明确告诉编译器"我知道风险,我要转"。

类比工厂:把 1000ml 量杯的水倒进 500ml 量杯——可以倒,但超出的部分会溢出丢失。

语法格式:(目标类型)变量名

「⚠️ 警示:显式转换可能造成数据精度损失或溢出,使用前必须确认数值范围。」

比如把 double 类型的温度值 85.6 强制转成 int,结果是 85,小数点后直接截断,不是四舍五入

这在工业场景里很危险——报警阈值如果精度丢失,可能导致设备该停不停。

2026-05-06
C#
0

设备温度报警阈值,你是直接在代码里写的 85.0 吗?

三个月后,领导说"把报警温度改成 90 度"。你翻遍整个项目,发现 85.0 出现了 17 次——哪些是温度?哪些是别的参数?你已经分不清了。

改了 12 处,漏了 5 处,上线后某台注塑机没触发报警,差点出了事故。

这不是假设,这是很多工厂项目的真实故事。今天学完 constenum,这种问题你以后不会再有。


📌 上节回顾

上一节我们学了变量与数据类型,掌握了用 intdoublestringbool 存储不同类型数据的方法。

今天在这个基础上,我们进一步学习不会变的数据有限选项的数据该怎么定义。


💡 核心知识讲解

先搞清楚:什么是"不该变的数据"?

在工厂项目里,有些数值是写死在规格书里的——设备额定电压是 380V,圆周率是 3.14159,一条产线最多 64 个工位。

这些数据从项目立项到退役,永远不会变。如果你把它们写成普通变量,代码运行时理论上可以被修改,这是个隐患。

const(常量)就是给这类数据用的。 它告诉编译器:这个值定死了,谁都别想改。

// 错误方式:用变量存不变的数据 double voltage = 380.0; // 万一哪里不小心 voltage = 0,完蛋 // 正确方式:用常量 const double RatedVoltage = 380.0; // 改都改不了

const 的本质:编译时就把值固定下来,运行时无法修改。」


const 的使用规则

特性说明
声明时必须赋值const int Max = 100;
不能运行时赋值const int Max = GetMax();
支持的类型数字、字符串、bool、char
作用范围类内、方法内均可用

有一点要记住:const 只能存编译时就能确定的值。比如你不能把一个从数据库读出来的值赋给 const,因为那个值要运行时才知道。


再说"有限选项":什么时候用 enum

工厂里设备的运行状态,就那么几种:停机、运行、报警、维护

如果你用数字表示,0=停机,1=运行,2=报警,3=维护,代码里就会出现:

csharp
if (deviceStatus == 2) // 2 是什么?谁记得住?

三个月后,你自己都不记得 2 代表什么。

enum(枚举)就是把这些有限选项起个名字,统一管理。 用了枚举之后:

csharp
if (deviceStatus == DeviceStatus.Alarm) // 一眼就懂

「枚举的本质:给一组有限的选项,贴上人能读懂的标签。」


enum 的底层是整数

枚举在内存里存的其实是整数,默认从 0 开始自动编号。

csharp
enum DeviceStatus { Stopped = 0, // 停机 Running = 1, // 运行 Alarm = 2, // 报警 Maintain = 3 // 维护 }

你也可以手动指定数值,比如对接 PLC(可编程逻辑控制器,工厂里控制设备的"大脑")时,PLC 返回的状态码是 10、20、30,你可以直接写:

csharp
enum PlcStatus { Stopped = 10, Running = 20, Fault = 30 }

这样枚举值和 PLC 的状态码一一对应,读取数据时直接强制转换,省去了大量的 if-else 判断。


const vs enum 怎么选?

场景推荐用法
单个固定数值(如报警阈值)const
一组互斥的状态/类型enum
多个相关常量打包管理enum
需要和 PLC 状态码对应enum(手动赋值)

「记住这个口诀:一个值用 const,一组状态用 enum。」


💻 VS2026 操作步骤

Step 1 新建控制台项目

打开 VS2026,选择 文件 > 新建 > 项目,搜索"控制台应用",选择 .NET 10 框架,项目名填 IndustrialConstEnumDemo,点击创建。

image.png

Copilot 辅助: 创建完成后,在 Program.cs 顶部右键点击"使用 Copilot 解释此文件",可以快速了解 .NET 10 顶级语句(Top-level statements)结构。

Step 2 定义常量和枚举

Program.cs 同目录下,右键 添加 > 新建类,命名为 ProductionConfig.cs。在这个文件里集中定义所有 constenum,便于统一维护。

Copilot 辅助: 在类文件里输入注释 // 定义注塑机设备状态枚举,然后按 Tab 键,Copilot 会自动补全一个符合工业场景的枚举结构,你只需要核对状态名称是否正确。

Step 3 使用 Vibe Coding 生成枚举逻辑

在 Copilot Chat 面板中输入以下 Prompt:

Prompt 示例: "帮我写一个 C# 方法,接收一个 InjectionMachineStatus 枚举值,用 switch 表达式返回对应的中文状态描述字符串,枚举包含:待机、运行、报警、模具切换四种状态。"

Copilot 会直接生成完整方法。

Step 4 运行验证

F5 启动调试,在控制台窗口确认输出的状态描述与预期一致。如有报错,选中错误行,右键选择 "让 Copilot 修复",通常一键即可解决。

2026-05-06
C#
0

你有没有遇到过这种情况:

PLC 采集回来的温度是 87.6,你存进变量里之后,界面上显示的却是 87

你反复检查通信代码,查了一个小时,最后发现——变量类型用错了,就这么简单。

这种低级错误,在工厂项目里出现的频率,远比你想象的高。

今天这节课,我们就把 C# 里最基础、也最容易踩坑的四个数据类型彻底搞清楚。


📌 上节回顾

「上一节我们学了解决方案、项目、文件的层级关系,掌握了在 VS2026 中组织代码结构的方法。今天在这个基础上,我们进一步学习变量与数据类型——这是你写出第一行"有意义的代码"的关键一步。」


💡 核心知识讲解

变量是什么?用工厂的话说

你可以把变量(程序里用来存数据的容器)理解成仓库里的料箱。

每个料箱有个标签(变量名),里面装着东西(数据)。

不同的料箱规格不一样,有的装整数,有的装小数,有的装文字。

「规格选错了,东西装不进去,或者装进去就变形了。」

这个"规格",就是数据类型


四种类型,对应四类工厂数据

先看一张对照表,建立整体感知:

数据类型存什么数据工厂典型用途
int整数产品计数、设备编号、工单号
double小数(高精度)温度、压力、电压、重量
string文字/字符串工单编号、操作员姓名、报警描述
bool真/假(开/关)设备运行状态、报警触发、门禁状态

四种类型,基本覆盖了工厂数据采集里 80% 以上的场景


int:整数的世界,不接受小数

int 是最常用的整数类型,范围是 -21亿 到 +21亿,日常工厂用途完全够用。

csharp
int productionCount = 1500; // 今日生产数量 int deviceId = 42; // 设备编号 int alarmCode = 3; // 报警代码

「记住:int 存的是整数,你给它赋值 87.6,它只会保留 87,小数点后面直接丢掉,不是四舍五入。」

这个特性,是温度显示出错的最常见原因。


double:精度优先,处理所有小数数据

工厂里的传感器数据,大多数是带小数的。

温度 87.6℃、电压 220.3V、重量 12.45kg——这些都该用 double

csharp
double deviceTemp = 87.6; // 设备温度(℃) double supplyVoltage = 220.3; // 供电电压(V) double productWeight = 12.45; // 产品重量(kg)

double 精度可以达到小数点后 15~16 位,传感器数据完全够用。

⚠️ 如果你做的是财务结算系统(比如计件工资),精度要求更高,应该用 decimal 而不是 double。但工业采集场景,double 就够了。

2026-05-06
C#
0

你有没有遇到过这种情况:

上一节刚创建了第一个控制台项目,兴冲冲往里面加代码。没几天,同事说"把报警模块单独拆出来",你打开资源管理器,愣了——这个 .cs 文件到底该放哪?新建一个项目?还是直接加文件?

这个问题,99% 的初学者都踩过。

搞清楚 解决方案(Solution)、项目(Project)、文件(File) 这三层关系,你就不会再乱了。


📌 上节回顾

「上一节我们学了如何在 VS2026 中创建第一个 C# 控制台项目,掌握了从新建到运行的基本流程。今天在这个基础上,我们进一步学习这个项目背后的"工程结构"——解决方案、项目、文件是怎么组织在一起的。」


💡 核心知识讲解

先用工厂类比,建立直觉

你在工厂管过物料仓库吗?

整个仓库叫仓储中心,里面分了几个库房,每个库房里摆着一排排货架和物料

VS2026 的工程结构,和这个逻辑一模一样:

工厂类比VS概念文件扩展名
仓储中心解决方案(Solution).sln
库房项目(Project).csproj
物料/货架文件(File).cs / .xaml

「一个解决方案可以包含多个项目,一个项目可以包含多个文件。这是 C# 工程的基本骨架。」


解决方案(Solution):总指挥

解决方案(Solution,读作"索鲁申")是整个工程的"总包"。

它本身不写代码,只是一个"管理者"。它的文件是 .sln,你双击它,VS 就能打开整套工程。

在工业项目里,一个解决方案通常对应一套系统

比如你做一套注塑机监控系统,整个系统就是一个解决方案:InjectionMoldingSystem.sln