C#的类型推断发展史

前言:随着C#的版本升级,C#编译器的类型推断功能也在不断的升级以适应语言进化过程当中的变化,并为这个过程作了相应的优化。算法

隐式类型的数组

在C#1和C#2中,做为变量声明和初始化的一部分,初始化数组的语句是至关整洁的。若是想在其余地方声明并初始化数组,就必须指定数组类型。例如,下面的语句编译起来没有任何问题:数组

string[] names = {"pangjainxin", "zhengyanan"};

但这种写法不适用于参数,假定要调用MyMethod方法,该方法被声明为void MyMethod(string[] names),那么如下代码是没法编译的:app

MyMethod({"pangjianxin","zhengyanan"});//报错:未提供与方法须要的形参所对应的实参

相反,你必需要告诉编译器你想要初始化的数组是什么类型:asp.net

MyMethod(new string[]{"pangjianxin","zhengyanan"});

而C#3容许这二者之间的一种写法:async

MyMethod(new[]{"pangjianxin","zhengyanan"});

显然,编译器必须本身判断要使用什么类型的数组。它首先构造一个集合,其中包括大括号内全部表达式的 编译时类型。在这个类型的集合中,若是其余全部类型都能隐式转换为其中一种类型,该类型即为数组的类型。不然(或者全部值都是无类型的表达式,好比不变的null值或者匿名方法,并且不存在强制类型转换), 代码就没法编译。函数

注意,只有表达式的类型才会成为一个候选的数组类型(也就是说编译器不会为你作类型转换)。这意味着你偶尔须要将一个值显式转型为一个不太具体的类型。例如,如下代码没法编译:优化

new[]{new MemoryStream(),new StringWriter()}

不存在从MemoryStream向StringWriter的转换,反之亦然。二者都能隐式转换为object和IDisposable,但编译器只能在表达式自己产生的原始集合中过滤(找到那个最适合的)。在这种状况下,若是修改其中的一个 表达式,把它的类型变成object或IDisposable,代码就能够编译了:ui

new[]{(IDisposable)new MemoryStream(),new StringWriter()}

最终,表达式的类型为IDisposable[]。固然,代码都写成这样了,还不如考虑一下显式声明数组的类型, 就 像在C# 1和C# 2中所作的那样,从而更清楚地表达你的意图。this

从以上的过程当中来看这个特性好像有一些垃圾,没有什么用,可是该特性用在匿名类型上面的话仍是很不错的。因为这里主要描述C#的类型推断,因此关于匿名类型的东西不作太多深刻,就给一个例子来代表类型推断在匿名类型中的使用:spa

 var family = new[]
            {
                new {name = "pangjianxin", age = 30},
                new {name = "zhengyanan", age = 29},
                new {name = "pangxihe", age = 1}
            };

而匿名类型又服务于一个更大的目标:LINQ。

方法组转换

在C#1中,若是要建立一个委托实例,就必须同时指定委托类型和要执行的操做。例如,若是须要建立一个KeyPressEventHandler时,会使用以下表达式:

new KeyPressEventHandler(LogKeyEvent)//LogKeyEvent是一个方法名

做为一个独立的表达式使用时,它并不“难看”。即便在一个简单的事件订阅中使用,它也是可以接受的。可是,在做为某个较长表达式的一部分使用时,看起来就有点“难看”了。一个常见的例子是在启动一个新线程时:

Thread t=new Thread(new ThreadStart(MyMethod));

同往常同样,咱们但愿以尽可能简单的方式启动一个新线程来执行MyMethod。为此,C#2支持从方法组到一个兼容委托类型的隐式转换。方法组(methodgroup)其实就是一个方法名,它能够选择添加一个目标——换言之,和在C#1中建立委托实例使用的表达式彻底相同。(事实上,表达式当时就已经叫作“方法组”,只是那时还不支持转换。)若是方法是泛型的,方法组也能够指定类型实参,不过根据个人经验,不多会这么作。新的隐式转换容许咱们将事件订阅转换成:

button.keyPress+=LogKeyEvent;//LogKeyEvent是一个方法名

相似的,线程建立代码能够简化成:

Thread t=new Thread(MyMethod);

若是只看一行代码,原始版本和改进的版本在可读性上的差别彷佛并不大。但在代码量很大时,它们对可读性的提高就很是明显了。为了弄清楚到底发生了什么,咱们简单看看这个转换具体都作了什么。首先研究一下例子中出现的表达式LogKeyEvent和MyMethod。它们之因此被划分为方法组,是由于因为重载,可能不止一个方法适用。隐式转换会将一个方法组转换为具备兼容签名的任意委托类型。因此,假定有如下两个方法签名:

