03-C#.Net-特性-学习笔记

一、特性的本质

1.1 什么是特性

  • 特性是一个类:直接或间接继承自 Attribute 抽象类
  • 命名规范:类名默认以 Attribute 结尾(使用时可省略)
  • 应用方式:用方括号 [] 包裹,标记在类或类成员上
  • 编译后存在:与注释不同,特性在编译后依然存在于程序集中

1.2 特性 vs 注释

// 注释:编译后不存在,仅用于描述
/// <summary>这是一个Student类</summary>

// 特性:编译后存在,可通过反射获取
[Serializable]  // 标记类可序列化
[Custom(123)]   // 自定义特性
public class Student { }

二、自定义特性

2.1 定义特性类

特性类就是普通的类,继承 Attribute。构造函数参数是标记时的位置参数,公开的属性/字段可以在标记时用命名参数赋值:

// 使用 AttributeUsage 约束特性的使用方式
[AttributeUsage(
    AttributeTargets.All,      // 可标记的目标(类、方法、属性等)
    AllowMultiple = true,      // 是否允许重复标记
    Inherited = true           // 是否可被继承
)]
public class CustomAttribute : Attribute
{
    public string _Name { get; set; }  // 命名参数:标记时可赋值
    public int _Age;                   // 命名参数:标记时可赋值

    public CustomAttribute(int id) { }       // 位置参数

    public CustomAttribute(string name)
    {
        _Name = name;
    }

    public void Do()
    {
        Console.WriteLine("CustomAttribute.Do()");
    }
}

2.2 标记特性

特性可以标记在几乎所有代码元素上:

[Custom(123)]
[CustomAttributeChild]              // 子类特性同样可以用
public class Student
{
    [Custom(123)]
    [Custom(123)]                   // AllowMultiple=true 时允许重复标记
    public int Id { get; set; }

    [Custom("陈大宝")]
    public string Name { get; set; }

    [Custom(123456, _Age = 17)]     // 位置参数 + 命名参数
    public string Description;

    [Custom("山水", _Name = "456789")]
    public void Study() { }

    [return: Custom(123)]           // 标记在返回值上
    public string Answer([Custom(456)] string name)  // 标记在参数上
    {
        return $"This is {name}";
    }
}

2.3 AttributeUsage 详解

AttributeUsage 本身也是一个特性,专门用来约束自定义特性的使用方式,三个参数:

  • AttributeTargets — 指定特性能标记在哪里。常用值:ClassMethodPropertyFieldParameterAll,可用 | 组合多个目标。建议自定义特性时明确指定,避免误用:
[AttributeUsage(AttributeTargets.Property)]                          // 只能标记在属性上
[AttributeUsage(AttributeTargets.Field)]                             // 只能标记在字段上
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]  // 类或方法
  • AllowMultiple — 同一位置是否允许重复标记,默认 false。验证场景里一个属性需要同时标 [Required][Length],就需要设为 true

  • Inherited — 子类是否继承父类上的特性,默认 trueStudentVip 继承 StudentInherited=true 时通过反射也能在 StudentVip 上获取到 Student 的特性。注意:接口上的特性不会被实现类继承,只有类的继承链有效。


三、通过反射调用特性

特性标记后无法直接调用,必须通过反射获取实例才能使用。

关键点:

  • 特性标记 ≠ 执行,反射是让特性生效的唯一途径
  • 先用 IsDefined() 判断是否存在,再用 GetCustomAttributes() 获取实例,避免无谓的对象创建
  • GetCustomAttributes(true)true 参数表示同时搜索继承链;false 只获取当前类型自身的特性
  • 类、属性、字段、方法、参数上的特性都可以通过对应的反射对象获取
public static void Show(Student student)
{
    Type type = student.GetType();

    // 获取类上的特性
    if (type.IsDefined(typeof(CustomAttribute), true))
    {
        foreach (CustomAttribute attr in type.GetCustomAttributes(true))
        {
            Console.WriteLine(attr._Name);
            attr.Do();  // 通过反射拿到实例后才能调用方法
        }
    }

    // 获取属性上的特性
    foreach (PropertyInfo prop in type.GetProperties())
    {
        if (prop.IsDefined(typeof(CustomAttribute), true))
        {
            foreach (CustomAttribute attr in prop.GetCustomAttributes(true))
                attr.Do();
        }
    }

    // 获取字段上的特性
    foreach (FieldInfo field in type.GetFields())
    {
        if (field.IsDefined(typeof(CustomAttribute), true))
        {
            foreach (CustomAttribute attr in field.GetCustomAttributes(true))
                attr.Do();
        }
    }

    // 获取方法参数上的特性
    foreach (MethodInfo method in type.GetMethods())
    {
        foreach (ParameterInfo para in method.GetParameters())
        {
            if (para.IsDefined(typeof(CustomAttribute), true))
            {
                foreach (CustomAttribute attr in para.GetCustomAttributes(true))
                    attr.Do();
            }
        }
    }
}

