代码之家  ›  专栏  ›  技术社区  ›  Steve Dunn supercat

.NET中的泛型方法不能推断其返回类型。为什么?

  •  48
  • Steve Dunn supercat  · 技术社区  · 14 年前

    static TDest Gimme<TSource,TDest>(TSource source) 
    { 
        return default(TDest); 
    }
    

    为什么我不能:

    string dest = Gimme(5);
    

    不获取编译器错误:

    error CS0411: The type arguments for method 'Whatever.Gimme<TSource,TDest>(TSource)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

    这个 5 int ,但是有一个限制,编译器不能/不能将返回类型解析为 string . 我在好几个地方读到 这是故意的

    有人知道为什么不能从泛型方法推断返回类型吗?这是不是一个答案如此明显,让你眼前一亮的问题?我希望不是!

    7 回复  |  直到 14 年前
        1
  •  92
  •   Eric Lippert    5 年前

    里面 外部 一个表达式。你举的例子非常简单。假设我们在对方法进行类型推断时希望类型信息流“双向” R G<A, R>(A a) ,并考虑一些疯狂的场景,这些场景会产生:

    N(G(5))
    

    假设有10个不同的重载N,每个重载都有不同的参数类型。我们应该为R做十个不同的推论吗?如果我们这样做了,我们是否应该选择“最好的”一个呢?

    double x = b ? G(5) : 123;
    

    G的返回类型应该是什么?Int,因为条件表达式的另一半是Int?或者双倍,因为最终这个东西会被分配给双倍?现在也许你开始明白这是怎么回事了;如果你要说你从外到内推理, ? 可能有 一路走来。看看当我们开始组合这些时会发生什么:

    N(b ? G(5) : 123)
    

    (我不是开玩笑;有些语言本质上 写一个小prolog程序,然后用一个逻辑引擎计算出所有的类型。例如,F#的类型推理比C#复杂得多。Haskell的类型系统实际上是图灵完备的;您可以对类型系统中任意复杂的问题进行编码,并要求编译器解决这些问题。正如我们稍后将看到的,C#中的重载解析也是如此-在C#类型系统中不能像在Haskell中那样对停止问题进行编码,但可以将NP难问题编码为重载解析问题。)( 见下文 )

    N(N(b ? G(5) * G("hello") : 123));
    

    现在我们要对G,也可能对N,多次求解这个问题,我们要求解它们 . 我们有五个过载解决问题要解决和解决 全部的 公平地说,他们中的大多数人应该同时考虑他们的论点和他们的语境类型。如果N有十种可能性,那么N(N(…))可能有一百种可能性,N(N(…))可能有一千种可能性,很快你就会让我们解决问题,很容易有几十亿种可能的组合,使编译器非常慢。

    这就是为什么我们有这样一个规则:类型信息只能单向流动。它防止了这种鸡和蛋的问题,在这种情况下,你试图从内部类型中确定外部类型,并从外部类型中确定内部类型,从而导致各种可能性的组合爆炸。

    注意,对于lambdas,类型信息确实是双向流动的!如果你说 N(x=>x.Length) there are situations in which you can easily make the compiler try out billions of possible combinations 找到唯一有效的组合。使泛型方法能够这样做的类型推理规则非常复杂,甚至让Jon Skeet都感到紧张。 This feature makes overload resolution NP-HARD .

    使lambda的类型信息双向流动,以便泛型重载解析正确有效地工作,我花了大约一年的时间。这是一个如此复杂的功能,我们只想采取它,如果我们绝对肯定会有一个惊人的投资回报。让LINQ工作是值得的。但是,没有像LINQ这样的相应特性可以证明在一般情况下进行这项工作所花费的巨大成本是合理的。


    更新 可以 对C型系统中任意困难的问题进行编码。C#具有带泛型逆变的标称泛型子类型,并且已经证明,您可以用泛型类型定义构建图灵机,并强制编译器执行该机器,可能会进入无限循环。在我写这个答案的时候,这种类型系统的不可判定性是一个悬而未决的问题。看到了吗 https://stackoverflow.com/a/23968075/88656 详情。

        2
  •  6
  •   Dave Markle    14 年前

    你必须做到:

    string dest = Gimme<int, string>(5);
    

    您需要指定泛型方法调用中的类型。它怎么知道你想在输出中输入一个字符串?

    String是一个坏例子,因为它是一个密封的类,但假设不是。如果您没有在调用中指定类型,编译器怎么知道您不需要它的子类呢?

    举个例子:

    System.Windows.Forms.Control dest = Gimme(5);
    

    编译器如何知道实际要生成什么控件?您需要这样指定:

    System.Windows.Forms.Control dest = Gimme<int, System.Windows.Forms.Button>(5);
    
        3
  •  5
  •   Jerod Houghtelling    14 年前

    打电话 Gimme(5)

        4
  •  3
  •   nobody    7 年前

    当我需要做这样的事情时,我会使用这种技巧:

    static void Gimme<T>(out T myVariable)
    {
        myVariable = default(T);
    }
    

    像这样使用:

    Gimme(out int myVariable);
    Print(myVariable); //myVariable is already declared and usable.
    

    注意,自C#7.0以来,out变量的内联声明是可用的

        5
  •  1
  •   Grzenio    14 年前

    我想这是个设计决定。我还发现它在用Java编程时很有用。

    与Java不同,C#似乎正在向函数式编程语言发展,您可以通过另一种方式获得类型推断,因此您可以:

    var dest = Gimme<int, string>(5);
    

    从而推断出目的地的类型。我想把它和java风格的推理结合起来可能会很难实现。

        6
  •  0
  •   supercat    14 年前

    如果一个函数应该返回少数类型中的一个,您可以让它返回一个类,该类定义了对这些类型的加宽转换。我认为不可能用泛型的方法来实现这一点,因为扩展的ctype操作符不接受泛型类型参数。

        7
  •  0
  •   Kin Liang Choong    6 年前
    public class ReturnString : IReq<string>
    {
    }
    
    public class ReturnInt : IReq<int>
    {
    }
    
    public interface IReq<T>
    {
    }
    
    public class Handler
    {
        public T MakeRequest<T>(IReq<T> requestObject)
        {
            return default(T);
        }
    }
    
    var handler = new Handler();
    string stringResponse = handler.MakeRequest(new ReturnString());
    int intResponse = handler.MakeRequest(new ReturnInt());