LINQ标准查询操作符学习笔记

        Linq是语言集成查询的简称。Linq简化了语言查询,是的程序员可以使用相同的语法查询不同的数据源。

        本文是闲暇之余学习Linq的产物。代码是花了大约一周的业余时间组织的,敲出来只是为了记住它。本文使用的Linq查询包含了两种方式,一种是直接查询,另一种是使用System.Linq中定义的扩展方法进行的查询。对于扩展方法查询的方式,使用了大量的Lambda表的是和系统定义的委托Func<T>。不熟悉这两个东西的童鞋们可以在本博找到相关的文章:

Lambda表达式学习记录】【Action<T>和Func<T>委托

一、准备供查询的数据 

本文首先构建一个提供数据的实体类供以后的查询使用。本文使用的实体类包含了1950到2008年一级方程式锦标赛的冠军车队和冠军赛手。这些数据是使用了实体类和列表来准备的。

首先定义代表赛手的实体类Racer。Racer类定义了几个属性和一个重载的ToString()方法,该方法以指定的格式显示赛手。Racer类实现了IFormattable接口,以支持格式字符串的不同变体。这个类还实现类IComparable<T>接口,用以根据赛手的LastName进行排序。为了执行高级查询Racer类同时包含了单值属性如FirstName、LastName、Wins(获胜次数)、Country(国籍)和Starts。同时也包含了多值属性如:Cars(赛手在获得冠军的年份所使用的赛车)和Years(赛手获得冠军的年份,赛手可以多次获得冠军)。这个类具体的实现如下:

Racer

第二个实体类是Team。这个类包含了车队冠军的名字和获得冠军的年份。

Racer

第三个类是Formula1.这个类定义方法GetChampions方法返回一组表示以及方程式赛车冠军的列表。

Racer

这个类还定义了一个方法:GetConstructorChampions()。用于返回所有车队冠军的列表。

        二、System.Linq中的扩展方法 

       扩展方法的作用是将方法写入最初没有定义该方法的类中。还可以将方法添加到实现某个特定接口的任何类中,这样多个类就可以使用相同的实现代码。对于扩展方法本文不做详细介绍,只是扩展方法在Linq查询中使用较多需要预先了解。尤其是System.Core程序集下定义的System.Linq方法。本文后边将会用到。

        三、使用Linq进行查询 

有了这些预备知识和实体类,我们就可以使用Linq进行查询了。

       1、简单的筛选

使用Where子句可以对数据源进行简单的筛选。下变例子是找出至少赢得15场比赛的奥地利车手:

        /// <summary>
/// 1、最简单的查询
/// </summary>
static void LinqQuery1()
{
var query = from r in Formula1.GetChampions()
where (r.Country == "Brazil" || r.Country == "Austria") && r.Wins > 15
orderby r.Wins descending
select r;
foreach (var racer in query)
{
Console.WriteLine("{0:A}", racer);
}
}

这段代码的功能可以使用System.Linq中的扩展方法来实现:

【代码】

            var query2 = Formula1.GetChampions()
.Where(r => r.Country == "Brazil" || r.Country == "Austria")
.Select(r=>r);

注:并不是所有的查询都可以使用Linq查询或者扩展方法都可以实现的。高级查询需要使用扩展方法。

       2、用索引进行筛选

Where()扩展方法的一个重载中,可以对该方法传递第二个参数:索引。索引是筛选器返回的每个结果的计数器,可以在表达式中使用索引,执行一些索引相关的计算。现编的代码使用Where()扩展方法,返回姓氏以A开头,索引为偶数的车手:

        /// <summary>
/// 2、使用索引i进行筛选
         /// </summary>
static void LinqQuery2()
{

//使用索引i进行筛选
             var query = Formula1.GetChampions()
.Where((r, i) => r.LastName.StartsWith("A") && i % 2 != 0);
foreach (var item in query)
{
Console.WriteLine("{0:A}", item);
}
}

       3、筛选出不同的数据类型

使用OfType()扩展方法,可以实现基于类型的筛选。下面的代码中定义了包含了string和int类型的对象,使用OfType()方法从集合中找出字符串:

        /// <summary>
/// 3、类型筛选
         /// </summary>
static void LinqQuery3()
{
object[] data = { "one", 1, 2, 3, "two", "three" };
var query = data.OfType<string>();

foreach (var item in query)
{
Console.WriteLine(item);
}
}

       4、复合的from子句

复合的from子句用于对这样一种情况的查询:需要根据对象的一个成员进行筛选,而这个成员本身是一组数据。

