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

C# 特性(Attributes)实战指南:从基础到高级应用

1. 初识C#特性不只是“装饰”的代码标签很多刚接触C#的朋友第一次看到代码里那些用方括号[]包起来的东西比如[Serializable]或者[Obsolete]可能会有点懵。这玩意儿是注释吗还是什么特殊的语法我刚开始学的时候也这么想过后来在项目里被它“坑”过几次又用它解决过不少头疼的问题才真正体会到它的妙处。你可以把它想象成给你写的类、方法或者属性贴上一个“智能标签”。这个标签不只是给人看的更重要的是给编译器、运行时环境或者你用的各种框架比如ASP.NET Core、Entity Framework看的。它们读到这些标签就知道该对这个代码元素做什么特殊处理。举个最生活化的例子你网购时收到的包裹上除了收件人信息是不是还贴着“易碎品”、“向上”、“生鲜”这类标签快递员看到“易碎品”处理起来就会格外小心看到“生鲜”就会优先配送。C#的特性Attribute干的就是类似的事儿。你给一个类贴上[Serializable]标签就等于告诉序列化工具“这个类可以安全地打包成字节流放心处理吧。”你给一个方法贴上[Obsolete]就等于在代码里立了个“前方施工请绕行”的牌子编译器看到就会提醒其他开发者“嘿这个方法快过时了别再用啦”所以特性本质上是一种声明式的编程方式。什么叫声明式就是你只需要声明“我想要什么”What而不需要详细写出“具体怎么做”How。比如你想让一个方法只在调试时输出日志你不用写一堆#if DEBUG ... #endif的条件编译代码只需要在方法上轻轻松松地加上一行[Conditional(DEBUG)]。至于怎么判断、何时调用那是编译器和运行时去操心的事。这种写法让代码变得异常简洁和清晰把那些繁琐的、重复的“管道”代码和你的核心业务逻辑分离开来。我后来做项目架构设计时特别喜欢用特性来处理像权限校验、日志记录、性能监控这些“横切关注点”代码干净得让人心情舒畅。2. 玩转内置特性站在巨人的肩膀上C#和.NET框架给我们准备了一整套开箱即用的内置特性覆盖了日常开发中的大多数常见场景。用好它们能让你少写很多代码避免很多低级错误。咱们挑几个最常用、最实用的结合我踩过的坑和实战心得好好聊一聊。2.1 基础必备每个C#开发者都应掌握的利器[Obsolete]- 你的代码升级导航员这个特性我愿称之为“团队协作神器”。当你要重构一个旧方法但又不能立刻删掉它因为还有很多地方在用或者你想推荐一个更好的新方法时[Obsolete]就派上用场了。public class PaymentService { // 第一版的老方法计算方式复杂 [Obsolete(此方法计算逻辑已过时请使用更精确的 CalculateTotalV2 方法。)] public decimal CalculateTotal(decimal price, int quantity) { return price * quantity; // 老逻辑没考虑税费 } // 第二版的新方法考虑了税费 public decimal CalculateTotalV2(decimal price, int quantity, decimal taxRate) { return price * quantity * (1 taxRate); } }当你或你的同事在代码里调用那个老的CalculateTotal方法时编译器会立刻给出一个警告并且把你写的那句提示信息显示出来。这比在开会时吼一嗓子“那个方法别用了”要有效得多。如果你确定老方法必须立刻废弃甚至可以把它升级成编译错误[Obsolete(此方法存在安全漏洞已禁止使用。请立即迁移到新方法。, true)] public void OldUnsafeMethod() { }加上error: true参数后任何使用该方法的地方都会直接导致编译失败强制团队进行升级。我在推动一个大型遗留系统升级时就用这招平稳地把几十个危险的老接口给替换掉了。[Conditional]- 调试与发布环境的智能开关这个特性对于管理调试日志特别有用。我们经常会在代码里写很多Console.WriteLine或者Debug.WriteLine来帮助排查问题但肯定不希望这些日志在发布版本中输出既影响性能还可能泄露敏感信息。using System.Diagnostics; public class AppLogger { [Conditional(DEBUG)] public void LogDebug(string message) { Console.WriteLine($[DEBUG] {DateTime.Now}: {message}); } [Conditional(TRACE)] public void LogTrace(string message) { // 更详细的跟踪信息可能记录到文件 File.AppendAllText(trace.log, $[TRACE] {message}\n); } public void LogError(string message) // 这个始终会编译 { Console.WriteLine($[ERROR] {message}); } }在Visual Studio里你可以在项目属性 - 生成 - 条件编译符号中定义DEBUG或TRACE。当你以Debug模式编译时LogDebug方法会被编译进去切换到Release模式这个方法就像被“剪掉”了一样完全不存在于最终的程序集中对性能零影响。这比手动用#if DEBUG包裹代码块要优雅和清晰得多。[Flags]- 让枚举化身为“多选开关”这是处理权限、状态集合等场景的绝佳工具。没有[Flags]的枚举一个变量只能表示一种状态。有了它一个变量可以同时表示多种状态的组合。[Flags] public enum FileAccessPermissions { None 0, // 0b0000 Read 1, // 0b0001 Write 2, // 0b0010 Execute 4, // 0b0100 Delete 8, // 0b1000 // 可以方便地定义常用组合 ReadWrite Read | Write, // 0b0011 (3) FullControl Read | Write | Execute | Delete // 0b1111 (15) } // 使用起来非常直观 FileAccessPermissions userPermissions FileAccessPermissions.Read | FileAccessPermissions.Write; // 检查是否拥有某项权限 if (userPermissions.HasFlag(FileAccessPermissions.Read)) { Console.WriteLine(用户可以读取文件。); } // 添加一项权限 userPermissions | FileAccessPermissions.Execute; // 移除一项权限 userPermissions ~FileAccessPermissions.Write;关键点在于枚举项的值必须是2的幂1,2,4,8...这样每个值在二进制表示中都只有一个位是1进行位运算|、时就不会互相干扰。我曾在做一个配置管理系统时用[Flags]枚举来管理上百个可配置模块的启用状态代码清晰运算高效。2.2 数据序列化的好帮手[Xml*]与[Json*]家族在Web API和微服务大行其道的今天对象和JSON/XML之间的序列化反序列化是家常便饭。内置的特性可以让你精细地控制这个过程。控制JSON序列化假设你有一个C#的模型类但需要对接一个使用蛇形命名snake_case的第三方APIusing System.Text.Json.Serialization; public class UserProfile { // C#属性名是PascalCase但序列化成JSON时变成snake_case [JsonPropertyName(user_id)] public int UserId { get; set; } [JsonPropertyName(full_name)] public string FullName { get; set; } // 这个属性在序列化时完全被忽略比如密码字段 [JsonIgnore] public string PasswordHash { get; set; } // 可以控制顺序某些严格的API要求字段顺序 [JsonPropertyOrder(1)] public string Username { get; set; } }使用System.Text.Json序列化时var user new UserProfile { UserId 123, FullName 张三, PasswordHash secret, Username zhangsan }; string json JsonSerializer.Serialize(user); // 输出: {user_id:123,full_name:张三,username:zhangsan} // 注意PasswordHash 不见了字段顺序也变了控制XML序列化如果你还在处理一些老的SOAP服务或者XML配置文件这些特性就更有用了using System.Xml.Serialization; [XmlRoot(Employee)] // 指定XML根元素名 public class Person { [XmlAttribute(id)] // 作为XML属性输出 public int Id { get; set; } [XmlElement(Name)] // 指定元素名 public string FullName { get; set; } [XmlIgnore] // 忽略这个属性 public DateTime DateOfBirth { get; set; } // 如果属性是数组或列表可以用 XmlArray [XmlArray(Projects)] [XmlArrayItem(Project)] public Liststring ProjectList { get; set; } new Liststring(); }3. 创造你自己的特性释放元数据编程的威力当内置特性无法满足你的特殊需求时就该自己动手打造了。自定义特性就像给你的项目打造一套专属的“标签体系”让代码能承载更丰富的业务语义。3.1 手把手创建第一个自定义特性咱们从一个最实际的需求出发给团队写的API方法添加负责人和版本信息。这样在生成文档或者排查问题时一眼就能知道这个方法是谁写的、属于哪个版本。第一步定义特性类所有自定义特性都必须继承自System.Attribute类。按照约定类名以Attribute结尾但使用时可以省略。using System; // AttributeUsage 是关键它定义了我们的特性可以用在什么地方 [AttributeUsage(AttributeTargets.Method, // 只能用在方法上 AllowMultiple false, // 同一个方法上不能用两次 Inherited true)] // 派生类重写的方法也会继承这个特性 public class AuthorInfoAttribute : Attribute { // 特性可以有自己的属性这些就是“标签”上的信息 public string Author { get; } public string Email { get; set; } // 可选的setter public Version Version { get; set; } // 构造函数参数对应的是使用特性时必须提供的信息 public AuthorInfoAttribute(string author) { Author author ?? throw new ArgumentNullException(nameof(author)); Version new Version(1, 0, 0); // 设置默认版本 } }这里重点说一下[AttributeUsage]它有三个重要参数AttributeTargets指定特性可以应用的目标。可以是Class、Method、Property、Field、Assembly等可以用|运算符组合多个目标比如AttributeTargets.Class | AttributeTargets.Struct。AllowMultiple如果设为true意味着你可以在同一个目标上多次应用同一个特性。比如你可以给一个方法打上多个[Tag(重要)]、[Tag(优化)]标签。Inherited如果设为true当特性应用在基类或虚方法上时派生类或重写方法也会继承这个特性。对于记录作者信息我们通常希望继承。第二步使用特性使用起来和内置特性一模一样非常直观public class OrderService { [AuthorInfo(王工程师, Email wangexample.com, Version new Version(2, 1))] public void CreateOrder(Order order) { // 创建订单的业务逻辑 } [AuthorInfo(李架构师)] [Obsolete(请使用新的 ProcessOrderV2 方法)] public void ProcessOrder(int orderId) { // 老的处理逻辑 } }3.2 通过反射读取特性信息让标签“活”起来特性本身只是静态的元数据它的魔力需要通过反射Reflection来激发。反射可以在运行时检查类型、读取特性信息从而动态地改变程序行为。接上面的例子我们可以写一个简单的文档生成器using System; using System.Linq; using System.Reflection; public static class ApiDocumentGenerator { public static void GenerateDocumentation(Type serviceType) { Console.WriteLine($ {serviceType.Name} 服务API文档 \n); // 获取所有公共实例方法 var methods serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly); foreach (var method in methods) { Console.WriteLine($方法: {method.Name}); // 1. 读取 AuthorInfo 特性 var authorAttr method.GetCustomAttributeAuthorInfoAttribute(); if (authorAttr ! null) { Console.WriteLine($ 作者: {authorAttr.Author}); if (!string.IsNullOrEmpty(authorAttr.Email)) Console.WriteLine($ 邮箱: {authorAttr.Email}); Console.WriteLine($ 版本: {authorAttr.Version}); } // 2. 读取 Obsolete 特性 var obsoleteAttr method.GetCustomAttributeObsoleteAttribute(); if (obsoleteAttr ! null) { Console.WriteLine($ ⚠️ 已过时: {obsoleteAttr.Message}); } // 3. 读取参数信息 var parameters method.GetParameters(); if (parameters.Any()) { Console.WriteLine($ 参数:); foreach (var param in parameters) { Console.WriteLine($ - {param.ParameterType.Name} {param.Name}); } } Console.WriteLine(); // 空行分隔 } } } // 使用它 ApiDocumentGenerator.GenerateDocumentation(typeof(OrderService));运行上面的代码你可能会得到类似这样的输出 OrderService 服务API文档 方法: CreateOrder 作者: 王工程师 邮箱: wangexample.com 版本: 2.1 方法: ProcessOrder 作者: 李架构师 版本: 1.0.0.0 ⚠️ 已过时: 请使用新的 ProcessOrderV2 方法 参数: - Int32 orderId你看我们并没有修改OrderService类的任何业务逻辑代码只是通过反射读取了附加在方法上的元数据就自动生成了包含作者、版本、过时警告的API文档。这就是声明式编程的魅力——关注点分离业务代码和元数据描述各司其职。性能提示反射操作是有开销的。如果在高频循环中反复调用GetCustomAttribute可能会成为性能瓶颈。一个常见的优化技巧是缓存结果。例如可以在程序启动时扫描一次所有需要的类型和方法将特性信息缓存到字典里后续直接读取缓存。4. 特性在实战中的高级玩法了解了基础咱们来看看特性在真实项目里能玩出什么花样。这些场景都是我亲身经历过的非常实用。4.1 实现轻量级数据验证框架在Web开发中模型验证是刚需。虽然ASP.NET Core有强大的DataAnnotations但有时我们想在更轻量的场景比如控制台应用、后台服务中做验证。用特性自己造一个轮子能让你对原理理解得更透彻。第一步定义验证特性我们先定义几个常用的验证规则特性// 必填验证 [AttributeUsage(AttributeTargets.Property)] public class RequiredAttribute : Attribute { public string ErrorMessage { get; set; } 字段不能为空。; } // 字符串长度验证 [AttributeUsage(AttributeTargets.Property)] public class StringLengthAttribute : Attribute { public int MaxLength { get; } public int MinLength { get; } public string ErrorMessage { get; set; } public StringLengthAttribute(int maxLength, int minLength 0) { MaxLength maxLength; MinLength minLength; ErrorMessage $长度必须在 {minLength} 到 {maxLength} 个字符之间。; } } // 范围验证用于数字 [AttributeUsage(AttributeTargets.Property)] public class RangeAttribute : Attribute { public double Min { get; } public double Max { get; } public string ErrorMessage { get; set; } public RangeAttribute(double min, double max) { Min min; Max max; ErrorMessage $值必须在 {min} 到 {max} 之间。; } } // 正则表达式验证 [AttributeUsage(AttributeTargets.Property)] public class RegexAttribute : Attribute { public string Pattern { get; } public string ErrorMessage { get; set; } public RegexAttribute(string pattern) { Pattern pattern; ErrorMessage 格式不正确。; } }第二步应用特性到模型定义一个用户注册模型并贴上我们刚创建的验证标签public class UserRegistrationModel { [Required(ErrorMessage 用户名是必填项)] [StringLength(20, 3, ErrorMessage 用户名长度需在3-20个字符之间。)] public string Username { get; set; } [Required] [Regex(^[^\s][^\s]\.[^\s]$, ErrorMessage 请输入有效的电子邮件地址。)] public string Email { get; set; } [Required] [StringLength(100, 6, ErrorMessage 密码长度至少6位。)] public string Password { get; set; } [Range(18, 120, ErrorMessage 年龄需在18至120岁之间。)] public int Age { get; set; } }第三步实现验证器核心就是一个通过反射读取特性并执行验证的静态类using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; public static class SimpleValidator { public static (bool IsValid, Liststring Errors) Validate(object obj) { var errors new Liststring(); var type obj.GetType(); var properties type.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var prop in properties) { var value prop.GetValue(obj); // 1. 检查 Required var requiredAttr prop.GetCustomAttributeRequiredAttribute(); if (requiredAttr ! null) { if (value null || (value is string str string.IsNullOrWhiteSpace(str))) { errors.Add(${prop.Name}: {requiredAttr.ErrorMessage}); continue; // 如果必填为空跳过后续验证 } } // 2. 检查 StringLength (仅对字符串类型) var stringLengthAttr prop.GetCustomAttributeStringLengthAttribute(); if (stringLengthAttr ! null value is string stringValue) { if (stringValue.Length stringLengthAttr.MinLength || stringValue.Length stringLengthAttr.MaxLength) { errors.Add(${prop.Name}: {stringLengthAttr.ErrorMessage}); } } // 3. 检查 Range (对数值类型) var rangeAttr prop.GetCustomAttributeRangeAttribute(); if (rangeAttr ! null value is IConvertible numericValue) { double doubleValue Convert.ToDouble(numericValue); if (doubleValue rangeAttr.Min || doubleValue rangeAttr.Max) { errors.Add(${prop.Name}: {rangeAttr.ErrorMessage}); } } // 4. 检查 Regex var regexAttr prop.GetCustomAttributeRegexAttribute(); if (regexAttr ! null value is string regexValue !string.IsNullOrEmpty(regexValue)) { if (!Regex.IsMatch(regexValue, regexAttr.Pattern)) { errors.Add(${prop.Name}: {regexAttr.ErrorMessage}); } } } return (!errors.Any(), errors); } }第四步使用验证器现在我们可以在任何地方使用这个通用的验证逻辑了var user new UserRegistrationModel { Username ab, // 太短会触发StringLength错误 Email invalid-email, Password 123, Age 15 // 小于18会触发Range错误 }; var (isValid, errors) SimpleValidator.Validate(user); if (!isValid) { Console.WriteLine(验证失败); foreach (var error in errors) { Console.WriteLine($ - {error}); } } // 输出 // 验证失败 // - Username: 用户名长度需在3-20个字符之间。 // - Email: 请输入有效的电子邮件地址。 // - Password: 密码长度至少6位。 // - Age: 年龄需在18至120岁之间。这个自制的验证框架虽然简单但清晰地展示了特性如何将验证规则元数据与验证逻辑代码解耦。模型类只关心自己有哪些属性以及这些属性应该遵守什么规则验证器只关心如何读取规则并执行检查。两者通过特性这个桥梁优雅地连接在一起。4.2 实现简易的依赖注入标记在小型项目或特定模块中你可能不想引入完整的IoC容器如Autofac、Microsoft.Extensions.DependencyInjection但又想享受依赖注入带来的松耦合好处。用特性可以快速实现一个简易的“服务注册与发现”机制。第一步定义标记特性// 标记一个类是可被注入的服务 [AttributeUsage(AttributeTargets.Class)] public class InjectableAttribute : Attribute { public LifeTime LifeTime { get; } public Type ServiceType { get; } // 通常注册为接口 public InjectableAttribute(Type serviceType, LifeTime lifeTime LifeTime.Transient) { ServiceType serviceType; LifeTime lifeTime; } } public enum LifeTime { Transient, // 每次请求都创建新实例 Scoped, // 同一作用域内单例 Singleton // 全局单例 } // 标记一个属性或构造函数参数需要被注入 [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] public class InjectAttribute : Attribute { }第二步标记服务与依赖// 定义接口 public interface ILogger { void Log(string message); } public interface IEmailService { void Send(string to, string body); } // 标记实现类 [Injectable(typeof(ILogger), LifeTime.Singleton)] public class ConsoleLogger : ILogger { public void Log(string message) Console.WriteLine($[LOG] {message}); } [Injectable(typeof(IEmailService))] public class SmtpEmailService : IEmailService { public void Send(string to, string body) Console.WriteLine($发送邮件给 {to}: {body}); } // 一个需要依赖注入的业务类 public class OrderProcessor { [Inject] // 标记此属性需要注入 public ILogger Logger { get; set; } private readonly IEmailService _emailService; // 标记构造函数参数需要注入 public OrderProcessor([Inject] IEmailService emailService) { _emailService emailService; } public void Process() { Logger?.Log(开始处理订单...); // ... 业务逻辑 _emailService.Send(customerexample.com, 您的订单已处理); Logger?.Log(订单处理完成。); } }第三步实现一个简单的容器using System; using System.Collections.Generic; using System.Linq; using System.Reflection; public class TinyContainer { private readonly DictionaryType, (Type ImplType, LifeTime LifeTime) _registrations new(); private readonly DictionaryType, object _singletons new(); public void RegisterAssembly(Assembly assembly) { // 扫描程序集找到所有带有 [Injectable] 特性的类 var injectableTypes assembly.GetTypes() .Where(t t.IsClass !t.IsAbstract) .Select(t new { Type t, Attr t.GetCustomAttributeInjectableAttribute() }) .Where(x x.Attr ! null); foreach (var item in injectableTypes) { _registrations[item.Attr.ServiceType] (item.Type, item.Attr.LifeTime); Console.WriteLine($注册服务: {item.Attr.ServiceType.Name} - {item.Type.Name} ({item.Attr.LifeTime})); } } public object Resolve(Type serviceType) { if (!_registrations.TryGetValue(serviceType, out var registration)) throw new InvalidOperationException($未找到服务注册: {serviceType.Name}); // 根据生命周期返回实例 switch (registration.LifeTime) { case LifeTime.Singleton: if (!_singletons.TryGetValue(serviceType, out var singleton)) { singleton CreateInstance(registration.ImplType); _singletons[serviceType] singleton; } return singleton; case LifeTime.Transient: default: return CreateInstance(registration.ImplType); } } public T ResolveT() (T)Resolve(typeof(T)); private object CreateInstance(Type implementationType) { var constructor implementationType.GetConstructors().FirstOrDefault(); if (constructor null) return Activator.CreateInstance(implementationType); // 解析构造函数参数 var parameters constructor.GetParameters(); var args new object[parameters.Length]; for (int i 0; i parameters.Length; i) { // 如果参数有 [Inject] 特性或者参数类型是已注册的服务则解析依赖 if (parameters[i].GetCustomAttributeInjectAttribute() ! null || _registrations.ContainsKey(parameters[i].ParameterType)) { args[i] Resolve(parameters[i].ParameterType); } else { // 否则尝试用默认值 args[i] parameters[i].HasDefaultValue ? parameters[i].DefaultValue : null; } } var instance constructor.Invoke(args); // 处理属性注入 var properties implementationType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var prop in properties) { if (prop.GetCustomAttributeInjectAttribute() ! null prop.CanWrite) { var propValue Resolve(prop.PropertyType); prop.SetValue(instance, propValue); } } return instance; } }第四步使用我们的小容器// 1. 创建容器并注册服务通过扫描程序集 var container new TinyContainer(); container.RegisterAssembly(Assembly.GetExecutingAssembly()); // 扫描当前程序集 // 2. 解析 OrderProcessor其依赖会自动注入 var orderProcessor container.ResolveOrderProcessor(); // 3. 使用它 orderProcessor.Process(); // 输出示例 // 注册服务: ILogger - ConsoleLogger (Singleton) // 注册服务: IEmailService - SmtpEmailService (Transient) // [LOG] 开始处理订单... // 发送邮件给 customerexample.com: 您的订单已处理 // [LOG] 订单处理完成。这个例子虽然简化了很多比如没有作用域生命周期、循环依赖检测等但它清晰地展示了如何利用特性来实现一种“约定优于配置”的依赖注入模式。你只需要给服务类打上[Injectable]标签给依赖项打上[Inject]标签容器就能自动组装它们。这在开发插件系统、模块化应用时特别有用。4.3 与AOP面向切面编程结合AOP是解决横切关注点如日志、事务、缓存、权限的经典范式。特性是.NET中实现AOP的一种非常自然的方式。上面我们演示了通过反射手动调用前后逻辑。在实际项目中我们更常借助一些轻量级的AOP库如Rougamo.Fody,MethodDecorator.Fody它们在编译时通过IL重写技术织入代码性能几乎无损。这里以记录方法执行时间的通用需求为例看看如何用特性优雅解决第一步定义一个计时特性[AttributeUsage(AttributeTargets.Method, AllowMultiple false)] public class TimeItAttribute : Attribute { public string Category { get; set; } Default; }第二步使用编译时织入以Rougamo为例首先通过NuGet安装Rougamo.Fody。然后using Rougamo; using Rougamo.Context; using System.Diagnostics; // 继承 MoAttribute这是Rougamo的基类 public class TimeItAopAttribute : MoAttribute { private Stopwatch _stopwatch; // 方法执行前 public override void OnEntry(MethodContext context) { _stopwatch Stopwatch.StartNew(); var category context.Method.GetCustomAttributeTimeItAttribute()?.Category ?? Default; Console.WriteLine($[{category}] 进入方法: {context.Method.Name}); } // 方法执行后成功返回 public override void OnSuccess(MethodContext context) { _stopwatch.Stop(); var category context.Method.GetCustomAttributeTimeItAttribute()?.Category ?? Default; Console.WriteLine($[{category}] 离开方法: {context.Method.Name}, 耗时: {_stopwatch.ElapsedMilliseconds}ms); } // 方法抛出异常时 public override void OnException(MethodContext context) { _stopwatch.Stop(); var category context.Method.GetCustomAttributeTimeItAttribute()?.Category ?? Default; Console.WriteLine($[{category}] 方法 {context.Method.Name} 异常: {context.Exception.Message}, 耗时: {_stopwatch.ElapsedMilliseconds}ms); } // 方法最终结束时无论成功或异常 public override void OnExit(MethodContext context) { // 清理资源如果有的话 } }第三步在方法上应用特性public class ExpensiveService { [TimeIt(Category Database)] [TimeItAop] // 注意这里应用了两个特性一个是我们定义的标记一个是AOP逻辑 public ListProduct GetProductsFromDatabase() { // 模拟数据库查询耗时 Thread.Sleep(new Random().Next(100, 500)); return new ListProduct { new Product(), new Product() }; } [TimeIt(Category Calculation)] [TimeItAop] public double CalculateComplexStuff() { // 模拟复杂计算 Thread.Sleep(new Random().Next(200, 800)); return 42.0; } }当你调用GetProductsFromDatabase或CalculateComplexStuff时控制台会自动输出方法的执行时间而你完全不需要在业务方法内写任何计时代码。Rougamo在编译时修改了IL代码将TimeItAopAttribute中的逻辑织入到了目标方法的前后。这种方式对代码侵入性极小只需要添加一个特性就能无痕地实现性能监控、日志记录、异常处理等通用功能。5. 特性使用的注意事项与最佳实践玩了这么多花样最后也得聊聊哪些“坑”要避开以及怎么用得更好。这些都是我多年实战总结的血泪经验。1. 性能考量反射是双刃剑通过GetCustomAttribute()读取特性确实方便但反射操作比直接调用代码要慢。在高性能、低延迟的场景例如高频交易系统、游戏主循环中需要谨慎。缓存缓存再缓存如果你需要反复读取某个类型的特性一定要把结果缓存起来。可以用ConcurrentDictionaryType, TAttribute或者LazyT来实现线程安全的缓存。考虑编译时方案对于极致的性能要求可以考虑使用Source GeneratorsC# 9.0在编译时生成代码完全避免运行时反射。比如你可以写一个Source Generator在编译时扫描所有带有特定特性的类并自动生成对应的注册代码或验证代码。2. 设计原则保持特性的“单纯”单一职责一个特性只应该做一件事。不要设计一个[LogAndValidateAndAuthorize]这样的“瑞士军刀”特性。把它拆分成[Log]、[Validate]、[Authorize]组合使用更灵活。不可变设计特性的属性Property最好只有getter通过构造函数参数初始化。这能保证特性元数据在应用后是不可变的更安全也更容易理解。命名清晰特性类名应该明确表达其用途比如[ValidateEmail]就比[Check1]好得多。遵循XXXAttribute的命名规范。3. 与框架的协作了解框架的“脾气”像ASP.NET Core、Entity Framework这些主流框架对特性有很深度的集成。在使用自定义特性时要确保它们不会和框架内置的特性行为冲突。例如不要定义自己的[Route]特性以免和MVC的路由系统混淆。特性不是银弹虽然特性很强大但不要滥用。简单的配置项可能用一个appsettings.json文件更合适简单的行为扩展用继承或组合模式可能更直观。特性最适合用来标注“元数据”而不是承载复杂的运行时逻辑。4. 调试与维护特性在调试器里是可见的在Visual Studio中调试时你可以在“即时窗口”或通过代码查看特性信息。善用这个功能来排查问题。为自定义特性编写单元测试特性的逻辑尤其是通过反射读取和使用特性的逻辑应该被充分测试。确保你的特性在各种边界条件下都能正确工作。说到底C#特性是一种强大的元编程工具。它让你能够将额外的信息“附加”到代码上从而影响编译器、运行时或工具的行为。从简单的标记过时方法到复杂的AOP和依赖注入框架特性都能提供一种声明式、非侵入式的解决方案。掌握它意味着你多了一种让代码更简洁、更灵活、更易于维护的武器。刚开始可能会觉得有点抽象但多写几次尤其是在自己的工具库或框架中尝试使用你很快就会爱上这种“贴标签”的编程方式。

