MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#特性(Attribute)元编程技术指南

2022-11-163.3k 阅读

1. C# 特性(Attribute)基础

1.1 特性是什么

在 C# 中,特性(Attribute)是一种允许你向程序的程序集、类型、方法、属性等元素添加元数据的语言结构。元数据是关于数据的数据,在这种情况下,它为代码元素提供额外的描述性信息。特性就像是标签,能够为代码赋予额外的含义和行为,而不会直接影响代码的执行逻辑。

例如,假设你有一个类 Customer,你可能想要标记这个类为 Serializable,表示它可以被序列化,以便在网络上传输或存储到文件中。在 C# 中,你可以这样做:

[Serializable]
public class Customer
{
    public string Name { get; set; }
    public int Age { get; set; }
}

这里的 [Serializable] 就是一个特性,它向编译器和运行时传达了 Customer 类具有可序列化的特性。

1.2 特性的基本语法

特性的语法很直观,总是用方括号 [] 括起来,并放置在要应用特性的目标元素之前。特性可以应用于许多不同的目标,包括:

  • 程序集:应用于整个程序集,例如 [assembly: AssemblyTitle("My Application")]。这通常放在 AssemblyInfo.cs 文件中。
  • 类型:如类、结构体、接口等,例如 [Serializable] public class MyClass {... }
  • 成员:方法、属性、字段等,例如 [Obsolete("Use NewMethod instead.")] public void OldMethod() {... }

特性可以接受参数,这些参数在特性名后的括号内指定。参数可以是位置参数或命名参数。例如:

[Obsolete("This method is obsolete as of version 2.0.", true)]
public void OldMethod()
{
    // 方法实现
}

这里,Obsolete 特性接受两个参数,第一个是描述该方法已过时的字符串信息,这是一个位置参数;第二个 true 表示如果代码调用这个方法,编译器会生成一个错误,这是一个命名参数。

1.3 预定义特性

C# 提供了许多预定义特性,以下是一些常见的预定义特性:

  • SerializableAttribute:标记一个类型可被序列化。如前文 Customer 类的示例。
  • ObsoleteAttribute:标记一个程序实体(类、方法等)已过时。当代码使用了标记为 Obsolete 的实体时,编译器会发出警告或错误,取决于 ObsoleteAttribute 的构造函数参数。
  • ConditionalAttribute:标记一个方法为条件方法,只有在指定的预处理符号被定义时,该方法才会被编译到最终的代码中。例如:
#define DEBUG_MODE
using System.Diagnostics;

public class Logger
{
    [Conditional("DEBUG_MODE")]
    public static void Log(string message)
    {
        Debug.WriteLine(message);
    }
}

在这个例子中,只有当定义了 DEBUG_MODE 预处理符号时,Log 方法才会被编译到最终的代码中。

2. 创建自定义特性

2.1 定义自定义特性类

要创建自定义特性,需要定义一个从 System.Attribute 类派生的类。通常,特性类名以 “Attribute” 结尾,但在使用特性时,可以省略 “Attribute” 后缀。

例如,假设我们要创建一个用于标记方法是否为业务规则方法的自定义特性:

public class BusinessRuleAttribute : System.Attribute
{
    // 特性类可以有字段和属性
    public string RuleDescription { get; set; }

    // 构造函数,位置参数
    public BusinessRuleAttribute(string ruleName)
    {
        RuleName = ruleName;
    }

    // 自定义属性
    public string RuleName { get; set; }
}

现在我们可以使用这个自定义特性:

public class OrderProcessor
{
    [BusinessRule("Validate order total is not negative")]
    public void ValidateOrderTotal(decimal total)
    {
        if (total < 0)
        {
            throw new ArgumentException("Order total cannot be negative.");
        }
    }
}

2.2 特性的目标限制

有时,你可能希望限制特性只能应用于特定类型的目标。例如,你可能希望你的自定义特性只能应用于方法,而不能应用于类或字段。在定义特性类时,可以使用 AttributeUsage 特性来指定这些限制。

