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

目录

引言

一、什么是特性

二、怎么自定义特性

基本语法

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

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

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

[AttributeUsage 参数表格](#AttributeUsage 参数表格)

[AttributeTargets 常用枚举值](#AttributeTargets 常用枚举值)

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

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

编辑

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

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

总结


引言

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

一、什么是特性

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

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

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

广泛应用:控制序列化、标记过时代码、简化日志记录、调用外部函数等。
特性是一种允许我们向程序的程序集 添加元数据 的语言结构

它是用于保存程序结构信息的某种特殊的类

特性提供功能强大的方法以将声明信息与C#代码(类型 方法 属性)相关联

特性与程序实体相关联后 即可在运行时 使用反射查询特性信息

特性的目的是 告诉编译器把程序结构的某组元数据镶嵌到程序集

他可以放置在几乎所有的申明中(类 变量 函数等等申明)

说人话:
特性本身是个类

我们可以利用特性类来为元数据添加额外信息

比如一个类、成员变量成员方法等等为他们添加更多的额外信息

之后可以通过反射来获取这些信息

二、怎么自定义特性

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

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

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

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

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

cs 复制代码
// 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)定义自定义特性类

cs 复制代码
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) 应用特性到类和方法

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

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

cs 复制代码
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 时,派生类会继承基类的特性(需配合 GetCustomAttributesinherit 参数)。

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

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

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

通过 [AttributeUsage] 指定:

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

  • 是否允许多次应用AllowMultiple

  • 是否被继承Inherited

示例:限制特性只能用于方法,且不可继承
cs 复制代码
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 时触发编译错误。

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

cs 复制代码
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 )** 参数必须为**可选参数**并带默认值。 由编译器自动填充值,无需手动传递。 看下面的代码,直接使用就好啦

cs 复制代码
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 未勾选

cs 复制代码
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)。

cs 复制代码
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#开发的重要一步。

相关推荐
OpenLoong 开源社区37 分钟前
技术视界 | 数据的金字塔:从仿真到现实,机器人学习的破局之道
人工智能·学习·机器人·开源社区·人形机器人·openloong
像风一样自由20201 小时前
图像识别系统 - Ubuntu部署指南(香橙派开发板测试)-学习记录1
linux·学习·ubuntu
2401_878454531 小时前
thymeleaf的使用和小结
前端·javascript·学习
Always_away1 小时前
数据库系统概论|第三章:关系数据库标准语言SQL—课程笔记6
数据库·笔记·sql·学习
鑫—萍2 小时前
C++——入门基础
c语言·开发语言·c++·学习·算法
FLLdsj4 小时前
如何在idea中写spark程序
java·学习·spark·intellij-idea
HHVic5 小时前
普通IT的股票交易成长史--20250428晚
学习·生活
yuhouxiyang13 小时前
学习海康VisionMaster之路径提取
学习·计算机视觉
PLUS_WAVE15 小时前
CogCoM: A Visual Language Model with Chain-of-Manipulations Reasoning 学习笔记
学习·语言模型·大模型·cot·vlm·推理模型·reasoning