相关文章:

C# 特性(Attributes)实战指南:从基础到高级应用

1. 初识C#特性:不只是“装饰”的代码标签 很多刚接触C#的朋友,第一次看到代码里那些用方括号[]包起来的东西,比如[Serializable]或者[Obsolete],可能会有点懵。这玩意儿是注释吗?还是什么特殊的语法?我刚开…...

Unity实现动态文字弧度变形效果

1. 从静态到动态:为什么你的文字需要“动”起来? 在游戏开发或者UI设计里,文字常常被当作一个“信息传递者”,规规矩矩地躺在屏幕上。但你想过没有,如果文字本身也能成为视觉焦点,甚至能“跳舞”&#xff0…...

立创ESP-Hi:基于ESP32-C3的超低成本AI对话机器狗开源项目全解析

立创ESP-Hi:基于ESP32-C3的超低成本AI对话机器狗开源项目全解析 最近在开源硬件社区里,一个叫“ESP-Hi”的小玩意儿挺火的。它本质上是一个能对话、能动的机器狗,但最吸引人的是它的核心——一块成本极低的ESP32-C3开发板。很多朋友问我&…...

Z-Image-Turbo-辉夜巫女部署案例:高校数字艺术课程AI绘画实验平台搭建实录

Z-Image-Turbo-辉夜巫女部署案例:高校数字艺术课程AI绘画实验平台搭建实录 1. 引言:当AI绘画走进高校艺术课堂 去年秋天,我在一所高校的数字艺术系做技术分享,系主任王老师提出了一个困扰他们很久的问题:“我们的学生…...

