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


71

我在泛型方法中有很多有趣的乐趣(希望有乐趣)。在大多数情况下,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)的类型参数。尝试显式指定类型参数。

我认为C#应该能够推断出它的原因是由于以下原因:

  1. 我提供了一个实现的对象IQuery<TResult>
  2. IQuery<TResult>类型实现的唯一版本是IQuery<string>,因此TResult必须是string
  3. 通过此信息,编译器将具有TResult和TQuery。

对我来说,最好的解决方案是更改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>参数。这样,它可以返回a TResult,这将从消费者的角度解决问题。我们需要在实现中使用反射来获得实际的实现,因为需要具体的查询类型(在我的情况下)。但是这里有动态类型来进行救援,这将为我们做反思。您可以在本文中阅读有关此内容的更多信息。


4
编译器不知道TResult使用什么。在需要做出决定时,它不知道您将把它放进去string(所以也许应该推断出string)。即使知道,在法律上也可能是Process<SomeQuery, customClass>在那里customClass的任何类衍生string。另请参阅:返回类型推断不适用于成员组
Raymond Chen

@Raymond说他不是.NET家伙... pshaw
Ed S.

@Raymond:返回类型与此无关。查看我的更新。我希望编译器TResult是一个string,因为提供的对象实现了IQuery<string>
史蒂文

2
@RaymondChen:非常感谢Raymond,但这与该问题无关。相关文章blogs.msdn.com/b/ericlippert/archive/2009/12/10/...
埃里克利珀

1
@Steven Oops,对不起,错过了这一部分。我顺应埃里克。
雷蒙·陈

Answers:


60

一群人指出,C#不会基于约束进行推理。这是正确的,并且与问题有关。通过检查参数及其对应的形式参数类型来进行推断,这是推断信息的唯一来源。

然后,很多人都链接到这篇文章:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/05/c-3-0-return-type-in​​ference-does-not-work-on-member-groups.aspx

那篇文章既过时又与这个问题无关。这是过时的,因为它描述了我们在C#3.0中做出的设计决策,然后我们在C#4.0中撤销了该决策,主要是基于对该文章的答复。我刚刚在文章中添加了对此效果的更新。

这无关紧要,因为本文涉及从方法组参数到泛型委托形式参数的返回类型推断。那不是原始海报询问的情况。

我要阅读的相关文章是这样的:

http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx

更新:我听说有消息说C#7.3稍微改变了何时应用约束的规则,使上述已有十年历史的文章不再准确。有空的时候,我将回顾一下我以前的同事所做的更改,看看是否值得在我的新博客上发布更正;在此之前,请谨慎使用,并查看C#7.3的实际操作。


3
感谢您的回答(答案)。C#编译器将来是否有可能进行约束推理,或者这是不可能的吗?
史蒂文

我想说,这样的推断是不可能的。埃里克(Eric)在评论中说:“这是根据C#语言设计的长期原则而刻意设计的选择。”
布赖恩

2
@EricLippert:考虑到您对C#7.3的更新,我对阅读有关此主题的更多内容非常感兴趣。
史蒂文

1
据我所知的唯一的事情,在C#7.3改变重类型的约束是unmanagedDelegateMulticastDelegate,和Enum现在允许(这些总是在CLR允许的,只是没有了C#语言本身)。唯一可能适用的其他更改是方法重载分辨率得到了改进,可以更好地消除多个候选对象之间的歧义。
伊恩·肯普

2
现在两个链接都为404
Shlomo

15

C#不会根据泛型方法的返回类型推断泛型类型,只会根据该方法的参数来推断。

它还没有将约束用作类型推断的一部分,从而消除了为您提供类型的一般约束。

有关详细信息,请参阅Eric Lippert关于该主题的帖子


11

它不使用约束来推断类型。相反,它会推断类型(如果可能),然后检查约束。

因此,尽管唯一可以TResultSomeQuery参数一起使用的参数,但不会看到。

还请注意,也有可能完全SomeQuery实现IQuery<int>,这是为什么这对编译器的限制可能不是一个坏主意的原因之一。


5

规范非常清楚地说明了这一点:

7.4.2节类型推断

如果提供的参数数量与方法中的参数数量不同,则推理将立即失败。否则,假定泛型方法具有以下签名:

Tr M(T1 x1…Tm xm)

使用形式为M(E1…Em)的方法调用,类型推断的任务是为每个类型参数X1…Xn查找唯一的类型参数S1…Sn,以使调用M(E1…Em)变为有效。

如您所见,返回类型不用于类型推断。如果方法调用未直接映射到类型参数,则推理将立即失败。

编译器不仅会假定您要string用作TResult参数,也不会。想象一下TResult从字符串派生的。两者都是有效的,那么该选择哪一个呢?最好明确一点。


使类型推断成功的不仅是返回类型。类型实现IQuery<string>
史蒂文

5
还请注意,第一个突出显示的语句是错误;当添加命名和可选参数时,此行从未更新过,并且与以扩展形式适用的“ params”方法的类型推断不一致。规范的将来版本将更正该行,以将这些考虑在内。
埃里克·利珀特

FWIW,引用的第7.4.2节似乎对应于规范第5版的12.6.3.1 。
alx9r

3

为什么得到了很好的回答,但有一个替代的解决方案。我经常面对同样的问题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'。


2
从C#7.0起,您可以拥有p.Process(query,out var result)
MistyK,

3

我不会再说为什么了,我没有比埃里克·利珀特(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.
}

2

解决此问题的另一种方法是为类型解析添加其他参数。为避免更改现有代码库,可以将此类参数添加到扩展方法中。例如,您可以添加以下扩展方法:

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);
}

这远非理想,但比显式提供类型要好得多。

PS在dotnet资源库中与此问题相关的链接:

https://github.com/dotnet/csharplang/issues/997

https://github.com/dotnet/roslyn/pull/7850

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.