For,do… while,while ,foreach是大多数编程语言中常用的循环控制语句,在C#中查询表达式也能实现同样的效果。
查询表达式使得编程风格从”命令式”变得更加的”声明式”。查询表达式定义想要的结果以及要达到该结果需要执行的方法,而不是具体的如何实现。这里重点是查询表达式,通过扩展方法,查询表达式使得能够比命令式循环构造更能够清楚的表达意图。
下面的语句显示了一个命令式风格的填充一个数组并打印到控制台上:
即使实现这么一小点功能,依然注重的是如何实现而不是怎样实现,将这段代码改为查询表达式能够使得代码更易读和使用:int[] foo = new int[100]; for (int num = 0; num < foo.Length; num++) { foo[num] = num * num; } foreach (int i in foo) { Console.WriteLine(i.ToString()); }
第二个循环可以利用扩展方法改写为:int[] foo = (from n in Enumerable.Range(0, 100) select n * n).ToArray();
foo.ForAll(n => Console.WriteLine(n));
或者更为简洁的:
foo.ForAll(Console.WriteLine);
在.NET BCL中ForEach扩展方法有针对List<T>的实现方法。我们可以添加一个针对IEnumerable<T>的ForAll扩展方法实现如下:
public static class Extensions { public static void ForAll<T>(this IEnumerable<T> sequence, Action<T> action) { foreach (T item in sequence) { action(item); } } }
可能看起来不起眼,只不过是一个扩展方法而已,但是这能够使得代码能够更多的重用,任何时候要对一个序列元素执行操作,ForAll就能够派上用场。
以上问题可能看上去比较简单,不足以看出使用查询语法所带来的好处,下面来看个复杂一点的。
很多操作需要嵌套的循环操作,假设需要产生一个从0到99的(X,Y)对,通常的做法是:
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices() { for (int x = 0; x < 100; x++) { for (int y = 0; y < 100; y++) { yield return Tuple.Create(x, y); } } }
或者改为查询表达式:
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices() { return from x in Enumerable.Range(1, 100) from y in Enumerable.Range(1, 100) select Tuple.Create(x, y); }
两者看起来差不多,但是随着问题的复杂,查询表达式仍然能够保持简洁。现在将问题改为:只产生x和y相加小于100的点对,比较两者的实现:
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices2() { for (int x = 0; x < 100; x++) { for (int y = 0; y < 100; y++) { if (x+y<100) yield return Tuple.Create(x, y); } } } public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices2() { return from x in Enumerable.Range(1, 100) from y in Enumerable.Range(1, 100) where x+y<100 select Tuple.Create(x, y); }
差距仍不明显,但是命令式语句开始隐藏我们想要的意图。再将问题变得复杂一些。现在,我们需要将返回的点按照距离原点位置的距离降序排列。
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices3() { var storage = new List<Tuple<int, int>>(); for (int x = 0; x < 100; x++) { for (int y = 0; y < 100; y++) { if (x+y<100) storage.Add(Tuple.Create(x, y)); } } storage.Sort((point1,point2)=> (point2.Item1*point2.Item1+point2.Item2*point2.Item2).CompareTo (point1.Item1 * point1.Item1 + point1.Item2 * point1.Item2) ); return storage; } public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices3() { return from x in Enumerable.Range(1, 100) from y in Enumerable.Range(1, 100) where x + y < 100 orderby (x * x + y * y) descending select Tuple.Create(x, y); }
现在,差距开始出现了,命令式的风格时的代码难以理解,如果不认真的看,很难一下子明白比较函数后面一堆东西返回的是什么。如果没有注释的话,这段命令式代码比较难懂。命令式风格的代码过多的强调了代码执行的过程,很容易在这复杂的过程中迷失我们最初想要达到的目的。
查询表达式比循环控制结构更好的一点是:查询表示式能够更好的组合,他能够将算法组织在一个很小的精简的片段内来执行一系列的操作。查询的惰性执行模型也使得开发者能够在一个循环类执行一系列的操作。而查询表达式则不能做到这一点。你必须存储每一次循环的结果,然后构造新的循环操作来执行上一次的操作结果。
最后一个例子演示了如何工作。操作组合了过滤(where子句),排序(orderby 子句)以及投影(select语句),所有这些操作在一次遍历操作中完成。而命令式版本的方法需要创建一个零时的存储对象storage,然后对这个对象分别执行一些列操作。
每一个查询表达式有一个对应的方法调用是的表达式,有时候,查询表达式更自然有时候方法调用是的表达式更自然。显然在上面的例子中,查询表达式版本更易读,下面是方法调用的版本:
public static IEnumerable<Tuple<Int32, Int32>> ProduceIndices3() { return Enumerable.Range(1, 100). SelectMany(x => Enumerable.Range(1, 100), (x, y) => Tuple.Create(x, y)).Where(pt => pt.Item1 + pt.Item2 < 100). OrderByDescending(pt => pt.Item1 * pt.Item1 + pt.Item2 * pt.Item2); }
在这个例子中,查询表达式可能比方法表达式更易读,但其他例子可能不同。有些方法表达式并没有对应的查询表达式。一些方法,如Take,TakeWhile,Skip,SkipWhile,Min,以及Max在一些情况下需要使用方法表达式。
有些人可能会人会查询表达式在执行速度上比循环执慢,这一点要看具体的情况。有时候清晰的代码可能比速度更重要。在改进程序算法之前,可以考虑LINQ的并行扩展库PLINQ,可以使用.AsParallel()来使的查询操作能够并行执行。
C#最开始是一种命令式编程语言,你能够使用之前最熟悉的方法去编写代码。但是这些传统的方法可能不是最好的。当你发现需要使用循环结构去执行某种操作的时候,试试能够将它改写为查询表达式的方式,如果查询表达式不起作用,试试方法表达式,在大多数情况下,你会发现使用查询表达式或者方法表达式会比使用传统的循环式的命令式结构能使得代码变得更加简洁和清晰。