注明:学习《用 LINQ 编写 C# 都有哪些一招必杀的技巧?》
LINQ To Object的本质
LINQ 有两种写法,LINQ 表达式写法和调用 LINQ 操作符(查询方法)。比如如下两个简单的查询,实现了相同的功能,将一个数组中的偶数挑出来:
使用 LINQ 表达式:
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8 }; var query = from x in arr where x % 2 == 0 select x; foreach (int x in query) Console.Write(x + "\t");
使用 LINQ 操作符:
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8 }; var query = arr.Where(x => x % 2 == 0); foreach (int x in query) Console.Write(x + "\t");
知识点一:LINQ 表达式会被 C# 编译器编译为对 LINQ 方法的调用。事实上,LINQ 表达式是 LINQ 的一个子集。这意味着,所有的 LINQ 表达式都可以用 LINQ 的方法调用实现,反之则不一定。知识点二: LINQ 方法的实现放在了 System.Linq 下,就是普通的 C# 代码,而没有幕后任何玄妙的机制。Lambda 表达式的本质是一个匿名的方法,箭头前面的部分是它的参数,后面的语句就是这个函数的返回值。(x => x < 5 的 x 是怎么回事,其实它相当于你定义了一个函数(和 myfunc 类似),而 x 是这个函数的参数,这个函数被传入 Where,由 Where 调用,每次遍历一个元素就会调用一次,每次 x 代表数组中的一个元素,判断你的条件并且返回是否应该被放入结果还是应该舍弃。)
LINQ的常见操作符及其使用技巧
Select
它的作用是投影,对一个序列的每一项做一个运算,得到一个结果,而 select 的结果是一个和原序列等长的新的序列,它的每一项是经过运算变化以后的每一个结果。
Select 有个很有用的重载形式,是Select<TSource, TResult>(IEnumerable<TSource>, Func<TSource, Int32, TResult>)的形式,注意其中的委托的
Int32 参数,写起来一般是Select((x,
i) => ?)这样的形式,这个 i 代表了此元素在序列中的位置,从0开始。
GroupBy
它的作用是分组,因此原始序列按照分组规则能分多少组,那么结果序列的长度就是几。而结果序列的每一项,又是一个序列,这个序列是所有符合这个分组规则的原始数据的每一项。对于结果序列来说,有一个 Key 属性,代表分组的规则。
SelectMany
它的作用是对于一个序列的序列,将一个序列的每一项提取出来作为结果的每一项。因此它有点类似 GroupBy 的反操作。SelectMany 最常用的操作是生成笛卡尔集,也就是把第一个集合的每一项和第二个集合的每一项匹配,得到数量为两个集合元素数量相乘的新的集合。
Skip和Take
Skip(n) 在指定的序列上跳过 n 个元素,而 Take(n) 则取 n 个元素。如果 Skip 和 Take 的 n 比序列上剩下的元素多,那么执行不会报错,但是会返回能返回的最多的元素。最常见的用法是做分页。
Aggregate
聚合函数,它对于查询的时候需要前面元素参与计算的需求非常有用。
LINQ在C#编程中的技巧案例
第一个例子:洗牌算法
将一个数组每个元素的顺序随机打乱。
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; var query = arr.OrderBy(x => Guid.NewGuid()); foreach (int i in query) { Console.Write(i + " "); }
运行结果:
4 7 6 8 5 10 2 3 1 9
注意,这个结果是随机的,每次运行的都不同。我们还可以用洗牌算法实现对一个 m 个元素的数组,任意选 n 个的操作:
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; var query = arr.OrderBy(x => Guid.NewGuid()).Take(3); foreach (int i in query) { Console.Write(i + " "); }
比如以上代码,就可以实现在 arr 里不重复地取 3 个。
第二个例子:排列组合
排列:
int[] arr = { 1, 2, 3, 4 }; IEnumerable<IEnumerable<int>> result = arr.Select(x => new List<int>() { x }); for (int i = 1; i < arr.Length; i++) result = result.SelectMany(x => arr.Except(x), (x, y) => x.Concat(new int[] { y })); foreach (var item in result) { foreach (int i in item) Console.Write(i + " "); Console.WriteLine(); }
组合:
static IEnumerable<IEnumerable<int>> SelectNElements(int[] arr, int n) { IEnumerable<IEnumerable<int>> result = arr.Select(x => new List<int>() { x }); for (int i = 1; i < n; i++) result = result.SelectMany(x => arr.Where(y => y > x.Max()), (x, y) => x.Concat(new int[] { y })); return result; } static void Main(string[] args) { int[] arr = { 1, 2, 3, 4 }; for (int i = 1; i <= arr.Length; i++) { var result = SelectNElements(arr, i); foreach (var item in result) { foreach (int x in item) Console.Write(x + " "); Console.WriteLine(); } } }
其中,SelectNElements 函数也可以单独拿出来用来做 m 选 n 的算法。
第三个例子:用 C# 制作一个年历
这是一个经典的面试题,用 LINQ 可以简化代码的编写。
代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { string calendar = ""; calendar = (from x in Enumerable.Range(1, 12) group x by (x + 2) / 3 into g select (BuildCalendar(DateTime.Now.Year, g.ToList()[0]).Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Union(new string[] { "\r\n" }) .Zip(BuildCalendar(DateTime.Now.Year, g.ToList()[1]).Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Union(new string[] { "\r\n" }), (x, y) => x.TrimEnd().PadRight(23, ' ') + y) .Zip(BuildCalendar(DateTime.Now.Year, g.ToList()[2]).Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Union(new string[] { "\r\n" }), (x, y) => x.TrimEnd().PadRight(46, ' ') + y)) .Zip(Enumerable.Repeat("\r\n", 8), (x, y) => x + y) .Aggregate((serials, current) => serials + current)) .Aggregate((serials, current) => serials + current); Console.WriteLine(DateTime.Now.Year + "\r\n" + calendar); } static string BuildCalendar(int year, int month) { string calendar = new string[] { month.ToString(), "SU MO TU WE TH FR SA" } .Union(Enumerable.Range( 1 - (int)new DateTime(year, month, 1).DayOfWeek, new DateTime(year, month, 1).AddMonths(1).AddDays(-1).Day + (int)new DateTime(year, month, 1).DayOfWeek ) .GroupBy(x => ((x + (int)(new DateTime(year, month, 1).DayOfWeek + 6)) / 7), (key, g) => new { GroupKey = key, Items = g }) .Select(x => x.Items.Select(y => y < 1 ? " " : Convert.ToString(y).PadLeft(2, '0') + " ") .Aggregate((serials, current) => serials + current)) ) .Aggregate((serial, current) => serial + "\r\n" + current); return calendar; } } }
这是运行结果:
第四个例子:使用 LINQ 代码简化递归遍历
对于数组或者集合,我们直接使用 foreach 调用就好了,但是如果我们要遍历层次结构怎么办呢?必须定义一个方法,递归调用。
虽然遍历的代码从结构上看大同小异,但是具体到不同的场景,比如遍历数据库中的字段、遍历控件、遍历文件系统、遍历二叉树、遍历 TreeView……,则需要编写不同的代码,似乎不太好进行代码的重用。
下面给出的例子,就是 Lambda 表达式大显身手的地方了。我们可以用它写出一个通用的代码,借助它,再遍历各种层次结构,都可以轻松搞定。
我们使用 VS 新建一个 WinForms 程序,在主窗体上添加一个菜单条(MenuStrip)、一个 TreeView、三个按钮(用于遍历菜单、TreeView 和控件)、一个 ListBox(用于输出结果)、以及若干控件构成的层次结构。
创建菜单条后,我们可以利用 “插入标准项” 功能快速插入一些标准的菜单条目,如图所示:
为了演示递归,我们给菜单条多加上一些层次和项目:
完成的界面如下:
添加一个类,比如叫 class1,然后编写如下代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace WindowsFormsApp1 { static class Class1 { private static IEnumerable<TNode> GetChildren<TNode>( TNode node, Func<TNode, IEnumerable<TNode>> GetNodes) { var nodes = GetNodes(node); return nodes.Concat(nodes.SelectMany(x => GetChildren(x, GetNodes))); } public static IEnumerable<TNode> GetChildrenRecursively<TRoot, TNode>( this TRoot obj, Func<TRoot, IEnumerable<TNode>> EnumRoot, Func<TNode, IEnumerable<TNode>> GetNodes) { var nodes = EnumRoot(obj); return nodes.Concat(nodes.SelectMany(x => GetChildren(x, GetNodes))); } } }
然后我们就可以使用了。首先双击第一个按钮,我们来遍历菜单条:
private void button1_Click(object sender, EventArgs e) { var items = menuStrip1.GetChildrenRecursively(x => x.Items.OfType<ToolStripMenuItem>(), x => x.DropDownItems.OfType<ToolStripMenuItem>()); listBox1.Items.Clear(); foreach (var item in items) listBox1.Items.Add(item.Text); }
运行结果:
然后双击第二个按钮,遍历 TreeView:
private void button2_Click(object sender, EventArgs e) { var items = treeView1.GetChildrenRecursively(x => x.Nodes.OfType<TreeNode>(), x => x.Nodes.OfType<TreeNode>()); listBox1.Items.Clear(); foreach (var item in items) listBox1.Items.Add(item.Text); }
运行结果:
最后,第三个按钮,遍历控件:
private void button3_Click(object sender, EventArgs e) { var items = this.GetChildrenRecursively(x => x.Controls.OfType<Control>(), x => x.Controls.OfType<Control>()); listBox1.Items.Clear(); foreach (var item in items) listBox1.Items.Add(item.Name); }
运行结果:
如果我们在结尾处加上OfType<TextBox>,那么我们可以遍历界面上所有的文本框。
如果我们需要对界面上所有的输入做一个统一判断,避免任意一个文本框为空,那么这段代码也可以用来做控件的验证。
此递归代码不但可以用来遍历各种树状结构,甚至也可以用来解决之前说的排列组合问题:
int[] arr = { 1, 2, 3, 4 }; var query = arr.GetChildrenRecursively<IEnumerable<int>, IEnumerable<int>>(x => arr.Select(y => new int[] { y }), x => arr.Except(x).Select(y => x.Concat(new int[] { y }.ToArray()))); foreach (var item in query) { foreach (var i in item) Console.Write(i + " "); Console.WriteLine(); }
完整的程序:https://ideone.com/jaQlel
运行结果:
1 2 3 4 1 2 1 3 1 4 1 2 3 1 2 4 1 2 3 4 1 2 4 3 1 3 2 1 3 4 1 3 2 4 1 3 4 2 1 4 2 1 4 3 1 4 2 3 1 4 3 2 2 1 2 3 2 4 2 1 3 2 1 4 2 1 3 4 2 1 4 3 2 3 1 2 3 4 2 3 1 4 2 3 4 1 2 4 1 2 4 3 2 4 1 3 2 4 3 1 3 1 3 2 3 4 3 1 2 3 1 4 3 1 2 4 3 1 4 2 3 2 1 3 2 4 3 2 1 4 3 2 4 1 3 4 1 3 4 2 3 4 1 2 3 4 2 1 4 1 4 2 4 3 4 1 2 4 1 3 4 1 2 3 4 1 3 2 4 2 1 4 2 3 4 2 1 3 4 2 3 1 4 3 1 4 3 2 4 3 1 2 4 3 2 1
可以看到,Lambda 表达式允许我们在一个方法中将需要重用的算法主体先写出来,然后将需要自定义的地方用 Lambda 表达式交给调用者实现,从而让编写的类库代码具有更大的重用价值。
第五个例子:读取和写入文件
在 .NET 4.0 以后的版本中,System.IO 命名空间的 File 静态类下,多了几个很实用的文件读写方法:
File.ReadAllLines File.WriteAllLines File.ReadAllBytes File.WriteAllBytes
使用它们可以很方便地读写文件。假设我们有一个文本文件,叫做 1.txt,里面包含以下内容:
1 2 3 4 5 6 7 8 9 10
我们希望编写一个程序求和,我们可以这么写:
var lines = System.IO.File.ReadAllLines(@"X:\path\1.txt"); var sum = lines.Select(x => int.Parse(x)).Sum(); Console.WriteLine(sum);
结果是 55。
用表达式树构建查询
在前面的介绍中,我们的 LINQ 查询都是在代码中写好的,然而有时候我们希望在代码运行的过程中动态产生一个条件判断的 Lambda 表达式,这就需要使用表达式树来构建。
让我们回到文章开始的那个例子,把数组中的偶数挑选出来。但是我们这次使用表达式树来动态生成和编译 Lambda 表达式。
为了使用表达式树,我们需要先导入如下命名空间:
using System.Linq.Expressions;
先看代码:
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8 }; var param = Expression.Parameter(typeof(int), "x"); var modexp = Expression.Modulo(param, Expression.Constant(2)); var body = Expression.Equal(modexp, Expression.Constant(0)); Expression<Func<int, bool>> cond = Expression.Lambda<Func<int, bool>>(body, param); var query = arr.Where(cond.Compile()); foreach (int x in query) Console.Write(x + "\t");
这段代码创建了一个叫做 cond 的表达式树,它实现了类似 x => x % 2 == 0 的 Lambda,我们在运行期间构造了它并且用 Compile 编译成方法,传入 where 执行了查询。
cond 的结构如下:
我用括号标注了对应的表达式等价的代码。可以看到 cond body modexp 三个节点分别代表 Lambda 表达式的 =>、== 和 % 三个二元操作符。0 和 2 代表常数节点,而 param 则是 Lambda 表达式的参数。