在 C# 开发中,特性(Attribute)是一个极易被低估、但工程价值极高的核心机制。
尤其在工业上位机、SCADA、HMI、设备监控、数据采集这类项目中,特性可以完美解耦业务配置和代码逻辑,让繁杂的点位配置、协议映射、参数校验、UI渲染逻辑变得极简优雅、可统一维护。
本文不讲空洞理论,只聚焦 是什么、为什么用、工业项目怎么落地。
文章目录
-
- 一、核心结论:特性不是装饰,是代码元数据
- 二、入门:所有人都用过的特性
- 三、特性基础语法
- 四、为什么必须学特性?
- 五、实战场景1:设备参数自动生成UI
-
- 1.自定义展示字段特性
- [2. 模型挂载特性,定义业务信息](#2. 模型挂载特性,定义业务信息)
- [3. 反射读取特性,自动解析配置](#3. 反射读取特性,自动解析配置)
- [4. 调用效果](#4. 调用效果)
- 价值
- 六、实战场景2:PLC/协议命令路由
-
- [1. 命令映射特性](#1. 命令映射特性)
- [2. 处理器方法挂载指令](#2. 处理器方法挂载指令)
- [3. 全局命令路由器(启动扫描、运行秒级调度)](#3. 全局命令路由器(启动扫描、运行秒级调度))
- 价值
- 七、实战场景3:参数统一校验
-
- [1. 数值范围校验特性](#1. 数值范围校验特性)
- [2. 模型绑定校验规则](#2. 模型绑定校验规则)
- [3. 全局通用校验器](#3. 全局通用校验器)
- 八、必备的C#内置特性
-
- [1. Obsolete 废弃标记](#1. [Obsolete] 废弃标记)
- [2. Serializable 可序列化](#2. [Serializable] 可序列化)
- [3. NonSerialized 忽略序列化](#3. [NonSerialized] 忽略序列化)
- [4. 数据注解 Required/MaxLength](#4. 数据注解 [Required]/[MaxLength])
- 九、自定义特性的工程设计原则
- [十、特性底层核心:反射 + 缓存](#十、特性底层核心:反射 + 缓存)
- 十一、特性避坑指南
- 十二、三种开发方案对比
-
- [1. 纯手写配置](#1. 纯手写配置)
- [2. 特性 + 反射 + 缓存(主推)](#2. 特性 + 反射 + 缓存(主推))
- [3. 特性 + SourceGenerator 代码生成](#3. 特性 + SourceGenerator 代码生成)
- 十三、总结
- 十四、优先落地场景
一、核心结论:特性不是装饰,是代码元数据
特性本质:给 程序集、类、属性、方法、参数 等代码目标,附加一段描述性元数据。
它不会直接修改代码运行逻辑,但可以被:
-
编译器识别校验
-
运行时反射读取
-
框架、序列化器、ORM、校验器、UI生成器消费执行
通俗理解:代码本体负责 怎么做 ,特性负责 是什么、该怎么被系统解析 。
二、入门:所有人都用过的特性
最经典的 [Obsolete] 过时特性,几乎所有开发者都接触过:
csharp
[Obsolete("请使用 NewReadTemperature()")]
public void ReadTemperature()
{
// 旧的测温逻辑
}
它不是单纯的注释装饰,会产生实际工程效果:
-
IDE 实时标红提示
-
编译器主动报警告
-
杜绝旧接口滥用,规范迭代逻辑
三、特性基础语法
特性三种常规写法,支持无参、常量参数、命名参数:
csharp
// 无参特性
[AttributeName]
// 带常量参数
[AttributeName("自定义参数")]
// 带属性赋值
[AttributeName(Property = "属性值")]
小知识点:自定义特性类命名规范以Attribute 结尾,但使用时可省略后缀。
例:[Serializable] 本质是 SerializableAttribute。
四、为什么必须学特性?
工业上位机、设备监控项目,普遍存在这些痛点:
-
设备点位、寄存器、配置参数海量且杂乱
-
表单UI多,校验规则重复冗余
-
多协议、多指令,switch-case 路由臃肿难维护
-
报警、权限、日志、校验规则散落各处
-
配置与业务代码强耦合,迭代维护成本极高
而特性的核心价值,就是把所有描述性、规则性的配置,从业务逻辑中抽离出来统一管理。
优势:
-
消灭重复代码,统一规则标准
-
支持 UI、校验、协议、导出自动化
-
依托反射实现轻量框架化设计
-
模型即业务契约,可读性、可维护性拉满
五、实战场景1:设备参数自动生成UI
上位机大量设备参数(温度、PID、地址、阈值),无需手写每一个UI配置,通过特性定义元信息,反射自动渲染、展示、分组。
1.自定义展示字段特性
csharp
using System;
[AttributeUsage(AttributeTargets.Property)]
public sealed class DisplayFieldAttribute : Attribute
{
public string Name { get; }
public string Unit { get; }
public string Group { get; }
public DisplayFieldAttribute(string name, string unit = "", string group = "")
{
Name = name;
Unit = unit;
Group = group;
}
}
2. 模型挂载特性,定义业务信息
csharp
public class TemperatureConfig
{
[DisplayField("温度上限", "℃", "报警参数")]
public double MaxTemp { get; set; }
[DisplayField("温度下限", "℃", "报警参数")]
public double MinTemp { get; set; }
[DisplayField("PID比例系数", "", "控制参数")]
public double Kp { get; set; }
[DisplayField("设备地址", "", "通讯参数")]
public int SlaveId { get; set; }
}
3. 反射读取特性,自动解析配置
csharp
using System.Reflection;
using System.Collections.Generic;
public static class DisplayFieldReader
{
public static List<FieldInfo> GetFieldInfos<T>()
{
var result = new List<FieldInfo>();
var type = typeof(T);
foreach (var prop in type.GetProperties())
{
var attr = prop.GetCustomAttribute<DisplayFieldAttribute>();
if (attr == null) continue;
result.Add(new FieldInfo
{
Group = attr.Group,
Name = attr.Name,
PropName = prop.Name,
Unit = attr.Unit
});
}
return result;
}
}
// 辅助实体
public class FieldInfo
{
public string Group { get; set; }
public string Name { get; set; }
public string PropName { get; set; }
public string Unit { get; set; }
}
4. 调用效果
只需一行代码,即可自动获取所有参数的展示信息,支持自动生成表单、配置页、报表:
csharp
var fields = DisplayFieldReader.GetFieldInfos<TemperatureConfig>();
价值
-
新增参数只需改模型,无需改UI、校验、导出代码
-
适配 WPF/WinForm/Web 多端展示
-
参数分组、名称、单位统一管控,零混乱
六、实战场景2:PLC/协议命令路由
传统协议处理全靠 switch-case,指令越多越臃肿。用特性实现命令与方法解耦,自动路由,新增指令无需改核心逻辑。
1. 命令映射特性
csharp
[AttributeUsage(AttributeTargets.Method)]
public sealed class CommandAttribute : Attribute
{
public string Code { get; }
public CommandAttribute(string code) => Code = code;
}
2. 处理器方法挂载指令
csharp
public class PlcCommandHandler
{
[Command("0x1001")]
public void StartMotor() => Console.WriteLine("电机启动");
[Command("0x1002")]
public void StopMotor() => Console.WriteLine("电机停止");
}
3. 全局命令路由器(启动扫描、运行秒级调度)
csharp
using System.Reflection;
using System.Collections.Generic;
public class CommandRouter
{
private readonly Dictionary<string, MethodInfo> _routes = new();
private readonly object _handler;
public CommandRouter(object handlerInstance)
{
_handler = handlerInstance;
BuildRouteMap();
}
// 启动时一次性扫描缓存,避免运行时重复反射
private void BuildRouteMap()
{
var methods = _handler.GetType()
.GetMethods(BindingFlags.Instance | BindingFlags.Public);
foreach (var method in methods)
{
var attr = method.GetCustomAttribute<CommandAttribute>();
if (attr != null)
_routes[attr.Code] = method;
}
}
// 运行时直接调度
public void Dispatch(string code)
{
if (_routes.TryGetValue(code, out var method))
method.Invoke(_handler, null);
}
}
价值
-
彻底消灭超长 switch-case
-
新增协议指令只需新增方法+挂载特性,无侵入
-
支持插件化、模块化协议扩展
七、实战场景3:参数统一校验
设备地址、采样周期、阈值等参数,规则固定,用特性统一约束,无需在UI、保存、通讯处重复写校验代码。
1. 数值范围校验特性
csharp
[AttributeUsage(AttributeTargets.Property)]
public sealed class RangeAttribute : Attribute
{
public double Min { get; }
public double Max { get; }
public RangeAttribute(double min, double max)
{
Min = min;
Max = max;
}
}
2. 模型绑定校验规则
csharp
public class DeviceSetting
{
[Range(1, 247)]
public int SlaveId { get; set; }
[Range(0.1, 1000)]
public double SampleIntervalMs { get; set; }
}
3. 全局通用校验器
csharp
using System.Reflection;
using System.Collections.Generic;
public static class ModelValidator
{
public static List<string> Validate(object model)
{
var errors = new List<string>();
var type = model.GetType();
foreach (var prop in type.GetProperties())
{
var range = prop.GetCustomAttribute<RangeAttribute>();
if (range == null) continue;
var value = prop.GetValue(model);
if (value == null) continue;
var num = Convert.ToDouble(value);
if (num < range.Min || num > range.Max)
{
errors.Add($"参数【{prop.Name}】超出合法范围:[{range.Min},{range.Max}]");
}
}
return errors;
}
}
八、必备的C#内置特性
1. Obsolete 废弃标记
协议迭代、接口升级专用,标记旧方法,强制规范开发。
2. Serializable 可序列化
用于配置快照、本地缓存、消息传输、历史数据存档。
3. NonSerialized 忽略序列化
过滤 Socket、串口句柄、运行时缓存等无需持久化的对象。
4. 数据注解 Required/MaxLength
快速实现必填、长度、空值校验,适配配置表单、权限管理。
九、自定义特性的工程设计原则
写特性,必须遵守5个原则,避免后期架构崩塌:
-
只存元数据,不写业务逻辑
-
职责单一,一个特性只干一件事
-
构造只读,运行时不可修改
-
面向消费方设计(UI/校验/协议)
-
配合缓存反射使用,杜绝高频扫描
十、特性底层核心:反射 + 缓存
特性本身不会自动执行,必须通过反射读取。高频使用API:
csharp
// 获取单个特性
GetCustomAttribute<T>()
// 获取所有特性
GetCustomAttributes()
// 只读元数据,性能最优(适合批量扫描)
GetCustomAttributesData()
工业项目最佳实践:启动全局扫描缓存,运行时直接读取缓存,零反射开销。
十一、特性避坑指南
-
动态数据不要用特性:在线状态、实时数值、临时授权放模型,不放特性
-
特性不是数据库:编译期固定,动态配置需走配置文件/数据库
-
杜绝高频反射:实时采集、循环逻辑禁止实时扫描特性
-
特性结构谨慎修改:属于基础设施,改动影响全局业务
十二、三种开发方案对比
1. 纯手写配置
简单、调试方便,但重复代码多、维护成本高,仅适合小工具。
2. 特性 + 反射 + 缓存(主推)
结构清晰、易扩展、适配自动化UI/校验/路由,完美适配中大型工业上位机。
3. 特性 + SourceGenerator 代码生成
零运行时反射、性能拉满,适合高性能、长期迭代的核心工控系统。
十三、总结
特性的核心价值不是炫技,而是将散落的业务规则统一显性化。
解决了工控项目最大痛点:规则分散在UI、通讯、校验、导出各处,无人统一维护。
模型承载业务定义,特性承载业务规则,反射承载自动化执行,这就是工业上位机优雅架构的核心之一。
十四、优先落地场景
-
表单字段:展示名、单位、分组、排序
-
参数校验:数值范围、必填、格式约束
-
协议通讯:指令路由、寄存器映射
-
报警系统:报警等级、提示文案、触发规则
-
报表导出:列名、排序、格式化
-
权限控制:功能标记、访问约束
创作权保护
本文由 leonkay 学习总结编写,水平有限,内容仅供参考,作为个人记录使用。若有疏漏,请不吝赐教。版权归作者所有,未经授权,禁止转载、摘编或以其他方式使用本文内容。如需合作或转载本文,请联系作者获得授权。