C# 特性(Attribute)——【1】基础讲解

在 C# 开发中,特性(Attribute)是一个极易被低估、但工程价值极高的核心机制。

尤其在工业上位机、SCADA、HMI、设备监控、数据采集这类项目中,特性可以完美解耦业务配置和代码逻辑,让繁杂的点位配置、协议映射、参数校验、UI渲染逻辑变得极简优雅、可统一维护。

本文不讲空洞理论,只聚焦 是什么、为什么用、工业项目怎么落地


文章目录


一、核心结论:特性不是装饰,是代码元数据

特性本质:给 程序集、类、属性、方法、参数 等代码目标,附加一段描述性元数据。

它不会直接修改代码运行逻辑,但可以被:

  • 编译器识别校验

  • 运行时反射读取

  • 框架、序列化器、ORM、校验器、UI生成器消费执行

通俗理解:代码本体负责 怎么做 ,特性负责 是什么、该怎么被系统解析

二、入门:所有人都用过的特性

最经典的 [Obsolete] 过时特性,几乎所有开发者都接触过:

csharp 复制代码
[Obsolete("请使用 NewReadTemperature()")]
public void ReadTemperature()
{
    // 旧的测温逻辑
}

它不是单纯的注释装饰,会产生实际工程效果:

  • IDE 实时标红提示

  • 编译器主动报警告

  • 杜绝旧接口滥用,规范迭代逻辑

三、特性基础语法

特性三种常规写法,支持无参、常量参数、命名参数:

csharp 复制代码
// 无参特性
[AttributeName]
// 带常量参数
[AttributeName("自定义参数")]
// 带属性赋值
[AttributeName(Property = "属性值")]

小知识点:自定义特性类命名规范以Attribute 结尾,但使用时可省略后缀。

例:[Serializable] 本质是 SerializableAttribute

四、为什么必须学特性?

工业上位机、设备监控项目,普遍存在这些痛点:

  • 设备点位、寄存器、配置参数海量且杂乱

  • 表单UI多,校验规则重复冗余

  • 多协议、多指令,switch-case 路由臃肿难维护

  • 报警、权限、日志、校验规则散落各处

  • 配置与业务代码强耦合,迭代维护成本极高

而特性的核心价值,就是把所有描述性、规则性的配置,从业务逻辑中抽离出来统一管理。

优势:

  1. 消灭重复代码,统一规则标准

  2. 支持 UI、校验、协议、导出自动化

  3. 依托反射实现轻量框架化设计

  4. 模型即业务契约,可读性、可维护性拉满

五、实战场景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个原则,避免后期架构崩塌:

  1. 只存元数据,不写业务逻辑

  2. 职责单一,一个特性只干一件事

  3. 构造只读,运行时不可修改

  4. 面向消费方设计(UI/校验/协议)

  5. 配合缓存反射使用,杜绝高频扫描

十、特性底层核心:反射 + 缓存

特性本身不会自动执行,必须通过反射读取。高频使用API:

csharp 复制代码
// 获取单个特性
GetCustomAttribute<T>()
// 获取所有特性
GetCustomAttributes()
// 只读元数据,性能最优(适合批量扫描)
GetCustomAttributesData()

工业项目最佳实践:启动全局扫描缓存,运行时直接读取缓存,零反射开销。

十一、特性避坑指南

  • 动态数据不要用特性:在线状态、实时数值、临时授权放模型,不放特性

  • 特性不是数据库:编译期固定,动态配置需走配置文件/数据库

  • 杜绝高频反射:实时采集、循环逻辑禁止实时扫描特性

  • 特性结构谨慎修改:属于基础设施,改动影响全局业务

十二、三种开发方案对比

1. 纯手写配置

简单、调试方便,但重复代码多、维护成本高,仅适合小工具。

2. 特性 + 反射 + 缓存(主推)

结构清晰、易扩展、适配自动化UI/校验/路由,完美适配中大型工业上位机。

3. 特性 + SourceGenerator 代码生成

零运行时反射、性能拉满,适合高性能、长期迭代的核心工控系统。

十三、总结

特性的核心价值不是炫技,而是将散落的业务规则统一显性化。

解决了工控项目最大痛点:规则分散在UI、通讯、校验、导出各处,无人统一维护。

模型承载业务定义,特性承载业务规则,反射承载自动化执行,这就是工业上位机优雅架构的核心之一。

十四、优先落地场景

  1. 表单字段:展示名、单位、分组、排序

  2. 参数校验:数值范围、必填、格式约束

  3. 协议通讯:指令路由、寄存器映射

  4. 报警系统:报警等级、提示文案、触发规则

  5. 报表导出:列名、排序、格式化

  6. 权限控制:功能标记、访问约束


创作权保护

本文由 leonkay 学习总结编写,水平有限,内容仅供参考,作为个人记录使用。若有疏漏,请不吝赐教。版权归作者所有,未经授权,禁止转载、摘编或以其他方式使用本文内容。如需合作或转载本文,请联系作者获得授权。

相关推荐
harmful_sheep24 分钟前
java group by常见用法
java·开发语言·python
SKH.29 分钟前
Linux软件编程(6)线程间通信
java·开发语言·数据库
界面开发小八哥1 小时前
界面控件DevExpress WinForms中文帮助文档 - 将基于DevExpress的.NET Framework应用迁移至最新版.NET
ui·.net·界面控件·winform·devexpress·ui开发
whcyhhh1 小时前
头歌实践教学平台:数据科学与大数据技术导论(十六)
大数据·开发语言·python
萌动的小火苗1 小时前
数据结构面试题【无答案】
c语言·开发语言·数据结构·链表
宸津-代码粉碎机1 小时前
AI工程化高阶实战|从Demo到生产落地核心架构、全套源码与避坑指南
java·大数据·开发语言·人工智能·python·架构
CSDN_RTKLIB1 小时前
数据库七大符号表
c#
XR1234567882 小时前
办公网自主创新建设怎么选?信创办公场景的选型逻辑
开发语言·php
leonkay2 小时前
C# 特性(Attribute)——【2】工业设备参数框架设计
经验分享·面试·架构·c#·学习方法·设计