处理数据是编程的一大任务。其中对字符串数据处理尤其重要,本篇略过字符串处理,只谈linq、foreach、标准查询运算符。
一、foreach
C#支持foreach迭代数据,和传统的for循环很类似,并且比for循环更易用。如:
foreach (var a in 数据源) { console.WriteLine(a) ;}
而for循环需要定义一个下标: for (int i = 0; i < 数据源.Length; i++) console.WriteLine (数据源[i]);
可见foreach更加简易。但是应该知道,for循环并非专门用来处理数据,它只是一个知道循环次数,然后不断执行循环体的基本程序结构。for 循环可以执行任何类型的编程任务;而foreach则是专程定制来迭代数据的。
foreach 要求数据源实现IEnumerable<T>接口(或非泛型版本),为何?
foreach 的工作机制类似:
IEnumerator<T> s = ((IEnumerable<T>) 数据源).GetEnumerator();
while (s.MoveNext()) { console.WriteLine(s.Current); }
首先,IEnumerable 并不处理实际的迭代工作,而是需要IEnumerator配合,IEnumerator有三个成员:
T Current //当前元素
bool MoveNext() //迭代到下一个位置
void Reset() //复原位置
注意,一开始的位置是在第一个元素之前,所以先MoveNext再调用Current才是第一个元素。当迭代超过尾端,MoveNext返回flase ,Current 无效。IEnumerator 可能引发异常 InvalidOperationException (无效操作异常)。
表面上看,foreach 的限制很大,需要数据源实现两个接口,但是.net大部分内置数据源,如数组,列表,集合等都实现了这两个接口,因此foreach 的可用性很高。
二、yield
既然我们知道数据源需要实现IEnumerable接口才能被强大的foreach迭代所用,那么第二步就是想办法让我们自定义的数据源支持该接口。可以用两个方法,第一个是对支持IEnumerable的数据源做一个简单的包装,如内置一个数组存放数据。第二个从零开始建造自己的数据源。
其中,你可以按部就班的实现接口的每一个函数和属性,但是c#提供了更简易的方法,那就是利用 yield 关键字直接生成IEnumerable实例。
IEnumerable create(int start, int end){ while (start < end) yield return start++; }
包含 yield 关键字的函数内部会产生一个IEnumerable对象,或者IEnmerator对象(视返回类型而定)用以返回。这个临时对象记录相关的位置信息,效果如同手工编写IEnumerator 实现类并生成对象。
yield return 语句并非是函数的返回,不要和return语句混淆,yield return 产生一个记录点,暂停当前函数的执行,并把当前结果返回,当下次迭代时(即调用IEnumerator.MoveNext() 方法),从该记录点后继续执行。直到函数执行结束或者遇到 yield break 语句。
如 while ( start < end) if (start == 100) yield break; yield return start++; }
yield 关键字很强大,背后的生成机制很神奇,但是yield生成的迭代对象还是有点不足。第一,不支持Reset() 复原位置,第二,我会感觉这个方案是临时性的。
三、linq
当你实现了数据源,调用foreach就能迭代该数据源,貌似一切问题都完结了。其实编程中有很多任务需要对数据源进行再加工,linq就是这种工具。它支持筛选,排序,生成新序列等,也就是等于将原序列映射到新的序列中,而得到的结果序列就能利用foreach继续执行任务。
(一)语法:
linq包含的基本子句为:from、where、select、group、orderby、join
定义变量子句:into、let
辅助关键字:in、on、equals、by、ascending、descending
1、linq表达式语法: linq 表达式从from 子句开头,group 或者 select子句结尾,中间可包含任何子句。
2、from语法: from 变量 in 数据源(IEnumerable<T>类型 或 IQueryable<T>类型)
作用:引入上下文变量名,表示当前迭代元素,有点类似foreach (var 变量 in 数据源)的作用。
from x in A from y in B 这样的结构等于双重迭代,优化的策略是找出B和A的关联,如B = x.b,即可以缩小迭代次数。
3、select 语法: select 表达式
表达式的返回结果就是元素的类型,即将以上迭代最终的成果通过表达式映射到最终序列。
4、group 语法: group 表达式 by 键
键组结构的序列
5、orderby语法:orderby 键,第二键(可选)… ascending(升序、可选) 或 descending(降序)
根据键排序结果
6、join 语法: join 变量 in 数据源 on 左键 equals 右键 into(可选) 组变量
将左集合和右集合通过键关联,如果有into部分,右集匹配部分就是一组数据,而不是单个数据。
后续上下文是变量还是组变量,全看是否有into部分。
7、into 语法:
group …into 变量
select …into 变量
join … into 变量
以上三种变量类似 from x in A 中的x,那么变量对应的数据源A分别就是:键组结构的序列、select 指定类型的序列、
按键分组,各分组组成的序列(即序列组成的序列)。
如:
from T x in A join U y in B on y.a equals x into K group new {x,k} by x into g select g.key into m where m < 10 select m;
相当于:
from g in (from T x in A join U y in B on y.a equals x into K group new {x,k} by x)
from m in (from T x in A join U y in B on y.a equals x into K group new {x,k} by x into g select g.key)
from K in (from T x in A join U y in B on y.a equals x group y by x into g select (from y in g select y))
在group和select 后续定义的into 变量是为了对结果附加操作;而在join之后附加into 定义变量,后续得到左集匹配的一组数据,比一一对应更适合某些情形。
8、let 语法:let 变量 = 表达式
简化表达式的书写,构建中间变量。
9、where 语法: where 条件表达式
根据条件表达式筛选元素,得到序列的子集
(二)转换到标准查询运算符
linq易于理解,但是有时候还需要依赖标准查询运算符进行更细致的操作。这个时候我觉得就需要弄懂linq是怎么转换到标准查询运算符的。
from 引入数据源,是标准的linq抬头,而标准查询运算符是扩展函数,直接应用到序列点运算符之后,因此自然就知道处理的是哪个序列。
如: from x in A ==> A.
x 是范围变量,表示迭代中的元素,而扩展函数对应的是接收传入的委托参数。
如: from x in A select x ==> A.Select( x =>x )
多重from的情况:
from x in A from y in B select x+y ==> A.SelectMany(x=>B, (x,y)=>x+y )
from x in A from y in B select y ==> A.SelectMany( x=>B )
from x in A from y in B select x ==> A.SelectMany( x=>B, (x,y)=>x )
from x in A from y in B from k in C select x+y+k ==>
A.SelectMany( x=>B, (x,y)=>C.Select(k=>x+y+k)).SelectMany(k=>k)
以上是我想到的方案,如果用一下串联的方法虽然更加易于理解,但是会丢失掉上一级的元素。
如:A.SelectMany(x=>B).SelectMany(y=>C, (y,k)=> y+k+x(x无法访问))
不过我反编译后发现编译器真的是通过这种方式实现的:
A.SelectMany(x=>B, (x,y)=>new{x,y})
.SelectMany(xy=>C,(xy,k)=>xy.x+xy.y+k);
例子2:from x in A where x==1 select x ==> A.Where(x=>x==1)
from x in A where x>1 select 1 ==> A.Where(x=>x>1).Select(x=>1)
from x in A where x>1 select x into y where y<10 select y ==> A.Where(x=>x>1).Select(x=>x).Where(x=>x<10)
from x in A from y in B where x>10 && y <3 select new {x,y} ==>
A.SelectMany(x=>B, (x,y)=>new {x,y}).Where(xy=>xy.x > 10 && xy.y < 3).Select(xy=>new {x= xy.x, y=xy.y})
和linq语法不同,Where函数返回的是序列,因此可以和Select按任意顺序串联起来,并且,如果最终结果是当前元素组成的序列,那么也不必非要附带Select结尾。而 linq强制要求from 开始 select结尾。
例子3:from x in A orderby x%3 descending, x%2 select x ==> A.OrderByDescending(x=>x%3).ThenBy(x=>x%2)
函数语法通过OrderBy 或 OrderByDescending 起始, ThenBy 或 ThenByDescending 做后续处理基于多个条件的排序。
例子4:from x in A join y in B on x equals y.a select new {x,y} ==> A.Join(B, x=>x, y=>y.a, (x,y)=>new {x,y})
from x in A join y in B on x equals y.a into C select new {x,C} ==>A.GroupJoin(B,x=>x,y=>y.a,(x,C)=>new {x,C})
例子5:from x in A from y in B group y by x ==>
A.SelectMany(x=>B, (x,y)=>new {x,y}).GroupBy(xy=>xy.x, xy=>xy.y)
四、标准查询运算符
排序:
排序不改变元素构成,只改变元素顺序。
函数 |
linq |
OrderBy( Func<source, key> ) | orderby 键选择表达式 |
OrderByDescending 降序版 | orderby key descending |
OrederBy( Func<source,key>, Icomparer<key> ) | |
降序版 | |
ThenBy 后续排序 | orderby key1,key2(多个) … |
ThenByDescending 降序版本 | orderby key1,key2(多个)… descending |
带比较器版本 | |
带比较器版本 | |
Reverse 颠倒顺序 |
集合运算:
返回子集或并集
函数 |
linq |
Distinct 移除重复元素 | |
Distinct( IEqualityComparer(T) ) | |
Except( rSource ) 两集合之差 | |
带相等比较器版本 | |
Intersect 两集合之交 | |
带相等比较器版本 | |
Union 两集合之并 | |
带相等比较器版本 |
筛选:
筛选并返回符合条件的子集
函数 |
linq |
OfType 返回特定类型元素序列 | |
Where ( Func<source, bool> ) | where 条件表达式 |
判定:
判断序列是否符合条件(返回bool 单值)
函数 |
linq |
All (Func<source, bool> ) 所有元素满足指定条件 | |
Any 是否有元素 | |
Any (Func<source,bool>)是否有符合条件的元素 | |
Contains( T ) 是否有指定元素 | |
Containz( T, IEqualityComparer<T>) 带相等比较器版本 |
映射:
将原序列映射到新生成的序列
函数 |
linq |
Select( Func<source, T> ) 转换序列 | from..in(source)…select T |
Select( Func<source, int, T> )带位置的选择器版本 | |
SelectMany( Func<source, IEnumerable<U>> ) 将序列转换为可枚举元素,并串联每个元素的枚举结果 | from ..in (source) from..in (IEnumerable<U>)(多个)… select U |
SelectMany 带位置的选择器版本 | |
SelectMany( Func<source, IEnumerable<U>>, Func<source, U, result > | from..in(source) from..in(IEnumerable<U>)(多个)… select result |
SelectMany( Func<source, int, IEnumerable<U>>, Func<source,U,result>带位置的选择器版本 |
分段:
展示数据的时候,经常需要分页(分段)显示,一次显示一小段便于用户查看。
函数 |
linq |
Skip(int) 跳过前n个元素 | |
SkipWhile(Func<source, bool>) 跳过符合条件的前n个元素 | |
SkipWhile( Func<source, int, bool>) 带位置版 | |
Take(int) 返回前n个元素 | |
Take(Func<source,bool>) 返回满足条件的前n个元素 | |
Take(Func<source,int,bool>) 带位置版 |
联接:
联接操作是将左集合和右集合的元素进行匹配。匹配的意义是:一、一次匹配等于一次迭代结果,不匹配就没有结果,最大化是左序列长度*右序列长度。二、上下文可以访问匹配的相关元素。三,可映射到新的序列。
如左集和右集匹配10次,就需要10次迭代该结果,每一次,都能访问这次迭代匹配的左集元素和右集合元素(即上下文)。
联接结果有:
左外部:即无匹配的左集合部分+交集
右外部:和左外部原理差不多
内部:即交集
全联接:即左未匹配部分+交集+右未匹配部分
术语解释:
同等联接:即基于键相等的联接
非同等联接:即基于其他条件的匹配。
交叉联接:左集每个元素和右集合所有元素匹配,即左集X右集。
函数 |
linq |
Join( IEnumerable<right>, Func<source, key>, Func<right, key>, Func<source, right, result>)右序列,左序列键选择器,右序列键选择器,匹配结果转换器。联接的结果是左序列一个元素和它匹配的右序列一个元素。 |
join …in (right) on key1 equals key2 select result |
带相等比较器版本 | |
GroupJoin( right, Func<source, key>, Func<right, key>, Func<source, IEnumerable<right>, result> )右序列,左序列键选择器,右序列键选择器,匹配结果转换器。分组联接的结果是左序列一个元素和它匹配的一组右序列元素 | jion …in (right) on key1 equals key2 into (IEnumerable<right>) select result |
带相等比较器版本 |
分组:
按照指定键分组序列元素,结果映射为 IGrouping<key,element> 或 ILookup<key, element>类型的键组结构的元素序列。
分组操作:前提是一组序列,结果是键组结构的序列。
集合操作:前提是一组或两组同类序列(通过值比较),结果是原类型元素序列的子集或者并集。
联接操作:前提是两组序列(通过键关联),结果是匹配组对或一对多组对(但并不形成键组结构实体),然后映射到指定类型序列。
函数 |
linq |
GroupBy(Func<source,key>) | group T by key |
GroupBy(Func<source,key>, IEqualityComparer<key>) 带相等比较器版本 | |
GroupBy(Func<source,key>, Func<source, result>) 分组并映射(元素版) | group result by key |
带相等比较器版本 | |
GroupBy(Func<source,key>, Func<key,IEnumerable<source>, result>) 分组并映射(键组版) | |
带相等比较器版本 | |
GroupBy(Func<source,key>,Func<source,U>,Func<key, IEnumerable<U>, result>) 键选择器,元素选择器, 结果转换器(键组版) | |
带相等比较器版本 | |
ToLookup(Func<source,key>) 映射到 ILookup<key,element>键组结构的序列 | |
带相等比较器版本 | |
ToLookup(Func<source,key>, Func<source, U>)键选择器,元素选择器 | |
带比较器版本 |
创建:
构建特定类型的序列。
函数 |
linq |
DefaultIfEmpty 如果集合空即创建一个默认元素的序列 | |
DefaultIfEmpty(T ) 指定值版 | |
Empty 创建空集 | |
Range(int start,int count) 创建从start开始到start+count –1 结束的递增整数序列。 | |
Repeat(T e, int count) 创建count个e 组成的序列 |
比较:
函数 |
linq |
SequenceEqual(IEnumerable<T>) 比较两序列是否相同 | |
带相等比较器版本 |
定位元素:
定位单个符合条件的元素。
函数 |
linq |
ElementAt(int index) 返回指定位置的元素 | |
ElementAtOrDefault(int) 超出范围返回默认值版本 | |
First 返回第一个元素 | |
FirstOrDefault 找不到返回默认值版本 | |
First(Func<source,bool>) 返回符合条件的第一个元素 | |
FirstOrDefault 找不到返回默认值版本 | |
Last 返回最后一个元素 | |
LastOrDefault 找不到返回默认值版本 | |
Last(Func<source,bool>) 返回符合条件的最后一个元素 | |
LastOrDefault 找不到返回默认值版本 | |
Single 返回序列中唯一一个元素,如非唯一元素或找不到引发InvalidOperationException异常 | |
SingleOrDefault 找不到返回默认值版本 | |
Single(Func<source,bool>)返回序列中唯一符合条件的元素,如非唯一或找不到,引发异常 | |
SingleOrDefault 找不到返回默认值版本 |
转换:
将序列转换到特定类型的新序列。
函数 |
linq |
AsEnumerable 调用非自定义实现 | |
AsQueryable 类似,IQueryable版 | |
Cast<T> 强制转换成指定类型序列 | from T element in 数据源 |
OfType<T> 将能转换为指定类型的元素组成序列 | |
ToArray 转换成数组 | |
ToDictionary (Func<source,key>) 根据键转换成字典序列 | |
带相等比较器版本 | |
ToDictionary(Func<source,key>,Func<source,element>) 键选择器,元素选择器 | |
带相等比较器版本 | |
ToList 转换成列表 | |
ToLookup 转换为ILookup<key,element>键组结构的序列 (请参照分组小节) |
串联:
将两序列首尾串联,合并成一个新的序列。
串联和并集的差别是它并不比较元素,只简单合并。
函数 |
linq |
Concat(IEnumerable<T>) 串联两序列 |
整体运算:
遍历整个序列得出想要的结果。
函数 |
linq |
Aggregate (Func<T 累积值, T 当前元素, T>) 累积操作。每次迭代利用上一次累积的值和当前元素计算结果。 | |
Aggregate( U 初始值, Func<U, source, U>) 累积器类型不同的版本 | |
Aggregate( U 初始值,Func<U,source,U>, Func<U, result>) 累加器类型和最终结果类型不同的版本 | |
Average 平均值 | |
带转换器版本,应用于非数值序列 | |
Count 元素个数 | |
Count( Func<source,bool> ) 符合条件的元素个数。 | |
LongCount 大小是Int64版本 | |
带判断器版本 | |
Max 查找最大值 | |
带转换器版本 | |
Min 查找最小值 | |
带转换器版本 | |
Sum 求总和 | |
带转换器版本 |