当前位置: 首页 > news >正文

C#进阶学习(十五)关于特性的认识

目录

引言

一、什么是特性

二、怎么自定义特性

基本语法

三、怎么使用自定义的特性

示例:定义、应用并读取自定义特性

四、限制自定义特性的使用范围

AttributeUsage 参数表格

AttributeTargets 常用枚举值

示例:限制特性只能用于方法,且不可继承

六、系统自带的特性-调用者信息特性

​编辑 

七、系统自带的特性-条件编译特性

八、系统自带的特性-外部Dll包函数特性

总结


引言

        在C#开发中,代码不仅仅是实现功能的工具,更是团队协作长期维护的核心载体。如何在代码中高效传递元数据信息(如作者、版本、调试标记等)?特性(Attribute)为此提供了优雅的解决方案。特性允许开发者通过声明式语法为代码元素(类、方法、属性等)附加额外信息,这些信息既不影响代码逻辑,又能被编译器、框架或反射机制读取,从而实现灵活的代码控制与自动化处理。无论是标记过时方法、记录调用者信息,还是与外部API交互,特性都在简化开发流程中扮演重要角色。本文将从特性基础出发,深入讲解自定义特性的设计与应用,并结合系统内置特性解析其实际场景中的价值。

一、什么是特性

        特性就像是给代码元素(类、方法、属性等)贴上的“标签”,用来记录一些额外的信息。比如你买了一本书,书里可能会夹一张便签,写着“这本书是2023年出版的”或者“这本书需要管理员权限才能阅读”。特性就是这样的便签,它们本身不会改变书的(代码的)内容,但能告诉其他人(编译器、框架或开发者)关于这本书(代码)的某些重要信息

元数据存储:特性为代码添加额外的描述信息(如作者、版本、用途等)。

运行时或编译时读取:通过反射可以在程序运行时读取这些信息,或者让编译器根据特性做出特定行为(如生成警告)。

广泛应用:控制序列化、标记过时代码、简化日志记录、调用外部函数等。

特性是一种允许我们向程序的程序集添加元数据的语言结构
它是用于保存程序结构信息的某种特殊的类

特性提供功能强大的方法以将声明信息与C#代码(类型 方法 属性)相关联
特性与程序实体相关联后 即可在运行时使用反射查询特性信息

特性的目的是 告诉编译器把程序结构的某组元数据镶嵌到程序集
他可以放置在几乎所有的申明中(类 变量 函数等等申明)

说人话:
特性本身是个类
我们可以利用特性类来为元数据添加额外信息
比如一个类、成员变量成员方法等等为他们添加更多的额外信息
之后可以通过反射来获取这些信息

二、怎么自定义特性

基本语法
  1. 定义特性类:继承 System.Attribute,类名以 Attribute 结尾(可省略)。

  2. 构造函数:定义必选参数。

  3. 公共属性/字段:定义可选参数。

  4. 限制使用范围:通过 [AttributeUsage] 指定特性可应用的目标。

例如:这是一个先进行特性类的申明,也就是说到这里我们已经有了一个特性类的,接下来就是对于该特性类的使用

// 1. 定义特性类:记录代码作者信息
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, // 允许用在类或方法上AllowMultiple = true,                             // 允许多次应用Inherited = false                                  // 不继承到子类
)]
public class AuthorInfoAttribute : Attribute
{// 必选参数:作者姓名(通过构造函数传入)public string Name { get; }// 可选参数:版本号(通过属性设置)public string Version { get; set; }// 构造函数:必须传入作者姓名public AuthorInfoAttribute(string name){Name = name;}
}

三、怎么使用自定义的特性

书接上文,咱们来看看咋用的:

        通过上一篇文章学习到的反射,进行使用,不过一般不会把特性得出来使用,一般就是在你想要添加特性的前面,加上特性标签就可以了

示例:定义、应用并读取自定义特性

(1)定义自定义特性类

using System;// 定义特性类,约定以Attribute结尾
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, // 允许用在类和方法上AllowMultiple = true,  // 允许多次应用Inherited = false      // 不继承到子类
)]
public class MyCustomAttribute : Attribute
{public string Info { get; }// 构造函数:必须传入参数public MyCustomAttribute(string info){Info = info;}// 可选方法public void TestFun(){Console.WriteLine("特性中的方法被调用");}
}

(2) 应用特性到类和方法 

// 应用特性到类(允许多个)
[MyCustom("这是Class的注释1")]
[MyCustom("这是Class的注释2")]
public class MyClass
{// 应用特性到方法[MyCustom("这是Method的注释")]public void MyMethod() { }
}

(3)通过反射读取特性信息 

