为什么c#不推断我的泛型类型?
本文关键字:我的 泛型类型 为什么 | 更新日期: 2023-09-27 18:17:57
我对泛型方法有很多有趣的(有意的)乐趣。在大多数情况下,c#类型推断足够聪明,可以找出它必须在泛型方法上使用的泛型参数,但现在我有一个c#编译器不能成功的设计,而我相信它可以成功地找到正确的类型。
谁能告诉我是否编译器是有点笨在这种情况下,或者有一个非常明确的原因,为什么它不能推断我的泛型参数?
代码如下:
类和接口定义:
interface IQuery<TResult> { }
interface IQueryProcessor
{
TResult Process<TQuery, TResult>(TQuery query)
where TQuery : IQuery<TResult>;
}
class SomeQuery : IQuery<string>
{
}
一些不能编译的代码:
class Test
{
void Test(IQueryProcessor p)
{
var query = new SomeQuery();
// Does not compile :-(
p.Process(query);
// Must explicitly write all arguments
p.Process<SomeQuery, string>(query);
}
}
为什么会这样?我遗漏了什么?
下面是编译器的错误信息(它没有给我们留下太多的想象空间):
方法IQueryProcessor.Process<TQuery的类型参数,TResult>(TQuery)不能从用法中推断出来。尝试指定显式地键入参数
我认为c#应该能够推断它的原因是:
- 我提供一个实现
IQuery<TResult>
的对象。 - 该类型实现的
IQuery<TResult>
版本只有IQuery<string>
,因此result必须为string
。 有了这些信息,编译器有了result和TQuery。
很多人指出c#不基于约束进行推断。这是正确的,并且与问题相关。通过检查参数及其相应的形式参数类型来进行推理,这是推理信息的唯一来源。
一群人随后链接到这篇文章:
https://learn.microsoft.com/en-us/archive/blogs/ericlippert/c-3-0-return-type-inference-does-not-work-on-method-groups 那篇文章既过时又与问题无关。它已经过时了,因为它描述了我们在c# 3.0中做出的一个设计决策,然后我们在c# 4.0中推翻了这个决策,主要是基于对那篇文章的回应。我刚刚更新了这篇文章的效果。这是无关的,因为本文是关于从方法组参数到泛型委托形式参数的返回类型推断。这不是原海报所问的情况。
我要读的相关文章是这篇:
https://learn.microsoft.com/en-us/archive/blogs/ericlippert/constraints-are-not-part-of-the-signature更新:我听说c# 7.3稍微改变了应用约束的规则,使得上面十年前的文章不再准确。当我有时间的时候,我会回顾一下我以前的同事所做的改变,看看是否值得在我的新博客上发表一篇更正;在那之前,请谨慎使用,看看c# 7.3在实践中会做些什么。
c#不会根据泛型方法的返回类型推断泛型类型,只会根据方法的参数推断泛型类型。
它也不使用约束作为类型推断的一部分,这消除了为您提供类型的泛型约束。
有关详细信息,请参阅Eric Lippert关于该主题的帖子。
它不使用约束来推断类型。相反,它推断类型(如果可能),然后检查约束。
因此,虽然唯一可能的TResult
可以与SomeQuery
参数一起使用,但它不会看到这个
还要注意,SomeQuery
完全有可能也实现IQuery<int>
,这就是为什么对编译器的限制可能不是一个坏主意的原因之一。
我不想再解释为什么,我不幻想能比Eric Lippert做得更好。
但是,有一种解决方案不需要在方法调用中进行后期绑定或额外的参数。然而,它不是超级直观的,所以我把它留给读者来决定它是否是一个改进。首先,修改IQuery
使其自引用:
public interface IQuery<TQuery, TResult> where TQuery: IQuery<TQuery, TResult>
{
}
你的IQueryProcessor
看起来像这样:
public interface IQueryProcessor
{
Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
where TQuery: IQuery<TQuery, TResult>;
}
实际查询类型:
public class MyQuery: IQuery<MyQuery, MyResult>
{
// Neccessary query parameters
}
处理器的实现可能看起来像:
public Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
where TQuery: IQuery<TQuery, TResult>
{
var handler = serviceProvider.Resolve<QueryHandler<TQuery, TResult>>();
// etc.
}
规范中很清楚地说明了这一点:
如您所见,返回类型不用于类型推断。如果方法调用没有直接映射到类型参数,则推理立即失败。7.4.2类型推断
如果提供的实参数量与方法中的形参数量不同,则推理立即失败。否则,假设泛型方法具有以下签名:
Tr M(T1 x1…Tm xm)
对于M(E1…Em)形式的方法调用,类型推断的任务是为每个类型参数X1…Xn找到唯一的类型参数S1…Sn,以便调用M(E1…Em)有效。
编译器不会假设您想要string
作为TResult
参数,它也不能。想象一个从string派生的TResult
。两者都是有效的,那么选择哪一个呢?最好是明说。
为什么已经得到了很好的回答,但是还有一个替代解决方案。我经常面临同样的问题,但是dynamic
或任何使用反射或分配数据的解决方案在我的情况下是没有问题的(电子游戏的乐趣…)
因此,我将返回作为out
参数传递,然后正确推断。
interface IQueryProcessor
{
void Process<TQuery, TResult>(TQuery query, out TResult result)
where TQuery : IQuery<TResult>;
}
class Test
{
void Test(IQueryProcessor p)
{
var query = new SomeQuery();
// Instead of
// string result = p.Process<SomeQuery, string>(query);
// You write
string result;
p.Process(query, out result);
}
}
我能想到的唯一缺点是它禁止使用'var'。
解决这个问题的另一个方法是为类型解析添加额外的参数。为避免更改现有代码库,可以将此参数添加到扩展方法中。例如,您可以添加以下扩展方法:
static class QueryProcessorExtension
{
public static TResult Process<TQuery, TResult>(
this IQueryProcessor processor, TQuery query,
//Additional parameter for TQuery -> IQuery<TResult> type resolution:
Func<TQuery, IQuery<TResult>> typeResolver)
where TQuery : IQuery<TResult>
{
return processor.Process<TQuery, TResult>(query);
}
}
现在我们可以使用这个扩展如下:
void Test(IQueryProcessor p)
{
var query = new SomeQuery();
//You can now call it like this:
p.Process(query, x => x);
//Instead of
p.Process<SomeQuery, string>(query);
}
这远不是理想的,但比显式提供类型要好得多。
注:这个问题在dotnet存储库中的相关链接:
https://github.com/dotnet/csharplang/issues/997https://github.com/dotnet/roslyn/pull/7850
这最初是在问题中发布的,并代表OP移动到这里。
对我来说,最好的解决方案是改变IQueryProcessor
接口,并在实现中使用动态类型:
public interface IQueryProcessor
{
TResult Process<TResult>(IQuery<TResult> query);
}
// Implementation
sealed class QueryProcessor : IQueryProcessor {
private readonly Container container;
public QueryProcessor(Container container) {
this.container = container;
}
public TResult Process<TResult>(IQuery<TResult> query) {
var handlerType =
typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult));
dynamic handler = container.GetInstance(handlerType);
return handler.Handle((dynamic)query);
}
}
IQueryProcessor
接口现在接受一个IQuery<TResult>
参数。这样它可以返回一个TResult
,这将从消费者的角度解决问题。我们需要在实现中使用反射来获得实际的实现,因为需要具体的查询类型(在我的例子中)。但是这里有了动态类型的帮助,它将为我们做反射。您可以在本文中了解更多相关内容。
我知道这是一个非常古老的线程,我不知道下面的代码片段在十年前是否有效(out var
肯定不是),但今天它正在编译:
interface IQuery<TQuery, TResult>
{
}
interface IQueryProcessor
{
void Process<TQuery, TResult>(IQuery<TQuery, TResult> query, out TResult result)
where TQuery : IQuery<TQuery, TResult>;
TResult ProcessAndReturn<TQuery, TResult>(IQuery<TQuery, TResult> query)
where TQuery : IQuery<TQuery, TResult>;
}
class SampleQueryResult
{
}
class SampleQuery : IQuery<SampleQuery, SampleQueryResult>
{
}
class Program
{
static void Main(string[] args)
{
IQueryProcessor qp = null; // get it from di ?
qp.Process(new SampleQuery(), out var r1);
var r2 = qp.ProcessAndReturn(new SampleQuery());
SampleQueryResult r;
r = r1;
r = r2;
}
}
所以今天,没有必要显式地设置TResult
类型来使用Process
方法。