特性(Attribute)
特性(Attribute)的概念
定义
特性是用于向代码元素(类、方法、属性等)添加元数据的类,继承自 System.Attribute。
元数据提供程序化的描述信息,供运行时或工具(如编译器、反射)使用。
作用:标记代码元素的附加信息(如序列化、权限控制、路由配置等)。通过反射机制,可在运行时动态读取并处理这些元数据。
内置案例
Obsolete
Obsolete
特性是 C# 中的一个内置特性,用于标记某个类型、方法、属性等程序元素已经过时,不建议再使用。它能给开发者提供明确的提示,告知其应该使用新的替代方案。下面为你介绍几种 Obsolete
特性的应用案例。
1. 方法更新替换
在软件开发过程中,随着需求的变化和技术的发展,原有的方法可能会被更高效、更安全或者功能更强大的新方法所替代。此时就可以使用 Obsolete
特性标记旧方法,提醒开发者使用新方法。
using System;class Calculator
{// 标记为过时的方法[Obsolete("AddNumbers 方法已过时,请使用 NewAddNumbers 方法。", false)]public int AddNumbers(int a, int b){return a + b;}// 新的替代方法public int NewAddNumbers(int a, int b){// 可以在这里添加更复杂的逻辑return a + b;}
}class Program
{static void Main(){Calculator calculator = new Calculator();// 使用旧方法会产生编译警告int result = calculator.AddNumbers(2, 3);Console.WriteLine(result);// 使用新方法result = calculator.NewAddNumbers(2, 3);Console.WriteLine(result);}
}
在这个案例中,AddNumbers
方法被标记为过时,提示开发者使用 NewAddNumbers
方法。当调用 AddNumbers
方法时,编译器会给出警告。
2. 类型变更
如果某个类或者结构体的设计发生了重大变更,需要开发者使用新的类型,那么可以使用 Obsolete
特性标记旧类型。
using System;// 标记为过时的类
[Obsolete("OldPerson 类已过时,请使用 NewPerson 类。", false)]
class OldPerson
{public string Name { get; set; }public int Age { get; set; }
}// 新的替代类
class NewPerson
{public string FullName { get; set; }public int YearsOld { get; set; }
}class Program
{static void Main(){// 使用旧类会产生编译警告OldPerson oldPerson = new OldPerson();oldPerson.Name = "John";oldPerson.Age = 30;Console.WriteLine($"Old Person: {oldPerson.Name}, {oldPerson.Age}");// 使用新类NewPerson newPerson = new NewPerson();newPerson.FullName = "John Doe";newPerson.YearsOld = 30;Console.WriteLine($"New Person: {newPerson.FullName}, {newPerson.YearsOld}");}
}
这里 OldPerson
类被标记为过时,开发者应该使用 NewPerson
类。
3. 库版本升级
在开发类库时,随着版本的升级,可能会对一些公共接口或方法进行调整。为了保证旧代码的兼容性,同时引导开发者使用新的接口或方法,可以使用 Obsolete
特性。
using System;namespace MyLibrary
{public class MyService{// 旧版本的方法[Obsolete("OldMethod 方法已过时,从版本 2.0 开始请使用 NewMethod 方法。", false)]public void OldMethod(){Console.WriteLine("执行旧方法");}// 新版本的方法public void NewMethod(){Console.WriteLine("执行新方法");}}
}class Program
{static void Main(){MyLibrary.MyService service = new MyLibrary.MyService();// 使用旧方法会产生编译警告service.OldMethod();// 使用新方法service.NewMethod();}
}
在这个库升级的案例中,OldMethod
方法被标记为过时,提示开发者从版本 2.0 开始使用 NewMethod
方法。
4. 强制使用新特性
有时候,为了推动开发者尽快采用新的特性或功能,可以将 Obsolete
特性的第二个参数设置为 true
,这样在使用过时元素时会产生编译错误,而不仅仅是警告。
using System;class DataProcessor
{// 标记为过时且使用时会产生编译错误[Obsolete("OldProcess 方法已过时,请使用 NewProcess 方法。", true)]public void OldProcess(){Console.WriteLine("执行旧处理逻辑");}public void NewProcess(){Console.WriteLine("执行新处理逻辑");}
}class Program
{static void Main(){DataProcessor processor = new DataProcessor();// 这里会产生编译错误// processor.OldProcess(); processor.NewProcess();}
}
此案例中,若尝试调用 OldProcess
方法,编译器会报错,强制开发者使用 NewProcess
方法。
Serializable
Serializable
特性在 C# 里用于表明某个类型能够被序列化。序列化指的是把对象的状态转化成可存储或可传输的格式,反序列化则是将这种格式恢复成对象。下面为你提供几个不同场景下使用 Serializable
特性的案例代码。
案例 1:基本对象序列化与反序列化
以下代码展示了如何对一个简单的 Person
对象进行序列化和反序列化操作。
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;// 标记类为可序列化
[Serializable]
public class Person
{public string Name { get; set; }public int Age { get; set; }public Person(string name, int age){Name = name;Age = age;}
}class Program
{static void Main(){// 创建一个 Person 对象Person person = new Person("John", 30);// 序列化对象到文件using (FileStream stream = new FileStream("person.dat", FileMode.Create)){BinaryFormatter formatter = new BinaryFormatter();formatter.Serialize(stream, person);}// 从文件反序列化对象Person deserializedPerson;using (FileStream stream = new FileStream("person.dat", FileMode.Open)){BinaryFormatter formatter = new BinaryFormatter();deserializedPerson = (Person)formatter.Deserialize(stream);}// 输出反序列化后的对象信息Console.WriteLine($"Name: {deserializedPerson.Name}, Age: {deserializedPerson.Age}");}
}
案例 2:包含集合的对象序列化
下面的例子展示了对包含集合的 Team
对象进行序列化和反序列化。
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;// 标记类为可序列化
[Serializable]
public class Team
{public string TeamName { get; set; }public List<Person> Members { get; set; }public Team(string teamName){TeamName = teamName;Members = new List<Person>();}
}// 标记类为可序列化
[Serializable]
public class Person
{public string Name { get; set; }public int Age { get; set; }public Person(string name, int age){Name = name;Age = age;}
}class Program
{static void Main(){// 创建一个 Team 对象并添加成员Team team = new Team("Dream Team");team.Members.Add(new Person("Alice", 25));team.Members.Add(new Person("Bob", 28));// 序列化对象到文件using (FileStream stream = new FileStream("team.dat", FileMode.Create)){BinaryFormatter formatter = new BinaryFormatter();formatter.Serialize(stream, team);}// 从文件反序列化对象Team deserializedTeam;using (FileStream stream = new FileStream("team.dat", FileMode.Open)){BinaryFormatter formatter = new BinaryFormatter();deserializedTeam = (Team)formatter.Deserialize(stream);}// 输出反序列化后的团队信息Console.WriteLine($"Team Name: {deserializedTeam.TeamName}");foreach (var member in deserializedTeam.Members){Console.WriteLine($"Member: {member.Name}, Age: {member.Age}");}}
}
案例 3:使用 NonSerialized
特性排除字段
NonSerialized
特性可用于标记某个字段在序列化时应被忽略。
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;// 标记类为可序列化
[Serializable]
public class Employee
{public string Name { get; set; }public int Age { get; set; }[NonSerialized]private string password;public Employee(string name, int age, string password){Name = name;Age = age;this.password = password;}public string GetPassword(){return password;}
}class Program
{static void Main(){// 创建一个 Employee 对象Employee employee = new Employee("Eve", 32, "secretpassword");// 序列化对象到文件using (FileStream stream = new FileStream("employee.dat", FileMode.Create)){BinaryFormatter formatter = new BinaryFormatter();formatter.Serialize(stream, employee);}// 从文件反序列化对象Employee deserializedEmployee;using (FileStream stream = new FileStream("employee.dat", FileMode.Open)){BinaryFormatter formatter = new BinaryFormatter();deserializedEmployee = (Employee)formatter.Deserialize(stream);}// 输出反序列化后的对象信息Console.WriteLine($"Name: {deserializedEmployee.Name}, Age: {deserializedEmployee.Age}");// 密码字段在反序列化后为 nullConsole.WriteLine($"Password: {deserializedEmployee.GetPassword()}");}
}
DllImport
DllImport
是 C# 中的一个特性,用于从非托管动态链接库(DLL)中调用函数。在 .NET 开发中,有时需要使用一些由 C、C++ 等非托管语言编写的库来完成特定的任务,DllImport
特性就提供了一种在托管代码(如 C#)中调用这些非托管代码的方式。下面为你提供几个不同场景下使用 DllImport
的代码案例及解析。
案例 1:调用 Windows API 函数 MessageBox
Windows API 中有许多有用的函数,MessageBox
函数可以用于显示一个消息框。以下是调用该函数的代码示例:
using System;
using System.Runtime.InteropServices;class Program
{// 使用 DllImport 特性导入 user32.dll 中的 MessageBox 函数[DllImport("user32.dll", CharSet = CharSet.Auto)]public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);static void Main(){// 调用导入的 MessageBox 函数MessageBox(IntPtr.Zero, "这是一个消息框示例", "消息框标题", 0);}
}
解析
-
DllImport
特性:[DllImport("user32.dll", CharSet = CharSet.Auto)]
表示要从user32.dll
这个动态链接库中导入函数。CharSet = CharSet.Auto
用于指定字符集,这里设置为自动选择合适的字符集。 -
extern
关键字:在 C# 中,extern
关键字用于声明一个外部函数,该函数的实现是在外部的非托管代码中。 -
Main
方法:在Main
方法中调用了导入的MessageBox
函数,传入相应的参数来显示一个消息框。
案例 2:调用自定义的 C++ DLL 中的函数
假设我们有一个自定义的 C++ DLL,其中包含一个简单的加法函数。以下是具体的实现步骤和代码示例。
// MyMathLibrary.h
#ifdef MYMATHLIBRARY_EXPORTS
#define MYMATHLIBRARY_API __declspec(dllexport)
#else
#define MYMATHLIBRARY_API __declspec(dllimport)
#endifextern "C" MYMATHLIBRARY_API int Add(int a, int b);// MyMathLibrary.cpp
#include "MyMathLibrary.h"extern "C" MYMATHLIBRARY_API int Add(int a, int b)
{return a + b;
}--------------------------------------------
using System;
using System.Runtime.InteropServices;class Program
{// 使用 DllImport 特性导入自定义 DLL 中的 Add 函数[DllImport("MyMathLibrary.dll", CallingConvention = CallingConvention.Cdecl)]public static extern int Add(int a, int b);static void Main(){int result = Add(2, 3);Console.WriteLine($"2 + 3 = {result}");}
}
案例 3:调用 DLL 中处理字符串的函数
假设存在一个 C++ DLL 中的函数,用于反转字符串。
C++ DLL 代码(StringLibrary.dll
)
cpp
// StringLibrary.h
#ifdef STRINGLIBRARY_EXPORTS
#define STRINGLIBRARY_API __declspec(dllexport)
#else
#define STRINGLIBRARY_API __declspec(dllimport)
#endifextern "C" STRINGLIBRARY_API void ReverseString(char* str);// StringLibrary.cpp
#include "StringLibrary.h"
#include <cstring>
#include <algorithm>extern "C" STRINGLIBRARY_API void ReverseString(char* str)
{int len = strlen(str);std::reverse(str, str + len);
}
C# 调用代码
using System;
using System.Runtime.InteropServices;class Program
{// 导入 DLL 中的 ReverseString 函数[DllImport("StringLibrary.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]public static extern void ReverseString([In, Out] char[] str);static void Main(){string input = "hello";char[] charArray = input.ToCharArray();ReverseString(charArray);string result = new string(charArray);Console.WriteLine($"反转后的字符串: {result}");}
}
解析
-
[In, Out]
特性:表明该参数既可以作为输入,也可以作为输出。 -
CharSet = CharSet.Ansi
:指定使用 ANSI 字符集。 -
Main
方法:将字符串转换为字符数组,调用ReverseString
函数进行反转,再将结果转换回字符串并输出。
Conditional
Conditional
是 C# 中的一个特性,用于根据预处理器符号来控制方法是否被调用。在不同的编译条件下,使用该特性可以灵活地包含或排除某些代码逻辑,常用于调试、日志记录等场景。下面为你提供几个不同的 Conditional
代码案例及解析。
案例 1:调试信息输出
在开发过程中,我们常常需要输出一些调试信息,但在发布版本中又不希望这些信息影响性能或暴露敏感数据。这时可以使用 Conditional
特性来控制调试信息的输出。
#define DEBUG
using System;
using System.Diagnostics;class DebugHelper
{// 使用 Conditional 特性,只有定义了 DEBUG 符号时该方法才会被调用[Conditional("DEBUG")]public static void DebugMessage(string message){Console.WriteLine($"Debug: {message}");}
}class Program
{static void Main(){// 调用调试信息输出方法DebugHelper.DebugMessage("这是一条调试信息");Console.WriteLine("程序继续执行...");}
}
解析
-
#define DEBUG
:这是一个预处理器指令,用于定义DEBUG
符号。在 Visual Studio 中,默认的调试配置会自动定义这个符号。 -
[Conditional("DEBUG")]
:Conditional
特性指定了一个预处理器符号"DEBUG"
。只有当定义了这个符号时,DebugMessage
方法才会被调用;如果没有定义该符号,编译器会忽略对这个方法的调用。 -
Main
方法:在Main
方法中调用了DebugMessage
方法。由于我们定义了DEBUG
符号,所以该方法会正常执行并输出调试信息。
案例 2:多条件控制
Conditional
特性也可以用于多个条件的控制。例如,我们可以根据不同的配置来决定是否执行某些特定的逻辑。
#define DEVELOPMENT
using System;
using System.Diagnostics;class FeatureManager
{// 只有定义了 DEVELOPMENT 符号时该方法才会被调用[Conditional("DEVELOPMENT")]public static void EnableDevelopmentFeature(){Console.WriteLine("开发环境特性已启用");}// 只有定义了 PRODUCTION 符号时该方法才会被调用[Conditional("PRODUCTION")]public static void EnableProductionFeature(){Console.WriteLine("生产环境特性已启用");}
}class Program
{static void Main(){// 调用开发环境特性方法FeatureManager.EnableDevelopmentFeature();// 由于没有定义 PRODUCTION 符号,该方法调用会被忽略FeatureManager.EnableProductionFeature();Console.WriteLine("程序继续执行...");}
}
解析
-
#define DEVELOPMENT
:定义了DEVELOPMENT
预处理器符号。 -
[Conditional("DEVELOPMENT")]
和[Conditional("PRODUCTION")]
:分别为两个方法指定了不同的预处理器符号。根据符号的定义情况,编译器会决定是否调用相应的方法。 -
Main
方法:调用了EnableDevelopmentFeature
和EnableProductionFeature
方法。由于只定义了DEVELOPMENT
符号,所以只有EnableDevelopmentFeature
方法会被执行。
案例 3:日志记录控制
在实际应用中,我们可能需要根据不同的日志级别来控制日志的输出。可以使用 Conditional
特性结合不同的预处理器符号来实现这一功能。
#define LOG_INFO
using System;
using System.Diagnostics;class Logger
{// 只有定义了 LOG_DEBUG 符号时该方法才会被调用[Conditional("LOG_DEBUG")]public static void DebugLog(string message){Console.WriteLine($"Debug: {message}");}// 只有定义了 LOG_INFO 符号时该方法才会被调用[Conditional("LOG_INFO")]public static void InfoLog(string message){Console.WriteLine($"Info: {message}");}// 只有定义了 LOG_ERROR 符号时该方法才会被调用[Conditional("LOG_ERROR")]public static void ErrorLog(string message){Console.WriteLine($"Error: {message}");}
}class Program
{static void Main(){// 由于没有定义 LOG_DEBUG 符号,该方法调用会被忽略Logger.DebugLog("这是一条调试日志");// 由于定义了 LOG_INFO 符号,该方法会被调用Logger.InfoLog("这是一条信息日志");// 由于没有定义 LOG_ERROR 符号,该方法调用会被忽略Logger.ErrorLog("这是一条错误日志");Console.WriteLine("程序继续执行...");}
}
解析
-
#define LOG_INFO
:定义了LOG_INFO
预处理器符号。 -
[Conditional("LOG_DEBUG")]
、[Conditional("LOG_INFO")]
和[Conditional("LOG_ERROR")]
:分别为三个日志方法指定了不同的预处理器符号。根据符号的定义情况,编译器会决定是否调用相应的日志方法。 -
Main
方法:调用了三个日志方法。由于只定义了LOG_INFO
符号,所以只有InfoLog
方法会被执行。
Route
在 C# 中,Route
常用于 ASP.NET Core 应用程序里,它的作用是把传入的 HTTP 请求映射到对应的处理程序(像控制器中的操作方法)。下面会给出不同场景下的 Route
代码案例以及详细解析。
案例 1:基本路由配置
这个例子展示了如何在控制器里使用 [Route]
特性来定义基本的路由。
using Microsoft.AspNetCore.Mvc;// 定义控制器的路由前缀
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{// 定义 Get 方法的路由[HttpGet]public IActionResult Get(){return Ok("获取所有产品");}// 定义 Get 方法,通过 id 获取单个产品[HttpGet("{id}")]public IActionResult Get(int id){return Ok($"获取产品 ID 为 {id} 的产品");}// 定义 Post 方法,用于创建新的产品[HttpPost]public IActionResult Post(){return Ok("创建新的产品");}
}
解析
-
[Route("api/[controller]")]
:此特性设定了控制器的路由前缀。[controller]
属于占位符,会被控制器的名称(去掉Controller
后缀)所替代,所以这个控制器的路由前缀是api/Products
。 -
[HttpGet]
:表示该方法处理 HTTP GET 请求。第一个[HttpGet]
方法对应的路由是api/Products
,当客户端发送 GET 请求到这个地址时,就会调用这个方法。 -
[HttpGet("{id}")]
:这个方法同样处理 HTTP GET 请求,不过它接收一个名为id
的路由参数。对应的路由是api/Products/{id}
,例如api/Products/1
,客户端发送这样的请求时,会调用这个方法。 -
[HttpPost]
:表示该方法处理 HTTP POST 请求,对应的路由是api/Products
,当客户端发送 POST 请求到这个地址时,会调用这个方法。
案例 2:自定义路由模板
这个例子展示了如何运用自定义的路由模板。
using Microsoft.AspNetCore.Mvc;[ApiController]
public class OrdersController : ControllerBase
{// 自定义路由模板[Route("orders/{year:int}/{month:int}")][HttpGet]public IActionResult GetOrdersByDate(int year, int month){return Ok($"获取 {year} 年 {month} 月的订单");}
}
解析
[Route("orders/{year:int}/{month:int}")]
:这是一个自定义的路由模板,它规定了路由必须包含两个整数类型的参数 year
和 month
。对应的路由是 orders/{year}/{month}
,例如 orders/2025/04
,客户端发送这样的请求时,会调用 GetOrdersByDate
方法。
案例 3:路由约束
此例展示了如何使用路由约束来限制路由参数的类型和范围。
using Microsoft.AspNetCore.Mvc;[ApiController]
public class UsersController : ControllerBase
{// 使用路由约束[Route("users/{id:min(1)}")][HttpGet]public IActionResult GetUser(int id){return Ok($"获取用户 ID 为 {id} 的用户");}
}
解析
-
[Route("users/{id:min(1)}")]
:这里使用了路由约束min(1)
,它规定了id
参数的值必须大于或等于 1。如果客户端发送的请求中的id
值小于 1,就不会匹配到这个路由。对应的路由是users/{id}
,例如users/2
,客户端发送这样的请求时,会调用GetUser
方法。
自定义特性
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;namespace WpfAppAttribute.UserAttribute
{// 定义自定义特性类// AttributeUsage 特性指定该自定义特性可以应用于哪些程序元素// 这里表示可以应用于类和方法[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]public class CustomDescriptionAttribute : Attribute{// 存储特性描述信息的属性public string Description { get; }// 构造函数,用于初始化描述信息public CustomDescriptionAttribute(string description){Description = description;}// 用于在方法执行前进行参数检查的方法public bool CheckParameters(object[] parameters){// 这里可以添加具体的参数检查逻辑// 示例:假设我们要求参数数组不为空且第一个参数为非空字符串if (parameters != null && parameters.Length > 0 && parameters[0] is string str && !string.IsNullOrEmpty(str)){return true;}return false;}}// 应用自定义特性到类[CustomDescription("这是一个示例类,用于演示自定义特性的使用。")]public class ExampleClass{// 应用自定义特性到方法[CustomDescription("这是一个示例方法,用于演示自定义特性的使用。")]public void ExampleMethod(string input){Console.WriteLine($"示例方法被调用,输入参数: {input}");}}public class AppAttribute{public void asdasd(){ExampleClass exampleClass = new ExampleClass();MethodInfo method = typeof(ExampleClass).GetMethod("ExampleMethod");CustomDescriptionAttribute attribute = method.GetCustomAttribute<CustomDescriptionAttribute>();string testInput = "test";object[] parameters = { testInput };if (attribute != null && attribute.CheckParameters(parameters)){method.Invoke(exampleClass, parameters);}else{Console.WriteLine("参数检查不通过,方法未执行。");}}}
}
解析:
直接在 asdasd
方法里面定义一个 CustomDescriptionAttribute
实例和通过反射从类或方法上获取特性实例,这两种方式存在明显区别,下面从几个方面进行详细分析:
1. 元数据的附着性
-
使用特性标记类和方法:
在原代码中,CustomDescriptionAttribute
是通过特性语法(如[CustomDescription("...")]
)直接标记在ExampleClass
和ExampleMethod
上的。这种方式使得描述信息成为了类和方法的元数据的一部分,它与类和方法紧密绑定,只要类和方法存在,这些元数据就可以在运行时通过反射机制随时获取。这种附着性让代码的描述信息具有更强的可读性和可维护性,开发者可以直观地看到类和方法的描述信息。 -
在方法内部定义特性实例:
如果直接在asdasd
方法内部创建CustomDescriptionAttribute
实例,那么这个实例只是方法内部的一个局部对象,它与ExampleClass
和ExampleMethod
本身没有直接关联。它仅仅是在方法执行时临时存在的一个对象,无法体现类和方法的固有属性。
2. 代码的可扩展性和复用性
-
使用特性标记类和方法:
当使用特性标记类和方法时,其他部分的代码也可以通过反射来获取这些特性信息,从而实现更多的功能。例如,可以编写一个通用的工具类,用于遍历程序集中的所有类和方法,获取它们的CustomDescriptionAttribute
描述信息,生成文档或者进行其他处理。这种方式使得特性可以在多个地方复用,提高了代码的可扩展性。 -
在方法内部定义特性实例:
在方法内部定义的特性实例只能在该方法内部使用,无法被其他方法或类访问。这就限制了特性的复用性,当需要在其他地方使用相同的检查逻辑时,就需要重复编写代码。
3. 代码的可读性和维护性
-
使用特性标记类和方法:
通过特性标记类和方法,代码的意图更加清晰。开发者在查看类和方法的定义时,一眼就能看到它们的描述信息和相关的检查逻辑。同时,当需要修改描述信息或检查逻辑时,只需要修改特性的定义和标记即可,不会影响到其他部分的代码。 -
在方法内部定义特性实例:
在方法内部定义特性实例会使方法的代码变得复杂,因为它不仅包含了方法的核心逻辑,还包含了特性的创建和使用逻辑。这会降低代码的可读性,并且当需要修改特性逻辑时,可能会影响到方法的其他部分。
特性的目标与参数
可通过 AttributeTargets
枚举指定适用目标(类、方法、属性、参数等)。
[AttributeUsage(AttributeTargets.Property)]
public class RangeCheckAttribute : Attribute { }
参数类型
-
位置参数:通过构造函数传递,必须按顺序赋值。
-
命名参数:通过属性赋值,可任意顺序。
[Author("张三", Version = 2.0)] // Version 是命名参数
public class Book { }
反射与特性
-
读取特性
-
通过反射 API(如
GetCustomAttributes()
)获取特性信息。
-
var attributes = typeof(ExampleClass).GetCustomAttributes(typeof(MyCustomAttribute), false);
-
典型应用场景
1.动态生成文档(如标记方法的用途)。 - 自动化测试框架(标记测试类和测试方法)。
- Web 框架路由配置(如 ASP.NET Core)。
注意事项
-
命名规范:特性类名通常以
Attribute
结尾,但使用时可以省略(如[MyCustom]
等价于[MyCustomAttribute]
)。 -
作用范围控制:使用
AttributeUsage
的Inherited
属性控制是否允许继承。 -
性能考量:反射操作可能影响性能,避免频繁调用。
总结
特性(Attribute)是 .NET 中强大的元数据机制,广泛应用于框架设计、代码分析、配置管理等场景。掌握其基本用法和反射结合方式,能显著提升代码的灵活性和可维护性。