本例中,Racer类定义的属性Cars就是这样的一个属性。Cars是一个字符串数组。

使用如下的Linq查询可以筛选出嘉实法拉利的所有冠军:

            var query = from r in Formula1.GetChampions()
from c in r.Cars
where c == "Ferrari"
orderby r.LastName
select r;

其中,第一个from子句用于访问从Formyla1.GetChampions方法返回的Racer对象,第二个from子句访问Racer类的Cars属性,返回所有string类型的赛车,最后使用Where子句从这些赛车中筛选出所有冠军。

这里C#编译器将符合的from子句和Linq查询转换成SelectMany()方法,SelectMany()方法可以用于迭代序列的序列。使用SelectMany()扩展方法的代码如下:

            var query = Formula1.GetChampions()
.SelectMany(r => r.Cars, (r, c) => new { Racer = r, Car = c })
.Where(r => r.Car == "Ferrari")
.OrderBy(r => r.Racer.FirstName)
.Select(r => r.Racer.FirstName + " " + r.Racer.LastName);

这里SelectMany()方法的第一个参数是隐式参数,他从GetChampions方法中接收Racer对象序列,第二个参数是collectionSelector委托,其中定义了内部序列。在Lambda表达式r=>r.Cars中,返回赛车集合。第三个参数是一个Func<T>委托,这里为每个Car调用该委托接收Racer和Cars对象。Lambda表达式创建了一个包含了Racer和Cars属性的匿名的类型。这个SelectMany方法的结果摊平了赛手和赛车的层次结构,为每个赛车返回匿名类型的一个新对象耦合。

这段解释有点拗口,因为Lambda表达式确实比较难以解释,还有使用了几个Func<T>委托,不了解委托看这段代码简直就是天书。不过VS的职能提示挺管用的,在敲出几个代码之后看看他的提示在继续写也是不错的。

       5、对筛选结果进行排序

使用orderby和orderby descending子句或者OrderBy()和OrderByDescending()扩展方法可以实现对筛选结果的排序。

下面这段代码是对筛选出来的赛手使用赢得比赛的次数进行排序:

            //简单的Linq语句查询排序
var query1 = from r in Formula1.GetChampions()
where r.Country == "Brazil"
orderby r.Wins descending
select r;

转换成扩展方法后的实现:

            //使用扩展方法
var query2 = Formula1.GetChampions()
.Where(r => r.Country == "Brazil")
.OrderByDescending(r => r.Wins)
.ThenByDescending(r=>r.LastName)
.Select(r => r);

最后还可以使用Take方法对结果进一步筛选:

            //使用Take子句提取前十项数据
var query3 = (from r in Formula1.GetChampions()
orderby r.Country, r.LastName, r.FirstName
select r)
.Take(10);

//使用扩展方法查询
var query = Formula1.GetChampions()
.OrderBy(r => r.Country)
.ThenBy(r => r.LastName)
.ThenBy(r => r.FirstName)
.Take(10)
.Select(r => r);

       6、对筛选结果进行分组

使用group子句和GroupBy()扩展方法可以对查询结果进行分组。下边的例子是将赛车冠军按照国家进行分组,并列出一个国家赛车冠军的总数:

            //简ò单蹋inq分?组哩?
var query = 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()
};

子句group r by r.Country into g 根据Country属性组合所有赛车手到一个新的标识符g中。g用于以后访问分组的结果信息。group子句的结果根据扩展方法Count()的结果进行排序。Select子句创建了一个带Country和Count属性的匿名类型。这里有一个对象需要注意:g.Key。这指的是是group方法筛选的依据,本例中g.Key就是分组依据Country。

直接使用扩展方法中的GroupBy()和ThenBy()方法也可以实现相应的筛选功能:

            var query = 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() });

这里可以看出子句group r by r.Country into g被解析为GroupBy(r=>r.Country),返回分组序列,之后进行了排序和筛选等见大的操作即得到了需要的结果。

       7、对嵌套对象进行分组

如果分组的对象应包含嵌套的序列,就可以改变select子句创建的匿名类型。先看下面的代码:

            var countrys =
from r in Formula1.GetChampions()
group r by r.Country into g //g是?按恪?照?国ú别纄分?组哩?后ó的?Formula1.GetChampions()
orderby g.Count() descending, g.Key //g.Key指?的?是?分?组哩?依皑?据Yr.Country 即′r中D的?Country
where g.Count() >= 2
select
new
{
Country = g.Key,
Count = g.Count(),
Racer = from r1 in g
orderby r1.LastName
select r1.FirstName + " " + r1.LastName
};

