为什么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#应该能够推断它的原因是:

  1. 我提供一个实现IQuery<TResult>的对象。
  2. 该类型实现的IQuery<TResult>版本只有IQuery<string>,因此result必须为string
  3. 有了这些信息,编译器有了result和TQuery。

为什么c#不推断我的泛型类型?

很多人指出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/997

https://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方法。