using System;
using System.Reflection;class Program
{static void Main(){// 获取类型信息(三种等效方式)Type t = typeof(MyClass);                     // 方式1:直接通过类型// Type t = new MyClass().GetType();          // 方式2:通过对象实例// Type t = Type.GetType("命名空间.MyClass"); // 方式3:通过完整类名// 判断类型是否应用了某个特性// 参数1:特性类型// 参数2:是否搜索继承链(属性和事件忽略此参数)bool isDefined = t.IsDefined(typeof(MyCustomAttribute), false);Console.WriteLine($"类型是否应用了MyCustom特性:{isDefined}"); // 输出:True// 获取所有自定义特性(包括其他特性)object[] allAttributes = t.GetCustomAttributes(true);Console.WriteLine($"特性数量:{allAttributes.Length}");// 遍历并处理MyCustom特性foreach (object attr in allAttributes){if (attr is MyCustomAttribute myAttr){Console.WriteLine($"特性信息:{myAttr.Info}");myAttr.TestFun(); // 调用特性中的方法}}// 专门获取MyCustom特性(直接过滤)var customAttributes = t.GetCustomAttributes<MyCustomAttribute>(false);Console.WriteLine($"找到的MyCustom特性数量:{customAttributes.Count()}");}
}

结果:

 

几个方法的说明:

方法作用
IsDefined(Type, bool)快速判断是否应用了某个特性(不实例化特性对象,性能更高)
GetCustomAttributes(bool)获取所有特性实例(true包含继承链中的特性)
GetCustomAttributes<T>()

直接获取指定类型的特性实例(推荐,代码更简洁)

注意:

AllowMultiple控制:若特性类未设置 AllowMultiple = true,重复应用同一特性会编译报错。

继承链搜索Inherited = true 时,派生类会继承基类的特性(需配合 GetCustomAttributes 的 inherit 参数)。

性能优化:频繁反射获取特性时,建议缓存结果。

四、限制自定义特性的使用范围

AttributeUsage 参数表格
参数名类型说明默认值
ValidOnAttributeTargets指定特性可以应用的目标(如类、方法、属性等)无(必填)
AllowMultiplebool是否允许同一目标多次应用同一个特性false
Inheritedbool是否允许派生类继承父类中应用的特性true
AttributeTargets 常用枚举值
枚举值说明
Assembly程序集
Class
Method方法
Property属性
Field字段
Constructor构造函数
Parameter方法参数

通过 [AttributeUsage] 指定:

  • 目标类型:用 AttributeTargets 枚举(如 ClassMethod)。

  • 是否允许多次应用AllowMultiple

  • 是否被继承Inherited

示例:限制特性只能用于方法,且不可继承
using System;[AttributeUsage(AttributeTargets.Method,   // 只能用于方法AllowMultiple = false,     // 不能重复应用Inherited = false          // 不可继承
)]
public class LogExecutionTimeAttribute : Attribute { }public class Demo
{[LogExecutionTime] // 正确:应用于方法public void Calculate() { }
}public class SubDemo : Demo
{// 尝试继承父类方法的特性会失败(Inherited=false)public void NewMethod() { }
}// 错误示例
public class InvalidUse
{[LogExecutionTime] // 错误:不能应用于字段public int Value;
}

五、系统自带的特性-过时特性

        标记代码已过时,编译时生成警告或错误。

用于提示用户 使用的方法等成员已经过时 建议使用新方法
一般加在函数前

语法结构:[Obsolete(string message, bool isError)]

message:必填,提示信息。

isError:可选(默认 false),设为 true 时触发编译错误。

代码示例:可以看到写代码时他就自动给你画横线了,说明该方法废弃了,主要是以后进行版本更迭的时候可能会使用到:

using System;public class PaymentService
{[Obsolete("请使用 NewProcess() 方法", false)] // 警告public void Process() { }[Obsolete("此方法已删除", true)] // 错误public void OldMethod() { }public void NewProcess() { }
}class Program
{static void Main(){var service = new PaymentService();service.Process();   // 编译时警告// service.OldMethod(); // 编译错误}
}

六、系统自带的特性-调用者信息特性

        自动获取调用者的方法名、文件路径、行号,用于日志或调试。

哪个文件调用
CallerFilePath 特性用于获取调用者的文件名

哪一行调用
CallerLineNumber 特性用于获取调用者的行号

哪个函数调用
CallerMemberName 特性用于获取调用者的函数名

需要应用命名空间 using System.Runtime.CompilerServices;
一般作为函数参数的特性

语法:void Log(
    [CallerMemberName] string member = "",
    [CallerFilePath] string file = "",
    [CallerLineNumber] int line = 0
)

参数必须为可选参数并带默认值。

由编译器自动填充值,无需手动传递。

看下面的代码,直接使用就好啦

using System;
using System.Runtime.CompilerServices;public static class Logger
{public static void Log(string message,[CallerMemberName] string caller = "",[CallerFilePath] string file = "",[CallerLineNumber] int line = 0){Console.WriteLine($"消息:{message}");Console.WriteLine($"调用者:{caller}");Console.WriteLine($"文件:{file}");Console.WriteLine($"行号:{line}");}
}class Program
{static void Main(){Logger.Log("测试日志"); // 自动填充调用者信息}
}

 

七、系统自带的特性-条件编译特性

        根据编译符号决定是否编译方法,常用于调试代码。

条件编译特性
Conditional
他会和与处理器指令#define 配合使用

需要引用命名空间 using System.Diagnostics;
主要可以用在一些调试代码上
有时想执行有时不想执行的代码

语法:[Conditional("SYMBOL_NAME")]

方法返回值必须为 void

编译符号需通过项目配置定义(如 DEBUG)。

可以去实际调试一下看看效果:

Debug 配置

        右键项目 → 属性 → Build → 勾选 Define DEBUG constant

Release 配置

        切换为 Release 模式 → 确保 Define DEBUG constant 未勾选

using System;
using System.Diagnostics;public class DebugHelper
{[Conditional("DEBUG")]public static void Log(string message){Console.WriteLine($"[DEBUG] {message}");}
}class Program
{static void Main(){DebugHelper.Log("这是一个调试信息"); Console.WriteLine("程序正常执行");}
}

八、系统自带的特性-外部Dll包函数特性

        调用非托管DLL(如Windows API)中的函数。

DllImport 特性用于导入外部Dll包函数

用于标记非.NET(C#)的函数,表明该函数在一个外部的Dll中定义
一般用来调用C或者C++的Dll包写好的方法
需要引用命名空间 using System.Runtime.InteropServices;

使用规则:[DllImport("dll名称", EntryPoint = "函数名", CharSet = CharSet.Auto)]
public static extern 返回类型 方法名(参数);

EntryPoint:指定DLL中的实际函数名。

CharSet:控制字符串编码(如 CharSet.Unicode)。

using System;
using System.Runtime.InteropServices;public class MessageBoxDemo
{[DllImport("user32.dll", CharSet = CharSet.Auto)]public static extern int MessageBox(IntPtr hWnd, string text, string caption, int options);
}class Program
{static void Main(){MessageBoxDemo.MessageBox(IntPtr.Zero, "Hello, World!", "提示", 0 // 0表示只有一个OK按钮);}
}

看!我们实现了一个框

总结

        特性(Attribute)作为C#中声明式编程的核心机制,通过为代码元素附加元数据,显著提升了代码的可读性可维护性。自定义特性允许开发者根据需求扩展元数据类型,例如通过AuthorInfoAttribute记录代码作者信息,或通过LogExecutionTimeAttribute标记性能监控点。借助反射技术,这些特性信息可在运行时被动态解析,从而实现自动化文档生成、权限校验或日志记录等功能。

        系统内置特性则进一步简化了常见开发场景。例如,Obsolete特性能够优雅地标记过时代码并引导迁移;CallerMemberName特性在日志系统中自动捕获调用者上下文,避免硬编码;Conditional特性通过条件编译灵活控制调试代码的生效范围;而DllImport特性则无缝桥接非托管代码,扩展了C#的生态兼容性。

        特性的核心价值在于其“非侵入性”——在不修改代码逻辑的前提下,通过元数据传递关键信息。这种设计使得代码更易于扩展和协作,尤其是在大型项目或框架开发中,特性能够规范编码标准、简化配置管理,并为工具链(如单元测试框架、序列化库)提供丰富的运行时上下文。掌握特性的设计与应用,是迈向高效、专业化C#开发的重要一步。

http://www.xdnf.cn/news/171739.html

相关文章:

  • Android10.0 Android.bp文件详解,以及内置app编写Android.bp文件
  • Spring 与 ActiveMQ 的深度集成实践(四)
  • 【大模型】图像生成 - Stable Diffusion 深度解析:原理、应用与实战指南
  • 基于STM32、HAL库的ADS1220IRVAR模数转换器ADC驱动程序设计
  • 服务器备份,服务器想要备份文件内容有哪些方法?
  • 【技术派后端篇】技术派并发访问性能优化
  • 多级缓存入门:Caffeine、Lua、OpenResty、Canal
  • 【上位机——MFC】文档
  • C语言结构体赋值与深拷贝
  • django admin 设置字段不可编辑
  • YOLO目标检测之模型剪枝
  • Go RPC 服务方法签名的要求
  • 有关爬虫中数据库的封装——单线程爬虫
  • Tauri窗口与界面管理:打造专业桌面应用体验 (入门系列五)
  • 【Fifty Project - D18】
  • 【2025 最新前沿 MCP 教程 06】构建你的第一个 MCP 服务器:分步指南(源码讲解)
  • 多节管件连接套总成弯扭复合旋转疲劳试验系统
  • PostSwigger Web 安全学习:CSRF漏洞2
  • 现代多核调度器的本质 调度三重奏
  • Github 热点项目 rowboat 一句话生成多AI智能体!5分钟搭建企业级智能工作流系统
  • 在 Cursor 中 配置 GitHub MCP Server
  • 基于ArcGIS的洪水灾害普查、风险评估及淹没制图技术研究​
  • docker(3) -- 图形界面
  • ReACT Agent 实战
  • 面试:结构体默认是对齐的嘛?如何禁止对齐?
  • 遥控器信号传输与信号灯指示要点!
  • 解决新搭建的centos虚拟器,yum下载不了的问题
  • 【音视频】SDL窗口显示
  • DIFY教程第一集:安装Dify配置环境
  • 广度优先搜索(BFS)算法详解