C#编译器无法自动推导泛型类型的原因及泛型仓储的优化实现方案咨询
C#编译器无法自动推导泛型类型的原因及泛型仓储的优化实现方案咨询
嘿,这个问题我之前做查询层开发时也碰到过,太懂这种重复写泛型参数的憋屈感了!咱们一步步来拆解问题~
一、为什么编译器没办法自动推导泛型类型?
简单来说,C#的泛型类型推导规则只认输入参数的直接类型,不会从约束条件反向推导,而且要绝对避免歧义。
你现在的方法签名是PerformQuery<TQuery, TResult>(TQuery query) where TQuery : IQuery<TResult>,编译器能轻松从query参数推导出TQuery是MyQuery,但TResult是通过约束TQuery : IQuery<TResult>关联的——这属于“反向推导”,C#编译器不做这个事,原因有两个:
- 歧义风险:理论上一个
TQuery可以实现多个不同TResult的IQuery<TResult>接口(虽然你现在的MyQuery只实现了一个,但编译器要考虑所有合法的C#代码场景)。比如如果有人写了:
那编译器完全不知道你要的public record MyQuery(int Number) : IQuery<MyResult>, IQuery<string>;TResult是MyResult还是string,总不能帮你随机选一个吧? - 规则限制:C#的类型推导设计时就只关注“从实参到泛型参数的正向映射”,约束条件是用来校验泛型参数是否合法的,不是用来推导参数的。约束是在推导完成后才会检查的环节,不是推导的依据。
二、更优雅的泛型仓储/查询处理方案
当然有更顺的写法!咱们可以从调整泛型签名或者引入查询处理器模式两个方向来优化:
方案1:简化仓储方法的泛型参数,让编译器能直接推导
如果你的仓储不需要依赖TQuery的具体类型(只是统一处理查询),可以把方法签名改成只基于IQuery<TResult>:
public class Repository { public TResult PerformQuery<TResult>(IQuery<TResult> query) { // 这里如果需要获取TQuery的具体类型,可以用query.GetType() var queryType = query.GetType(); // 替换成你的实际查询逻辑 return default; } }
调用的时候就完全不用写泛型参数了:
var repository = new Repository(); var query = new MyQuery(25); var result = repository.PerformQuery(query); // 编译器直接推导出TResult是MyResult,完美!
这个方案的唯一小问题是:如果你的仓储逻辑需要直接使用TQuery的具体成员(而不是通过IQuery<TResult>接口),那你需要用模式匹配或者类型转换,比如:
public TResult PerformQuery<TResult>(IQuery<TResult> query) { if (query is MyQuery myQuery) { // 直接用myQuery.Number return (TResult)(object)new MyResult($"处理编号:{myQuery.Number}"); } // 其他查询类型的处理逻辑 return default; }
方案2:引入查询处理器模式(更适合复杂场景)
如果你的系统有很多查询类型,每个查询的处理逻辑都不一样,那用查询处理器+调度器的模式会更优雅,也符合单一职责原则:
- 先定义查询处理器接口:
public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { TResult Handle(TQuery query); }
- 为你的
MyQuery实现对应的处理器:
public class MyQueryHandler : IQueryHandler<MyQuery, MyResult> { public MyResult Handle(MyQuery query) { // 这里写MyQuery专属的查询逻辑,比如查数据库、计算等 return new MyResult($"你查询的编号是:{query.Number}"); } }
- 实现一个查询调度器(代替原来的Repository),负责找到对应的处理器并执行:
public class QueryDispatcher { // 用字典存储处理器,key是查询类型,value是对应的处理器实例 private readonly Dictionary<Type, object> _handlers = new(); // 注册处理器的方法 public void RegisterHandler<TQuery, TResult>(IQueryHandler<TQuery, TResult> handler) where TQuery : IQuery<TResult> { _handlers.Add(typeof(TQuery), handler); } // 执行查询的方法,编译器能自动推导TResult public TResult Execute<TResult>(IQuery<TResult> query) { var queryType = query.GetType(); // 从字典中取出对应的处理器 if (!_handlers.TryGetValue(queryType, out var handlerObj)) { throw new InvalidOperationException($"没有找到{queryType.Name}对应的处理器"); } // 强转后调用处理方法 var handler = (IQueryHandler<TQuery, TResult>)handlerObj; return handler.Handle((TQuery)query); } }
- 调用的时候超级清爽:
var dispatcher = new QueryDispatcher(); // 注册处理器(实际项目中可以用DI自动注册,不用手动写) dispatcher.RegisterHandler(new MyQueryHandler()); var query = new MyQuery(25); var result = dispatcher.Execute(query); // 自动推导TResult是MyResult
这个方案的好处是:每个查询的处理逻辑都封装在自己的处理器里,代码更干净,扩展新查询只需要加新的处理器,完全符合开闭原则。
内容来源于stack exchange
相关产品推荐
相关产品推荐