计算机毕业设计题目Web开发实战:从选题到部署的全链路技术指南

作为一名计算机专业的过来人,我深知毕业设计(毕设)是检验四年学习成果的关键一战。尤其是Web开发方向,选题看似简单,但想做出一个结构清晰、功能完整、能拿得出手的项目,却常常让同学们感到迷茫和焦虑。今天…...

5大维度赋能企业自动化:OpenRPA开源RPA平台从部署到价值落地全指南

5大维度赋能企业自动化:OpenRPA开源RPA平台从部署到价值落地全指南 【免费下载链接】openrpa Free Open Source Enterprise Grade RPA 项目地址: https://gitcode.com/gh_mirrors/op/openrpa OpenRPA作为免费开源的企业级RPA(机器人流程自动化&am…...

Python实战:SMOTE、Borderline SMOTE与ADASYN在不平衡金融风控数据中的应用对比

1. 金融风控中的“数据失衡”困局:为什么你的模型总抓不住“坏人”? 在金融风控和反欺诈领域,我踩过最大的坑,不是模型不够复杂,而是数据本身“不公平”。想象一下,你手头有10万笔信用卡交易记录&#xff0…...

折射率匹配液与光路胶在光通信中的关键应用与选择

1. 光通信里的“隐形助手”:折射率匹配液与光路胶到底是什么? 如果你拆开过家里的光猫,或者见过工厂里那些精密的光学模块,可能会发现里面有些地方看起来“油油的”,或者有些透明的“胶水”把不同的玻璃片粘在一起。别…...

