如何在C#中处理多命名空间下结构相同的Error类?
这确实是对接外部API时非常头疼的常见问题——多个命名空间下的“孪生”Error类,结构完全一致却因为类型隔离没法复用错误处理逻辑,复制多份函数又会导致代码冗余到爆炸。下面给你几个实用的解决方案,按实现成本和适用场景排序:
1. 用动态类型(Dynamic)快速落地
这是最省心的方案,不需要额外封装,直接修改你的CheckErrors函数,用dynamic来兼容所有结构一致的Error类:
void CheckErrors(IEnumerable<dynamic> errors) { if (errors != null && errors.Any()) { var firstError = errors.First(); // 直接访问属性,动态类型会在运行时解析,只要所有Error类都有这些属性就没问题 throw new Exception($"{firstError.ErrorMessage} (Error Code: {firstError.errorNumber})"); } }
优点:零额外代码,5分钟就能搞定;缺点:没有编译时检查,如果某个Error类的属性名拼写错误(比如把ErrorMessage写成ErrorMsg),只会在运行时抛出异常。适合快速迭代或者对性能要求不高的场景。
2. 反射+属性缓存,兼顾可靠性与性能
如果你担心动态类型的隐式风险,可以用反射来显式获取属性值,再加上缓存避免重复反射的性能损耗:
首先封装一个通用的错误信息提取方法,搭配类型缓存:
private static readonly Dictionary<Type, (PropertyInfo MessageProp, PropertyInfo NumberProp)> _errorPropCache = new(); private (string Message, int Number) ExtractErrorDetails(object error) { var errorType = error.GetType(); if (!_errorPropCache.TryGetValue(errorType, out var props)) { // 查找对应的属性,确保类型匹配 var messageProp = errorType.GetProperty("ErrorMessage", typeof(string)); var numberProp = errorType.GetProperty("errorNumber", typeof(int)); if (messageProp == null || numberProp == null) throw new InvalidOperationException($"Type {errorType.FullName} doesn't match expected Error structure"); props = (messageProp, numberProp); _errorPropCache[errorType] = props; } return ( props.MessageProp.GetValue(error) as string ?? "Unknown error", (int)props.NumberProp.GetValue(error) ); }
然后修改CheckErrors函数:
void CheckErrors(IEnumerable<object> errors) { if (errors == null || !errors.Any()) return; var (message, number) = ExtractErrorDetails(errors.First()); throw new Exception($"Error {number}: {message}"); }
优点:有编译时类型检查(参数为object),缓存后反射的性能开销可以忽略不计;缺点:需要写一点反射逻辑,但比复制N份错误处理函数要划算得多。
3. 隐式转换包装类,编译时安全的轻量方案
如果追求完全的编译时安全,又不想写太多重复代码,可以写一个统一的ErrorWrapper类,为每个客户的Error类添加隐式转换运算符:
public class ErrorWrapper { public string ErrorMessage { get; } public int ErrorNumber { get; } private ErrorWrapper(string message, int number) { ErrorMessage = message; ErrorNumber = number; } // 为每个命名空间的Error类添加隐式转换 public static implicit operator ErrorWrapper(NamespaceA.Error error) => new ErrorWrapper(error.ErrorMessage, error.errorNumber); public static implicit operator ErrorWrapper(NamespaceB.Error error) => new ErrorWrapper(error.ErrorMessage, error.errorNumber); // 其他命名空间的Error类依次添加... }
然后修改CheckErrors函数接受ErrorWrapper类型:
void CheckErrors(IEnumerable<ErrorWrapper> errors) { if (errors != null && errors.Any()) throw new Exception(errors.First().ErrorMessage); }
调用时直接传入客户的Error集合即可,隐式转换会自动生效:
// 假设client返回的是List<NamespaceA.Error> var clientErrors = MyClient.GetData(); CheckErrors(clientErrors);
优点:完全的编译时安全,性能无损耗;缺点:需要为每个Error类写一行转换代码,但比复制整个错误处理逻辑要简洁得多——尤其是当CheckErrors的实际逻辑很复杂时,只需要维护这一份核心逻辑。
4. 源生成器,大规模场景的终极方案
如果你的项目需要对接十几个甚至更多这类服务,且追求极致的性能和可维护性,可以用C#的源生成器(Source Generator)在编译时自动生成适配代码。
源生成器可以扫描所有程序集中符合结构的Error类,自动生成对应的扩展方法或适配器,让你无需手动编写任何转换代码就能统一调用CheckErrors。比如:
- 生成一个通用的
IError接口,然后为每个Error类生成实现该接口的适配器; - 直接生成针对每个Error类的
CheckErrors重载方法。
这个方案的优点是完全自动化,编译时生成代码无运行时开销,但缺点是需要编写源生成器的逻辑,有一定的学习成本,适合大型长期维护的项目。
内容的提问来源于stack exchange,提问作者zacaj