AttributeUsage 特性接受一个 AttributeTargets 枚举值,用于指定特性可以应用的目标类型。例如,要使 BusinessRuleAttribute 只能应用于方法,可以这样定义:

[AttributeUsage(AttributeTargets.Method)]
public class BusinessRuleAttribute : System.Attribute
{
    // 特性类定义不变
    public string RuleDescription { get; set; }
    public BusinessRuleAttribute(string ruleName)
    {
        RuleName = ruleName;
    }
    public string RuleName { get; set; }
}

如果尝试将 BusinessRuleAttribute 应用于类或字段,编译器会报错。

2.3 特性的继承与多态

自定义特性类可以像普通类一样支持继承和多态。例如,假设我们有一个基础的 AuditAttribute,并且希望创建一些更具体的审计特性,如 CreateAuditAttributeUpdateAuditAttribute

public class AuditAttribute : System.Attribute
{
    public string AuditMessage { get; set; }
    public AuditAttribute(string message)
    {
        AuditMessage = message;
    }
}

public class CreateAuditAttribute : AuditAttribute
{
    public CreateAuditAttribute() : base("Create operation audited")
    {
    }
}

public class UpdateAuditAttribute : AuditAttribute
{
    public UpdateAuditAttribute() : base("Update operation audited")
    {
    }
}

然后在代码中使用这些特性:

public class UserService
{
    [CreateAudit]
    public void CreateUser(User user)
    {
        // 创建用户逻辑
    }

    [UpdateAudit]
    public void UpdateUser(User user)
    {
        // 更新用户逻辑
    }
}

3. 使用反射读取特性

3.1 反射基础

反射是 C# 中的一项强大功能,它允许程序在运行时检查和操作类型、成员以及程序集的元数据。通过反射,我们可以在运行时获取应用在类型、方法等元素上的特性信息。

要使用反射,需要引入 System.Reflection 命名空间。

3.2 获取类型上的特性

假设我们有一个带有自定义特性的类:

[BusinessRule("This class represents a core business entity")]
public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

我们可以使用反射获取 Product 类上的 BusinessRuleAttribute 特性信息:

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        Type productType = typeof(Product);
        object[] attributes = productType.GetCustomAttributes(typeof(BusinessRuleAttribute), true);
        if (attributes.Length > 0)
        {
            BusinessRuleAttribute ruleAttribute = (BusinessRuleAttribute)attributes[0];
            Console.WriteLine($"Business rule for Product class: {ruleAttribute.RuleDescription}");
        }
    }
}

在这个例子中,productType.GetCustomAttributes(typeof(BusinessRuleAttribute), true) 方法返回应用在 Product 类上的 BusinessRuleAttribute 特性数组。true 参数表示包括继承的特性(如果有的话)。

3.3 获取成员上的特性

对于类的成员(方法、属性等),同样可以使用反射获取其特性。以之前的 OrderProcessor 类为例:

class Program
{
    static void Main()
    {
        Type orderProcessorType = typeof(OrderProcessor);
        MethodInfo validateMethod = orderProcessorType.GetMethod("ValidateOrderTotal");
        object[] methodAttributes = validateMethod.GetCustomAttributes(typeof(BusinessRuleAttribute), true);
        if (methodAttributes.Length > 0)
        {
            BusinessRuleAttribute methodRuleAttribute = (BusinessRuleAttribute)methodAttributes[0];
            Console.WriteLine($"Business rule for ValidateOrderTotal method: {methodRuleAttribute.RuleDescription}");
        }
    }
}

这里通过 orderProcessorType.GetMethod("ValidateOrderTotal") 获取 ValidateOrderTotal 方法的 MethodInfo,然后使用 GetCustomAttributes 方法获取该方法上的 BusinessRuleAttribute 特性。

4. 特性在元编程中的应用

4.1 元编程概念

元编程是一种编程技术,其中程序可以将自身或其他程序作为数据进行操作。在 C# 中,特性与反射结合使用,可以实现强大的元编程功能。通过特性为代码元素添加元数据,再利用反射在运行时读取这些元数据,我们可以根据元数据动态地改变程序的行为。