四、特性的两大核心用途

用途一:获取额外信息(枚举描述)

问题: 数据库存数字(1、2、3),界面要显示文字描述。传统 if-else 的问题:

// 传统方式 ❌
if (userState == UserStateEnum.Normal)
    Console.WriteLine("正常状态");
else if (userState == UserStateEnum.Frozen)
    Console.WriteLine("已冻结");
// 问题:分支多、修改描述需要改多处

解决: 把描述直接标记在枚举字段上,反射+扩展方法统一读取。

// 1. 定义描述特性,只允许标记在字段上
[AttributeUsage(AttributeTargets.Field)]
public class RemarkAttribute : Attribute
{
    private string _Description;
    public RemarkAttribute(string description) => _Description = description;
    public string GetRemark() => _Description;
}

// 2. 标记枚举
public enum UserStateEnum
{
    [Remark("正常状态")] Normal  = 1,
    [Remark("已冻结")]   Frozen  = 2,
    [Remark("已删除")]   Deleted = 3,
    [Remark("其他")]     Other   = 4
}

// 3. 扩展方法封装反射逻辑,调用方无需关心反射细节
public static class RemarkAttributeExtension
{
    public static string GetRemark(this Enum @enum)
    {
        Type type = @enum.GetType();
        FieldInfo field = type.GetField(@enum.ToString());

        if (field.IsDefined(typeof(RemarkAttribute), true))
        {
            RemarkAttribute attr = field.GetCustomAttribute<RemarkAttribute>();
            return attr.GetRemark();
        }
        return @enum.ToString(); // 没有标记就返回枚举名称本身
    }
}

// 4. 使用
UserStateEnum state = UserStateEnum.Frozen;
Console.WriteLine(state.GetRemark()); // 输出:已冻结

// 在实体中直接暴露描述属性
public class UserInfo
{
    public UserStateEnum State { get; set; }
    public string UserStateDescription => State.GetRemark();
}

新增枚举值只加一行 [Remark("xxx")],描述改了只改标记处,获取逻辑完全不用动。


用途二:获取额外功能(数据验证)

问题: 保存前需要验证字段,传统 if 判断的问题:

  • 字段多了代码量爆炸
  • 验证逻辑散落各处,无法复用

解决: 把验证规则标记在属性上,反射统一执行。

// 1. 抽象基类定义统一接口(开闭原则:扩展新规则只需继承,不改已有代码)
public abstract class AbstractAttribute : Attribute
{
    public abstract ApiResult Validate(object oValue);
}

public class ApiResult
{
    public bool Success { get; set; }
    public string ErrorMessage { get; set; }
}

// 2. 具体验证规则
[AttributeUsage(AttributeTargets.Property)]
public class RequiredAttribute : AbstractAttribute
{
    public string _ErrorMessage;
    public RequiredAttribute(string message) => _ErrorMessage = message;

    public override ApiResult Validate(object value)
    {
        bool ok = value != null && !string.IsNullOrWhiteSpace(value.ToString());
        return new ApiResult { Success = ok, ErrorMessage = ok ? null : _ErrorMessage };
    }
}

[AttributeUsage(AttributeTargets.Property)]
public class LengthAttribute : AbstractAttribute
{
    public string _ErrorMessage;
    private int _Min, _Max;
    public LengthAttribute(int min, int max) { _Min = min; _Max = max; }

    public override ApiResult Validate(object value)
    {
        if (value == null) return new ApiResult { Success = true };
        int len = value.ToString().Length;
        bool ok = len >= _Min && len <= _Max;
        return new ApiResult { Success = ok, ErrorMessage = ok ? null : _ErrorMessage };
    }
}

// 3. 标记实体属性
public class UserInfo
{
    public int Id { get; set; }

    [Required("Name的值不能为空")]
    public string Name { get; set; }

    [Required("Mobile的值不能为空")]
    [Length(11, 11, _ErrorMessage = "手机号必须为11位数")]
    public string Mobile { get; set; }
}