【autoDL】新手避坑指南:从文件上传到镜像克隆的实用技巧

1. 新手第一课:理解AutoDL的“无卡”与“有卡”模式 刚接触AutoDL的朋友,看到“无卡模式”和“有卡模式”这两个词,是不是有点懵?别急,这其实是平台为了帮我们省钱设计的一个非常实用的功能。我刚开始用的时候&#xf…...

Python自动购票工具:3大技术突破实现抢票效率提升300%

Python自动购票工具:3大技术突破实现抢票效率提升300% 【免费下载链接】Automatic_ticket_purchase 大麦网抢票脚本 项目地址: https://gitcode.com/GitHub_Trending/au/Automatic_ticket_purchase 在热门演出票务抢购场景中,手动操作往往因页面加…...

庐山派K230开发板MicroPython编程入门指南:从语法到硬件交互

庐山派K230开发板MicroPython编程入门指南:从语法到硬件交互 最近有不少朋友拿到了庐山派K230开发板,想用Python来玩转硬件,但又觉得从C语言入门门槛太高。别担心,今天我就带你用MicroPython上手,让你用熟悉的Python语…...

Simulink低通滤波器实战:从原理到参数调优

1. 低通滤波器:不只是“过滤”,更是信号的“美容师” 大家好,我是老张,在信号处理和控制系统这块摸爬滚打了十几年,用Simulink搭过的模型连起来能绕办公室好几圈。今天咱们不聊那些高深的理论,就说说一个几…...

CodeBuddy提示词实战指南:从零构建高效开发工作流

最近在项目里尝试用 CodeBuddy 来辅助编码,发现提示词写得好不好,效果天差地别。有时候它能精准地生成我想要的函数,有时候却答非所问,或者代码风格完全不对路。相信不少刚开始接触 AI 编程助手的同学都有类似的困扰。今天&#x…...

使用LaTeX撰写技术报告:Lingbot深度模型算法原理与实验分析

使用LaTeX撰写技术报告:Lingbot深度模型算法原理与实验分析 写技术报告或者论文,最头疼的往往不是研究本身,而是怎么把那些复杂的公式、算法、图表和参考文献,整整齐齐、漂漂亮亮地排版出来。Word虽然简单,但遇到复杂…...

阿里中文语音识别模型实测:一键部署,会议录音转文字就是这么简单

阿里中文语音识别模型实测:一键部署,会议录音转文字就是这么简单 1. 引言:告别繁琐,让语音转文字变得触手可及 你有没有过这样的经历?开完一场两小时的会议,看着手机里录下的音频文件发愁——要把里面的内…...

OFA视觉问答模型API开发指南:FastAPI高效封装教程

OFA视觉问答模型API开发指南:FastAPI高效封装教程 1. 引言 想象一下,你有一个能够看懂图片并回答问题的AI模型,现在需要让它成为人人都能使用的在线服务。这就是我们今天要解决的问题:如何用FastAPI将OFA视觉问答模型封装成高性…...

