类型推断

来源:互联网 发布:多益网络总部地址 编辑:程序博客网 时间:2024/04/30 16:50

By Bill Wagner

May 2012

序言

本文与下面这行无法编译的代码有关:

var lambda = x => x.M();

编译器给出的错误是:CS0815: “Cannot assign lambda expression to implicitly typed local variable(无法将Lambda表达式赋给一个隐性类型局部变量).”

本文中,我会解释控制对隐式类型局部变量进行类型推断的规则,并探讨c#语言不支持上面写法的原因。

 What doesn’t work

考虑下逻辑上哪些是编译器必须要做的,就会发现不允许将一个Lambda表达式赋值给一个隐式类型局部变量是有原因的。隐式类型局部变量(使用var关键字声明的局部变量)通过初始化表达式来推断他们自身类型。匿名函数(可能写成lambda表达式或是匿名方法表达式)没有类型,但赋值时会被转换为兼容的委托或是表达式树类型。这意味着之前的代码将告诉编译器用右手边的表达式类型来推断左手边表达式的类型,同时用左手边表达式的类型去推断右手边表达式的类型。C#编译器擅于领会代码意图,但是这种循环逻辑需要人类的思维。

我会对这个简短的答案进行详细阐述,以便于你对有关特性进一步的了解。

var声明的变量的类型推断规则

多数开发人员对隐式类型变量还是熟悉的,所以此处我只做简略介绍。用关键字Var声明的变量,说明该变量的类型就是其右侧初始化表达式的静态类型。此处有一个容易被开发人员忽略的重要概念。隐式类型变量是基于初始化器的静态类型的,而不是运行时类型。看一下下面的代码片段:

public class B { }
public class D : B { }
static B FactoryForD()
{
    return new D();
}
 
var v = FactoryForD();

上面代码中的变量v它的静态类型是B而不是D.编译器会使用FactoryForD方法的返回值静态类型,并将该类型作为v声明的类型。

C#语言规格说明书中的章节8.5.1针对可以使用隐式类型的声明限制进行了描述。与lambda表达式有关的描述是:初始化器表达式必须具有编译时类型。最简单的例子就是你不能用null关键字初始化一个隐式类型变量(即:var x = null;)。因为空(null)没有类型。但可以用一个缺省表达式(default)来初始化一个隐式类型变量,因为缺省表达式是有类型的(如:var x= default(string);)

null的使用限制更清楚的说明了不能在初始化器中使用lambda表达式的原因。并且两个问题解决的方式也一样:强制初始化器具有一个已知的静态类型。缺省表达式(default)是强类型的,因此在需要确定声明变量类型时,编译器可以从缺省表达式处获得类型信息。对Lambda表达式的修改要多过将一个null简单的修改为一个缺省表达式(default(静态类型))

匿名函数与推论

Lambda表达式是符合匿名函数的一种形式。通常在保持兼容性的前提下比匿名方法表达式要简洁一些。C#针对匿名函数的规则中有两个非常重要的目标需要满足。首先匿名函数应该可以用做兼容的委托或是表达式树类型。其次,语法应该尽可能的简单。

Lambda表达式凭借其简洁的语法证实了自己满足第二个目标。开发人员如此频繁的使用Lambda表达式皆因其语法简洁且易于阅读。Lambda表达式能够出现的部分原因是想提供更加简洁的语法,特别是对于LINQ查询的使用非常有益。下面两种语法定义了等价的匿名函数:

Func<int, int> lambda = x => x + 1;
Func<int, int> anonMethod = delegate(int x) { return x + 1; };

 

然而,上面两个表达式右半边的定义还是有着微妙的差别的。注意一下我在前面提到的:匿名方法可以被转换为任意一种委托(像上面展示的一样)或是表达式。Lambda表达式能够转换为表达式:

Expression<Func<int, int>> lambda = x => x + 1;

但匿名方法表达式不转换为表达式树:

// does not compile:

//无法编译:

Expression<Func<int, int>> anonMethod = delegate(int x) { return x + 1; };

 

而这点正是匿名方法与lambda表达式之间的显著区别。因为这些规则你可能碰到一些相当令人惊讶的行为。看看下面的代码:

public class B { }
public class D : B { }
 
 
public static void M(Func<B> f) { }
public static void M(Expression<Func<D>> f) { }
 
 
M(() => new D());
M(() => new B());
M(delegate { return new D(); });
M(() => { return new D(); });

 

第一行M方法调用使用了M方法的第二个过载,也就是输入参数为一个表达式(Expression<Func<D>>)的过载。原因是lambda表达式更匹配该方法的形参(参数是一个返回类D实例的表达式)。第二行M方法调用使用了M方法的第一个过载。因为它传递了一个返回类B实例的表达式。第三行M方法调用也使用了过载一。’delegate’关键字声明了一个代表一个委托的匿名方法,而不是一个表达式树。第四行调用语句处有个问题:出现了一个编译时错误。原因是lambda表达式虽然指向第二个过载。但该lambda表达式体中包含了一行完整语句(而不是一个表达式),编译时进行类型转换时失败。按照规则确定的最匹配调用版本在编译时却是无效的。在可以用表达式来替换完整语句的情况下,可以通过将lambda表达式体换成表达式来避免该问题出现。这样更通用。

稍稍深入一下lambda表达式转换的微妙之处来说明首要目标:lambda表达式能够在任意应用委托与表达式的地方使用。该目标在语言规则中的表达是:匿名方法虽没类型但应能隐式转换为一个兼容的委托类型或是表达式树类型。

像你前面看到的Lambda表达式” x => x + 1” 可以转换为Func<int,int>或是Expression<Func<int,int>>.这只是众多可能的委托或表达式树类型中的两个普通形式,可能并不显眼。再看看这个声明:

public delegate int IntFunc(int a);

 

Lambda表达式” x => x + 1”也可以转换为上面的IntFunc或是Expression<IntFunc>。实践中该lambda表达式可以转换成任意输入参数与返回值类型均是int的委托类型。此处,语言规则不允许在不同委托类型间转换。(委托类型间隐式转换是非法的,然而不是说所有委托类型间的转换都是非法的。)意味着IntFunc不能分配给一个类型是Func<int,int>的变量,反之亦然。

当然,我们找到的“x => x + 1”可能转为的委托及表达式树类型只是形似的。其实它也可以转换为任意输入参数和返回值类型是任意数值类型(short, byte, long, float, decimal, double)。然而这还不是全部,它还可以转换为输入参数为一个支持+操作符的委托类型,比如:

public class Counter
{
    private int counter = 0;
 
    public static Counter operator +(Counter l, int r)
    {
        var rVal = new Counter();
        rVal.counter = l.counter + r;
        return rVal;
    }
}

现在的“x => x + 1”已经可以转换为Func<Counter, Counter>Expression<Func<Counter, Counter>>了。

现在回头看看为什么编译器有这样明确的规定:不能给一个隐式类型局部变量分配一个lambda表达式。你可能认为你所需要的变量类型是显而易见的。定下的语言规则需要同时保证其它目标的实现,即:lambda表达式能被转换的类型为兼容的委托或表达式树类型的最大集合。使用匿名函数是为了获取更大的灵活性,你无法将一个匿名函数分配给一个隐式类型局部变量。

本文描述的语言规则都是针对c#的。其它语言有其它的目标,所以它们制定其它的规则。Vb中允许这样的写法:

Dim lambda = Function(x) x + 1

编译器会为变量lambda生成一个包含一个int类型的输入参数和一个int类型的返回参数的委托类型。这样保持了VB程序员效率最优化的目标,即使这样做对表达式的精确性缺乏控制。而且,VB中还允许具有相同参数和返回值类型的匿名函数间转换。下面的代码在VB中合法但在C#中非法:

Sub Main()
    Dim lamdba = Function(x) x + 1
    Test(lambda)
End Sub
 
Sub Test(ByVal f As Func(Of Int16, Int16))
End Sub

F#的规则介于c#VB.NET之间。F#较之c#,允许更多隐式类型转换及需要更少的显示类型声明。但它的规则较之VB.NET要更严谨一些。

不同语言,即使是一般意图也始于不同的目标。一个设计良好的语言将会始终保证其在语言规则中定义的目标。C#中为了评估匿名函数而制定的规则都是围绕程序员生产率而设计的,但仍提供了如何将你的代码翻译成可执行指令的控制。选择了保持匿名函数能转换成委托或表达式树的最大灵活性。c#语言将潜在的不确定交给你。VB.NET语言选择自己来猜测你的意图,从中找到你要的方式,但是错失一些可能性。

 

原创粉丝点击