在上面的例子中,返回的国家不仅包含国家名和赛手数量这两个属性,还包括赛手姓名序列。这个序列用一个赋予Racers属性的from/in内部子句指定,内部的from子句使用分组标识符g获取该分组中的所有赛车手,并排序,再根据姓名创建一个新的字符串返回。

这段代码转换成扩展方法可表示如下:

            var countrys = 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(),
Racer = g.OrderBy(r => r.LastName).Select(r => r.FirstName + " " + r.LastName)
});

       8、连接查询

使用join子句可以根据特定的条件合并两个数据源,但之前要获得两个要连接的列表。

在一级方程式比赛中,有比赛冠军和车队冠军,下边的代码用来筛选出每年的赛手冠军和车队冠军。实现这一功能有几种方法,下面一一列举:

(1)使用多次查询

先定义两个查询,分别找出2003年之后每年的赛手冠军和车队冠军:

//1、查询1
 var racer = from r in Formula1.GetChampions()
from y in r.Years
where y > 2003
select new
{
Year = y,
Name = r.FirstName + " " + r.LastName
};

//2、查询2
 var teams = from t in Formula1.GetConstructorChampions()
from y in t.Years
where y > 2003
select new
{
Year = y,
Name = t.Name
};

之后在通过join in …on…进行连接:

//3、将两次查询结果连接
var query = from r in racer
join t in teams on r.Year equals t.Year
select new
{
Year = r.Year,
Racer = r.Name,
Team = t.Name
};

(2)也可将两次查询合并为一个Linq查询 ,不过比较麻烦的说:

//使用一条语句完成所有连接查询
var query2 = from r in
from
r1 in Formula1.GetChampions()
from yr in r1.Years
where yr > 2003
select new
{
Year = yr,
Name = r1.FirstName + " " + r1.LastName
}
join t in
from
t1 in Formula1.GetConstructorChampions()
from yt in t1.Years
where yt > 2003
select new
{
Year = yt,
Name = t1.Name
}
on r.Year equals t.Year
select new
{
Year = r.Year,
Racer = r.Name,
Team = t.Name
};

       9、集合查询

System.Linq中的扩展方法Distinct()、Union()、Intersect()、Except()的都是集合操作的方法。这些方法使用不难,主要涉及到一些对集合操作的理解上。

下面用一段代码演示集合操作符的使用:

//1、先进性一次简单的查询
var query = from r in Formula1.GetChampions()
from y in r.Years
where y > 2003
select r;
//2、再对结果进行集合操作.这里的几个操作没有实际意义,只是为了演示用法。
 var q = query.Union(query).Distinct().Intersect(query);

上面的代码中,先进行了一个间的的查询,之后对查询结果进行了集合操作。以下是上面代码转换成使用扩展方法的代码:

 //3、直接使用扩展方法进行查询
var query2 = Formula1.GetChampions()
.SelectMany(r => r.Years, (r, y) => new { Racer = r, Year = y }) //使用了SelectMany()嵌套查询
.Where(r => r.Year > 2003)
.Select(r => r.Racer)
.Union(query).Distinct().Intersect(query); //仅演示,没实际意义

       10、合并

合并操作Zip()是.NET4新增的。这个操作运行用一个谓词函数把两个相关的序列合并为一个。我不是很理解这个合并操作,仅给出树上的代码,不做解释:

var racerNames = from r in Formula1.GetChampions()
where r.Country == "Italy"
orderby r.Wins descending
select new
{
Name = r.FirstName + " " + r.LastName
};
var racerNameAndStarts = from r in Formula1.GetChampions()
where r.Country == "Italy"
orderby r.Wins descending
select new
{
LastName = r.LastName,
Starts = r.Starts
};
//.Net4中的Zip()方法。用一个谓词函数把两个相关的序列合并为一个。注意Zip的参数
var racers = racerNames.Zip(racerNameAndStarts, (first, second) => first.Name + ",starts " + second.Starts);
foreach (var item in racers)
{
Console.WriteLine(item);
}

       11、分区

扩展方法Take()和Skip()等用于分区操作,这些操作可用于数据分页。在下面的Linq查询中,把扩展方法添加到查询最后,Skip()方法或略掉根据分页大小和页数得到的项数,Take()方法根据分页大小提取一定数目的项:

int pagesize = 5;
int numberPages = (int)Math.Ceiling(Formula1.GetChampions().Count / (double)pagesize);
for (int i = 0; i < numberPages; i++)
{
Console.WriteLine("Page {0}", i);
var racers = (from r in Formula1.GetChampions()
orderby r.LastName
select r.FirstName + " " + r.LastName)
.Skip(i * pagesize) //跳?过y指?定¨数y目?的?元a素?
.Take(pagesize) //提á取?指?定¨数y目?的?元a素?
.TakeWhile(r => r.Length > 11); //额?外a的?限T制?条?件t
foreach (var item in racers)
{
Console.WriteLine(item);
}
Console.WriteLine();

输出结果是几组赛手。

       12、聚合操作

聚合操作用于对集合项进行简单的运算,返回结果是一个值而不是一组数据。

下面演示使用Count()方法筛选出冠军次数操作3的赛手:

//统计获得冠军次数大于3次的运动员和获胜次数
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 item in query)
{
Console.WriteLine(item.Name + " " + item.TimesChampion.ToString());
}
Console.WriteLine();

又如Sum()方法,返回序列中的所有数字的和。下面的示例演示了使用Sum()方法计算一个国际赢得的比赛次数,首先根据国家对赛手分组,在创建的匿名类中,对Wins属性进行计算:

//计算一个国际获得冠军的总次数
var countries =
from c in
from
r in Formula1.GetChampions()
group r by r.Country into c1
select new
{
Country = c1.Key,
Wins = (from r1 in c1 select r1.Wins).Sum()
}
orderby c.Wins descending
select
c;

       13、结果转换

查询结果可以通过调用扩展方法转换成其他类型。但是需要注意的是,如果进行了类型转换,Linq查询将会立即执行而不是推迟到访问数据项时才执行。

下面是一个简单的例子:

//简单查询
var query = (from r in Formula1.GetChampions()
where r.Starts > 150
orderby r.Starts descending
select
r).ToList();
foreach (var item in query)
{
Console.WriteLine("{0} {0:S}",item);
}
Console.WriteLine();

//扩展方法查询
var query2 = Formula1.GetChampions()
.Where(r => r.Starts > 150)
.OrderByDescending(r => r.Starts)
.ToList()
.Select(r => r) ;
foreach (var item in query2)
{
Console.WriteLine("{0} {0:S}", item);
}
Console.WriteLine();
//生成查找表
var query3 = (from r in Formula1.GetChampions()
from c in r.Cars
select new { Car = c, Racer = r })
.ToLookup(cr => cr.Car, cr => cr.Racer);
if (query3.Contains("Williams"))
{
foreach (var item in query3["Williams"])
{
Console.WriteLine(item);
}
}

这里建查询结果使用ToList()方法转换成了List<T>类型。

       14、生成操作符

生成操作符Ranger()、Empty()、Repear()不是扩展方法,而是返回序列的正常静态方法。

下面例子返回一个填充了一个范围数字的变量,使用了Ranger()方法对变量进行填充

//var values = Enumerable.Range(1, 12);
var values = Enumerable.Range(1, 12)
.Where(r=>r%2 == 0)
.Select(r => r * 2);
foreach (var item in values)
{
Console.Write("{0} ",item);
}
Console.WriteLine();

       15、并行Linq

并行Linq是.Net4新增加的。在.Net 4的System.Linq命名空间中新增加了类ParallelEnumerable,可以分解查询工作使其工作在多个线程上。

这里首先准备一个大型集合,用随机数填充之后使用Linq筛选数据,获取筛选数据的总和。该查询使用where子句定义一个筛选器,汇总值小于20的项,最后使用Sum()方法计算: 

const int arraySize = 100000000;
var data = new int[arraySize];
var r = new Random();
for (int i = 0; i < arraySize; i++)
{
data[i] = r.Next(40);
}
var sum = (from x in data.AsParallel()
where arraySize < 20
select x).Sum();
sum = data.AsParallel().Where(x => x > 20).Select(x => x).Sum();
Console.WriteLine(sum);

可以看出,这里的查询与前面的查询仅仅是使用了AsParallel()方法。

运行这段代码时启动任务管理器就可以看见效果了。此时系统上所有的CPU都处于忙碌状态,如果删除了AsParallel方法,就不会有这个效果了。

终于写完了,最后感谢你看了我的文字并附上本文所用的代码:

https://files.cnblogs.com/zyqgold/MyLinqTest.rar

Technorati 标签: Linq
原文地址:https://www.cnblogs.com/zyqgold/p/1936382.html