Hello World
Spiga

C#编译器对泛型方法调用作类型推断的奇怪问题

2009-08-20 16:50 by 老赵, 7554 visits

泛型是.NET平台上重要的功能,泛型即为一个“不确定”的类型。C# 3.0中加强了对于类型推断的力度。如果缺少了类型推断,那么C#中的大部分功能,如泛型方法的调用,Lambda表达式都会丧失大部分的可用性——因为过于复杂,所以没有人会去用(还记得这里的Java代码吗?)。

“类型推断”的功能便是希望编译器可以自动从上下文中“意识到”某个泛型参数的具体类型,而不用代码具体指明。但是有些时候我们会发现,C#的代码推断作的相当不完整。例如,我们准备了这样的代码:

public interface ISome
{
    int Method(string arg);
}

public class Mock<T>
{
    public void Setup<TResult>(Func<T, TResult> func) { }
}

public static class It
{
    public static T IsAny<T>() { return default(T); }
}

熟悉Moq框架的朋友一定发现,这段代码和Moq的准备代码有些接近(当然,这里是委托,而Moq则用了表达式树)。于是我们往往希望写这样的代码:

var mockSome = new Mock<ISome>();
mockSome.Setup(s => s.Method(It.IsAny()));

由于ISome接口的Method方法签名已经完全确定了,因此编译器完全可以推断出Setup方法和IsAny方法的泛型参数如何。但是如果你这么做的话,C#编译器会给出这样的错误信息:

The type arguments for method 'It.IsAny<T>()' cannot be inferred from the usage. Try specifying the type arguments explicitly.

它要求指定It.IsAny方法的泛型参数。事实上,光指定这个也不够,它继续要求Setup方法的泛型参数。因此,我们必须这么写:

var mockSome = new Mock<ISome>();
mockSome.Setup<int>(s => s.Method(It.IsAny<string>()));
mockSome.Setup(s => s.Method(It.IsAny<string>()));

现在是int或string可能还没有太多问题,但如果您遇到了IGrouping<string, SortedDictionary<int, DateTime>>这种强大有力的类型,估计您会和我一样欲哭无泪。相信您也遇到过这样的问题。

本以为在大多数情况下不会遇见这样的问题(事实上平时的确用的挺爽),不过我刚才却不小心撞见了另一个古怪的情况,它耗费了我十几分钟的时间,最后在别人的提示下才发现问题所在。原因在于我写的通用扩展方法之一:

public static TDictionary RemoveKeys<TDictionary, TKey, TValue>(
    this TDictionary source, IEnumerable<TKey> keys)
    where TDictionary : IDictionary<TKey, TValue>
{
    foreach (var key in keys)
    {
        source.Remove(key);
    }

    return source;
}

这个扩展方法的作用是从一个IDictionary对象中移除部分key对应的内容,内容本身很简单,但是使用时却出现了问题:

var values = new RouteValueDictionary();
values.RemoveKeys(...);

RouteValueDictionary是一个实现了IDictionary<string, object>接口的字典对象,因此应该一切正常吧,但是编译器却告诉我出了问题:

'System.Web.Routing.RouteValueDictionary' does not contain a definition for 'RemoveKeys' and no extension method 'RemoveKeys' accepting a first argument of type 'System.Web.Routing.RouteValueDictionary' could be found.

这个错误提示和之前的不同,它告诉我们缺少RemoveKeys方法,而并没有要求我们补全泛型参数。但是一旦补全了泛型参数就没有问题了:

values.RemoveKeys<RouteValueDictionary, string, object>(...);

但是您会愿意写这样的代码吗?

因此,最后我不得不补充了另一个方法,误打误撞地绕开了“强制指定泛型参数”的方法:

public static IDictionary<TKey, TValue> RemoveKeys<TKey, TValue>(
    this IDictionary<TKey, TValue> source, IEnumerable<TKey> keys)
{
    foreach (var key in keys)
    {
        source.Remove(key);
    }

    return source;
}

这个方法直接使用了IDictionary<TKey, TValue>类型,而不是把它作为泛型参数的限制条件——问题就这么解决了。别问我为什么,我也不知道……

虽然从表面上解决了这个问题,但是它带来的限制也是很明显的:虽然新的方法也可以作用在字典类型上,但是新方法只能返回IDictionary<TKey, TValue>类型,旧的方法返回的则是原有的类型(如RouteValueDictionary)。返回原有的类型就可以利用其Fluent Interface,让代码编写更为方便。

这个应该算是C#编译器的bug吧,不知道有没有其他朋友遇到过。C#编译器半吊子的类型推断特性,总是能够不断给我们“惊喜”。还是一些函数式语言的类型推断最为强大,如Haskell或F#。在编写代码的时候,只会由于F#的类型推断功能太强而降低了代码的可读性(因此有时候我们甚至于会“主动”加上类型),而不会在某个“是人都能看出来类型”的情况下,编译器缺如瞎了一般非要我们提供具体类型。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