Alpamayo-R1-10B一文详解:10B参数VLA模型在物理AI AV数据集上的表现

Alpamayo-R1-10B一文详解:10B参数VLA模型在物理AI AV数据集上的表现 1. 引言:当自动驾驶学会“思考” 想象一下,你坐在一辆自动驾驶汽车里,前方是一个复杂的十字路口,有行人、自行车和变道的车辆。传统的自动驾驶系统…...

基于TI TMS320F28P550的灰度传感器ADC驱动移植与循迹应用实战

基于TI TMS320F28P550的灰度传感器ADC驱动移植与循迹应用实战 最近在准备电赛,很多同学问我怎么用TI的DSP开发板(LCKFB)来读取灰度传感器,实现小车循迹。这确实是个很实用的项目,今天我就手把手带大家走一遍完整的流程…...

Figma中文插件革新:突破语言壁垒的设计效率工具

Figma中文插件革新:突破语言壁垒的设计效率工具 【免费下载链接】figmaCN 中文 Figma 插件,设计师人工翻译校验 项目地址: https://gitcode.com/gh_mirrors/fi/figmaCN 作为中文设计师,你是否曾因Figma全英文界面而放慢创作节奏&#…...

揭秘AI如何重塑音乐创作:AICoverGen声音转换技术全解析