// 4. 统一验证管理器,只依赖抽象基类
public static class ValidateInvokeManager
{
    public static ApiResult ValiDate<T>(this T t) where T : class
    {
        Type type = t.GetType();

        foreach (PropertyInfo prop in type.GetProperties())
        {
            if (prop.IsDefined(typeof(AbstractAttribute), true))
            {
                object value = prop.GetValue(t);

                // 一个属性可能有多个验证特性,逐一执行
                foreach (AbstractAttribute attr in prop.GetCustomAttributes())
                {
                    ApiResult result = attr.Validate(value);
                    if (!result.Success)
                        return result; // 遇到第一个失败立即返回
                }
            }
        }

        return new ApiResult { Success = true };
    }
}

// 5. 使用
UserInfo user = new UserInfo { Id = 1, Name = "张三", Mobile = "123" };
ApiResult result = ValidateInvokeManager.ValiDate(user);
if (!result.Success)
    Console.WriteLine(result.ErrorMessage); // 手机号必须为11位数

要新增验证规则(如正则、数字范围),只需新建一个继承 AbstractAttribute 的类,ValidateInvokeManager 完全不用改。


五、特性在自定义 ORM 中的应用

问题: 数据库表名/字段名与类名/属性名不一致,ORM 无法自动映射。

解决: 用特性标记映射关系,ORM 通过反射读取特性生成正确的 SQL。

// 定义映射特性
[AttributeUsage(AttributeTargets.Class)]
public class TableNameAttribute : Attribute
{
    public string Name { get; }
    public TableNameAttribute(string name) => Name = name;
}

[AttributeUsage(AttributeTargets.Property)]
public class ColumnNameAttribute : Attribute
{
    public string Name { get; }
    public ColumnNameAttribute(string name) => Name = name;
}

// 标记实体
[TableName("LM_User")]
public class SysUser : BaseModel
{
    [ColumnName("user_name")]  public string Name  { get; set; }
    [ColumnName("user_phone")] public string Phone { get; set; }
}

// ORM 查询时通过反射读取特性生成 SQL
public T Find<T>(int id) where T : BaseModel
{
    Type type = typeof(T);

    string tableName = type.IsDefined(typeof(TableNameAttribute), false)
        ? type.GetCustomAttribute<TableNameAttribute>().Name
        : type.Name;

    var columns = type.GetProperties().Select(p =>
        p.IsDefined(typeof(ColumnNameAttribute), false)
            ? p.GetCustomAttribute<ColumnNameAttribute>().Name
            : p.Name);

    string sql = $"SELECT {string.Join(",", columns)} FROM {tableName} WHERE Id={id}";
    // ... 执行查询并反射赋值
}

泛型缓存优化反射性能: 同一类型的 SQL 模板每次查询都一样,没必要每次都反射。用泛型类的静态构造函数缓存结果,同一类型只反射一次:

public class ConstantSqlString<T>
{
    private static readonly string FindSql;

    static ConstantSqlString()
    {
        // 静态构造函数只执行一次,之后直接读缓存
        Type type = typeof(T);
        FindSql = $"SELECT {string.Join(",", type.GetProperties().Select(p => $"[{p.Name}]"))} FROM {type.Name} WHERE Id=";
    }

    public static string GetFindSql(int id) => $"{FindSql}{id}";
}

// 使用
string sql = ConstantSqlString<SysUser>.GetFindSql(1);

ConstantSqlString<SysUser>ConstantSqlString<SysCompany> 是两个独立的类,各自有独立的静态字段,互不干扰,线程安全由 CLR 的类型初始化机制保证,无需加锁。


六、核心总结

特性的三大核心步骤

  1. 定义特性:继承 Attribute,用 AttributeUsage 约束使用范围
  2. 标记特性:用 [] 标记在目标上,传位置参数或命名参数
  3. 反射调用:运行时通过反射获取特性实例,执行其中的逻辑

特性的两大价值

  1. 获取额外信息:枚举描述、配置信息、映射关系
  2. 获取额外功能:数据验证、AOP 切面、权限控制

最佳实践

  • 明确指定 AttributeTargets,告诉使用者这个特性用在哪里
  • 先用 IsDefined() 判断再获取,避免无谓的对象创建
  • 用扩展方法封装反射调用逻辑,对外暴露干净的 API
  • 用抽象基类统一处理同类特性,保持可扩展性
  • 反射有性能开销,高频调用场景用泛型缓存优化

常见框架中的特性速查

场景 常见特性
序列化 [Serializable]
ASP.NET Core MVC [HttpGet] [Route] [Authorize] [FromBody]
依赖注入 [Inject] [Service]
ORM(EF Core) [Table] [Column] [Key] [ForeignKey]
数据验证 [Required] [Range] [StringLength] [EmailAddress]
posted @ 2026-03-19 13:57  龙猫•ᴥ•  阅读(4)  评论(0)    收藏  举报