.NET面试题系列[12] - C# 3.0 LINQ的准备工作
"为了使LINQ能够正常工作,代码必须简化到它要求的程度。" - Jon Skeet
为了提高园子中诸位兄弟的英语水平,我将重要的术语后面配备了对应的英文。
隐式类型的局部变量
隐式类型允许你用var修饰类型。用var修饰只是编译器方便我们进行编码,类型本身仍然是强类型的,所以当编译器无法推断出类型时(例如你初始化一个变量却没有为其赋值,或赋予null,此时就无法推断它的类型),用var修饰就会发生错误。另外,只能对局部变量使用隐式类型。
使用隐式类型的几个时机:
- 当变量的类型太长或者难以推测,但类型本身不重要时,比如你的LINQ语句中用了Groupby,那么一般来说基本很少人可以准确地推测出结果的类型吧。。。
- 当变量初始化时,此时可以根据new后面的类型得知变量类型,故不会对可读性造成影响
- 在Foreach循环中你迭代的对象,此时一般不需要显式指出类型
总的来说,如果使用隐式类型导致你的代码的可读性下降了,那么就改用显式类型。一般第二条原则已经是一个不成文的规定了。Resharper在检测到变量初始化时,如果你没有使用隐式类型,也会提醒你可以用var代替之。
LINQ中隐式类型的体现:你可以统统用var来修饰LINQ语句返回的类型。一般来说LINQ语句的返回类型通常名字都比较长,而且也不是十分显而易见。如果没有隐式类型,在写代码时就会比较痛苦。
自动实现的属性
现在应该满世界都在用自动实现的属性了。注意在结构体中使用自动实现的属性(注意字段不需要),需要显式的调用无参构造函数this()。这是结构体和类的一个区别。
public struct Foo { public int a { get; private set; } Foo(int A) : this() { a = A; } }
上面代码如果去掉this()将会发生错误,在默认无参构造函数将结构体的属性设为默认值之前,不能使用这些属性。如果将上面代码的属性改为字段,则即使不调用this()也不会有问题。
匿名类型(Anonymous Type)
匿名类型允许你直接在括号中建立一个类型。虽然不需要指定成员的具体类型,但匿名类型的成员都是强类型的。
static void Main(string[] args) { var tom = new {Name = "Tom", Age = 15}; Console.WriteLine("{0}: {1}", tom.Name, tom.Age); }
对匿名类型进行初始化之后,就可以如同实际类型一样使用点符号获取匿名类型的成员,但变量tom只能用var或者object修饰。如果两个匿名类型有相同数量的成员,且所有成员拥有相同的类型名称和值的类型,而且以相同的顺序出现,则编译器会将它们看作是同一个类型。
static void Main(string[] args) { var family = new[] { new {Name = "Tom", Age = 15}, new {Name = "Jerry", Age = 16} }; var cat = new {Age = 27, Name = "Cat"}; var dog = new {Age = 2222222222222222, Name = "Dog"}; }
如果在初始化中交换了属性的顺序,或者某个属性使用了long而不是int,则会引入一个新的匿名类型。
匿名类型包含了一个默认的构造函数,它获取你赋予的所有初始值。另外,它包含了你定义的类型成员,以及继承自object类型的若干方法(重写的Equals, 重写的GetHashCode, ToString等等)。同一个匿名类型的两个实例在判断相等性时,采用的是依次比较每个成员的值的方式。
在LINQ中,我们可以使用匿名类型来装载查询返回的数据,尤其是最后使用Select或SelectMany等方法返回若干列时。在每次查询都要为返回数据定制一个类显得太繁琐了,虽然有时候是需要的(ViewModel),但也有时候只是为了一次性的展示数据。如果你要创建的类型只在一个方法中使用,而且其中只有简单的字段或者属性而没有方法,则可以考虑使用匿名类型。
表达式和表达式树(Expression & Expression Tree)
Express是表达的意思(它还有很多其他意思,例如快速的),加上名词后缀-sion即为表达式。
表达式是当今编程语言中最重要的组成成分。简单的说,表达式就是变量、数值、运算符、函数组合起来,表示一定意义的式子。例如下面这些都是(C#的)表达式:
- 3 //常数表达式
- a //变量或参数表达式
- !a //一元逻辑非表达式
- a + b //二元加法表达式
- Math.Sin(a) //方法调用(lambda)表达式
- new StringBuilder() //new 表达式
表达式的一个重要的特点是它可以无限组合,只要符合正确的类型和语义。表达式树则是将表达式转换为树形结构,其中每个节点都是表达式。表达式树通常被用于转换为其他形式的代码。例如LINQ to SQL将表达式树转译为SQL。
最基本的几种表达式
- 常量表达式:Expression.Constant(常量的值);
- 变量表达式:Expression.Parameter(typeof(变量类型), "变量名称")
- 二元表达式,即需要两个表达式作为参数进行操作的表达式:Expression.[某个二元表达式的方法,例如加减乘除,模运算等](表达式1, 表达式2);
- Lambda表达式:表达一个方法,可以接受一个代码段或一个方法调用表达式作为方法,以及一组方法参数。Lambda为一希腊字母,无法翻译。希腊字母还有很多,例如阿尔法,贝塔等。之所以选择这个字母是因为来自数学上的原因(数学上有lambda运算)
构建一个最简单的表达式树1+2+3
表达式树是对象构成的树,其中每个节点都是表达式。可以说,每个表达式都是一个表达式树,特别的,某些表达式可以看成只有一个节点的表达式树,例如常量表达式。System.Linq.Expressions命名空间下的Expression类和它的诸多子类就是这一数据结构的实现。Expression类是一个抽象类,主要包含一些静态工厂方法。Expression类也包含两个属性:
- Type:代表表达式求值之后的.net类型,例如Expression.Constant(1)和Expression.Add(Expression.Constant(1), Expression.Constant(2))的类型都是Int32。
- NodeType:代表表达式的种类。例如Expression.Constant(1)的种类是Constant,Expression.Add(Expression.Constant(1), Expression.Constant(2))的种类是Add。
每个表达式都可以表示成Expression某个子类的实例。例如BinaryExpression就表示各种二元运算符(例如加减乘除)的表达式。它需要两个运算数(注意运算数也是表达式):
public static BinaryExpression Add(Expression left, Expression right);
Expression各个子类的构造函数都是不公开的,要创建表达式树只能使用Expression类提供的静态方法。
要创建一个表达式树,首先我们要画出这个树,并找出它需要什么类型的表达式。例如如果我们要创建1 + 2 + 3这个表达式的表达式树,因为它太简单而且不包含多于一种运算(如果有加有乘还要考虑优先级),我们可以一眼看出,其只需要两种表达式,常量表达式(形容1,2,3)和二元表达式(形容加法),所以可以这样写:
ConstantExpression exp1 = Expression.Constant(1); ConstantExpression exp2 = Expression.Constant(2); BinaryExpression exp12 = Expression.Add(exp1, exp2); ConstantExpression exp3 = Expression.Constant(3); BinaryExpression exp123 = Expression.Add(exp12, exp3);
这个应该非常好理解。但如果我们想写出Math.Sin(a)这个表达式的表达式树怎么办呢?为了解决这个问题,Lambda表达式登场了,它可以表示一个方法。
使用Lambda表达式表示一个函数
我们的目标是使用Lambda表达式表示Math.Sin(a)这个表达式。Lambda表达式代表一个函数,现在它具有一个输入a(我们使用变量表达式ParameterExpression来代表,它应该是double类型),以及一个方法调用,这需要MethodCallExpression类型的表达式,方法名为Sin,位于Math类中。我们需要使用反射找出这个方法。
代码如下:
ParameterExpression expA = Expression.Parameter(typeof(double), "a"); //参数a MethodCallExpression expCall = Expression.Call(typeof(Math).GetMethod("Sin", BindingFlags.Static | BindingFlags.Public), expA); //Math.Sin(a) LambdaExpression exp = Expression.Lambda(expCall, expA); // a => Math.Sin(a)
使用Lambda表达式:通过Expression<TDelegate>
Expression<TDelegate>泛型类继承了LambdaExpression类型,它的构造函数接受一个Lambda表达式。此处TDelegate指泛型委托,它可以是Func或者Action。泛型类以静态的方式确定了返回类型和参数的类型。
对于上个例子,我们的输入和输出均为一个Double类型,故我们需要的委托类型是Func<double, double>:
Expression<Func<double, double>> exp2 = d => Math.Sin(d);
可以使用Compile方法将Expression<TDelegate>编译成TDelegate类型(在这个例子中,编译之后的对象类型为Func<double,double>),这是一个将表达式树编译为委托的简便方法(不需要再一步一步来,并且使用反射了)。编译器自动实现转换。
然后就可以直接调用,获得表达式计算的结果:
Expression<Func<double, double>> exp2 = d => Math.Sin(d); Func<double, double> func = exp2.Compile(); Console.WriteLine(func(0.5));
练习:使用两种方法构建表达式树(a, b, m, n) => m * a * a + n * b * b
假定所有的变量类型都是double。
代码法:
//(a, b, m, n) => m * a * a + n * b * b ParameterExpression expA = Expression.Parameter(typeof(double), "a"); //参数a ParameterExpression expB = Expression.Parameter(typeof(double), "b"); //参数b ParameterExpression expM = Expression.Parameter(typeof(double), "m"); //参数m ParameterExpression expN = Expression.Parameter(typeof(double), "n"); //参数n BinaryExpression multiply1 = Expression.Multiply(expM, expA); BinaryExpression multiply2 = Expression.Multiply(multiply1, expA); BinaryExpression multiply3 = Expression.Multiply(expN, expB); BinaryExpression multiply4 = Expression.Multiply(multiply3, expB); BinaryExpression add = Expression.Add(multiply2, multiply4);
委托法:
Expression<Func<double, double, double, double, double>> exp4 = (a, b, m, n) => m*a*a + n*b*b; var ret = exp4.Compile(); Console.WriteLine(ret.Invoke(1, 2, 3, 4)); // =3*1*1+4*2*2=3+16=19
通过Expression<TDelegate>以及Compile方法,我们可以方便的计算表达式的结果。但如果一步步来,我们还需要手动遍历这棵树。既然使用代码构造表达式如此麻烦,为什么还要这样做呢?只是因为在手动遍历和计算表达式结果时,可以插入其他操作。LINQ to SQL就是通过递归遍历表达式树,将LINQ语句转换为SQL查询的,这是委托所不能替代的。
不是所有的Lambda表达式都能转化成表达式树。不能将带有一个代码块的Lambda转化成表达式树。表达式中还不能有赋值操作,因为在表达式树中表示不了这种操作。
参考资料:表达式树上手指南 http://www.cnblogs.com/Ninputer/archive/2009/08/28/expression_tree1.html
扩展方法(Extension Method)
扩展方法可以理解成,为现有的类型(现有类型可以为自定义的类型和.Net 类库中的类型)扩展(添加)一些功能,附加到该类型中。
当我们要扩展某个类的功能时,有以下几种方法:一是直接修改类的代码,这可能会导致向后兼容的破坏(不符合开闭原则)。一是派生子类,但这增加了维护的工作量,而且对于结构和密封类根本不能这么做。扩展方法允许我们在不创建子类,不更改类型本身的情况下,仍然可以修改类型。
扩展方法必须定义于静态的类型中,且所有的扩展方法必须是静态的。还是那句话,当你了解了类型对象时,你就很自然的理解了为何扩展方法必须是静态的。(它自类型对象被创建时就应当在对象的方法表中)
扩展方法的第一个输入参数要加上this(第一个参数的类型表示被扩展的类型)。扩展方法必须至少要有一个输入参数。
被扩展的类型的所有子类自动获得该扩展方法。
当你的工程内有特定逻辑,且其基于一个比较普遍的类时,考虑使用扩展方法。如果你想为类型添加一些成员,但又不能更改类型本身(因为不属于你)时,考虑使用扩展方法。例如当你需要频繁判断字符串是否为Email时,你可以扩展String类,将这个判断方法单独置于一个叫做StringExtension的类型中,方便管理。之后你就可以通过调用String.IsEmail来方便的使用这个方法了。
C#中提供了两个特别醒目的类:Enumerable和Queryable。两者都在System.Linq命名空间中。在这两个类中,含有许许多多的扩展方法。Enumerable的大多数扩展的是IEnumerable<T>,Queryable的大多数扩展的是IQueryable<T>。它们赋予了集合强大的查询能力,共同构成了LINQ的重要基础。
什么是闭包(Closure)?C#如何实现一个闭包?
闭包是一种语言特性,它指的是某个函数获取到在其作用域外部的变量,并可以与之互动。Closure这个单词显然来自动词close,有点动词名词化的意思。
通过匿名函数或者lambda表达式,我们可以实现一个简单的闭包:
static void Main(string[] args) { //外部变量 var i = 0; //lambda表达式捕获外部变量 //在外部变量的作用域内声明了一个方法 MethodInvoker m = () => { //使用外部变量 i = i + 1; }; m.Invoke(); //打印出1 Console.WriteLine(i); }
此处函数和来自外部的变量i进行了互动。
匿名函数(Anonymous Function)
匿名函数出现于C# 2.0,它允许在一个委托实例的创建位置内联地指定其操作。
例如我们可以这样写:
Compare(c1, c2, delegate(Circle a, Circle b) { if (a.Radius > b.Radius) return 1; if (a.Radius < b.Radius) return -1; return 0; });
匿名方法的语法:先是一个delegate关键字,再是参数(如果有的话),随后是一个代码块,定义了对委托实例的操作。逆变性不适用于匿名方法,必须指定和委托类型完全匹配的参数类型(在本例中是两个Circle类型)。
通过在匿名方法中加入return来获得返回值。.NET 2中很少有委托有返回值(因为多个委托形成委托链之后,前面的返回值会被后面的覆盖),但LINQ中大部分委托都有返回值(通过Func泛型委托)。
使用匿名方法的主要好处是:不需要为一个函数命名,尤其是那种只用一次的函数,或者很短很简单的函数。当你了解了lambda表达式之后,就会发现在linq中,到处都是lambda表达式,而里面其实都是匿名函数(即委托)。如果我们在频繁使用linq的过程中,每次都要在外部建立一个函数,那代码的体积将会大大增加。
另外匿名函数还有很重要的一点,就是自动形成闭包。匿名函数内定义的变量称为匿名函数的局部变量,和普通函数不同的是,匿名函数除了可以使用局部变量,传入的变量之外,还可以使用捕获变量。当外部的变量被匿名函数在函数方法中使用时,称为该变量被捕获(即它成为了一个捕获变量)。
捕获的是变量的实例而不是值,也就是说,在匿名函数内的捕获变量和外部的变量是同一个。当变量被捕获时,值类型的变量自动“升级”,变成一个密封类。创建委托实例不会导致执行。
捕获变量(Captured Variable)的作用
捕获变量可以方便我们在创建匿名方法(或委托)时,获得所需要的变量。例如如果你有一个整型的列表,并希望写一个匿名方法筛选出小于某数limit的另一个列表,此时如果没有捕获变量,在匿名方法中我们就只能硬编码Limit的值,或者使用原始的委托,将变量传入委托的目标方法。
static IEnumerable<int> Filter(List<int> aList, int limit) { //lambda表达式捕获外部变量Limit return aList.Where(a => a < limit); }
捕获变量的生存期
只要还有委托引用这个捕获变量,它就会一直存在。不管这个捕获变量是值类型还是引用类型,编译器会为其生成一个额外的类。
public delegate void MethodInvoker(); static void Main(string[] args) { MethodInvoker m = CreateDelegate(); //由于有委托引用a,a将会一直存在 //捕获变量a不再位于栈上,编译器将其视为一个额外的类 //CreateDelegate方法拥有对这个额外的类的一个实例的引用 //当委托被回收之前,不会回收这个额外的类 m(); } static MethodInvoker CreateDelegate() { int a = 1; MethodInvoker m = () => { Console.WriteLine(a); a++; }; m(); return m; }
打印出1和2。输出1是因为在调用CreateDelegate时,变量a是可用的。当CreateDelegate返回之后,调用m,a仍然是可用的,并没有随之消失。由于被捕获而形成闭包,a由一个栈上的值类型变成了引用类型。编译器生成了一个额外的密封类(名字是比较没有可读性的,例如c__DisplayClass1),它拥有一个成员a和一个方法,该方法内部的代码就是MethodInvoker中的代码。
CreateDelegate持有一个类型c__DisplayClass1的引用,所以它一直都能使用c__DisplayClass1中的成员a。
internal class Program { public delegate void MethodInvoker(); [CompilerGenerated] private sealed class <>c__DisplayClass1 { public int a; public void <CreateDelegate>b__0() { Console.WriteLine(this.a); this.a++; } } private static void Main(string[] args) { Program.MethodInvoker methodInvoker = Program.CreateDelegate(); methodInvoker(); Console.ReadKey(); } private static Program.MethodInvoker CreateDelegate() { Program.<>c__DisplayClass1 <>c__DisplayClass = new Program.<>c__DisplayClass1(); <>c__DisplayClass.a = 1; Program.MethodInvoker methodInvoker = new Program.MethodInvoker(<>c__DisplayClass.<CreateDelegate>b__0); methodInvoker(); return methodInvoker; } }
面试题:共享和非共享的捕获变量
在闭包和for循环一起使用时,如果多个委托捕捉到了同一个变量,则会有两种情况:捕捉到了同一个变量仅有的一个实例,和捕捉到同一个变量,但每个委托拥有自己的一个实例。
static void Main() { int copy; List<Action> actions = new List<Action>(); for (int counter = 0; counter < 10; counter++) { //只有一个变量copy,它在循环开始之前已经创建 //所有的委托共享这个变量 copy = counter; //创建委托时不会执行 actions.Add(() => Console.WriteLine(copy)); } foreach (Action action in actions) { //执行委托时打印copy当前的值 //copy当前的值是9 action(); } Console.ReadKey(); }
在这个例子中,捕获变量是copy,它只有一个实例(它的定义在外面,被捕获之后,自动升级为引用类型),所有委托共享这个实例。最后打印出10个9。
static void Main() { int copy; List<Action> actions = new List<Action>(); for (int counter = 0; counter < 10; counter++) { copy = counter; //现在有十个内部变量,每个委托有一个实例,不同委托拥有的实例值是不同的 //从而委托可以输出0-9 int copy1 = copy; //创建委托时不会执行 actions.Add(() => Console.WriteLine(copy1)); } foreach (Action action in actions) { //执行委托时打印copy1的值 action(); } Console.ReadKey(); }
使用内部变量解决多个委托共享一个捕获变量实例的问题。下面的代码中,包含了上面所说的两种情况,可以思考下最终的打印结果:
static void Main(string[] args) { var list = new List<MethodInvoker>(); for (int index = 0; index < 5; index++) { var counter = index*10; list.Add(delegate { Console.WriteLine("{0}, {1}", counter, index); counter++; }); } list[0](); list[1](); list[2](); list[3](); list[4](); list[0](); list[0](); list[0](); Console.ReadKey(); }
其中循环内部建立了五个MethodInvoker。它们共享一个变量index的实例,但各自有自己的变量counter的实例。所以最终打印的结果中,index的值将总是5,而counter的值则每次都不同。
最后额外执行了第一个委托三次,此时counter的值会使用第一次,第一个委托运行之后counter的值,故会打出1,之后打印2,3同理。如果你额外执行第二个委托一次,将会打出11。这充分说明了每个委托都持有一个counter的实例,且它们是相互独立的。而无论执行任意一个委托多少次,index的值都是5。
foreach循环中捕获变量的变化
在C# 5中,foreach循环的行为变了,不会再出现多个委托共享一个变量的行为。所以我们即使不声明内部变量,方法也会打印出令人容易理解的结果:
static void Main() { List<string> values = new List<string> {"a", "b", "c"}; var actions = new List<Action>(); foreach (string s in values) { //匿名方法捕获变量s //类比for循环最后的10个9,s最后的值是c //理论上会打印出三个c //但在c# 5中,会打印出a,b,c actions.Add(() => Console.WriteLine(s)); } foreach (Action action in actions) { action(); } Console.ReadKey(); }
但对于for语句,行为和之前一样,仍然需要注意捕获变量被共享的问题。