void MyMehtod();
viod MyMethod(object sender,Eventargs e);

那么在向一个ThreadStart或者一个EventHandler赋值时,均可以将MyMethod做为方法组使用:

THreadStart x=MyMehtod;
EventHandler y=MyMethod;

然而,对于自己已重载成能够获取一个ThreadStart或者一个EventHandler的方法,就不能把它(MyMethod)做为方法的参数使用——编译器会报告该转换具备歧义。一样,不能利用隐式方法组转换来转换成普通的System.Delegate类型,由于编译器不知道具体建立哪一个委托类型的实例。这确实有点不方便,但使用显式转换,仍然能够写得比在C#1中简短一些。例如:

Delegate invalid=SomeMethod;
Delegate valid=(ThreadStart)SomeMethod;

对于方法组的转换,大多数状况下咱们要在本身的实验过程当中得出真知,这没有什么困难的。

类型推断和重载决策的改变

类型推断和重载决策所涉及的步骤在C#3中发生了变化,以适应Lambda表达式,并使匿名方法变得更有 用。这些虽然不算是C#的新特性,但在理解编译器所作的事情方面,这些变化是至关重要的。规则之因此发生了变化,是为了使Lambda表达式可以以一种简洁的方式工做,让咱们稍微深刻地探讨一下假如C#团队坚守老的规则不变,将会遇到什么问题。

改变的原由:精简泛型方法调用

在几种状况下会进行类型推断。经过之前的讨论,咱们知道隐式类型的数组以及将方法组转换为委托类型都须要类型推断,但将方法组做为其余方法的参数进行转换时,会显得极其混乱:要调用的方法有多个重载的方法,方法组内的方法也有多个重载方法,并且还可能涉及泛型泛型方法,一大堆可能的转换会令人晕头转向。到目前为止,最多见的类型推断调用方法时不指定任何类型实参。在LINQ里面,这类事情是时刻都在发生的——查询表达式的工做方式严重依赖于此。这个过程被处理得如此顺畅,以致于很容易忽视编译器帮你作的大量工做,而这一切都是为了使你的代码更清晰,更简洁。

随着Lambda表达式的引入,C#3中的状况变得更复杂——若是用一个Lambda表达式来调用一个泛型方法,同时传递一个隐式类型的参数列表,编译器就必须先推断出你想要的是什么类型,而后才能检查Lambda表达式的主体。用实际的代码更容易说明问题。下面的代码清单列出了咱们想解决的一类问题:用Lambda表达式调用一个泛型方法。

static void PrintSomeValue<TInput, TOutput>(TInput input, Converter<TInput,TOutput> convert)
        {
            Console.WriteLine(convert(input));
        }
....
PrintSomeValue("i am a string",x=>x.Length);

PrintSomeValue方法直接获取一个输入的值和委托,将该值转换成不一样类型的委托。它未就类型参数TInput和TOutput做出任何假设(没有定义类型参数约束),所以彻底是通用的。如今,让咱们研究一下在本例中最后一行调用该方法时实参的类型究竟是什么。第1个实参明显是字符串,但第2个呢?它是一个Lambda表达式,因此须要把它转换成一个Converter<TInput,TOutput>——而那意味着要知道TInput和TOutput的类型。

C#2的类型推断规则时单独针对每个实参来进行的,从一个实参推断出的类型没法直接用于另外一个实参。在当前这个例子中,这些规则会妨碍咱们为第2个实参推断出TInput和TOutput的类型。因此,若是仍是沿用C#2的规则,代码清单9-11的代码就会编译失败。本节的最终目标就是让你明白是什么使上述代码清单在C#3中成功经过编译,但让咱们先从一些难度适中的内容入手。

推断匿名函数的返回类型

下面的代码清单展现了貌似能编译,可是不符合C#2类型推断规则的示例代码。

 delegate T MyFunc<T>();

 static void WriteResult<T>(MyFunc<T> function)
        {
            Console.WriteLine(function());
        }
.........
WriteResult(delegate{return 5});

这段代码在C#2中会报错:

error CS04011:The type argument for method ..... can not be inferred from the usage.try specifying the type arguments explicitly.

能够采起两种方式修正这个错误:要么显式指定类型实参(就像编译器推荐的那样),要么将匿名方法强制转换为一个具体的委托类型:

WriteResult<int>(delegate{return 5;});
WriteResult((MyFunc<int>)delegate {return 5;});

这两种方式均可行,但看起来都有点儿使人生厌。咱们但愿编译器能像对非委托类型所作的那样,执行相同的类型推断,也就是根据返回的表达式的类型来推断T的类型。那正是C#3为匿名方法和Lambda表达式所作的事情——但其中存在一个陷阱。虽然在许多状况下都只涉及一个return语句,但有时会有多个。

