默认 tostring() 仅返回类型全名,缺乏调试价值;需用反射自动处理任意对象,但须规避只读属性异常、延迟加载、循环引用、null/值类型误判、非公有成员遗漏、集合深度爆炸、性能开销及不可变类型反射失效等问题。

为什么 ToString() 默认输出不实用
默认的 ToString() 只返回类型全名,比如 "MyApp.User",对调试、日志或临时排查完全没信息量。你不是真想重写每个类的 ToString(),而是希望一套逻辑自动处理任意对象——这时候反射是唯一可行路径,但直接遍历所有 GetProperties() 会踩一堆坑。
- 只读属性(
get有实现但set为private或缺失)会被正常读取,但某些框架生成的代理类(如 EF Core 的追踪代理)可能抛TargetInvocationException -
ToString()里调用自身属性可能触发延迟加载(Lazy Loading),导致意外数据库查询或死锁 - 循环引用(A → B → A)不做检测会直接栈溢出,
Object.ToString()不处理这个 - 值类型(
struct)和空引用(null)必须分开判断,否则GetProperty("X").GetValue(obj)在obj == null时直接炸
用 GetProperties(BindingFlags) 控制字段可见性
别用默认无参的 GetProperties(),它只返回 public 实例属性,漏掉 internal、protected 甚至带 [DebuggerBrowsable(DebuggerBrowsableState.Never)] 的字段。实际日志中你往往需要看内部状态,比如 Entity Framework 的 EntityEntry.State 是 internal。
- 显式传入
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance - 过滤掉索引器(
IsIndexer == true)、静态成员(IsStatic == true)、编译器生成的属性(GetCustomAttribute<compilergeneratedattribute>() != null</compilergeneratedattribute>) - 跳过
typeof(object)、typeof(string)等基础类型——它们的ToString()已有意义,再反射纯属浪费
避免循环引用和无限递归的简单办法
不用完整拓扑排序,一个 HashSet<object></object> 记录已访问实例地址就够了。注意:必须用 ReferenceEquals 判断,不能用 .Equals(),否则值类型(如 DateTime)会被误判为重复。
- 每次进入递归前检查
visited.Add(obj)返回false?如果是,就输出"[Circular Reference]"并终止该分支 - 对集合类型(
IEnumerable且非string)限制展开深度,比如最多 3 层,避免List<list>>></list>把栈吃光 - 跳过
IDisposable实现类的属性(如DbContext),它们的内部字段通常无业务意义且易触发异常
性能敏感场景下要绕开反射缓存
反复调用 GetType().GetProperties() 开销不小,尤其在高频日志中。但别急着上 ConcurrentDictionary<type propertyinfo></type> ——多数时候你只需要缓存「该类型是否值得展开」这个布尔值。
- 对
int、Guid、DateTime这类简单类型,缓存其ToString()结果,永远不走反射 - 对自定义类,缓存其
PropertyInfo[]数组,但仅在首次访问时构建;后续直接遍历数组调用GetValue() - 避免缓存
Func<object string></object>委托(即 Expression.Compile),它在 .NET Core 3.1+ 之后 GC 压力明显,不如原生反射稳定
最麻烦的其实是不可变对象(record、init 属性)和匿名类型——它们的属性 getter 可能被 JIT 内联,反射读取时偶尔返回 default(T) 而不报错。这种 case 没银弹,只能加一层 try/catch TargetInvocationException 并 fallback 到 obj.GetType().Name。










