资讯动态

C#反射核心:PropertyInfo原理、性能优化与实战应用

发布时间:2026/8/12 10:39:04 来源:尧图企业网站定制
1. 项目概述为什么我们需要深入理解 PropertyInfo在C#的日常开发中尤其是处理数据映射、序列化、反射构建动态功能或者编写通用数据访问层时我们经常会遇到一个看似简单却至关重要的需求如何动态地获取一个对象的所有属性名和对应的值比如你可能需要将一个实体对象的所有属性和值转换成字典以便存入缓存或进行JSON序列化或者你需要编写一个通用的数据导出工具能够自动将任何实体类的数据导出到Excel而无需为每个类单独编写映射代码。这时PropertyInfo这个类就从幕后走到了台前。它不仅仅是反射Reflection机制中的一个组成部分更是连接静态类型世界与动态运行时操作的桥梁。很多开发者对它的理解停留在“能用就行”的层面但真正掌握其细节、性能特性和使用边界往往能让你在解决复杂问题时游刃有余避免掉入一些隐蔽的陷阱。简单来说这个项目就是一次对PropertyInfo的深度探索。我们将不满足于简单的GetValue和SetValue而是要拆解其工作原理分析性能瓶颈并分享在实际项目中如构建ORM轻量级框架、实现动态表单、编写AOP拦截器等如何高效、安全地使用它来获取实体类的属性名称和属性值。无论你是正在为面试准备“C#反射”相关八股文还是在实际开发中遇到了动态处理对象的难题这篇文章都将提供从原理到实战的完整指南。2. 核心原理PropertyInfo 在反射体系中的角色与工作机制要玩转PropertyInfo首先得明白它在.NET反射这座大厦里站在哪一层。反射的核心目的是在运行时探查和操作类型信息。当我们写下typeof(MyEntity)或myInstance.GetType()时我们拿到的是一个Type对象它就像是这个类的“蓝图”。2.1 Type 与 PropertyInfo 的关系Type对象包含了该类型的所有元数据方法MethodInfo、字段FieldInfo、属性PropertyInfo、事件等。属性Property在C#中本质上是方法getter和setter的语法糖但在元数据层面它被抽象为一个独立的、更高级别的概念。PropertyInfo就是描述这个属性的元数据对象。你可以通过Type的GetProperties方法来获取一个类型的所有PropertyInfoType entityType typeof(MyEntity); PropertyInfo[] allProperties entityType.GetProperties();这个方法有多个重载最常用的是不传参数获取所有公共实例属性和传入BindingFlags来精确控制获取的范围如非公共属性、静态属性等。理解BindingFlags是高效使用反射的第一步错误的使用会导致性能浪费或获取不到预期的属性。2.2 属性值的获取GetValue 方法探秘PropertyInfo最核心的方法莫过于GetValue。它的签名很简单public object? GetValue(object? obj)。但这里面有几个关键点参数obj对于实例属性这里需要传入属性所属的对象实例。对于静态属性则需要传入null。如果传错了会抛出TargetException。返回值object?GetValue返回的是object类型。这意味着无论属性原本是int、string还是自定义类返回时都会被“装箱”如果是值类型或作为引用类型返回。这是反射操作性能开销的主要来源之一。索引器属性如果属性是带参数的索引器如this[int index]GetValue还有另一个重载GetValue(object? obj, object?[]? index)需要传入索引参数。一个重要的实操心得频繁调用GetValue进行装箱和拆箱操作是性能杀手。在需要高性能的场景下可以考虑使用表达式树Expression Tree或System.Reflection.Emit动态生成IL代码来编译一个高效的属性访问委托这能带来数量级的性能提升。不过对于大多数业务逻辑和一次性操作GetValue的简洁性足以胜任。2.3 属性名称的获取Name 属性相比获取值获取属性名就简单直接得多。PropertyInfo的Name属性直接返回一个字符串就是你在代码中定义的属性名。这个操作几乎没有性能开销。但是这里有一个常见的“坑”这个名称是代码中声明的名称而不是你期望的用于序列化或显示的“友好名称”。例如你的属性叫CustomerName但你想在JSON输出中显示为customerName或在前端显示为“客户姓名”。这就需要用到System.ComponentModel.DataAnnotations命名空间下的[Display]特性或者像Newtonsoft.Json中的[JsonProperty]特性。你可以通过PropertyInfo.GetCustomAttributes()方法来获取这些特性从而得到更丰富的元信息。PropertyInfo prop entityType.GetProperty(CustomerName); DisplayAttribute displayAttr prop.GetCustomAttributeDisplayAttribute(); string displayName displayAttr?.Name ?? prop.Name; // 获取友好名称没有则用属性名3. 实战演练从简单遍历到高级场景应用理解了基本原理后我们通过几个由浅入深的例子来看看如何在实际项目中运用这些知识。3.1 基础示例将实体对象转换为字典这是最常见的需求之一。假设我们有一个Person类public class Person { public int Id { get; set; } public string Name { get; set; } public DateTime BirthDate { get; set; } }我们需要一个通用方法将任意这样的对象转换成一个Dictionarystring, object其中键是属性名值是属性值。public static Dictionarystring, object? ToDictionary(object obj) { if (obj null) throw new ArgumentNullException(nameof(obj)); var dictionary new Dictionarystring, object?(); Type type obj.GetType(); PropertyInfo[] properties type.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (PropertyInfo property in properties) { // 检查属性是否有getter if (property.CanRead) { object? value property.GetValue(obj); dictionary[property.Name] value; } } return dictionary; } // 使用 Person person new Person { Id 1, Name 张三, BirthDate new DateTime(1990, 1, 1) }; var dict ToDictionary(person); // dict 包含 { Id: 1, Name: 张三, BirthDate: 1990/1/1 0:00:00 }注意事项我们使用了BindingFlags.Public | BindingFlags.Instance这是最常用的组合只获取公共的实例属性。如果你需要获取私有或受保护的属性需要添加BindingFlags.NonPublic。在循环中通过property.CanRead进行判断是一个好习惯可以避免尝试获取只写属性虽然不常见时抛出异常。返回值中的object?需要小心处理。字典里的值可能是null也可能是任何类型的对象。后续使用时要做好类型检查和转换。3.2 进阶应用实现一个简单的对象差异比较器Delta Compare在数据同步或审计日志场景中我们经常需要比较两个对象通常是同一类型的哪些属性发生了变化。利用PropertyInfo可以优雅地实现。public static Dictionarystring, ValueChange GetPropertyChangesT(T original, T current) where T : class { if (original null || current null) throw new ArgumentNullException(); var changes new Dictionarystring, ValueChange(); Type type typeof(T); PropertyInfo[] properties type.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (PropertyInfo prop in properties) { if (!prop.CanRead) continue; object? originalValue prop.GetValue(original); object? currentValue prop.GetValue(current); // 简单的值相等比较对于复杂对象需要重写Equals或更复杂的逻辑 if (!object.Equals(originalValue, currentValue)) { changes[prop.Name] new ValueChange { PropertyName prop.Name, OriginalValue originalValue, CurrentValue currentValue }; } } return changes; } public class ValueChange { public string PropertyName { get; set; } public object? OriginalValue { get; set; } public object? CurrentValue { get; set; } }实操心得这里的object.Equals比较对于字符串、数值等基本类型是有效的。但如果属性是自定义的引用类型如另一个实体object.Equals默认比较的是引用地址。在这种情况下你可能需要递归地比较或者要求这些类型实现IEquatableT接口或者只比较其主键ID。这是实现通用比较器时需要仔细设计的部分。3.3 高级场景动态构建查询条件模拟轻量级ORM假设我们在开发一个通用的数据查询辅助类需要根据传入的对象动态生成类似WHERE Name Name AND Age Age的SQL条件。我们可以利用PropertyInfo来遍历对象中非空的属性将其作为查询条件。public class QueryFilterT where T : class { public (string whereClause, Dictionarystring, object parameters) BuildCondition(T filterModel) { if (filterModel null) return (string.Empty, new Dictionarystring, object()); var conditions new Liststring(); var parameters new Dictionarystring, object(); Type type typeof(T); PropertyInfo[] properties type.GetProperties(); int paramIndex 0; foreach (PropertyInfo prop in properties) { if (!prop.CanRead) continue; object? value prop.GetValue(filterModel); // 假设 null 值不作为过滤条件 if (value null) continue; // 这里可以根据属性类型或自定义特性决定使用 , , LIKE 等操作符 // 例如如果有 [Range] 特性可以生成 BETWEEN string paramName $p{paramIndex}; conditions.Add(${prop.Name} {paramName}); parameters.Add(paramName, value); } string whereClause conditions.Any() ? WHERE string.Join( AND , conditions) : string.Empty; return (whereClause, parameters); } } // 使用 public class EmployeeFilter { public string? Department { get; set; } public int? MinAge { get; set; } } var filter new EmployeeFilter { Department Sales, MinAge 30 }; var queryFilter new QueryFilterEmployeeFilter(); var (where, paras) queryFilter.BuildCondition(filter); // where WHERE Department p0 AND MinAge p1 // paras { [p0] Sales, [p1] 30 }这个例子展示了如何将PropertyInfo用于构建动态逻辑。你可以通过为属性添加自定义特性如[Operator(Op.GreaterThan)]来扩展它使其支持更丰富的查询操作符。4. 性能优化与避坑指南反射的强大伴随着性能的代价。在频繁调用或高性能要求的场景下未经优化的反射代码会成为瓶颈。下面分享几个关键的优化策略和常见陷阱。4.1 性能优化策略缓存缓存还是缓存最立竿见影的优化就是缓存Type和PropertyInfo。每次调用GetProperties()都有开销。对于已知的类型应该在程序启动时或首次使用时将其缓存起来。public static class PropertyCacheT { private static readonly PropertyInfo[] _cachedProperties; private static readonly Dictionarystring, PropertyInfo _cachedPropertiesByName; static PropertyCache() { _cachedProperties typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); _cachedPropertiesByName _cachedProperties.ToDictionary(p p.Name, p p); } public static PropertyInfo[] GetProperties() _cachedProperties; public static PropertyInfo GetProperty(string name) _cachedPropertiesByName[name]; }使用委托代替反射调用对于需要频繁读取或设置某个特定属性的场景可以预先编译一个委托。这通常使用表达式树Expression Tree来实现它能将反射调用转换为近乎直接成员访问的性能。public static Funcobject, object? CreatePropertyGetter(PropertyInfo property) { var instance Expression.Parameter(typeof(object), obj); 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(); } // 使用缓存 private static DictionaryPropertyInfo, Funcobject, object? _getterCache new(); public static object? GetValueFast(object obj, PropertyInfo property) { if (!_getterCache.TryGetValue(property, out var getter)) { getter CreatePropertyGetter(property); _getterCache[property] getter; } return getter(obj); }经过这样的优化后续的属性读取将绕过PropertyInfo.GetValue的反射开销性能提升可达几十甚至上百倍。谨慎使用 BindingFlagsGetProperties(BindingFlags.Default)返回的是空数组。一定要明确指定需要的标志。获取非公共属性NonPublic的开销通常比获取公共属性大。如果只需要公共实例属性就只传Public | Instance不要画蛇添足。4.2 常见陷阱与排查技巧NullReferenceException调用GetValue(null)访问实例属性或者访问的PropertyInfo对象本身为null。务必在调用前检查参数和PropertyInfo的来源。TargetException当传递给GetValue的对象实例不是该属性所属类型或其派生类型时抛出。确保你从正确的Type对象获取PropertyInfo。性能问题定位如果发现某段代码突然变慢且其中包含反射可以使用性能剖析工具如Visual Studio的诊断工具、JetBrains dotTrace等来定位热点。通常在循环内部调用GetProperties()或GetValue()是首要怀疑对象。索引器属性的处理通过Type.GetProperty(“Item”)可以获取默认索引器但需要注意它可能有多个重载参数不同。处理索引器时务必使用正确的GetValue重载并提供索引参数。只读/只写属性尝试用GetValue读取一个只有set访问器的属性会抛出ArgumentException。同样尝试用SetValue写入一个只有get访问器的属性也会失败。操作前用CanRead和CanWrite进行检查是良好的防御性编程习惯。5. 在典型项目场景中的综合运用让我们把上面的知识点串联起来看看在几个常见的真实项目场景中如何系统性地应用PropertyInfo。5.1 场景一构建通用的数据导出到Excel工具需求开发一个组件能将任何IEnumerableT数据源导出到Excel文件表头使用属性的[Display]特性名或属性名。实现思路通过泛型T获取类型信息并缓存其PropertyInfo数组。遍历PropertyInfo通过GetCustomAttributeDisplayAttribute()获取显示名称作为Excel列标题。遍历数据集合对每个对象遍历PropertyInfo并使用GetValue获取每个属性的值。处理特殊类型如日期时间格式化为字符串枚举显示其描述等。使用像EPPlus或NPOI这样的库将数据写入Excel。核心代码片段public void ExportToExcelT(IEnumerableT data, string filePath) { var type typeof(T); var properties type.GetProperties() .Where(p p.CanRead) .ToArray(); using (var package new ExcelPackage()) { var worksheet package.Workbook.Worksheets.Add(Sheet1); // 写入标题行 for (int i 0; i properties.Length; i) { var prop properties[i]; var displayAttr prop.GetCustomAttributeDisplayAttribute(); string header displayAttr?.Name ?? prop.Name; worksheet.Cells[1, i 1].Value header; } // 写入数据行 int row 2; foreach (var item in data) { for (int col 0; col properties.Length; col) { var value properties[col].GetValue(item); worksheet.Cells[row, col 1].Value FormatValue(value); } row; } package.SaveAs(new FileInfo(filePath)); } } private object FormatValue(object value) { // 实现日期、枚举等类型的格式化逻辑 return value; }5.2 场景二实现动态表单的数据绑定与验证在WPF或ASP.NET Core MVC中有时需要根据模型动态生成UI表单。我们可以利用PropertyInfo来扫描模型的属性并根据属性类型通过PropertyType属性和附加的特性如[Required],[StringLength],[DataType(DataType.EmailAddress)]来动态渲染对应的输入控件文本框、下拉框、日期选择器等并设置数据绑定和验证规则。关键点PropertyInfo.PropertyType决定了渲染哪种控件string- TextBox,bool- CheckBox,DateTime- DatePicker。GetCustomAttributes()方法用于获取验证和显示相关的特性从而动态添加客户端和服务端验证逻辑。在WPF中可以使用Binding对象其Path可以设置为属性名实现动态绑定。5.3 场景三编写轻量级的对象映射器AutoMapper的简化版虽然已有成熟的AutoMapper但理解其原理很有帮助。一个简化的映射器核心就是利用反射遍历源类型的PropertyInfo在目标类型中查找同名的属性PropertyInfo然后进行值的复制。简化实现public TTarget MapTSource, TTarget(TSource source) where TTarget : new() { var target new TTarget(); var sourceProps typeof(TSource).GetProperties(); var targetProps typeof(TTarget).GetProperties().ToDictionary(p p.Name); foreach (var srcProp in sourceProps) { if (!srcProp.CanRead) continue; if (targetProps.TryGetValue(srcProp.Name, out var tgtProp) tgtProp.CanWrite) { // 这里需要处理类型转换简单实现假设类型兼容 object? value srcProp.GetValue(source); tgtProp.SetValue(target, value); } } return target; }一个完整的映射器还需要处理嵌套对象、集合、类型转换、自定义映射规则等但其核心思想离不开对PropertyInfo的遍历和操作。6. 总结与扩展思考通过以上从原理到实战的梳理我们可以看到PropertyInfo远不止是“获取属性名和值”那么简单。它是C#反射生态中的关键一环是构建灵活、动态、通用代码的基础。掌握它意味着你能够突破静态编码的局限处理未知类型、编写可扩展框架、实现元数据驱动的编程。我个人在实际项目中的体会是反射是一把双刃剑。它在提供无限灵活性的同时也带来了性能损耗和运行时错误的风险。因此我的原则是“能静态不动态若动态必缓存用之前先思考”。对于大多数业务代码明确的、编译时类型安全的代码是首选。只有当面对真正的通用性、可配置性需求时如插件系统、通用数据访问层、序列化框架才应祭出反射这把利器并且一定要配合缓存、表达式树编译等优化手段。最后关于扩展PropertyInfo的世界还可以与System.Linq.Expressions命名空间深度结合。通过表达式树你不仅可以编译出高效的属性访问器还能动态构建复杂的查询条件、排序表达式等这在实现高度动态的查询引擎时非常有用。这或许是掌握了基础PropertyInfo操作后的下一个进阶方向。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价