下面的代码清单是上面代码稍加修改的一个版本,其中匿名方法有时返回int,有时返回object:

..........
WriteResult(delegate
            {
                if (DateTime.Now.Hour < 10) return 5;
                else return new object();
            });

在这种状况下,编译器采用和处理隐式类型的数组时相同的逻辑来肯定返回类型,详情可参见上面。它构造一个集合,其中包含了来自匿名函数主体中的return语句的全部类型1(本例是int和object),并检查是否集合中的全部类型都能隐式转换成其中的一个类型。int到object存在一个隐式转换(经过装箱),但object到int就不存在了。因此,object被推断为返回类型。若是没有找到符合条件的类型,或者找到了多个,就没法推断出返回类型,编译器会报错。

咱们如今知道了怎样肯定匿名函数的返回类型,可是,参数类型能够隐式定义的lambda表达式又如何呢?

分两个阶段进行的类型推断

C#3中的类型推断的细节与C#2中相比,要复杂的多,你能够参考C#语言规范中要求的那样,一步一步的来,在这里,咱们要采起一种较为简单明了的方式来思考类型推断--效果和你粗读一遍规范差很少。但这种方式更容易理解。而若是编译器不能彻底按照你的意愿进行推断,最后也只是会生成一个错误的提示,而不会编译成错误的结果(程序),因此没什么大不了的。

第一个巨大的改变是全部方法实参在C#3中是一个“团队”总体。在C#2中,每一个方法实参都被单独用于尝试肯定一些类型参数。针对一个特定的类型参数,若是根据两个方法实参推断出不一样的结果,编译器就会报错——即便推断结果是兼容的。但在C#3中,实参可提供一些信息——被强制隐式转换为具体类型参数的最终固定变量的类型。用于推断固定值(下面会提到的一个术语)所采用的逻辑与推断返回类型和隐式类型的数组是同样的。

下面展现一个例子:

 static void PrintType<T>(T first, T second)
        {
            Console.WriteLine(typeof(T));
        }
...
 PrintType(1,new object());

C#2中,上述代码虽然在语法上是有效的,但不能成功编译:类型推断会失败,由于从第一个实参判断出T应该是int,第二个判断出T确定是object,两个就冲突了。可是在C#3中,编译器的推断过程已经更加全面,推断返回类型时所采用的规则就是其中一个具备表明性的例子。

第二个改变在于,类型推断如今是分两个阶段进行的。第一个阶段处理的是“普通”的实参,其类型是一开始便知道的。这包括那些参数列表是显式类型的匿名函数。

稍后进行的第二个阶段是推断隐式类型的Lambda表达式和方法组的类型。其思想是,根据咱们迄今为止拼凑起来的信息,判断是否足够推断出Lambda表达式(或方法组)的参数类型。若是能,编译器就能够检查Lambda表达式的主体并推断返回类型——这个返回类型一般能帮助咱们肯定当前正在推断的另外一个类型参数。若是第二个阶段提供了更多的信息,就重复执行上述过程,直到咱们用光了全部线索,或者最终推断出涉及的全部类型参数。

下面的流程图展现了这一过程,不过这只是该算法简化后的版本。

下面用两个例子来展现这个过程。下面使用了上面展现出来的一段代码:

static void PrintSomeValue<TInput, TOutput>(TInput input, Converter<TInput,TOutput> convert)
        {
            Console.WriteLine(convert(input));
        }
....
PrintSomeValue("i am a string",x=>x.Length);

上述代码清单须要推断的类型参数是TInput和TOutput。具体步骤以下。

一、阶段1开始。

二、第1个参数是TInput类型,第1个实参是string类型。咱们推断出确定存在从string到TInput的隐式转换。

三、第2个参数是Converter<TInput,TOutput>类型,第2个实参是一个隐式类型的Lambda表达式。此时不执行任何推断,由于咱们没有掌握足够的信息。

四、阶段2开始。

五、TInput不依赖任何非固定的类型参数,因此它被肯定为string。

六、第2个实参如今有一个固定的输入类型,但有一个非固定的输出类型。咱们可把它视为(stringx)=>x.Length,并推断出其返回类型是int。所以,从int到TOutput一定会发生一个隐式转换。

七、重复“阶段2”。

八、TOutput不依赖任何非固定的类型参数,因此它被肯定为int。

九、如今没有非固定的类型参数了,推断成功。

