C# LINQ学习笔记一:走进LINQ的世界
本笔记摘抄自:,记录一下学习过程以备后续查用。
linq 简介:
语言集成查询(linq)是visual studio 2008和.net framework 3.5版中引入的一项创新功能。
传统上,针对数据的查询都是以简单的字符串表示,而没有编译时类型检查或intellisense支持。此外,您还必须针对以下各种数据源学习一种不同的查询
语言:sql数据库、xml文档、各种web服务等。通过linq,可以使用语言关键字和熟悉的运算符针对强类型化对象集合编写查询。
在visual studio中,可以为以下数据源编写linq查询:sql server数据库、xml文档、ado.net数据集以及支持 ienumerable或泛型 ienumerable<t>
接口的任意对象集合,使用要求:项目 ≥ .net framework 3.5。
一、linq查询
查询是一种从数据源检索数据的表达式。随着时间的推移,人们已经为各种数据源开发了不同的语言。例如,用于关系数据库的sql和用于xml的xquery。
因此,开发人员不得不针对他们必须支持的每种数据源或数据格式而学习新的查询语言。linq通过提供一种跨数据源和数据格式使用数据的一致模型,简化
了这一情况。在linq查询中,始终会用到对象。可以使用相同的编码模式来查询和转换xml文档、sql数据库、ado.net数据集、.net集合中的数据以及
对其有linq提供程序可用的任何其他格式的数据。
1.1 查询操作的三个部分
操作三部曲:①取数据源 ②创建查询 ③执行查询
下面代码演示linq to object:
class program { static void main(string[] args) { #region linq to object //查询三部曲:1、获取数据源 var nums = new int[7] { 0, 1, 2, 3, 4, 5, 6 }; //查询三部曲:2、创建查询 var query = from num in nums where (num % 2) == 0 select num; //查询三部曲:3、执行查询 foreach (var num in query) { console.write($"{num} "); } console.read(); #endregion } }
运行结果如下:
下图显示了完整的查询操作。在linq中,查询的执行与查询本身截然不同。换句话说,查询本身指的是只创建查询变量,不检索任何数据。
1.2 数据源
在上一个示例中(linq to object),由于数据源是数组,因此它隐式支持泛型 ienumerable<t> (可枚举)接口。支持 ienumerable<t> 或派生接口(如
泛型iqueryable<t>)的类型称为可查询类型。
可查询类型不需要进行修改或特殊处理就可以用作linq数据源。如果源数据还没有作为可查询类型出现在内存中,则linq提供程序必须以此方式表示源数
据。例如,linq to xml将xml文档加载到可查询的 xelement 类型中。
下面代码演示linq to xml:
创建一个test.xml文件,放在主目录下。
<documentelement> <category> <mo_no>moa1911070001</mo_no> <mrp_no>8198712090963008</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110701</bil_no> </category> <category> <mo_no>moa1911070002</mo_no> <mrp_no>8193000000003172</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110702</bil_no> </category> <category> <mo_no>moa1911070003</mo_no> <mrp_no>8193002043133003</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110702</bil_no> </category> <category> <mo_no>moa1911070004</mo_no> <mrp_no>8193002043133004</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110702</bil_no> </category> <category> <mo_no>moa1911070005</mo_no> <mrp_no>8193002043133005</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110702</bil_no> </category> <category> <mo_no>moa1911070006</mo_no> <mrp_no>8198922092971001</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110703</bil_no> </category> <category> <mo_no>moa1911070007</mo_no> <mrp_no>8198922092971002</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110703</bil_no> </category> <category> <mo_no>moa1911070008</mo_no> <mrp_no>8198922092971010</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110703</bil_no> </category> <category> <mo_no>moa1911070009</mo_no> <mrp_no>8198922092971200</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110703</bil_no> </category> <category> <mo_no>moa1911070010</mo_no> <mrp_no>8199862094443008</mrp_no> <qty>1.00000000</qty> <bil_no>mpa19110704</bil_no> </category> </documentelement>
class program { static void main(string[] args) { #region linq to xml var xe = xelement.load(@"..\..\test.xml"); var query = from item in xe.descendants("category") select item; foreach (var item in query) { console.writeline($"mo_no={item.element("mo_no").value}"); } console.read(); #endregion } }
运行结果如下:
在linq to sql中,首先需要生成对象模型映射到关系数据库,然后针对这些对象编写查询,由linq to sql在运行时处理与数据库的通信。
准备一:下文中用到的northwind数据库,下载地址为:
准备二:若本机没有安装linq to sql工具,可参考安装教程:
准备三:添加新建项,选择linq to sql类,命名为sample。
准备四:服务器资源管理器的数据连接中,右键添加连接,依导向连接至sql server数据库。
准备五:将相关数据表拖至sample.dbml中并保存。
准备六:添加引用system.data.linq。
下面代码演示linq to sql:
class program { static void main(string[] args) { #region linq to sql var db = new sampledatacontext(); var query = from cust in db.customers where cust.city == "london" select cust; foreach (var item in query) { console.writeline($"customerid={item.customerid}"); } console.read(); #endregion } }
运行结果如下:
查询指定要从数据源中检索的信息,可以指定在返回这些信息之前如何对其进行排序、分组和结构化。 查询存储在查询变量中,并用查询表达式进行初始
化。之前的示例中的查询是从整数数组中返回所有的偶数。该查询表达式包含三个子句:from、where和select。如果您熟悉sql,您会注意到这些子句的
顺序与sql中的顺序相反,from 子句指定数据源,where 子句指定应用筛选器,select 子句指定返回的元素的类型。目前需要注意的是,在linq中,查询
变量本身不执行任何操作并且不返回任何数据,它只是存储在以后某个时刻执行查询时为生成结果而必需的信息。
1.4 查询执行
1.延迟执行
如前所述,查询变量本身只是存储查询命令,实际的查询执行会延迟到在foreach语句中循环访问查询变量时发生,此概念称为“延迟执行”。
2.强制立即执行
对一系列源元素执行聚合函数的查询必须首先循环访问这些元素,count、max、average和first就属于此类查询。由于查询本身必须使用foreach以便
返回结果,因此这些查询在执行时不使用显式foreach语句。另外还要注意,这些类型的查询返回单个值,而不是ienumerable集合。
class program { static void main(string[] args) { #region linq查询强制立即执行一 var nums = new int[7] { 0, 1, 2, 3, 4, 5, 6 }; var query = from num in nums where (num % 2) == 0 select num; var numcount = query.count(); console.writeline($"numcount={numcount}"); console.read(); #endregion } }
若要强制立即执行任意查询并缓存其结果,可以调用tolist<tsource>或toarray<tsource>方法。
class program { static void main(string[] args) { #region linq查询强制立即执行二 var nums = new int[7] { 0, 1, 2, 3, 4, 5, 6 }; var query2 = (from num in nums where (num % 2) == 0 select num).tolist(); var query3 = (from num in nums where (num % 2) == 0 select num).toarray(); console.writeline($"numcount={query2.count}"); console.writeline($"numcount={query3.length}"); console.read(); #endregion } }
运行结果如下:
二、基本 linq 查询操作
2.1 获取数据源:from
在linq查询中,第一步是指定数据源。像在大多数编程语言中一样,必须先声明变量,才能使用它。在linq查询中,最先使用from子句的目的是引入数据
源和范围变量。
class program { static void main(string[] args) { #region linq to sql var db = new sampledatacontext(); //query是ienumerable<cutsomer>类型 //数据源(db.customers)和范围变量(cust) var query = from cust in db.customers where cust.city == "london" select cust; foreach (var item in query) { console.writeline($"customerid={item.customerid}"); } console.read(); #endregion } }
范围变量类似于foreach循环中的迭代变量,但在查询表达式中,实际上不发生迭代。执行查询时,范围变量将用作对customers中的每个后续元素的引用。
因为编译器可以推断cust的类型,所以您不必显式指定此类型。
2.2 筛选:where
也许最常用的查询操作是应用布尔表达式形式的筛选器,此筛选器使查询只返回那些表达式结果为true的元素。使用where子句生成结果,实际上,筛选器
指定从源序列中排除哪些元素。
您可以使用熟悉的c#逻辑and(&&)和or(||)运算符来根据需要在where子句中应用任意数量的筛选表达式。
class program { static void main(string[] args) { #region linq 筛选:where var db = new sampledatacontext(); var query1 = from cust in db.customers where cust.city == "london" && cust.customerid == "arout" select cust; var query2 = from cust in db.customers where cust.city == "london" || cust.city == "paris" select cust; foreach (var item in query1) { console.writeline($"query1->city={item.city},customerid={item.customerid}"); } foreach (var item in query2) { console.writeline($"query2->city={item.city},customerid={item.customerid}"); } console.read(); #endregion } }
运行结果如下:
2.3 排序:orderby
通常可以很方便地将返回的数据进行排序。orderby子句将使返回的序列中的元素按照被排序的类型的默认比较器进行排序。
class program { static void main(string[] args) { #region linq 排序:orderby var db = new sampledatacontext(); var query = from cust in db.customers where cust.city == "london" orderby cust.customerid select cust; foreach (var item in query) { console.writeline($"customerid={item.customerid}"); } console.read(); #endregion } }
运行结果如下:
2.4 分组:group
使用group子句,您可以按指定的键分组结果。
class program { static void main(string[] args) { #region linq 分组一:group var db = new sampledatacontext(); var query = from cust in db.customers where cust.city == "london" || cust.city == "paris" group cust by cust.city; foreach (var group in query) { console.writeline(group.key); foreach (var cust in group) { console.writeline($"city={cust.city},customerid={cust.customerid}"); } } console.read(); #endregion } }
运行结果如下:
在本例中,cust.city是键。
在使用group子句结束查询时,结果采用列表的列表形式。列表中的每个元素是一个具有key成员及根据该键分组的元素列表的对象。在循环访问生成组
序列的查询时,您必须使用嵌套的foreach循环。外部循环用于循环访问每个组,内部循环用于循环访问每个组的成员。
如果您必须引用组操作的结果,可以使用into关键字来创建可进一步查询的标识符。
class program { static void main(string[] args) { #region linq 分组二:group var db = new sampledatacontext(); var query = from cust in db.customers where cust.city == "london" || cust.city == "paris" group cust by cust.city into custgroup where custgroup.count() > 2 orderby custgroup.key select custgroup; foreach (var group in query) { console.writeline(group.key); foreach (var cust in group) { console.writeline($"city={cust.city},customerid={cust.customerid}"); } } console.read(); #endregion } }
运行结果如下:
2.5 联接:join
联接运算创建数据源中没有显式建模的序列之间的关联。例如,您可以执行联接来查找位于同一地点的所有客户和经销商。在linq中,join子句始终针对
对象集合而非直接针对数据库表运行。
class program { static void main(string[] args) { #region linq 联接:join var db = new sampledatacontext(); var qurey = from order in db.orders join cust in db.customers on order.customerid equals cust.customerid select new { order.orderid, order.customerid, cust.contactname }; foreach (var item in qurey.take(5)) { console.writeline($"orderid={item.orderid},customerid={item.customerid},contactname={item.contactname}"); } console.read(); #endregion } }
运行结果如下:
2.6 选择(投影):select
select子句生成查询结果并指定每个返回的元素的“形状”或类型。
例如,您可以指定结果包含的是整个customer对象、仅一个成员、成员的子集或者某个基于计算或新对象创建的完全不同的结果类型。当select子句生成除源
元素副本以外的内容时,该操作称为“投影”。
三、使用 linq 进行数据转换
语言集成查询 (linq) 不但是检索数据的利器,而且还是一个功能强大的数据转换工具。通过使用linq查询,您可以将源序列用作输入,并采用多种方式修改
它以创建新的输出序列。您可以通过排序及分组来修改该序列,而不必修改元素本身。但是,linq 查询的最强大的功能是能够创建新类型。这一功能在select子
句中实现。
3.1 将多个输入联接到一个输出序列
/// <summary> /// 学生类 /// </summary> class student { public string name { get; set; } public int age { get; set; } public string city { get; set; } public list<int> scores { get; set; } } /// <summary> /// 教师类 /// </summary> class teacher { public string name { get; set; } public int age { get; set; } public string city { get; set; } } class program { static void main(string[] args) { #region linq 将多个输入联接到一个输出序列 //创建第一个数据源 var students = new list<student>() { new student() { age = 19, city = "广州", name = "小a", scores = new list<int>(){85,88,83,97} }, new student() { age = 19, city = "深圳", name = "小b", scores = new list<int>(){86,80,85,92} } }; //创建第二个数据源 var teachers = new list<teacher>() { new teacher() { age = 30, city = "广州", name = "张a" }, new teacher() { age = 31, city = "广州", name = "李a" } }; //创建查询 var query = ( from student in students where student.city == "广州" select student.name ).concat ( from teacher in teachers where teacher.city == "广州" select teacher.name ); //执行查询 foreach (var person in query) { console.writeline(person); } console.read(); #endregion } }
运行结果如下:
3.2 选择各个源元素的子集
1. 若要只选择源元素的一个成员,请使用点运算。
var query = from cust in db.customers select cust.city;
2. 若要创建包含源元素的多个属性的元素,可以使用具有命名对象或匿名类型的对象初始值设定项。
var query = from cust in db.customer select new { cust.name, cust.city };
3.3 将内存中的对象转换为xml
/// <summary> /// 学生类 /// </summary> class student { public string name { get; set; } public int age { get; set; } public string city { get; set; } public list<int> scores { get; set; } } class program { static void main(string[] args) { #region linq 将内存中的对象转换为xml //创建数据源 var students = new list<student>() { new student() { age = 19, city = "广州", name = "小a", scores = new list<int>(){85,88,83,97} }, new student() { age = 19, city = "深圳", name = "小b", scores = new list<int>(){86,80,85,92} } }; //创建查询 var studentstoxml = new xelement ( "root", from student in students let x = $"{student.scores[0]},{student.scores[1]},{student.scores[2]},{student.scores[3]}" select new xelement ( "student", new xelement("name", student.name), new xelement("age", student.age), new xelement("scores", x) ) ); //执行查询 console.writeline(studentstoxml); console.read(); #endregion } }
运行结果如下:
3.4 对源元素执行操作
输出序列可能不包含源序列的任何元素或元素属性,它可能是通过将源元素用作输入参数计算出的值的序列。
class program { static void main(string[] args) { #region linq 对源元素执行操作 //数据源 double[] radius = { 1, 2, 3 }; //创建查询 var query = from radiu in radius select $"{3.14 * radiu * radiu}"; //执行查询 foreach (var item in query) { console.writeline(item); } console.read(); #endregion } }
运行结果如下:
四、linq 查询操作的类型关系
linq 查询操作在数据源、查询本身及查询执行中是强类型的。查询中变量的类型必须与数据源中元素的类型和foreach语句中迭代变量的类型兼容。强类型可
以保证在编译时捕获类型错误,以便及时改正。
4.1 不转换源数据的查询
下图演示不对数据执行转换的linq to object查询操作。源包含一个字符串序列,查询输出也是一个字符串序列。
①数据源的类型参数决定范围变量的类型。
②select语句返回name属性,而非完整的customer对象。因为name是一个字符串,所以custnamequery的类型参数是string,而非customer。
③因为custnamequery是一个字符串序列,所以foreach循环的迭代变量也必须是string。
4.2 转换源数据的查询
下图演示对数据执行简单转换的linq to sql查询操作。查询将一个customer对象序列用作输入,并只选择结果中的name属性。因为name是一个字符串,
所以查询生成一个字符串序列作为输出。
①数据源的类型参数决定范围变量的类型。
②select语句返回name属性,而非完整的customer对象。因为name是一个字符串,所以custnamequery的类型参数是string,而非customer。
③因为custnamequery是一个字符串序列,所以foreach循环的迭代变量也必须是string。
下图演示另一种转换。select 语句返回只捕获原始customer对象的两个成员的匿名类型。
①数据源的类型参数始终为查询中的范围变量的类型。
②因为select语句生成匿名类型,所以必须使用var隐式类型化查询变量。
③因为查询变量的类型是隐式的,所以foreach循环中的迭代变量也必须是隐式的。
4.3 让编译器推断类型信息
您也可以使用关键字,可用于查询操作中的任何局部变量。但是,编译器为查询操作中的各个变量提供强类型。
五、linq 中的查询语法和方法语法
我们编写的linq查询语法,在编译代码时,clr会将查询语法转换为方法语法。这些方法调用标准查询运算符的名称类似where、select、groupby、join、
max和average,我们也是可以直接使用这些方法语法的。
查询语法和方法语法语义相同,但是,许多人员发现查询语法更简单、更易于阅读。某些查询必须表示为方法调用。例如,必须使用方法调用表示检索元素
的数量与指定的条件的查询,还必须使用方法需要检索元素的最大值在源序列的查询。system.linq命名空间中的标准查询运算符的参考文档通常使用方法语法。
5.1 标准查询运算符扩展方法
class program { static void main(string[] args) { #region linq 标准查询运算符扩展方法 var nums = new int[4] { 1, 2, 3, 4 }; //创建查询表达式 var query1 = from num in nums where num % 2 == 0 orderby num descending select num; console.writeline("query1's result:"); foreach (var num in query1) { console.writeline(num); } //使用方法进行查询 var query2 = nums.where(num => num % 2 == 0).orderbydescending(num => num); console.writeline("query2's result:"); foreach (var num in query2) { console.writeline(num); } console.read(); #endregion } }
运行结果如下:
两个示例的输出是相同的。您可以看到两种形式的查询变量的类型是相同的:ienumerable<t>。
若要了解基于方法的查询,让我们进一步地分析它。注意,在表达式的右侧,where子句现在表示为对numbers对象的实例方法,在您重新调用该对象时其类型
为ienumerable<int>。如果您熟悉泛型 ienumerable<t> 接口,那么您就会了解,它不具有where方法。但是,如果您在visual studio ide中调用intellisense完成
列表,那么您不仅将看到where方法,而且还会看到许多其他方法,如select、selectmany、join 和orderby。
下面是所有标准查询运算符:
尽管看起来ienumerable<t>似乎已被重新定义以包括这些附加方法,但事实上并非如此,这些标准查询运算符都是作为“扩展方法”实现的。
5.2 lambda 表达式
在前面的示例中,通知该条件表达式 (num % 2 == 0) 是作为内联参数。where方法:where (num => num % 2 == 0) 此内联表达式称为lambda表达式。将代码
编写为匿名方法或泛型委托或表达式树是一种便捷的方法,否则编写起来就要麻烦得多。=>是lambda运算符,可读为“goes to”。运算符左侧的num是输入变量,
与查询表达式中的num相对应。编译器可推断num的类型,因为它了解numbers是泛型ienumerable<t>类型。lambda表达式与查询语法中的表达式或任何其他c#
表达式或语句中的表达式相同,它可以包括方法调用和其他复杂逻辑,“返回值”就是表达式结果。
5.3 查询的组合性
在上面的代码示例中,请注意orderby方法是通过在对where的调用中使用点运算符来调用的。where生成筛选序列,然后orderby通过对该序列排序来对它进行
操作。因为查询会返回ienumerable,所以您可通过将方法调用链接在一起,在方法语法中将这些查询组合起来。这就是在您通过使用查询语法编写查询时编译器在
后台所执行的操作,并且由于查询变量不存储查询的结果,因此您可以随时修改它或将它用作新查询的基础,即使在执行它后。