4.2 基于特性的依赖注入

依赖注入(Dependency Injection,DI)是一种软件设计模式,它允许对象依赖关系由外部提供,而不是在对象内部自行创建。我们可以使用特性和反射来实现一个简单的基于特性的依赖注入机制。

假设我们有一些服务接口和实现类:

public interface IMessageService
{
    string GetMessage();
}

public class WelcomeMessageService : IMessageService
{
    public string GetMessage()
    {
        return "Welcome!";
    }
}

public class GoodbyeMessageService : IMessageService
{
    public string GetMessage()
    {
        return "Goodbye!";
    }
}

我们定义一个自定义特性来标记需要注入依赖的属性:

public class InjectAttribute : System.Attribute
{
}

然后在需要依赖注入的类中使用这个特性:

public class MessagePrinter
{
    [Inject]
    public IMessageService MessageService { get; set; }

    public void PrintMessage()
    {
        if (MessageService != null)
        {
            Console.WriteLine(MessageService.GetMessage());
        }
    }
}

接下来,通过反射实现依赖注入:

class Program
{
    static void Main()
    {
        MessagePrinter printer = new MessagePrinter();
        Type printerType = printer.GetType();
        PropertyInfo[] properties = printerType.GetProperties();
        foreach (PropertyInfo property in properties)
        {
            object[] attributes = property.GetCustomAttributes(typeof(InjectAttribute), true);
            if (attributes.Length > 0 && property.PropertyType.IsInterface)
            {
                // 这里简单示例根据接口类型创建实现类实例
                if (property.PropertyType == typeof(IMessageService))
                {
                    property.SetValue(printer, new WelcomeMessageService());
                }
            }
        }
        printer.PrintMessage();
    }
}

在这个例子中,我们通过反射检查 MessagePrinter 类的属性上是否有 InjectAttribute 特性,如果有且属性类型是接口类型,就根据接口类型创建对应的实现类实例并注入到属性中。

4.3 基于特性的验证框架

我们可以利用特性和反射构建一个简单的验证框架。假设我们有一个 Customer 类,并且希望对其属性进行验证:

public class RequiredAttribute : System.Attribute
{
}

public class MinLengthAttribute : System.Attribute
{
    public int MinLength { get; set; }
    public MinLengthAttribute(int minLength)
    {
        MinLength = minLength;
    }
}

public class Customer
{
    [Required]
    public string Name { get; set; }

    [MinLength(5)]
    public string Address { get; set; }
}

然后创建一个验证方法,通过反射检查特性并进行验证:

class Program
{
    static void Main()
    {
        Customer customer = new Customer();
        customer.Name = "";
        customer.Address = "123";

        Type customerType = customer.GetType();
        PropertyInfo[] properties = customerType.GetProperties();
        foreach (PropertyInfo property in properties)
        {
            object[] requiredAttributes = property.GetCustomAttributes(typeof(RequiredAttribute), true);
            if (requiredAttributes.Length > 0)
            {
                object value = property.GetValue(customer);
                if (value == null || string.IsNullOrEmpty(value.ToString()))
                {
                    Console.WriteLine($"Property {property.Name} is required.");
                }
            }

            object[] minLengthAttributes = property.GetCustomAttributes(typeof(MinLengthAttribute), true);
            if (minLengthAttributes.Length > 0)
            {
                MinLengthAttribute minLengthAttribute = (MinLengthAttribute)minLengthAttributes[0];
                object value = property.GetValue(customer);
                if (value != null && value.ToString().Length < minLengthAttribute.MinLength)
                {
                    Console.WriteLine($"Property {property.Name} must be at least {minLengthAttribute.MinLength} characters long.");
                }
            }
        }
    }
}

在这个验证框架中,通过反射获取 Customer 类属性上的 RequiredAttributeMinLengthAttribute 特性,并根据特性的要求对属性值进行验证。

5. 特性的高级话题

5.1 特性与泛型

特性可以与泛型结合使用,为泛型类型和方法添加元数据。例如,假设我们有一个泛型缓存类,我们希望通过特性标记缓存策略:

public class CacheStrategyAttribute : System.Attribute
{
    public string StrategyName { get; set; }
    public CacheStrategyAttribute(string strategyName)
    {
        StrategyName = strategyName;
    }
}

[CacheStrategy("MemoryCache")]
public class GenericCache<T>
{
    private T _cachedValue;
    private bool _isCached;

    public T GetValue(Func<T> valueFactory)
    {
        if (!_isCached)
        {
            _cachedValue = valueFactory();
            _isCached = true;
        }
        return _cachedValue;
    }
}

在这个例子中,GenericCache<T> 泛型类使用 CacheStrategyAttribute 特性标记了缓存策略为 “MemoryCache”。

5.2 特性与动态代码生成

在一些高级场景中,我们可能需要根据特性动态生成代码。例如,我们可以使用 System.Reflection.Emit 命名空间来动态生成程序集、类型和方法,并根据特性定义它们的行为。

假设我们有一个自定义特性 GenerateMethodAttribute,用于标记一个类,指示为该类动态生成一个方法:

public class GenerateMethodAttribute : System.Attribute
{
    public string MethodName { get; set; }
    public GenerateMethodAttribute(string methodName)
    {
        MethodName = methodName;
    }
}

[GenerateMethod("DynamicGeneratedMethod")]
public class DynamicClass
{
    // 类体
}

然后通过 System.Reflection.Emit 来动态生成这个方法:

using System;
using System.Reflection;
using System.Reflection.Emit;

class Program
{
    static void Main()
    {
        AssemblyName assemblyName = new AssemblyName("DynamicAssembly");
        AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");

        Type dynamicClassType = typeof(DynamicClass);
        object[] attributes = dynamicClassType.GetCustomAttributes(typeof(GenerateMethodAttribute), true);
        if (attributes.Length > 0)
        {
            GenerateMethodAttribute generateAttribute = (GenerateMethodAttribute)attributes[0];

            TypeBuilder typeBuilder = moduleBuilder.DefineType("DynamicClassGenerated", TypeAttributes.Public | TypeAttributes.Class);
            MethodBuilder methodBuilder = typeBuilder.DefineMethod(generateAttribute.MethodName, MethodAttributes.Public | MethodAttributes.Static);
            ILGenerator ilGenerator = methodBuilder.GetILGenerator();
            ilGenerator.Emit(OpCodes.Ldstr, "This is a dynamically generated method.");
            ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }));
            ilGenerator.Emit(OpCodes.Ret);

            Type generatedType = typeBuilder.CreateType();
            MethodInfo generatedMethod = generatedType.GetMethod(generateAttribute.MethodName);
            generatedMethod.Invoke(null, null);
        }
    }
}

在这个例子中,我们通过反射获取 DynamicClass 类上的 GenerateMethodAttribute 特性,然后使用 System.Reflection.Emit 动态生成一个名为 “DynamicGeneratedMethod” 的方法,并执行该方法。

5.3 特性的性能考虑

虽然特性和反射是强大的工具,但在使用时需要考虑性能问题。反射操作通常比直接调用代码要慢,因为它涉及到在运行时动态查找和加载类型、成员等元数据。

在频繁调用的代码路径中,应尽量避免使用反射来读取特性。如果必须使用,可以考虑缓存反射获取的结果,以减少重复的查找开销。例如,对于经常需要检查特性的类型或成员,可以在应用启动时一次性获取并缓存特性信息,而不是每次都通过反射去获取。

另外,过多地使用特性可能会使代码变得复杂,增加维护成本。因此,在设计中要权衡特性带来的灵活性和代码的可读性、性能之间的关系。

通过深入了解 C# 特性(Attribute)以及它们在元编程中的应用,开发人员可以编写出更加灵活、可维护且功能强大的代码。从基础的特性定义和使用,到利用反射实现高级的元编程场景,特性为 C# 编程带来了丰富的可能性。同时,注意特性使用过程中的性能和设计权衡,能够确保代码在各种场景下都能高效运行。