下一个例子更好的展现了重复阶段2的重要性。他执行了两个转换,第一个输出成为第二个的输入。在推断出第一个转换的输出类型以前,咱们不知道第二个的输入类型,因此也不能推断出它的输出类型。

 public static void ConvertTwice<TInput,TMiddle,TOutput>(TInput input ,
            Converter<TInput,TMiddle> firstConverter,
            Converter<TMiddle,TOutput> secondConverter)
        {
            TMiddle middle = firstConverter(input);
            TOutput output = secondConverter(middle);
            Console.WriteLine(output);
        }
.............
ConvertTwice("another string",text=>text.Length,length=>Math.Sqrt(length));

要注意的第一件事是方法签名看起来至关恐怖,但当你再也不惧怕,并仔细观察它时,发现它也没那么恐怖——固然示范用法使它看上去更直观。咱们获取一个字符串,对它执行一次转换:这个转换和以前的转换是相同的,只是一次长度计算。而后,咱们获取长度(int),并计算它的平方根(double)。类型推断的“阶段1”告诉编译器确定存在从string到TInput的一个转换。第一次执行“阶段2”时,TInput固定为string,咱们推断确定存在从int到TMiddle的一个转换。第二次执行“阶段2”时,TMiddle固定为int,咱们推断确定存在从double到TOutput的一个转换。第三次执行“阶段2”时,TOutput固定为doluble,类型推断成功。当类型推断结束后,编译器就能够正确地理解Lambda表达式中的代码。

说明 检查lambda表达式的主体 Lambda表达式的主体只有在输入参数的类型已知以后才能进行检查。若是x是一个数组或者字符串,那么Lambada表达式x=>x.Length就是有效的,但在其余许多状况下它是无效的。当参数类型是显式声明的时候,这并非一个问题,但对于一个隐式(类型)参数列表,编译器就必须等待,直到他执行了相应的类型推断以后,才能尝试去理解lambda表达式的含义。

这些例子每次只展现了一个改变①——在实际应用中,围绕不一样的类型变量可能产生多个方面的信息,这些信息多是在不一样的重复阶段发现的。为了不你(和我)绞尽脑汁,我决定再也不展现任何更复杂的例子了——你只需理解常规的机制就能够了,即便确切的细节可能仍然是模模糊糊的也没关系。

①所说的“改变”是指本节所描述的C#3与C#2相比,在类型推断上的两个改变,一个改变是方法实参协同肯定最后的类型实参,另外一个改变是类型推断如今分两个阶段进行。但这些改变并非孤立的,而是相互联系,共同发挥做用的。可是,做者前面的例子并无反映出这一点,他的每一个例子只是展现了其中的一个改变

虽然这种状况表面上很是罕见,彷佛不值得为其设立如此复杂的规则,但它在C#3中实际是很是广泛的,尤为是对LINQ而言。事实上,在C#3中,你能够在不用思考的状况下大量地使用类型推断——它会成为你的一种习惯。然而,若是推断失败,你就会奇怪为何。届时,你能够从新参考这里的内容以及语言规范。

还有一个改变须要讨论,但听到下面的话,你会很高兴,这个改变比类型推断简单:方法重载。

选择正确的被重载的方法

若是多个方法的名字相同但签名不一样,就会发生重载。有时,具体该用哪一个方法是显而易见的,由于只有它的参数数量是正确的,或者只有用它,全部实参才能转换成对应的参数类型。可是,假如多个方法看起来都合适,就比较麻烦了。7.5.3节规范中的具体规则至关复杂--但关键在于每一个实参类型转换成参数类型的方式。例如,假定有如下方法签名,彷佛他们都是在同一个类中声明的:

void Write(int x);
void Write(double y);

Write(1.5)的含义显而易见,由于不存在从double到int的隐式转换,但Write(1)对应的调用就麻烦一些。因为存在从int到double的隐式转换,因此以上两个方法彷佛都合适。在这种状况下,编译器会考虑从int到int的转换,以及从int到double的转换。从任何类型“转换成它自己”被认为好于“转换成一个不一样的类型”。这个规则称为“更好的转换”规则。因此对于这种特殊的调用,Write(intx)方法被认为好于Write(doubley)。

若是方法有多个参数,编译器须要确保存在最适合的方法。若是一个方法所涉及的全部实参转换都至少与其余方法中相应的转换“同样好”,而且至少有一个转换严格优于其余方法,咱们就认为这个方法要比其余方法好。

现给出一个简单的例子,假定如今有如下两个方法签名:

void Write(int x,double y);
void Write(double x,int y);

对Write(1,1)的调用会产生歧义,编译器会强迫你至少为其中的一个参数添增强制类型转换,以明确你想调用的是哪一个方法。每一个重载都有一个更好的实参转换,所以都不是最好的。

