您的位置:首页 > 其它

Linq 学习笔记<转帖>

2012-02-15 21:37 225 查看

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()方法返回一个迭代器,该迭代器把同一个值重复特定的次数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: