Linq 学习笔记<转帖>
转自 http://blog.sina.com.cn/s/blog_5df2629a0100lr3u.html
一·前言
Linq 英文全称Language Integrated Query,它提供了C#编程语言中的查询语法,可以使用相同的语法访问不同的数据源。并且Linq还提供了不同数据源的抽象层,所以可以使用相同的语法。本次笔记的主要内容如下:
● 用List<T>在对象上执行传统查询
● 扩展方法
● λ表达式
● LINQ 查询
● 标准查询操作符
● 表达式树
● LINQ 提供程序
一。用List<T>在对象上执行传统查询
首先我们来看一个最简单的例子,请看下面的代码:
List<string> strList = new List<string>();
strList.Add("AAA");
strList.Add("BABB");
strList.Add("ACCC");
strList.Add("BBB");
strList.Add("CCC");
var res = strList.FindAll(delegate(string s) { return s.Contains("A"); });
res.Sort(delegate(string a, string b) { return a.CompareTo(b); });
this.GridView1.DataSource = res;
this.GridView1.DataBind();
这段代码是从strList中查找所有包含字母A的记录,并且将得到的结果进行进行从小到大的排序(如果将return a.CompareTo(b); 换成return b.CompareTo(a);结果则是从大到小的顺序进行排列了)。相信很多人用过这样的方式来对结果集进行过滤和排序操作。但是如果你想在任何集合都可以使用这两个方法,那你就可能会使用到扩展方法了。扩展方法是C#3.0的新增特性,这也是上述例子迈向LINQ 的第一个变化。下面我们就来介绍一下扩展方法。
二。扩展方法
请看下面关于扩展方法的例子
public static class helper
{
public static string MD5Hash(this string s)
{
return System.Web.Security.FormsAuthentication.HashPasswordForStoringInConfigFile(s, "MD5");
}
public static bool In(this object o, IEnumerable b)
{
foreach (object obj in b)
{
if (obj == o)
return true;
}
return false;
}
}
// 调用扩展方法
Response.Write("123456".MD5Hash());
Response.Write("1".In(new[]{"1","2","3"}));
上面的例子是对string进行了扩展,这样的话只要在当前命名空间下任何string对象都可以利用该扩展方法。这样会给代码带来很大的方便。
既然有了扩展方法,我们的Linq自然就很容易就出现了。就如同下面的Linq例子一样
MethodInfo[] methods = typeof(string).GetMethods();
var result = from m in methods
where m.IsStatic != true
select m.Name;
foreach (var r in result)
{
Response.Write(r.ToString()+"<br/>");
}
这就是Linq最典型的样子。是不是比较像我们平时用的SQL呢?呵呵虽然还有不小的差别,但是Linq的引入实在是C#的一种革命性的变化。他不仅好用,更给枯燥的C#代码注入了一股鲜活的思想。
三。λ表达式
C# 3.0 给匿名方法提供了一个新的语法——λ表达式(也叫做Lambda表达式)。除了把匿名方法传送给Where()、OrderbyDescen
ding()和Select()方法之外,还可以使用λ表达式。
λ 表达式参见第7 章。λ表达式在LINQ 中非常重要,所以下面复习一下该语法。详细信息可参见第7
章。
比较λ表达式和匿名委托,会发现许多类似之处。λ运算符=>的左边是参数,不需要添加参数类型,因
为它们是由编译器解析的。λ运算符的右边定义了执行代码。在匿名方法中,需要花括号和return 语句。在
λ表达式中,不需要这些语法元素,因为它们是由编译器处理的。如果λ运算符右边有多个语句,也可以使
用花括号和return 语句。
例如下面的例子
var list = new[] { "aa", "bb", "ca" };
var resultList = Array.FindAll(list, s => (s.IndexOf("a") > -1));
foreach (var v in resultList)
Response.Write(v+"<br/>");
s就像是我们在第一个例子中使用的delegate(string s)里面的s一样只是这种使用方法更加的灵活和方便。
四。LINQ 查询
我们就以NorthWind数据库为例,添加一个Linq to Sql Classes命名为Customer.dbml,然后打开Server Explorer设置好连接参数,选择Customers表直接拖拽到Customer.dbml,打开Customer.design.cs文件我们可以看到Visual Studio为我们生成了很多代码。这些代码我们会在后面的内容里详细的讲解。现在主要来讲一下Linq查询以及后面的内容。
LINQ 查询是C#语言中的一个简化查询记号。编译器编译查询表达式,调用扩展方法。查询表达式只是C#中的一个语法,但不需要修改底层的IL 代码。查询表达式必须以from子句开头,以select或group子句结束。在这两个子句之间,可以使用where、orderby、join、let 和其他from子句。注意,变量query只指定了LINQ查询。该查询不是通过这个赋值语句执行的,只要使用foreach循环访问查询,该查询就会执行。
推迟查询的执行
在运行期间定义查询表达式时,查询就不会运行。查询会在迭代数据项时运行。再看看扩展方法Where()。它使用yield return语句返回谓词为true的元素。因为使用了yield return语句,所以编译器会创建一个枚举器,在访问枚举中的项后,就返回它们。
public static IEnumerable<T> Where<T>(this IEnumerable<T> source,Func<T, bool> predicate)
{
foreach (T item in source)
if (predicate(item))
yield return item;
}
请看一下下面的代码
var names = new List<string>{"Nino","Alberto", "Juan", "Mike", "Phil"};
var namesWirhJ = (from name in names where name.StartsWith("J") orderby name select name);
Response.Write("First Iteration<br/>");
foreach (var name in namesWirhJ)
Response.Write(name + "<br/>");
names.Add("John");
names.Add("Jim");
names.Add("Jack");
names.Add("Denny");
Response.Write("Second iteration<br/>");
foreach (var name in namesWirhJ)
Response.Write(name + "<br/>");
其返回结果为
First iteration
Juan
Second iteration
Jack
Jim
John
Juan
但是当我们修改一下代码将 var namesWirhJ = (from name in names where name.StartsWith("J") orderby name select name);换做var namesWirhJ = (from name in names where name.StartsWith("J") orderby name select name).ToList();那么结果就成了
First iteration
Juan
Second iteration
Juan
这就是我们需要注意的地方了,每次在迭代中使用查询时,都会调用扩展方法。在大多数情况下,这是非常有效的,因为我们可以检测出源数据中的变化。但是在一些情况下,这是不可行的。调用扩展方法ToArray()、ToEnu
merable()、ToList()等可以改变这个操作。这也就是为什么我们需要演示ToList()方法了。ToList 迭代集合,返回一个实现了List<string>的集合。之后对返回的列表迭代两次,在这个过程中,数据源被修改了,但是原来的集合并没有改变。这样可以用来实现一些比较复杂的业务逻辑。
五。标准查询操作符
Where、OrderByDescending 和Select 只是LINQ 的几个查询操作符。LINQ 查询为最常用的操作符定
义了一个声明语法。还有许多标准查询操作符。下表列出了Linq的标准查询操作符。
Where OfType<TResult> | 过滤操作符定义了返回元素的条件。在Where 查询操作符中,可以使用谓词, 例如λ表达式定义的谓词,来返回布尔值。OfType<TResult>根据类型过滤元素,只 返回TResult 类型的元素 |
Select和SelectMany | 投射操作符用于把对象转换为另一个类型的对象。Select 和SelectMany 定义 了根据选择器函数选择结果值的投射 |
OrderBy,ThenBy OrderByDescending ThenByDescending Reverse | 排序操作符改变所返回的元素的顺序。OrderBy 按升序排序, OrderByDescending 按降序排序。如果第一次排序的结果很类似,就可以使用 ThenBy 和ThenBy Descending 操作符进行第二次排序。Reverse 反转集合中元 素的顺序 |
Join,GroupJoin | 连接运算符用于合并不直接相关的集合。使用Join 操作符,可以根据键选择 器函数连接两个集合,这类似于SQL 中的JOIN。GroupJoin 操作符连接两个集 合,组合其结果 |
GroupBy | 组合运算符把数据放在组中。GroupBy 操作符组合有公共键的元素 |
Any,All,Contains | 如果元素序列满足指定的条件,量词操作符就返回布尔值。Any,All 和Contains 都是量词操作符。Any 确定集合中是否有满足谓词函数的元素;All 确定集合 中的所有元素是否都满足谓词函数;Contains 检查某个元素是否在集合中。这些操作 符都返回一个布尔值 |
Take,Skip, TakeWhile SkipWhile | 分区操作符返回集合的一个子集。Take、Skip、TakeWhile 和SkipWhile 都是 分区操作符。使用它们可以得到部分结果。使用Take 必须指定要从集合中提 取的元素个数;Skip 跳过指定的元素个数,提取其他元素,TakeWhile 提取条件为真的 元素 |
Distinct,Union Intersect,Except | Set 操作符返回一个集合。Distinct 从集合中删除重复的元素。除了Distinct 之外,其他Set 操作符都需要两个集合。Union 返回出现在其中一个集合中的 元素。Intersect 返回两个集合中都有的元素。Except 返回只出现在一个集合 中的元素 |
First FirstOrDefault Last LastOrDefault ElementAt ElementAtOrDefault Single SingleOrDefault | 这些元素操作符仅返回一个元素。First 返回第一个满足条件的元素。 FirstOrDefault 类似于First,但如果没有找到满足条件的元素,就返回类型 的默认值。Last 返回最后一个满足条件的元素。ElementAt 指定了要返回的元 素的位置。Single 只返回一个满足条件的元素。如果有多个元素都满足条件, 就抛出一个异常 |
Count,Sum,Min, Max,Average, Aggregate | 合计操作符计算集合的一个值。利用这些合计操作符,可以计算所有值的总和、 元素的个数、值最大和最小的元素,平均值等 |
ToArray ToEnumerable ToList ToDictionary toType<T> | 这些转换操作符将集合转换为数组、IEnumerable、IList、IDictionary 等 |
Empty,Range, Repeat | 这些生成操作符返回一个新集合。使用Empty,集合是空的,Range 返回一系列数 字,Repeat 返回一个始终重复一个值的集合 |
下面就来介绍一些查询的示例:
1。Linq查询
var racers = from r in Formula1.GetChampions()
where r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria") select r;
foreach (var r in racers)
{
Responose.Write("{0:A}", r);
}
使用扩展方法的查询
并不是所有的查询都可以用LINQ查询完成。也不是所有的扩展方法都映射到LINQ查询子句上。高级查询需要使用扩展方法。为了更好地理解带扩展方法的复杂查询,最好看看简单的查询是如何映射的。使用扩展方法Where()和Select(),会生成与前面LINQ 查询非常类似的结果:
var racers = Formula1.GetChampions().Where(r => r.Wins > 15 && (r.Country == "Brazil" || r.Country == "Austria")).Select(r = > r);
2。用索引来过滤
不能使用LINQ 查询的一个例子是Where()方法的重载。在Where()方法的重载中,可以传送第二个参数——索引。索引是过滤器返回的每个结果的计数器。可以在表达式中使用这个索引,执行基于索引的计算。下面的代码由Where()扩展方法调用,它使用索引返回姓氏以A开头、索引为偶数的赛手:
var racers = Formula1.GetChampions().Where((r, index) => r.LastName.StartsWith("A") &&
index % 2 != 0);
foreach (var r in racers)
{
Responose.Write("{0:A}", r);
}
3。类型过滤
为了进行基于类型的过滤,可以使用OfType()扩展方法。这里数组数据包含string和int对象。使用OfType()扩展方法,把string类传送给泛型参数,就从集合中返回字符串。
object[] data = { "one", 2, 3, "four", "five",6 };
var query = data.OfType<string>();
foreach (var s in query)
{
Console.WriteLine(s);
}
4。复合的from子句
var ferrariDrivers = from r in Formula1.GetChampions() from c in r.Cars where c == "Ferrari"
orderby r.LastName select r.FirstName + " " + r.LastName;
C#编译器把复合的from 子句和LINQ 查询转换为SelectMany()扩展方法。SelectMany()可用于迭代序列的序列。示例中SelectMany()方法的重载版本如下所示:
public static IEnumerable<TResult> SelectMany<TSource,TCollection,TResult>(this IEnumerable<TSource> source,Func<TSource,IEnumerable<TCollection>> collectionSelector,Func<TSource,TCollection,TResult>resultSelector);
第一个参数是隐式参数,从GetChampions()方法中接收Racer对象序列。第二个参数是collectionSelector委托,它定义了内部序列。在λ表达式r=>r.Cars中,应返回赛车集合。第三个参数是一个委托,现在为每个赛车调用该委托,接收Racer和Car对象。λ表达式创建了一个匿名类型,它带Racer和Car属性。这个SelectMany()方法的结果是摊平了赛手和赛车的层次结构,为每辆赛车返回匿名类型的一个新对象集合。
这个新集合传送给Where()方法,过滤出驾驶Ferrari 的赛手。最后,调用OrderBy()和Select()方法:
var ferrariDrivers = Formula1.GetChampions().SelectMany(r => r.Cars,
(r, c) => new { Racer = r, Car = c }).Where(r => r.Car == "Ferrari").OrderBy(r =>r.Racer.LastName).Select(r => r.Racer.FirstName + " " +r.Racer.LastName);
把SelectMany()泛型方法解析为这里使用的类型,所解析的类型如下所示。在这个例子中,数据源是
Racer类型,所过滤的集合是一个string数组,当然所返回的匿名类型的名称是未知的,这里显示为TResult:
public static IEnumerable<TResult> SelectMany<Racer, string, TResult>(
this IEnumerable<Racer> source,Func<Racer,IEnumerable<string>> collectionSelector,
Func<Racer,string,TResult> resultSelector);
查询仅从LINQ 查询转换为扩展方法,所以结果与前面的相同。
5。排序
var racers = from r in Formula1.GetChampions() where r.Country == "Brazil" orderby r.Wins descending select r;
orderby子句解析为OrderBy()方法,orderby descending子句解析为OrderBy Descending()方法:
var racers = Formula1.GetChampions().Where(r => r.Country == "Brazil").OrderByDescending(r => r.Wins).Select(r => r);
OrderBy()和OrderByDescending()方法返回IOrderEnumerable<TSource>。这个接口派生于接口IEnumerable<TSource>,但包含一个额外的方法CreateOrderedEnumerable-<TSource>()。这个方法用于进一步给序列排序。如果根据关键字选择器来排序,两项的顺序相同,就可以使用ThenBy()和ThenByDescending()方法继续排序。这两个方法需要IOrderEnumerable<TSource>才能工作,但也返回这个接口。所以,可以添加任意多个ThenBy()和ThenByDescending()方法,对集合排序。
使用LINQ 查询时,只需把所有用于排序的不同关键字(用逗号分隔开)添加到orderby 子句中例如
var racers = (from r in Formula1.GetChampions() orderby r.Country, r.LastName, r.FirstName select r).Take(10);
6。分组
var countries = from r in Formula1.GetChampions() group r by r.Country into g orderby g.Count() descending, g.Key where g.Count() >= 2 select new { Country = g.Key, Count = g.Count() };
foreach (var item in countries)
{
Response.Write("{0, -10} {1}",item.Country, item.Count);
}
扩展方法的分组表示:
var countries = Formula1.GetChampions().GroupBy(r => r.Country).OrderByDescending(g => g.Count()).
ThenBy(g => g.Key).Where(g => g.Count() >= 2).Select(g => new { Country = g.Key,Count = g.Count()});
7。对嵌套的对象分组
如果分组的对象应包含嵌套的对象,就可以改变select 子句创建的匿名类型。
var countries = from r in Formula1.GetChampions() group r by r.Country into g orderby g.Count() descending, g.Key where g.Count() >= 2 select new
{
Country = g.Key, Count = g.Count(),
Racers = from r1 in g orderby r1.LastName select r1.FirstName + " " + r1.LastName
};
foreach (var item in countries)
{
Response.Write("{0, -10} {1}", item.Country, item.Count);
foreach (var name in item.Racers)
{
Response.Write("{0}; ", name);
}
Response.Write("<br/>");
}
8。连接
使用join 子句可以根据特定的条件合并两个数据源,但之前要获得两个要连接的列表。
var racers = from r in Formula1.GetChampions() from y in r.Years where y > 2003 select new
{
Year = y,
Name = r.FirstName + " " + r.LastName
};
var teams = from t in Formula1.GetContructorChampions() from y in t.Years where y > 2003
select new { Year = y, Name = t.Name };
有了这两个查询,再通过子句join t in teams on r.Year equals t.Year就可以得到结果集了。
var racersAndTeams = from r in racers join t in teams on r.Year equals t.Year select new
{
Year = r.Year,
Racer = r.Name,
Team = t.Name
};
Response.Write("Year Champion " + "Constructor Title");
foreach (var item in racersAndTeams)
{
Response.Write("{0}: {1,-20} {2}",item.Year, item.Racer, item.Team);
}
9。设置操作
扩展方法Distinct()、Union()、Intersect()和Except()都是设置操作。
var ferrariDrivers = from r in Formula1.GetChampions() from c in r.Cars where c == "Ferrari"
orderby r.LastName select r;
现在建立另一个相同的查询,但where 子句的参数不同,以获得所有驾驶McLaren 的冠军。最好不要再次编写相同的查询。而可以创建一个方法,给它传送参数car:
private static IEnumerable<Racer> GetRacersByCar(string car)
{
return from r in Formula1.GetChampions() from c in r.Cars where c == car orderby r.LastName select r;
}
但是,因为该方法不需要在其他地方使用,所以应定义一个委托类型的变量来保存LINQ 查询。变量racerByCar 必须是一个委托类型,它需要一个字符串参数,返回IEnumerable <Racer>,类似于前面实现的方法。为此,定义了几个泛型委托Func<>,所以不需要声明自己的委托。把一个λ表达式赋予变量racerByCar。λ表达式的左边定义了一个car 变量,其类型是Func 委托的第一个泛型参数(字符串)。右边定义了LINQ 查询,它使用该参数和where 子句:
Func<string, IEnumerable<Racer>> racersByCar = Car => from r in Formula1.GetChampions() from c in r.Cars where c == car orderby r.LastName select r;
现在可以使用Intersect()扩展方法,获得驾驶Ferrari 和McLaren 的所有冠军:
Response.Write("World champion with " + "Ferrari and McLaren");
foreach (var racer in racersByCar("Ferrari").
Intersect(racersByCar("McLaren")))
{
Response.Write(racer);
}
10。分区
扩展方法Take()和Skip()等的分区操作可用于分页,例如显示5×5 个赛手。在下面的LINQ 查询中,扩展方法Take()和Skip()添加到查询的最后。Skip()方法先忽略根据页面的大小和实际的页数计算出的项数,再使用方法Take()根据页面的大小提取一定数量的项:
int pageSize = 5;
int numberPages = (int)Math.Ceiling(Formula1.GetChampions().Count()/(double)pageSize);
for (int page = 0; page < numberPages; page++)
{
Response.Write("Page {0}", page);
var racers = (from r in Formula1.GetChampions() orderby r.LastName select r.FirstName + " " + r.LastName).Skip(page * pageSize).Take(pageSize);
foreach (var name in racers)
{
Response.Write(name);
}
Response.Write();
}
11。合计操作符
合计操作符如Count()、Sum()、Min()、Max()、Average()和Aggregate(),不返回一个序列,而返
回一个值。
var query = from r in Formula1.GetChampions() where r.Years.Count() > 3 orderby r.Years.Count() descending select new
{
Name = r.FirstName + " " +r.LastName,
TimesChampion = r.Years.Count()
};
foreach (var r in query)
{
Response.Write("{0} {1}", r.Name, r.TimesChampion);
}
Sum()方法汇总序列中的所有数字,返回这些数字的和。下面的Sum()用于计算一个国家赢得比赛的
总次数。首先根据国家对赛手分组,再在新创建的匿名类型中,给Wins 属性赋予某个国家赢得比赛的总
次数。
var countries = (from c in from r in Formula1.GetChampions() group r by r.Country into c
select new
{
Country = c.Key,
Wins = (from r1 in c select r1.Wins).Sum()
}
orderby c.Wins descending, c.Country select c).Take(5);
foreach (var country in countries)
{
Response.Write("{0} {1}",country.Country, country.Wins);
}
方法Min()、Max()、Average()和Aggregate()的使用方式与Count()和Sum()相同。Min()返回集合
中的最小值,Max()返回集合中的最大值,Average()计算集合中的平均值。对于Aggregate()方法,可
以传送一个λ表达式,对所有的值进行汇总。
12。转换
查询可以推迟到访问数据项时再执行。在迭代中使用查询,查询会执行。而使用转换操作符会立即执行查询,把结果放在数组、列表或字典中。在下面的例子中,调用ToList()扩展方法,立即执行查询,把结果放在List<T>中:
List < Racer > racers = (from r in Formula1.GetChampions()
where r.Starts > 150 orderby r.Starts descending select r).ToList();
foreach (var racer in racers)
{
Response.Write("{0} {0:S}", racer);
}
把返回的对象放在列表中并没有这么简单。例如,对于集合中从赛车到赛手的快速访问,可以使用新类Lookup<TKey, TElement>。
提示:
Dictionary<TKey, TValue>只支持一个键对应一个值。在System.Linq 命名空间的类Lookup<TKey,
TElement>中,一个键可以对应多个值。这些类详见第10 章。
使用复合的from 查询,可以摊平赛手和赛车序列,创建带有Car 和Racer 属性的匿名类型。在返回的Lookup 对象中,键的类型应是表示汽车的string,值的类型应是Racer。为了进行这个选择,可以给ToLookup()方法的一个重载版本传送一个键和一个元素选择器。键选择器表示Car 属性,元素选择器表示Racer 属性。
ILookup<string, Racer> racers = (from r in Formula1.GetChampions() from c in r.Cars select new
{ Car = c, Racer = r }).ToLookup(cr = > cr.Car, cr = > cr.Racer);
if (racers.Contains("Williams"))
{
foreach (var williamsRacer in racers["Williams"])
{
Response.Write(williamsRacer);
}
}
如果需要在未类型化的集合上使用LINQ查询,例如ArrayList,就可以使用Cast()方法。在下面的例子中,基于Object 类型的ArrayList 集合用Racer 对象填充。为了定义强类型化的查询,可以使用Cast()方法。
System.Collections.ArrayList list = new System.Collections.ArrayList(Formula1.GetChampions() as
System.Collections.ICollection);
var query = from r in list.Cast <Racer>() where r.Country == "USA" orderby r.Wins descending select r;
foreach (var racer in query)
{
Response.Write("{0:A}", racer);
}
13。生成操作符
生成操作符Range()、Empty()和Repear()不是扩展方法,而是返回序列的正常静态方法。在LINQto Objects 中,这些方法可用于Enumerable 类。有时需要填充一个范围的数字,此时就应使用Range()方法。这个方法把第一个参数作为起始值,把第二个参数作为要填充的项数。
var values = Enumerable.Range(1, 20);
foreach (var item in values)
{
Response.Write("{0} ", item);
}
Range()方法不返回填充了所定义值的集合,这个方法与其他方法一样,也推迟执行查询,返回一个RangeEnumerator,其中只有一个yield return 语句,来递增值。可以把该结果与其他扩展方法合并起来,获得另一个结果,例如使用Select()扩展方法:
var values = Enumerable.Range(1, 20).Select(n = > n * 3);
Empty()方法返回一个不返回值的迭代器,它可以用于参数需要一个集合,且可以给参数传送空集
合的情形。Repeat()方法返回一个迭代器,该迭代器把同一个值重复特定的次数。