尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C#反射核心:PropertyInfo动态获取属性名与值的高性能实践

C#反射核心:PropertyInfo动态获取属性名与值的高性能实践 1. 项目概述为什么我们需要深入理解 PropertyInfo在C#开发中尤其是涉及数据映射、序列化、反射构建通用工具或实现动态数据绑定的场景里我们经常面临一个看似简单却至关重要的任务如何动态地获取一个实体类Entity Class所有属性的名称Name和其当前的值Value这个需求远不止于简单的“获取”它背后关联着ORM框架如何将数据库记录映射为对象、Web API如何将JSON反序列化为模型、以及我们如何编写不依赖于具体类型的通用数据处理代码。PropertyInfo就是 .NET 反射Reflection机制中专门用于描述和操作类型属性的核心类。它像一把“万能钥匙”允许我们在运行时Runtime而非编译时Compile Time探查和操纵对象的内部结构。直接使用object.PropertyName是静态的、强类型的而通过PropertyInfo则是动态的、弱类型的这为我们打开了编写灵活、可扩展代码的大门。举个例子假设你正在开发一个通用的数据导出到Excel的功能。用户可能选择导出“订单”类也可能导出“用户”类。你不可能为每一个实体类都写一套几乎相同的导出逻辑。这时通过PropertyInfo动态获取选中实体类的所有属性名作为Excel表头和每个实例的属性值作为Excel行数据一套代码就能适配所有实体类。再比如实现一个简单的对象对比器Object Comparer用于比较两个同类型对象哪些属性值发生了变化PropertyInfo也是不可或缺的工具。因此掌握PropertyInfo来获取属性名和值是C#中级开发者向高级进阶必须跨越的一道门槛。它不仅是技术点更是一种编程思维的转变——从“写死”的逻辑转向“动态”的架构。接下来我将结合十多年的实战经验从原理到细节从基础操作到高阶避坑为你彻底拆解这个主题。2. 核心原理与基础操作拆解2.1 反射与 PropertyInfo 的本质要理解PropertyInfo必须先理解 .NET 的反射机制。你可以把程序集.dll 或 .exe想象成一个装满元数据Metadata的“黑盒”。元数据详细描述了其中定义的所有类型类、结构体、枚举等、类型的成员方法、属性、字段等以及这些成员的详细信息名称、类型、修饰符等。反射就是程序在运行时“照镜子”或“拆解黑盒”的能力它允许我们读取和操作这些元数据。PropertyInfo类位于System.Reflection命名空间下它是MemberInfo的一个派生类专门封装了关于属性Property的元数据。一个PropertyInfo对象代表一个特定的属性。它本身不存储属性的值而是存储关于这个属性的“描述信息”例如名称Name属性的标识符。属性类型PropertyType该属性是string、int还是某个自定义类。可读性CanRead是否定义了get访问器。可写性CanWrite是否定义了set访问器。声明类型DeclaringType定义该属性的类型。修饰符如 IsPublic, IsStatic访问级别和是否静态。获取属性值实际上是调用该属性底层get访问器所关联的方法。PropertyInfo.GetValue方法就是触发这个调用的入口。2.2 获取类型与 PropertyInfo 集合操作的第一步是获取目标类型的Type对象。Type类是反射的入口点。// 假设我们有一个实体类 public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } private string InternalCode { get; set; } // 私有属性 } // 获取 Type 对象的几种常见方式 // 1. 使用 typeof 运算符编译时已知类型 Type productType typeof(Product); // 2. 通过对象实例获取运行时已知实例 Product myProduct new Product { Id 1, Name Laptop }; Type typeFromInstance myProduct.GetType(); // 3. 通过类型名称字符串动态获取常用于插件式架构 string typeName MyNamespace.Product, MyAssembly; Type typeByName Type.GetType(typeName); // 需要程序集限定名拿到Type对象后就可以获取其属性信息了。Type.GetProperties方法是最常用的。// 获取所有公共实例属性最常用 PropertyInfo[] allPublicProperties productType.GetProperties(); // 获取所有属性包括非公共的、静态的需要指定 BindingFlags // BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic PropertyInfo[] allProperties productType.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); // 获取特定名称的属性 PropertyInfo idProperty productType.GetProperty(Id);注意GetProperties()默认只返回公共的实例属性。如果你需要获取私有或静态属性必须显式使用BindingFlags。BindingFlags的组合使用需要小心例如同时指定Public和NonPublic才能获取所有访问级别的属性。2.3 获取属性名称与属性值获取属性名非常简单直接访问PropertyInfo.Name属性即可。获取属性值则需要一个对象实例因为值是属于特定对象的。使用PropertyInfo.GetValue方法。Product product new Product { Id 101, Name Wireless Mouse, Price 29.99m }; // 获取 Id 属性的 PropertyInfo PropertyInfo idPropInfo product.GetType().GetProperty(Id); // 获取属性名 string propertyName idPropInfo.Name; // Id // 获取该实例下此属性的值 object idValue idPropInfo.GetValue(product); // 101 (装箱为 object) // 获取 Name 属性的值 PropertyInfo namePropInfo product.GetType().GetProperty(Name); object nameValue namePropInfo.GetValue(product); // Wireless Mouse // 遍历所有公共属性并打印名称和值 foreach (PropertyInfo prop in product.GetType().GetProperties()) { string name prop.Name; object value prop.GetValue(product); Console.WriteLine(${name}: {value}); } // 输出 // Id: 101 // Name: Wireless Mouse // Price: 29.99这里有一个关键点GetValue返回的是object类型。这是因为在编译时PropertyInfo并不知道它操作的具体属性是什么类型所以返回值必须是最通用的object。如果你需要强类型操作必须进行类型转换或使用泛型等高级技巧后续会讲到。2.4 处理索引器属性普通属性我们熟悉了但还有一种特殊的属性索引器Indexer。它的PropertyInfo获取方式略有不同。public class SampleCollection { private string[] arr new string[100]; // 索引器定义 public string this[int i] { get { return arr[i]; } set { arr[i] value; } } } // 获取索引器的 PropertyInfo // 索引器在C#中的默认名称为 Item除非用 [IndexerName(...)] 特性指定 SampleCollection col new SampleCollection(); col[0] Hello; Type collectionType typeof(SampleCollection); // 通过指定参数类型来获取索引器属性 PropertyInfo indexerProperty collectionType.GetProperty(Item, new Type[] { typeof(int) }); if (indexerProperty ! null) { // 获取索引器值时需要提供索引参数 object indexerValue indexerProperty.GetValue(col, new object[] { 0 }); Console.WriteLine(indexerValue); // 输出: Hello }实操心得在编写通用代码时如果无法确定目标类型是否有索引器或者索引器参数类型是什么可以通过Type.GetProperties()获取所有属性后检查每个PropertyInfo的GetIndexParameters()方法返回的数组长度。长度大于0的就是索引器属性。处理索引器时务必向GetValue传递正确数量和类型的索引参数数组否则会抛出TargetParameterCountException。3. 高级应用与性能优化实战掌握了基础我们就可以解决更复杂的实际问题并开始关注至关重要的性能问题。反射虽然强大但其性能开销也是众所周知的。3.1 复杂场景嵌套对象、集合与空值处理现实中的实体类很少像Product那么简单。它们可能包含嵌套对象、集合并且属性值可能为null。场景一嵌套对象属性值的获取假设Order类包含一个Customer类型的属性。public class Customer { public string Name { get; set; } } public class Order { public int OrderId { get; set; } public Customer Buyer { get; set; } } Order order new Order { OrderId 1, Buyer new Customer { Name Alice } }; PropertyInfo buyerProp typeof(Order).GetProperty(Buyer); object buyerValue buyerProp.GetValue(order); // 这是一个 Customer 对象 // 如果我们想进一步获取 Buyer 的 Name if (buyerValue ! null) { Type customerType buyerValue.GetType(); PropertyInfo nameProp customerType.GetProperty(Name); object nameValue nameProp.GetValue(buyerValue); // “Alice” }场景二处理集合类型属性例如一个Order有多个OrderItem。public class Order { public ListOrderItem Items { get; set; } new ListOrderItem(); } public class OrderItem { public string ProductName { get; set; } public int Quantity { get; set; } } Order order new Order(); order.Items.Add(new OrderItem { ProductName Book, Quantity 2 }); PropertyInfo itemsProp typeof(Order).GetProperty(Items); object itemsValue itemsProp.GetValue(order); // 这是一个 ListOrderItem 对象 if (itemsValue is System.Collections.IEnumerable enumerable) { foreach (var item in enumerable) { // 对每个 item 再进行反射操作... Type itemType item.GetType(); var nameProp itemType.GetProperty(ProductName); Console.WriteLine(nameProp.GetValue(item)); } }场景三空值Null与可空值类型Nullable这是反射中最容易出错的地方之一。public class Entity { public string? OptionalDescription { get; set; } // 可为空的引用类型 public int? NullableInt { get; set; } // 可空值类型 Nullableint } Entity entity new Entity(); // 两个属性都为 null PropertyInfo descProp typeof(Entity).GetProperty(OptionalDescription); object descValue descProp.GetValue(entity); // 返回 null这是安全的 PropertyInfo intProp typeof(Entity).GetProperty(NullableInt); object intValue intProp.GetValue(entity); // 也返回 null // 问题如何区分一个返回的 null 是引用类型的 null还是 NullableT 的 HasValue 为 false // 对于可空值类型GetValue 返回的是 NullableT 这个结构体被装箱后的对象。 // 如果 HasValue 为 false这个装箱后的对象就是 null。 // 所以从 object 结果上你无法直接区分。需要在获取前检查属性类型。 if (intProp.PropertyType.IsGenericType intProp.PropertyType.GetGenericTypeDefinition() typeof(Nullable)) { // 这是一个可空值类型 // 如果 intValue 为 null表示数据库中的 DBNull 或未赋值 }避坑指南在处理可能为null的嵌套对象属性时务必在调用下一层GetValue或GetProperty前进行判空。否则会抛出NullReferenceException。对于可空值类型直接判断GetValue返回的object是否为null即可无需拆箱。3.2 性能瓶颈与优化策略表达式树与委托直接使用PropertyInfo.GetValue在循环或高频调用中会成为性能瓶颈因为它涉及方法查找、权限检查、参数打包/解包等一系列开销。一个常见的优化策略是将反射操作“编译”成高效的委托。方案一使用Funcobject, object委托我们可以为每个属性的 getter 创建一个委托。public static Funcobject, object CreatePropertyGetter(PropertyInfo property) { // 参数对象实例 var instance Expression.Parameter(typeof(object), instance); // 将 object 转换为实际类型 var castInstance Expression.Convert(instance, property.DeclaringType); // 访问属性 var propertyAccess Expression.Property(castInstance, property); // 将属性值转换为 object var castResult Expression.Convert(propertyAccess, typeof(object)); // 构建 lambda 表达式并编译为委托 var lambda Expression.LambdaFuncobject, object(castResult, instance); return lambda.Compile(); } // 使用优化后的方式 Product product new Product { Id 1 }; PropertyInfo idProp typeof(Product).GetProperty(Id); var idGetter CreatePropertyGetter(idProp); // 一次性编译 // 后续百万次调用性能接近直接属性访问 for (int i 0; i 1_000_000; i) { object value idGetter(product); }方案二使用泛型委托FuncT, object如果知道具体类型可以进一步优化避免Expression.Convert的开销。public static FuncT, object CreatePropertyGetterT(PropertyInfo property) { var instance Expression.Parameter(typeof(T), instance); var propertyAccess Expression.Property(instance, property); var castResult Expression.Convert(propertyAccess, typeof(object)); var lambda Expression.LambdaFuncT, object(castResult, instance); return lambda.Compile(); } // 使用 var idGetterGeneric CreatePropertyGetterProduct(idProp); object value idGetterGeneric(product);方案三直接使用Delegate.CreateDelegate(适用于无参属性Getter)对于简单的get访问器有更直接的优化方法。public static Funcobject, object CreatePropertyGetterFast(PropertyInfo property) { // 获取属性的 get 方法 MethodInfo getMethod property.GetGetMethod(); // 创建开放委托Open Delegate第一个参数是实例 var delegateType typeof(Func,).MakeGenericType(property.DeclaringType, property.PropertyType); Delegate concreteDelegate Delegate.CreateDelegate(delegateType, null, getMethod); // 再包装一层将输入 object 转换并调用 Funcobject, object result instance concreteDelegate.DynamicInvoke(instance); return result; } // 注意此方法简化了错误处理实际使用需考虑DeclaringType为null静态属性等情况。性能实测对比在一个获取100万次属性值的简单测试中直接属性访问p.Id耗时约5ms使用编译后的委托方案二耗时约30ms而使用原生PropertyInfo.GetValue耗时可能超过1000ms。差距高达两个数量级。因此在需要高性能反射的场景如序列化库、ORM框架的核心映射层预编译委托是标准做法。3.3 利用缓存避免重复反射另一个重要的优化点是缓存。我们不应该在每次需要属性信息时都去调用GetProperties或GetProperty尤其是在循环中。using System.Collections.Concurrent; public static class PropertyCacheT { private static readonly ConcurrentDictionarystring, PropertyInfo _propertyCache new ConcurrentDictionarystring, PropertyInfo(); private static PropertyInfo[] _allProperties; public static PropertyInfo[] GetAllProperties() { if (_allProperties null) { _allProperties typeof(T).GetProperties(); } return _allProperties; } public static PropertyInfo GetProperty(string name) { return _propertyCache.GetOrAdd(name, n typeof(T).GetProperty(n)); } } // 使用缓存 var props PropertyCacheProduct.GetAllProperties(); // 第一次反射后续直接返回数组 var idProp PropertyCacheProduct.GetProperty(Id); // 第一次反射后缓存结合委托编译和缓存可以构建一个高性能的属性访问器工厂。public static class PropertyAccessor { private static readonly ConcurrentDictionaryPropertyInfo, Funcobject, object _getterCache new ConcurrentDictionaryPropertyInfo, Funcobject, object(); public static Funcobject, object GetCachedGetter(PropertyInfo property) { return _getterCache.GetOrAdd(property, CreatePropertyGetter); } private static Funcobject, object CreatePropertyGetter(PropertyInfo property) { // 使用上文方案一的表达式树创建委托 // ... 实现代码同上 ... } } // 终极用法一次编译永久高速访问 Product p new Product(); PropertyInfo pi PropertyCacheProduct.GetProperty(Name); Funcobject, object getter PropertyAccessor.GetCachedGetter(pi); string name (string)getter(p); // 极速访问4. 实战案例构建一个简易对象映射器现在让我们综合运用以上所有知识构建一个实用的工具一个简易的“对象到字典”映射器。这在日志记录、API响应格式化、动态UI数据绑定等场景非常有用。4.1 需求分析与设计目标编写一个泛型方法ToDictionaryT将任意对象T的所有公共实例属性转换成一个Dictionarystring, object其中 Key 是属性名Value 是属性值。要求处理嵌套对象将其值直接放入字典或进行递归展开本例选择直接放入。处理集合属性将其作为IEnumerable放入字典。正确处理空值。有基本的性能考虑使用缓存。4.2 核心实现代码using System.Collections.Concurrent; using System.Linq.Expressions; using System.Reflection; public static class ObjectDictionaryMapper { // 缓存类型对应的属性Getter委托列表 private static readonly ConcurrentDictionaryType, ListPropertyGetter _propertyGettersCache new ConcurrentDictionaryType, ListPropertyGetter(); private class PropertyGetter { public string Name { get; set; } public Funcobject, object GetValue { get; set; } } public static Dictionarystring, object ToDictionary(object obj) { if (obj null) throw new ArgumentNullException(nameof(obj)); var type obj.GetType(); var getters _propertyGettersCache.GetOrAdd(type, t { var properties t.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p p.CanRead); // 只读属性也可以获取值 var list new ListPropertyGetter(); foreach (var prop in properties) { // 跳过索引器 if (prop.GetIndexParameters().Length 0) continue; list.Add(new PropertyGetter { Name prop.Name, GetValue CreatePropertyGetter(prop) // 使用编译后的委托 }); } return list; }); var dictionary new Dictionarystring, object(); foreach (var getter in getters) { object value getter.GetValue(obj); dictionary[getter.Name] value; // 这里 value 可能是 null也可能是嵌套对象或集合 } return dictionary; } private static Funcobject, object CreatePropertyGetter(PropertyInfo property) { var instance Expression.Parameter(typeof(object), instance); var castInstance Expression.Convert(instance, property.DeclaringType); var propertyAccess Expression.Property(castInstance, property); var castResult Expression.Convert(propertyAccess, typeof(object)); var lambda Expression.LambdaFuncobject, object(castResult, instance); return lambda.Compile(); } }4.3 使用示例与扩展// 测试类 public class Order { public int Id { get; set; } public string OrderNumber { get; set; } public Customer Customer { get; set; } public ListOrderItem Items { get; set; } public DateTime? ShippedDate { get; set; } } // 准备数据 var order new Order { Id 1001, OrderNumber ORD-2023-001, Customer new Customer { Name Bob }, Items new ListOrderItem { new OrderItem { ProductName Keyboard, Quantity 1 }, new OrderItem { ProductName Mouse, Quantity 2 } }, ShippedDate null }; // 转换为字典 var dict ObjectDictionaryMapper.ToDictionary(order); // 输出结果 foreach (var kvp in dict) { Console.WriteLine(${kvp.Key}: {kvp.Value}); } // 输出类似 // Id: 1001 // OrderNumber: ORD-2023-001 // Customer: MyNamespace.Customer (ToString的结果) // Items: System.Collections.Generic.List1[MyNamespace.OrderItem] // ShippedDate:当前实现的局限性嵌套对象如Customer和集合如Items只是简单调用了ToString()在字典中显示的是类型名这可能不是我们想要的。没有处理循环引用A 引用 BB 又引用 A会导致栈溢出。扩展方向深度转换修改ToDictionary使其递归地将嵌套对象也转换为字典。需要添加一个HashSetobject参数来跟踪已处理的对象防止循环引用。选择性转换通过特性Attribute标记需要忽略的属性或者只转换标记了的属性。处理特定类型为DateTime、Guid等类型提供自定义的字符串格式化。线程安全优化当前的缓存是线程安全的ConcurrentDictionary但委托的创建过程如果非常耗时在极高并发下首次访问可能造成重复创建。可以考虑使用LazyT包装委托的创建过程。5. 常见陷阱、疑难排查与最佳实践即使掌握了原理和优化在实际使用PropertyInfo时依然会遇到不少坑。这里记录了一些典型问题和解决方案。5.1 典型异常与处理方案异常类型触发场景原因分析解决方案NullReferenceException调用propInfo.GetValue(null)或嵌套属性值为null时继续反射。目标对象实例为null。在调用GetValue前检查对象实例是否为null。对于嵌套属性每层获取后都要判空。TargetException传递的对象实例类型与PropertyInfo.DeclaringType不匹配。例如用ClassA的PropertyInfo去获取ClassB实例的属性值。确保PropertyInfo是从正确的Type对象获取的。使用泛型或类型检查来保证安全。TargetParameterCountException为索引器属性调用GetValue时未提供索引参数或参数数量/类型不匹配。索引器需要参数。检查PropertyInfo.GetIndexParameters()如果返回数组长度0则必须提供匹配的索引参数数组给GetValue。ArgumentException向GetValue的索引参数数组传递了错误类型的参数。索引器参数类型不匹配。确保索引参数数组中的元素类型与GetIndexParameters()返回的参数类型一致。MethodAccessException尝试获取非公共属性private,protected,internal的值且未使用相应的BindingFlags。反射访问违反了访问权限。使用BindingFlags.NonPublic获取属性信息但需注意这破坏了封装性应谨慎使用。5.2 值类型与装箱拆箱的坑当操作结构体struct的属性时需要特别注意装箱Boxing问题。public struct Point { public int X { get; set; } public int Y { get; set; } } Point p new Point { X 10, Y 20 }; Type pointType typeof(Point); PropertyInfo xProp pointType.GetProperty(X); // 错误做法这会导致 p 被装箱修改的是装箱后的副本原 p 不变。 object boxedP p; xProp.SetValue(boxedP, 30); Console.WriteLine(p.X); // 输出 10未改变 // 正确做法通过引用装箱 object boxedPRef p; // 装箱 xProp.SetValue(boxedPRef, 30); // 修改装箱副本 p (Point)boxedPRef; // 拆箱并赋值回原变量 Console.WriteLine(p.X); // 输出 30对于GetValue值类型属性返回的是装箱后的object。频繁操作会导致大量堆内存分配影响性能。这也是为什么高性能场景推荐使用编译后的泛型委托它可以避免不必要的装箱。5.3 属性与字段的混淆初学者容易混淆属性Property和字段Field。PropertyInfo用于属性而字段的信息由FieldInfo类表示。它们通过不同的方法获取// 获取字段 FieldInfo[] fields typeof(MyClass).GetFields(); // 获取属性 PropertyInfo[] properties typeof(MyClass).GetProperties();关键区别属性本质上是方法getter/setter的语法糖可能有逻辑字段是直接的数据存储。在序列化、数据绑定等场景中通常使用属性而非公共字段因为属性提供了更好的封装和控制如验证逻辑。5.4 最佳实践总结明确需求避免滥用反射反射会降低性能、增加代码复杂度、并可能破坏封装。如果编译时就能确定类型优先使用强类型。反射应留给插件系统、序列化、ORM、动态代理等真正需要动态性的场景。缓存缓存再缓存将Type、PropertyInfo数组、编译后的委托等所有可以缓存的信息都缓存起来。使用ConcurrentDictionary或MemoryCache是常见选择。关注性能使用表达式树或Delegate.CreateDelegate对于高频调用的属性访问一定要将反射调用转换为委托调用。表达式树Expression提供了灵活且相对安全的编译方式。做好错误处理和边界检查总是检查GetProperty返回的PropertyInfo是否为null。处理嵌套属性时层层判空。考虑索引器、静态属性、只读/只写属性等特殊情况。考虑使用现成的库对于复杂的对象映射、序列化需求优先考虑使用成熟的库如AutoMapper对象映射、Newtonsoft.Json或System.Text.Json序列化。它们已经解决了性能、循环引用、复杂类型处理等绝大多数问题并且经过了千锤百炼的测试。单元测试反射代码容易因类型变化而断裂。为你的反射工具类编写充分的单元测试覆盖各种边界情况空值、值类型、嵌套类型、循环引用、索引器等。通过以上五个部分的详细拆解我们从PropertyInfo的基本用法走到了高性能实战和复杂场景处理。记住反射是一把强大的双刃剑理解其原理并遵循最佳实践才能让它在你手中安全、高效地发挥作用赋能于那些需要高度灵活性和动态性的C#应用程序模块。
返回列表