揭秘AI如何重塑音乐创作:AICoverGen声音转换技术全解析 【免费下载链接】AICoverGen A WebUI to create song covers with any RVC v2 trained AI voice from YouTube videos or audio files. 项目地址: https://gitcode.com/gh_mirrors/ai/AICoverGen 在数字…...

Llama-3.2V-11B-cot部署案例:Kubernetes集群中水平扩展图文推理微服务

Llama-3.2V-11B-cot部署案例:Kubernetes集群中水平扩展图文推理微服务 想象一下,你的电商平台每天要处理上百万张用户上传的商品图片,需要自动生成描述、识别瑕疵、分析场景。如果只靠一台服务器,高峰期直接卡死,用户…...

Figma界面汉化插件让中文用户实现无障碍设计工作流

Figma界面汉化插件让中文用户实现无障碍设计工作流 【免费下载链接】figmaCN 中文 Figma 插件,设计师人工翻译校验 项目地址: https://gitcode.com/gh_mirrors/fi/figmaCN 解决英文界面障碍的本地化方案 Figma作为主流设计工具,其全英文界面一直…...

7个秘诀掌握思源宋体CN:从免费到专业的字体应用完全指南

7个秘诀掌握思源宋体CN:从免费到专业的字体应用完全指南 【免费下载链接】source-han-serif-ttf Source Han Serif TTF 项目地址: https://gitcode.com/gh_mirrors/so/source-han-serif-ttf 为什么选择思源宋体CN?破解字体使用的三大困境 在设计…...