92 条回复

  1. 天在无云[未注册用户]
    *.*.*.*
    链接

    天在无云[未注册用户] 2009-08-20 16:54:00

    很好很细致,碰巧也能坐上沙发

  2. 天在无云[未注册用户]
    *.*.*.*
    链接

    天在无云[未注册用户] 2009-08-20 16:55:00

    老赵对于问题的研究总是如此透彻和细致,除了顶之外没有第二个字。

  3. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-08-20 16:57:00

    呵呵,这边文章好,解决了我之前对泛型的疑惑

  4. 寻自己
    *.*.*.*
    链接

    寻自己 2009-08-20 17:02:00

    呵呵,老赵又出精品了,先评论,占个位置,再仔细看

  5. 老赵
    admin
    链接

    老赵 2009-08-20 17:08:00

    @温景良(Jason)
    什么疑惑?

  6. 未注册用户[未注册用户]
    *.*.*.*
    链接

    未注册用户[未注册用户] 2009-08-20 17:13:00

    不得不顶,虽然今天看了老赵三篇雄文一篇也没完全看懂。

  7. 非空
    *.*.*.*
    链接

    非空 2009-08-20 17:18:00

    我只想知道一天发3篇博文然后还能集中精力工作的?
    莫非这3篇都是今天碰到的问题?

  8. 老赵
    admin
    链接

    老赵 2009-08-20 17:25:00

    @非空
    不是两篇吗?
    没错都是今天或昨天遇到的,明天还有两篇,已经准备好了。
    我打算以后写的密度高一些,任何事情都可以成为文章。

  9. SZW
    *.*.*.*
    链接

    SZW 2009-08-20 17:25:00

    恩,这个问题我也碰到过几次,由于泛型都是自定义类型,最后只好使用像文中这样的方法显式指定了:
    var mockSome = new Mock<ISome>();
    mockSome.Setup<int>(s => s.Method(It.IsAny<string>()));

  10. SZW
    *.*.*.*
    链接

    SZW 2009-08-20 17:30:00

    补充一下,以前也尝试过一个解决方法,就是在参数里面输入一个委托的变量,让程序知道这个泛型的确切类型(当然这个方法并不是在所有情况下都适用,需要一定的环境),而实际上这个参数在方法中其实并没有用到,只是一个“诱饵”。
    只不过总觉得这么做有违参数设定的一些原则,并且使调用更加复杂,于是最后还是选择了显式标明一下类型。

  11. stubas[未注册用户]
    *.*.*.*
    链接

    stubas[未注册用户] 2009-08-20 17:33:00

    var mockSome = new Mock<ISome>();
    mockSome.Setup(s => s.Method(It.IsAny<string>()));

    不需要
    mockSome.Setup(s => s.Method(It.IsAny<string>()));

    lamda已经可以推动出Setup是 <int>.

    你纠正下
    ---------------------------------------------
    var mockSome = new Mock<ISome>();
    mockSome.Setup(s => s.Method(It.IsAny()));
    其实上面按照你说的,就可以推演了,连那个 It.IsAny<string> 也不需要。可以将你这个问题发到官方,完善 推演功能

  12. 未注册用户[未注册用户]
    *.*.*.*
    链接

    未注册用户[未注册用户] 2009-08-20 17:35:00

    Jeffrey Zhao:
    @非空
    不是两篇吗?


    老赵的文章现在在首页的不是有两篇了吗?第二页还有两篇,哈哈,不过是昨天写的,所以对于早上就开始看博客园的人来说,显示在首页并且看到的就是三篇,痴痴地等新作。

  13. 老赵
    admin
    链接

    老赵 2009-08-20 17:42:00

    @stubas
    多谢提醒,已改正,试过了的确是你说的这样,呵呵。

  14. gotolovo
    *.*.*.*
    链接

    gotolovo 2009-08-20 17:45:00

    好文,可惜 不懂

  15. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-08-20 17:55:00

    Jeffrey Zhao:
    @温景良(Jason)
    什么疑惑?


    就是Func<T, TResult> func的用法,一直不太清楚这个要怎么用,看到你之前写得表达式树里经常有这个东西,查了msdn,没看明白,今天明白了

  16. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 17:57:00

    只看了前半部分,泛型方法的泛型类型推断只能从参数而不是返回值来推断。

    这个在C#规范中应该能找到。

    主要原因应该是C#在进行方法的Bind的时候,是只考虑参数类型的。

  17. 的1[未注册用户]
    *.*.*.*
    链接

    的1[未注册用户] 2009-08-20 17:58:00

    老赵:我希望你出个系列的,不能想到哪写到哪?这样让人有种系统的感觉,而且以后这些东西整理出本书也很好!

  18. 老赵
    admin
    链接

    老赵 2009-08-20 17:58:00

    @温景良(Jason)
    那这篇文章最多是让你吃饱的最后那个饼吧……

  19. 老赵
    admin
    链接

    老赵 2009-08-20 18:00:00

    @Ivony...
    我觉得你的理由推不出问题的原因……这只是C#没有去做而已吧,不是什么做不到的事情。

  20. 老赵
    admin
    链接

    老赵 2009-08-20 18:01:00

    @的1
    博客不就是用来记录心得体会的吗?
    我就是系列不起来,所以写不了书啊。
    你刊博客园各位兄弟那么多写书了,我就写不了,呵呵。

  21. stubas[未注册用户]
    *.*.*.*
    链接

    stubas[未注册用户] 2009-08-20 18:10:00

    dotnet里面推演功能比较新,是一个逐步完善的过程。我觉得不必纠缠里面 推演的机理。只要把握好 推演逻辑上的依赖关系即可。c#这样的语言是必须做到 ‘逻辑上认为可以推演的,编译过程都予以支持

    例如以下:
    var mockSome = new Mock<ISome>();
    mockSome.Setup(s => s.Method(It.IsAny()));
    这样逻辑上已经完备了,不需要再制定什么类型参数

  22. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 18:14:00

    Jeffrey Zhao:
    @Ivony...
    我觉得你的理由推不出问题的原因……这只是C#没有去做而已吧,不是什么做不到的事情。



    我们假设有这样的方法:

    public TR Test<TA, TR>( TA args )
    我们又有几个这样的重载:
    public int Test<TA>( TA args )
    public object Test( int args )
    显然这都是合法的。


    那么在调用这样的表达式的时候:
    object o = Test( new object() )
    采取怎样的重载会是一件很麻烦的事情。
    第三个重载不可用,第二个和第一个重载之间,应该采用何种规则呢?
    到底是object Test<object, object>( object )更吻合还是int Test<object>( object )更吻合呢?我觉得已经无法通过直觉来判断了,所以我还是赞成现有的这种规则比较好。

    当然还存在更多的特殊情况,,,,以后想到了再说

  23. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 18:15:00

    当然我们可以令所有不明确的情况编译器都报错误,但我觉得C#的语言的精神就在于简单规则,如果那样反而混乱。

    这是见仁见智的问题了。

  24. 老赵
    admin
    链接

    老赵 2009-08-20 18:17:00

    @Ivony...
    就像上面朋友说的,我也认为编译器应该支持逻辑上成立的类型推演。
    在“多种可能”,没法自动推断的情况下,应该给出编译错误。
    就像你举的例子,不是“能够推断”但“看不清”,而是“混淆”,应该出现编译错误。

  25. James.Ying
    *.*.*.*
    链接

    James.Ying 2009-08-20 18:21:00

    类型推断这东西,还是有点搞,继续学习。。。。

  26. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 18:49:00

    未注册用户:

    Jeffrey Zhao:
    @非空
    不是两篇吗?


    老赵的文章现在在首页的不是有两篇了吗?第二页还有两篇,哈哈,不过是昨天写的,所以对于早上就开始看博客园的人来说,显示在首页并且看到的就是三篇,痴痴地等新作。



    我也支持多点好文章把些乱七八糟的东西刷下去。

  27. 老赵
    admin
    链接

    老赵 2009-08-20 18:56:00

    @Ivony...
    其实我觉得如果你的文章排版好些在说的清楚一些,也是不错的文章阿……

  28. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 19:03:00

    Jeffrey Zhao:
    @Ivony...
    其实我觉得如果你的文章排版好些在说的清楚一些,也是不错的文章阿……



    我在网吧写的,哪能整的那么好啊。。。。。哎。。。。。没写着写着丢了就算不错了。

    那个Y组合子我回家用电脑,打开Word(没装Live Writer)随便弄弄,就很不错了。

  29. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 19:06:00

    有电脑的时候,写文章总是写了觉得不好又搁下了,写了一大堆也没发什么,没电脑的时候,想写文章也写不好,只能写点简单的,写一点赶紧发布成草稿,然后慢慢改,所以反而发的越来越多。

  30. PIERRR[未注册用户]
    *.*.*.*
    链接

    PIERRR[未注册用户] 2009-08-20 19:12:00

    不错,敢于向Anders挑战!!

  31. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-08-20 19:18:00

    呵呵……老赵,是“泛型”,不是“范型”啊;这个手滑得改过来,俩词意思差很多。

    回到正题。可以从信息流向的角度来看问题。C#里运算中数据流向的顺序基本上都是从前向后(或者说从左向右),只有赋值相关语句是从等号右边流向等号左边的(一个例外是委托/表达式树相关的赋值包含了从左向右流动的类型信息)。如果有嵌套的方法调用,如F(G(x)),运算顺序是x->G(x)->F(G(x)),“信息”的流动方向也是一样,“从内向外”。
    为什么用var来声明变量的时候,不干脆设计成MyClass o = new();呢?因为它跟信息流动的方向不吻合,虽然在这种特例上方便了编译器的实现(其实也没差多少),但在其它场景就不适用了(如var x = 1 + 2;,不涉及new)。
    LINQ的语法设计也是这样的。设计过程中先是有个方案跟SQL非常像,select在前from在后。但考虑到信息流动的方向,从前向后比从后向前更便于理解,也便于IntelliSense和编译器的实现,就把顺序换了过来。

    C#允许方法重载,但用于区分重载的条件只是:1、方法名;2、参数列表。返回值类型无法用于区分重载。因此,如果知道一个方法名及其参数列表中的参数个数和各参数的类型,就能断定其返回值类型。反之,知道方法名和返回值类型,不一定能断定其参数列表的状况,也就不一定能断定应该选用哪个版本的重载。即便实际上该方法名只有一个版本的重载,为了保持语言的一致性,C#也没有允许这样的重载判定。
    如果用“信息流向”的角度看,从参数判断返回值是“从前向后”的流动;从返回值判断参数则是“从后向前”的流动。

    C# 3.0的lambda涉及的类型推导算法分为两段,在C#规范第三版的7.4.2小节里有所描述,有兴趣了解细节的话可以读一下。规范可以在Visual Studio 2008的VC#\Specifications目录下找到。ECMA-334规范最新只到第4版,对应的是C# 2.0,不够新。

    老赵文中的第一组例子败在It.Any<T>()的推导上。根据规范中对推导算法的规定,它在调用的最内层,无法从外界获得关于返回值类型T的提示,而它自己又没有参数涉及T类型,推导就失败了。其实跟下面这种代码一样:

    using System;
    
    static class Program {
        static T Foo<T>() { return default(T); }
    
        static void Main(string[] args) {
            Func<int> func = () => Foo(); // compile error
            Console.WriteLine(func());
        }
    }
    

    或者直接写成:
    using System;
    
    static class Program {
        static T Foo<T>() { return default(T); }
    
        static void Main(string[] args) {
            Func<int> func = Foo; // compile error
            Console.WriteLine(func());
        }
    }
    

    这里,“Foo”这个名字代表了一个MethodGroup,然后我们知道我们要的是返回类型为int的重载。我们“一眼”就看到泛型版本的Foo可以胜任,但……如果还是从信息流向的角度看,“从内向外”是帮不上忙了,作为“内”的参数列表什么信息也提供不了,而作为“外”的返回类型就推导不出来了,即便我们知道它应该赋值给Func<int>……

    C#有很多规定都不是说“我们要做到尽”的。例如说类型推导可以寻找公共基类:
    using System;
    
    interface I { }
    class A : I { }
    class B : A { }
    
    static class Program {
        static void Foo<T>(T t1, T t2) { }
    
        static void Main(string[] args) {
            var a = new A(); 
            var b = new B();
            Foo(a, b);
        }
    }
    

    却不会去寻找公共接口:
    using System;
    
    interface I { }
    class A : I { }
    class B : I { }
    
    static class Program {
        static void Foo<T>(T t1, T t2) { }
    
        static void Main(string[] args) {
            var a = new A(); 
            var b = new B();
            Foo(a, b); // compile error
        }
    }
    

    主要是,当两个类型不在同一个继承链上的时候,要找出它们的公共接口就要涉及比较复杂的unification;如果两个类都正好实现了IEnumerable<T>和IComparable<T>,那选哪个版本好?
    这个问题也不是总是不能解决(可以根据方法体中的调用状况来推导),但要像Hindley–Milner类型系统那样推导的话,搞不好会“凭空”造出些类型出来,而C#小组觉得那样不好。

    F#虽然有H-M系的类型推导,但在应付.NET类型的时候它也经常会败下阵来,要程序员显式指定类型。这些东西要仔细想想会发现很多诡异的地方……

    然后所有feature都需要资源去实现。像Eric Lippert所说,“one of the design principles of C# is "if you say something wrong then we tell you rather than trying to guess what you meant"”和“we have a very limited time and money budget for design, implementation, testing, documentation and maintenance, so we want to make sure we’re spending it on the highest-value features”。如果大家能一起说服C#小组让他们觉得使用更强的类型推导算法能带来非常高的价值,比很多其它feature都重要,说不定他们就会去实现出来 XD

    之前我发过email问Eric为什么C# 4.0没有支持statement-lambda到表达式树的转换,他说不是他们不想做,是时间来不及了而已,涉及到编译器里的一些遗留问题。很多时候我们也只好妥协了……

  32. 老赵
    admin
    链接

    老赵 2009-08-20 19:25:00

    @RednaxelaFX
    你为什么就不能发一篇文章说这些东西……

  33. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-08-20 19:27:00

    @Jeffrey Zhao
    因为……太短了?(逃
    对了老赵,标题手滑了,是“泛型”不是“范型”~

  34. 嗯哼居然有人用[未注册用户]
    *.*.*.*
    链接

    嗯哼居然有人用[未注册用户] 2009-08-20 19:29:00

    where : 泛型 这种我没用过。where 就是用来约束饭型的,把它约束成另一个泛型,个人感觉在逻辑上不合理。

  35. 老赵
    admin
    链接

    老赵 2009-08-20 19:31:00

    @嗯哼居然有人用
    这种用法实在太多了。

  36. 老赵
    admin
    链接

    老赵 2009-08-20 19:31:00

    @RednaxelaFX
    输入法还没有教会……
    一会儿我把你的这个评论发成一篇文章。

  37. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 19:32:00

    @RednaxelaFX

    嗯,我觉得这个解释比较OK了。其实这个问题是个老问题了,习惯了就好了。


    我倒是觉得C#不能像VB那样直接通过调用参数类型反推lambda表达式类型,也就是说((x,y)=>x+y)(1,2)这样对于C#是不合法的,对于VB而言是合法的,这个让我对VB挺眼馋。

    C#合法的最简写法是:
    ((Func<int,int,int>)((x,y)=>x+y))(1,2)

    即需要写成((DelegateType)(lambda-expression))( args )
    但这个写法在VS2010又不支持(装配脑袋测试)
    只能写成
    new DelegateType( lambda-expression )( args )

  38. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-08-20 19:42:00

    @Ivony...
    嗯,C#的lambda让我最痛的地方还就是Delegate和Expression<TDelegate>是用同一种语法写的,并要求程序员显式指明到底要的是哪个。我觉得要是能用反单引号括住一段代码让C#编译器认为那是“需要编程表达式树的东西”就好了,样子就像LISP的quote/quasi-quote之类的。反正C#还没用到反单引号作为有特殊含义的字符……
    也正是因为这个问题,赋值语句的信息流动才会有特例的……一点都不美啊 T T

  39. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-08-20 19:44:00

    @Jeffrey Zhao
    OK~
    我刚才是在想如果我要发的话,恨不得把人家整本类型系统的书都搬过来作为论据,心理才踏实 XD

  40. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 19:46:00

    @RednaxelaFX

    哈哈,你不说我都快忘了这个曾经让我痛不欲生的东西了,郁闷死啊。因为最近用expression-tree比较少了。

    好了,继续来看老赵的第二个问题吧。

  41. 嗯哼居然有人用[未注册用户]
    *.*.*.*
    链接

    嗯哼居然有人用[未注册用户] 2009-08-20 19:49:00

    烦请老赵给个例子。另外我觉得根据参数反推也不可取,目前没有理由就是感觉。

  42. 嗯哼居然有人用[未注册用户]
    *.*.*.*
    链接

    嗯哼居然有人用[未注册用户] 2009-08-20 19:50:00

    Jeffrey Zhao:
    @嗯哼居然有人用
    这种用法实在太多了。


    忘引了

  43. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 19:51:00

    看了一下,应该是同样的原理,从参数中只能推断出TKey的类型,而不能推断TValue的类型,基于此,IDictionary<Tkey, TValue>也不能确定,就不能确定DictionaryType是不是满足第一个参数,就没发调用这个东西了,如果我的推测不错的话,老赵加个伪参数TValue就可以成功调用了。。。。

  44. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 19:54:00

    或者可以让第一个参数来推演所有类型

    public static Dictionary<TKey, TValue> RemoveKeys<TKey, TValue>(
    this Dictionary<TKey, TValue> source, IEnumerable<TKey> keys)


    这样应该也是合法的。

  45. 老赵
    admin
    链接

    老赵 2009-08-20 20:05:00

    @Ivony...
    啥叫伪参数TValue?

  46. 老赵
    admin
    链接

    老赵 2009-08-20 20:07:00

    Ivony...:
    public static Dictionary<TKey, TValue> RemoveKeys<TKey, TValue>(
    this Dictionary<TKey, TValue> source, IEnumerable<TKey> keys)
    这样应该也是合法的。


    这不就是把我的IDictionary改成了Dictionary吗?

  47. 老赵
    admin
    链接

    老赵 2009-08-20 20:07:00

    嗯哼居然有人用:烦请老赵给个例子。另外我觉得根据参数反推也不可取,目前没有理由就是感觉。


    Enterprise Library, Fluent NHibernate, SharpArchitecture……比比皆是啊。
    还有“类型推演”啥叫不可取,这是很常见理论啊,很多语言都实现了……

  48. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-20 20:09:00

    public static TDictionary RemoveKeys<TDictionary, TKey, TValue>(
    this TDictionary source, IEnumerable<TKey> keys, TValue value )
    where TDictionary : IDictionary<TKey, TValue>

    调用时:
    values.RemoveKeys( keys, (Route) null );

    (Route) null就是伪参数,当然看起来很难看。

  49. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-08-20 20:26:00

    @嗯哼居然有人用

    嗯哼居然有人用:烦请老赵给个例子。另外我觉得根据参数反推也不可取,目前没有理由就是感觉。


    是指前面Ivony...给的那个例子么?

    ((x,y)=>x+y)(1,2)


    其实这个还算不上参数反推吧,虽然lambda写在前面,但信息的流动是从参数流向lambda里,实际参数类型已知(1和2默认是int),那么形式参数的类型也可以顺着推导出来。C#不能这么做主要还是因为这样不足以区分委托和表达式树(嗯实际上信息是足够了,只是C#编译器没去用而已)。

    要说真正意义上的反推,或许是这样?
    (x, y) => x + y
    也就是说没足够上下文来告诉编译器到底x和y的类型是什么。这种情况下,像OCaml、F#就会认为这是int -> int -> int,因为+默认以int为操作数,除非显式指定别的类型。(同属ML系语言的Standard ML就不是这样,整型和实数的加号不一样)。

  50. Zhenway
    *.*.*.*
    链接

    Zhenway 2009-08-20 20:46:00

    这个问题类似于c#不支持仅仅返回值类型不同的overload

  51. 鹤冲天
    *.*.*.*
    链接

    鹤冲天 2009-08-20 21:31:00

        public static class It
        {
            public static T IsAny<T>() { return default(T); }
        }
    

    将<T>提升一级,如下:
        public static class It<T>
        {
            public static T IsAny() { return default(T); }
        }
    

    编译报错:使用泛型类型 It<T>”需要“1”个类型实参。

    看来现在C#只能从最末端向前推,而不能从开始向末端推或者从两头向中间推!

    这两种写法在这里应该是一回事吧?!

  52. 非空
    *.*.*.*
    链接

    非空 2009-08-20 21:48:00

    我记得老赵在codeplex、code gallery、还有这个刚从老赵链接上看见的http://gist.github.com/这个,老赵是不是以后要出个藏宝图啊

    然后在图里或者某个代码里藏着钥匙,让你的拥趸们找啊O(∩_∩)O哈哈~

  53. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-08-20 21:56:00

    这个问题我也遇到过,解决方法也是跟你差不多。

  54. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-08-20 22:04:00

    @鹤冲天

    鹤冲天:

        public static class It
        {
            public static T IsAny<T>() { return default(T); }
        }
    

    将<T>提升一级,如下:
        public static class It<T>
        {
            public static T IsAny() { return default(T); }
        }
    

    编译报错:使用泛型类型 It<T>”需要“1”个类型实参。

    看来现在C#只能从最末端向前推,而不能从开始向末端推或者从两头向中间推!

    这两种写法在这里应该是一回事吧?!


    “提升”之后,It就不是一个类型了;It<T>是一个未实例化的泛型类型;It<string>之类的则是实例化的泛型类型。这样,如果还是写It.Any(),自然就会出错。不然,IEnumerable跟IEnumerable<T>这样的名字相同、一个不是泛型类型另一个是泛型类型的情况,就区分不了了 = =

  55. 老赵
    admin
    链接

    老赵 2009-08-20 22:16:00

    大家继续讨论啊,我周末看看是不是值得总结。:)

  56. 老赵
    admin
    链接

    老赵 2009-08-20 22:17:00

    @非空
    分工明确阿。
    codeplex用来放开源项目,带svn的和wiki。
    msdn code gallary放大型的示例项目,带wiki。
    github用来放小型代码片断,带一点revision控制。

  57. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-08-20 22:52:00

    @Jeffrey Zhao
    确实,增加一个dummy参数来提供TValue的实际类型时,推导就能够完成了。我再好好读一下规范看看类型推导跟generic bounds相关的部分是怎么处理的……

  58. 特是他[未注册用户]
    *.*.*.*
    链接

    特是他[未注册用户] 2009-08-20 22:55:00

    @RednaxelaFX
    期待这位仁兄 的 文章,因为 您 的文章 介绍基础比较详细 ,老赵的文章没有过多的基础讲解,看着吃力。

  59. 老赵
    admin
    链接

    老赵 2009-08-20 23:09:00

    RednaxelaFX:
    @Jeffrey Zhao
    确实,增加一个dummy参数来提供TValue的实际类型时,推导就能够完成了。我再好好读一下规范看看类型推导跟generic bounds相关的部分是怎么处理的……


    记得一定要给个答复啊,我记下了,会总结一下的。:)

  60. 芭蕉
    *.*.*.*
    链接

    芭蕉 2009-08-20 23:29:00

    public static T IsAny<T>() { return default(T); }

    C# compiler在编绎时缺乏足够的信息,它没办法根据default(T)推断出到底返回什么类型,当然,你可能会说根据上下文,T明显是string类型啊,但是c# compiler不会这么做,它只是首先推断该方法的参数类型,然后根据用它推断出来的参数,据据方法体推断返回值类型。如果推断不出,就直接告诉你无法推断了,不会再做进一步的推断。

  61. 老赵
    admin
    链接

    老赵 2009-08-20 23:45:00

    @芭蕉
    这个和default肯定没有任何关系,外部方法调用不会推导到方法内部去,IsAny方法可以独立编译。
    最简单的验证方式:
    1、不要用It.IsAny,使用一个接口方法。
    2、把It类放到另外的程序集去。
    3、把IsAny方法限制为class,然后内部返回null。
    4、IsAny的实现写成(T)new object()这种形式。
    效果都是一样的,说明和内部实现无关,推导依据只是签名,信息够与不够都是签名说了算。
    而且说缺乏足够信息也肯定不对,信息是足够的,只是C#编译器由于某些原因(例如实现时“偷懒”了)没有接受而已。

  62. 芭蕉
    *.*.*.*
    链接

    芭蕉 2009-08-20 23:58:00

    @Jeffrey Zhao
    可能我没说清楚,我并不说是default引起的啊。。。我是说根据default(T)没法推断出返回值类型。。。我说的你爱信不信,呵呵

  63. 老赵
    admin
    链接

    老赵 2009-08-21 00:02:00

    @芭蕉
    default(T)的类型就是T,需要推断什么啊?
    // 有没有相关资料可以瞅瞅?

  64. 芭蕉
    *.*.*.*
    链接

    芭蕉 2009-08-21 00:05:00

    @Jeffrey Zhao
    在编绎时需要确定T的具体类型啊。。。default(T),你说compiler能知道T到底是啥类型么?

  65. 芭蕉
    *.*.*.*
    链接

    芭蕉 2009-08-21 00:14:00

    我刚往上翻了下,确实有人说的更清楚,你可以按他说的,去翻翻specification,记忆中好象有很长一部分是专门在讲type inference的,呵呵

  66. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-08-21 00:20:00

    @芭蕉

    芭蕉:
    @Jeffrey Zhao
    在编绎时需要确定T的具体类型啊。。。default(T),你说compiler能知道T到底是啥类型么?


    不知道我有没有理解错你的意思,你是说单就default(T)而言类型不确定么?
    我的意思是,跟老赵的例子没关系,单就这一点来说,default(T)的类型就是T,毫无疑问。至于T是什么,在声明处C#编译器不用关心,在使用处在需要做类型检查/类型推导时需要关心。但说到底T的实例化(动态意义上的)并不是C#编译器完成的,而是由运行时的执行引擎来完成的。IsAny<T>()由C#编译器编译出来的MSIL只会有一份,就是T还未确定的那一份。

    如果你的意思是IsAny<T>()在老赵的例子中的类型推导过程里起不到作用,那是没错的。它自己在从参数推导返回值的过程就失败了,也就没办法提供类型信息给外面的s.Method()。

  67. 老赵
    admin
    链接

    老赵 2009-08-21 00:31:00

    @芭蕉
    好吧,你不该提default的……因为就算不用default,T也是不确定的类型,这里和default没有任何关系
    type inference的依据总是签名,和实现没有关系。

  68. 芭蕉
    *.*.*.*
    链接

    芭蕉 2009-08-21 00:32:00

    @RednaxelaFX
    嗯,我就是想说在它举的例子中,compiler没法推断出该方法返回值的类型,因为例子中是return default(T),我就直接说没法根据default(T)无法推断出返回值了

  69. 沐枫
    *.*.*.*
    链接

    沐枫 2009-08-21 10:43:00

    1. ((x,y)=>x+y)(1,2)
      这个错误是可以理解的,因为这个lambda不是泛型,所以,无法确定参数的类型,从而根本无法生成lambda。而后面的调用(1,2)其实有根没有都不影响编译结果了。
      假如lambda能支持泛型参数,则此问题即可解决,如:

    (<T>(T x, T y)=>x+y)(1,2)
    (<T>(T x, T y)=>x+y)(3.2, 5.7)
    


    2. public static TDictionary RemoveKeys<TDictionary,TKey, TValue>(this TDictionary source, IEnumerable<TKey> keys){...
      where做为类型约束,应该不参与类型推断了。
      C#在泛型方面步子还是迈得太小了。假如支持如下的定义,就能完美的解决此问题:
    public static T<K,Y> RemoveKeys<T<K,Y>>(this T source, IEnumerable<T> keys)
    //where T<K,Y>:IDictionary<K,Y>
    {
    //...
    }
    


    以上,(1)的实践在C++0x中;(2)的实践在C++98中,效果极佳。

  70. 老赵
    admin
    链接

    老赵 2009-08-21 10:58:00

    @沐枫
    第一点在C#里必须这么做:
    ((Func<int, int, int>)((x, y) => x + y))(1, 2);

    第二点,我觉得也不是解决这个问题必要的,因为实际上现在的情况已经是可以实现了,只是C#没有去推断。你提出的相对C#已经是新特性了。

  71. ravenex[未注册用户]
    *.*.*.*
    链接

    ravenex[未注册用户] 2009-08-21 11:59:00

    @沐枫
    lambda本身不是泛型,但它的参数类型可省略,性质跟var关键字类似。函数调用包含实际参数到形式参数的绑定,也就是赋值,跟var的应用场景非常相似。因而,直接在lambda后提供实际参数,应已提供足够信息做推导,而且信息流向也符合从内到外。C#不让这么做是编译器执意要用户指定lambda代表的是委托还是表达式树

  72. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-21 12:25:00

    lambda不支持泛型指的是lambda表达式不能表示泛型方法。事实上不仅是lambda,匿名方法也不能是一个泛型方法。

    简单的说,下面这样的方法不能用匿名方法或lambda表示,如果要写成lambda表达式,就必须把T先明确。
    public static T ω<T>(Func<Delegate, T> f)
    {
    return f(f);
    }

    譬如说:

    Func<Func<Delegate, double>, double> ω = f => f(f);
    这才是合法的,而不能:
    Func<Func<Delegate, T>, T> ω<T> = f => f(f)

    如果将lambda表达式直接执行,就像这样:
    ((a,b) => a+b)(1,2)
    编译器会报错搞不清lambda表达式到底是匿名方法还是Expression。
    但实际原因还不仅如此。
    因为lambda表达式也不支持直接赋值给Delegate:
    Delegate d = () => 4;
    这样的也是不合法的,但显然编译器已经没有借口说搞不清是匿名方法还是Expression了。
    当然,这样也不合法:
    ((Delegate) () => 4)();
    必须使用强类型,就可以合法:
    Func<int> d = () => 4;
    应该就OK了,年代久远有些记不清,记错了大家勿怪,最好自行测试。
    而且,也根本不支持类型推断:
    Func d = () => 4;
    这也不合法。没记错的话:
    Expression e = () => 4;
    也不合法。

    估计类型僵化将来会是C#变得更灵活的一个很大的阻碍。

  73. 老赵
    admin
    链接

    老赵 2009-08-21 12:47:00

    @Ivony...
    其实F#和Scala已经为我们证明了,静态类型不会成为灵活性的阻碍(至少已经无比灵活了),就看你要不要那些灵活性了。

  74. ravenex[未注册用户]
    *.*.*.*
    链接

    ravenex[未注册用户] 2009-08-21 13:40:00

    @Ivony...

    Ivony...:
    譬如说:
    Func<Func<Delegate, double>, double> ω = f => f(f);
    这才是合法的,而不能:
    Func<Func<Delegate, T>, T> ω<T> = f => f(f)


    这是受到.NET的“值限制”问题。委托都是“值”,而.NET中“值”不可以包含未确定的泛型参数。
    F#很明显受这个限制而有奇怪的表现。例如:
    [code]
    let l = [];;
    [/code]
    这个没问题,其类型是'a list(C#写法是list<'a>),'a是未确定泛型。但这个就不行:
    [code]
    let map f = List.map f;;
    [/code]
    会提示有“值限制”错误。原因是List.map原本要接收2个参数,就是说类型是('a -> 'b) -> 'a list -> 'b list。上例只给了第一个参数,应该curry为'a list -> 'b list,但此时的map是一个“值”,其中不能包含未确定泛型参数,因而F#拒绝这种写法。空表允许未确定泛型参数只是极少的特例情况。

    C#不接受(Delegate)形式来指明委托与表达式树的差异确实让人遗憾。

    注:新的F#倾向于使用list<A>的写法而不是'a list的写法了,但我还是习惯跟随ML原本的形式来写。

  75. ravenex[未注册用户]
    *.*.*.*
    链接

    ravenex[未注册用户] 2009-08-21 13:47:00

    @Ivony...
    如果T所在的上下文中有泛型声明,则T在该上下文中是一个“确定类型”,就可以写出泛型lambda:
    [code=csharp]
    using System;

    public class Test {
    public static void Main(string[] args) {
    var omega = Omega<int>.Instance;
    }

    public class Omega<T> {
    public static readonly Func<Func<Delegate, T>, T> Instance = f => f(f);
    }
    }
    [/code]

  76. ravenex[未注册用户]
    *.*.*.*
    链接

    ravenex[未注册用户] 2009-08-21 13:53:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    @Ivony...
    其实F#和Scala已经为我们证明了,静态类型不会成为灵活性的阻碍(至少已经无比灵活了),就看你要不要那些灵活性了。


    静态类型本身不是问题,问题是.NET的类型系统太弱。
    函数可以以函数为参数,以函数为返回值,从而形成高阶函数。同理,类型也可以以类型为参数,可以生产出类型才对。.NET当前的类型系统无法支持高阶类型,在.NET上的语言要实现高阶类型需要自己做手脚,而且肯定无法满足CTS要求。
    (generics另一种叫法是parametric type,“参数化类型”。类型的“参数”就是这个意思。)

  77. 老赵
    admin
    链接

    老赵 2009-08-21 14:01:00

    ravenex:
    新的F#倾向于使用list<A>的写法而不是'a list的写法了,但我还是习惯跟随ML原本的形式来写。


    这个“倾向”是哪里来的啊?我也习惯'a list。

  78. 老赵
    admin
    链接

    老赵 2009-08-21 14:01:00

    @ravenex
    没理解你的意思,F#也是.NET上的语言,不也实现得很灵活吗?
    为什么说C#做不到呢?我觉得就是语言设计者是否希望去做罢了。

  79. ravenex[未注册用户]
    *.*.*.*
    链接

    ravenex[未注册用户] 2009-08-21 14:15:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    @ravenex
    没理解你的意思,F#也是.NET上的语言,不也实现得很灵活吗?
    为什么说C#做不到呢?我觉得就是语言设计者是否希望去做罢了。


    OCaml就不做“值限制”。F#的实现向.NET的类型系统做了不少妥协。另外在F#里也无法使用高阶类型,同样是受到.NET类型系统的限制。可以work-around,但不优雅,也不方便与其它.NET语言互操作,所以Don Syme也在纠结。
    F#的类型推导、模式匹配等功能对F#自己的内建类型工作得很好,但对一般.NET类型就没辙。

    Jeffrey Zhao:

    ravenex:
    新的F#倾向于使用list<A>的写法而不是'a list的写法了,但我还是习惯跟随ML原本的形式来写。


    这个“倾向”是哪里来的啊?我也习惯'a list。


    老赵有没有试用Visual Studio 2010 Beta 1里的F#?或者F# 1.9.6.16?它明确提到要向.NET习惯靠拢,把API都修正为list<A>这种写法了,一些内建函数的名字也变了,例如fold_right变成了foldBack。

  80. 老赵
    admin
    链接

    老赵 2009-08-21 14:19:00

    @ravenex
    能不能再谈一下“高阶类型”是指什么呢?
    其实我觉得每个语言都有自己的命名方式,.NET本来就是个平台而不是为特定语言服务的,为什么要让F#向C#等靠拢。这点不咋接受。

  81. ravenex[未注册用户]
    *.*.*.*
    链接

    ravenex[未注册用户] 2009-08-21 14:47:00

    @Jeffrey Zhao

    Jeffrey Zhao:
    @ravenex
    能不能再谈一下“高阶类型”是指什么呢?
    其实我觉得每个语言都有自己的命名方式,.NET本来就是个平台而不是为特定语言服务的,为什么要让F#向C#等靠拢。这点不咋接受。


    每种语言都有自己的命名方式,连VB.NET都跟C#不同,我也很不接受F#这点上的“改进”。但Don Syme他们应该更多是从一般用户接受度来考虑吧,毕竟C#先入的程序员多,OCaml先入再转向.NET的程序员少。

    关于高阶类型,怎么说呢,类型也应该是值,可以构造、组合之类。我也说不清楚,还是用higher-order type来搜一下比较好。要说例子的话,Haskell的类型系统就比.NET的有更好的表达力。

  82. 芭蕉
    *.*.*.*
    链接

    芭蕉 2009-08-21 14:59:00

    @Jeffrey Zhao
    关于靠扰一说,F#以前只是微软的研究人员搞着玩玩的,语法基本和OCaml兼容,现在要工业化了,向它自己.net的语法去靠,也很正常吧

  83. 老赵
    admin
    链接

    老赵 2009-08-21 15:03:00

    @芭蕉
    我不太能够接受你的看法,有点“根据事实来猜测原因”的意思。
    .net本就是平台,不是特定语言的,例如已经实现了Python和Ruby,都有自己的命名方式。
    而且在JVM上,Scala,Groovy的命名方式也没见得要像Java靠拢啊。

  84. 芭蕉
    *.*.*.*
    链接

    芭蕉 2009-08-21 15:08:00

    @Jeffrey Zhao
    好吧,你又不接受我的看法 :)
    但F#真的变化很大很快,我想很大一部分原因是跟community交流后做的改进吧

  85. 老赵
    admin
    链接

    老赵 2009-08-21 15:09:00

    ravenex:
    关于高阶类型,怎么说呢,类型也应该是值,可以构造、组合之类。我也说不清楚,还是用higher-order type来搜一下比较好。要说例子的话,Haskell的类型系统就比.NET的有更好的表达力。


    看来我再去看看Haskell比较好,我觉得F#的类型系统也不差于Haskell啊。

  86. 老赵
    admin
    链接

    老赵 2009-08-21 15:11:00

    @芭蕉
    呵呵。:)
    我真不喜欢这么做,语言就要有语言自己的风格么。
    还好,命名方式这并不是语言的关键之处,这么做就这么做吧,呵呵。

  87. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-21 18:16:00

    ravenex:
    @Ivony...
    如果T所在的上下文中有泛型声明,则T在该上下文中是一个“确定类型”,就可以写出泛型lambda:

    using System;
    
    public class Test {
        public static void Main(string[] args) {
            var omega = Omega<int>.Instance;
        }
        
        public class Omega<T> {
            public static readonly Func<Func<Delegate, T>, T> Instance = f => f(f);
        }
    }
    




    太感谢了,刚想找个人来测试这个。

    这样就可以确定是因为不存在泛型委托实例,而不是匿名方法不能是泛型了。

    其实(Delegate)对于有参数的lambda表达式而言还是类型信息不够的(我举的例子还是不恰当),但对于无参数的lambda和匿名方法类型信息是足够的,所以C#至少是不支持从表达式推断返回值类型。

  88. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-08-21 21:52:00

    @Ivony...

    Ivony...:
    因为lambda表达式也不支持直接赋值给Delegate:
    Delegate d = () => 4;
    这样的也是不合法的,但显然编译器已经没有借口说搞不清是匿名方法还是Expression了。


    C# 3.0规范的15.1小节说:

    A delegate type is a class type that is derived from System.Delegate.


    就像System.ValueType是所有值类型的基类但其自身不是值类型,System.Delegate也不是委托类型。C# 3.0规范的7.14小节说:

    An anonymous function does not have a value in and of itself, but is convertible to a compatible delegate or expression tree type. The evaluation of an anonymous function conversion depends on the target type of the conversion: If it is a delegate type, the conversion evaluates to a delegate value referencing the method which the anonymous function defines. If it is an expression tree type, the conversion evaluates to an expression tree which represents the structure of the method as an object structure.


    也就是说lambda自己没有值,但是可以转换为匹配的委托类型或表达式树类型。既然System.Delegate不是委托类型,那……

    反正根据规范,Delegate d = () => 4;确实是不合法。不过我们“一眼”确实能看出它要合法的话唯一的可能性就是把lambda看作委托。又一个C#不肯“做到尽”的例子 ^ ^

  89. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-08-22 11:34:00

    高阶类型可能指的是这样的语法吧:

    public class HighOrderType<T> : T
    {

    }

    乃至于:
    public class HighOrderType<T> : interface( T )
    public class HighOrderType<T> : base( T )

    public base( T ) GetBaseInstance<T>( T instance )

    public class HighOrderType<T1,T2> : base ( T1, T2 )//共同基类

  90. 法巫师
    *.*.*.*
    链接

    法巫师 2009-08-30 23:31:00

    //一个简化的反证的例子
    class MyClass
    {
    static T FunA<T>(){return default(T);}
    static void FunB<T>(T t){}
    static void Main(string[] args)
    {
    FunB<int>(FunA());//它无法仅从约束或返回值推断类型参数。
    }
    }

    不用C#3.0规范,这里己经有很好的说明了:
    http://msdn.microsoft.com/zh-cn/library/twcad0zb%28VS.80%29.aspx
    //编译器能够根据传入的方法参数推断类型参数;
    //它无法仅从约束或返回值推断类型参数。
    //因此,类型推断不适用于没有参数的方法。
    //类型推断在编译时、编译器尝试解析任何重载方法签名之前进行。
    //编译器向共享相同名称的所有泛型方法应用类型推断逻辑。
    //在重载解析步骤中,编译器仅包括类型推断取得成功的那些泛型方法。

    另:C#3.0的规范己经有了:
    http://msdn.microsoft.com/en-us/vcsharp/aa336745.aspx

  91. 微夜风
    *.*.*.*
    链接

    微夜风 2010-03-26 15:33:00

    如果委托为泛型,调用时使用的话,似乎无法为其指定类型,只能依赖于编译器的自动推断了。
    比如
    int[] nums={1,2,3,4,5};
    string str=String.Join(",",Array.ConvertAll<int,string>(nums,delegate(int p){return p.ToString();}));
    像这样的一个方法,如果使用这种最简单的写法的话,泛型委托Converter只能由编译器来推断其类型而无法指定。
    当然可以写成这样new Converter<int,string>(delegate (int p){return p.ToString();})
    这样可以为其强行指定泛型的类型。
    但是为什么直接使用匿名方法的时候不允许指定泛型的类型呢

  92. 百晓生
    61.148.75.*
    链接

    百晓生 2014-07-30 12:07:59

    老赵09年就发现这个问题,我是到最近才遇到,不知道老赵有没有其他的解决方法啊。 遇到的问题如下:

    class AA { }
    class BB : AA { }
    
    class A {
        public void Method<T>(string t, Action<T> fn) where T : AA {
            // fn
        }
    }
    
    class B {
        public void Fn(BB b) {
        }
    }
    
    A a = new A();
    B b = new B();
    a.Method(b.Fn);
    

    提示错误: error CS0411: The type arguments for method `A.Method(string, System.Action)' cannot be inferred from the usage. Try specifying the type arguments explicitly

    很明显,Fn的类型BB是AA的子类,但是编译器就是推导不出来……

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我