一样的逻辑在C#3中仍然适用,但额外添加了与匿名函数(lambda表达式和匿名方法的统称)有关的一个规则(匿名函数永远不会指定一个返回类型)。在这种状况下,推断的返回类型在“更好的转换”规则中使用。

下面来看一个须要新规则的例子。下列代码包含两个名为Execute的方法,另外还有一个使用了Lambda表达式的调用。

 static void Execute(Func<int> action)
        {
            Console.WriteLine($"action result is an int {action()}");
        }

        static void Execute(Func<double> action)
        {
            Console.WriteLine($"action result is a double {action()}");
        }
......
Execute(()=>1);

对Execute方法的调用能够换用一个匿名方法来写,也能够换用一个方法组----无论以什么方式,凡是涉及转换,所应用的规则都是同样的。那么,最后会调用哪一个Execute方法呢?重载规则指出,在执行了对实参的转换以后,若是发现两个方法都合适,就对那些实参转换进行检查,看哪一个转换“更好”。这里的转换并非从一个普通的.NET类型到参数类型,而是从一个Lambda表达式到两个不一样的委托类型。那么,哪一个转换“更好”?

使人吃惊的是,一样的状况若是在C#2中发生,那么会致使一个编译错误——由于没有针对这种状况的语言规则。但在C#3中,最后会选中参数为Func<int>的方法。额外添加的规则能够表述以下:若是一个匿名函数能转换成参数列表相同,但返回类型不一样的两个委托类型,就根据从“推断的返回类型”到“委托的返回类型”的转换来断定哪一个委托转换“更好”。

若是不拿一个例子来做为参考,这段话会绕得你头晕。让咱们回头研究一下代码清单:如今是从一个无参数、推断返回类型为int的Lambda表达式转换成Func<int>或Func<double>。两个委托类型的参数列表是相同的(空),因此上述规则是适用的。而后,咱们只需判断哪一个转换“更好”就能够了:int到int,仍是int到double。这样就回到了咱们熟悉的问题上——如前所述,int到int的转换更好。所以,代码清单会在屏幕上显示:action result is an int:1。

类型推断和重载决策

这一节说了那么多可是我感受仍是没有很明白的讲清楚,由于这是一个很庞大的主体,它自己就是复杂的。总结一下这一小节的内容吧:

  • 匿名函数(匿名方法和Lambda表达式)的返回类型是根据全部return语句的类型来推断的;
  • Lambda表达式要想被编译器理解,全部参数的类型必须为已知;
  • 类型推断不要求根据不一样的(方法)实参推断出的类型参数的类型彻底一致,只要推断出来的结果是兼容的就好;
  • 类型推断如今分阶段进行,为一个匿名函数推断的返回类型可做为另外一个匿名函数的参数类型使用;
  • 涉及匿名函数时,为了找出“最好”的重载方法,要将推断的返回类型考虑在内。

asp.net core中的管道注册

上面说了这么多,不把他用于实践中也是不划算的。在asp.net core中的管道注册使用了RequestDelegate这个委托来表示asp.net core中的抽象,可是这个管道拼接的过程又是由一个Func<RequestDelegate,RequestDelegate>来表示的。让咱们深究一下。咱们先来看一段注册的例子:

代码1:

 app.Use(async (context, next) =>
            {
                if (context.Request.Path == "/foo")
                {
                    await context.Response.WriteAsync("foo");
                }
                else
                {
                    await next();
                }
            });

上面代码就是一个使用Use注册asp.net core管道的例子,他接受的参数为一个Func<HttpContext,Func<Task>,Task>的委托。在这个方法的内部它是调用另外一个Use方法来实现的:

代码2:

 public static IApplicationBuilder Use(this IApplicationBuilder app, Func<HttpContext, Func<Task>, Task> middleware)
    {
      return app.Use((Func<RequestDelegate, RequestDelegate>) (next => (RequestDelegate) (context =>
      {
        Func<Task> func = (Func<Task>) (() => next(context));
        return middleware(context, func);
      })));
    }

代码1中Use方法实现如代码2所示,在调用代码1中的Use方法时,传入了一个表明Func<HttpContext,Func<Task>,Task>委托的middleware变量,而在代码2表示的Use方法内部,又调用了另外一个Use方法,这个Use方法接收一个Func<RequestDelegate, RequestDelegate> 委托类型的参数,并最终将这个委托添加到IApplicationBuilder实现类的一个列表上去。代码2中的context是根据Use方法传入的Func<RequestDeleagte,RequestDelegate>类型的第二个类型参数推断出来的。

相关文章
相关标签/搜索