新一代AVIF图像格式插件:Photoshop高效图像处理完整解决方案

新一代AVIF图像格式插件:Photoshop高效图像处理完整解决方案 【免费下载链接】avif-format An AV1 Image (AVIF) file format plug-in for Adobe Photoshop 项目地址: https://gitcode.com/gh_mirrors/avi/avif-format 一、为何选择AVIF?破解图像…...

第一期 立创·梁山派训练营(自由赛道)全记录:从开营到结营的项目实战与资源汇总

第一期 立创梁山派训练营(自由赛道)全记录:从开营到结营的项目实战与资源汇总 大家好,我是老张,一个在嵌入式行业摸爬滚打了十来年的工程师。最近,立创EDA和梁山派联合搞了个嵌入式训练营,我作…...

AI智能客服开发实战:从架构设计到生产环境避坑指南

最近在做一个AI智能客服的项目,从零到一,再到上线稳定运行,踩了不少坑,也积累了一些实战经验。今天就来聊聊从架构设计到生产环境部署,那些值得分享和需要避坑的地方。 根据行业报告,超过85%的智能客服差评…...

wan2.1-vae多场景落地:短视频封面/小红书配图/知识付费课程插图生成

wan2.1-vae多场景落地:短视频封面/小红书配图/知识付费课程插图生成 1. 引言:从创意到画面的新生产力 你有没有遇到过这样的场景?短视频脚本写好了,却为找不到一张吸引眼球的封面图而发愁;小红书笔记内容很精彩&…...

DeEAR开源模型效果展示:跨语种语音(中/英/日)唤醒度识别泛化能力实测

DeEAR开源模型效果展示:跨语种语音(中/英/日)唤醒度识别泛化能力实测 1. 引言:当AI能听懂你的情绪 想象一下,你刚录完一段产品介绍的语音,想快速知道自己的语气听起来是充满激情还是过于平淡。或者&#…...

工业控制场景:CYBER-VISION零号协议辅助分析STM32F103C8T6数据采集逻辑

工业控制场景:CYBER-VISION零号协议辅助分析STM32F103C8T6数据采集逻辑 最近在做一个工业现场的数据采集项目,用的就是大家都很熟悉的STM32F103C8T6最小系统板,采集温度、压力和振动信号。数据是采上来了,但看着那一串串不断刷新…...

Topit:重新定义Mac窗口管理的效率工具

Topit:重新定义Mac窗口管理的效率工具 【免费下载链接】Topit Pin any window to the top of your screen / 在Mac上将你的任何窗口强制置顶 项目地址: https://gitcode.com/gh_mirrors/to/Topit 在数字化工作环境中,专业人士每天平均需要在20个以…...