一、揭开linq的神秘面纱
(一)概述
LINQ的全称是Language Integrated Query,中文译成“语言集成查询”。LINQ作为一种查询技术,首先要解决数据源的封装,大致使用了三大组件来实现这个封装,分别是LINQ to Object、LINQ to ADO.NET、LINQ to XML。它们和.NET语言的关系如下:
要使用LINQ来编程,首先要学习使用LINQ的子句以及由查询语法构成的查询表达式。C#3.0和VB9开始将这种查询语法引入到了编程语言,并新增了一系列的关键字。但对于CLR本身来说,它并不了解查询语法,它能理解的是由编程语言的编译器将这种查询语法转换成的方法。这些方法叫“标准查询运算符”,它们具有类似这样的名称——Where、Select、GroupBy、Join。下面就以C#为例,从编程语言的层面来具体介绍这些查询语法(注意VB9也支持这种查询语法)。
LINQ的查询由3基本部分组成:获取数据源,创建查询,执行查询。
// 1,获取数据源 List<int> numbers = new List<int>() { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 }; // 2,创建查询 var numQuery = from num in numbers where num % 2 == 0 select num; // 3,执行查询 foreach (var num in numQuery) { Console.WriteLine("{0,1}", num); }
下图显示了完整的查询操作。在 LINQ 中,查询的执行与查询本身截然不同;换句话说,如果只是创建查询变量,则不会检索任何数据。
如上例所示,Linq的数据源要求必须实现IEnumerable或IEnumerable<T>接口,数组隐式支持这个接口。numQuery叫做查询变量,它存储了一个查询表达式。注意,声明查询变量并不会执行查询,真正的执行查询延迟到了foreach语句中。
(二)与LINQ有关的语言特性
1.隐式类型
(1)源起
在隐式类型出现之前,我们在声明一个变量的时候,总是要为一个变量指定他的类型,甚至在foreach一个集合的时候,也要为遍历的集合的元素,指定变量的类型隐式类型的出现,程序员就不用再做这个工作了。
(2)使用方法
来看下面的代码:
var a = 1; //int a = 1; var b = "123";//string b = "123"; var myObj = new MyObj();//MyObj myObj = new MyObj()
上面的每行代码,与每行代码后面的注释,起到的作用是完全一样的
也就是说,在声明一个变量(并且同时给它赋值)的时候,完全不用指定变量的类型,只要一个var就解决问题了
(3)你担心这样写会降低性能吗?
我可以负责任的告诉你,这样写不会影响性能!上面的代码和注释里的代码,编译后产生的IL代码(中间语言代码)是完全一样(编译器根据变量的值,推导出变量的类型,才产生的IL代码)
(4)这个关键字的好处:
你不用在声明一个变量并给这个变量赋值的时候,写两次变量类型
(这一点真的为开发者节省了很多时间)
在foreach一个集合的时候,可以使用var关键字来代替书写循环变量的类型
(5)注意事项
你不能用var关键字声明一个变量而不给它赋值
因为编译器无法推导出你这个变量是什么类型的。
2.匿名类型
(1) 源起
创建一个对象,一定要先定义这个对象的类型吗?不一定的!
来看看这段代码
(2) 使用
var obj = new {Guid.Empty, myTitle = "匿名类型", myOtherParam = new int[] { 1, 2, 3, 4 } }; Console.WriteLine(obj.Empty);//另一个对象的属性名字,被原封不动的拷贝到匿名对象中来了。 Console.WriteLine(obj.myTitle); Console.ReadKey();
new关键字之后就直接为对象定义了属性,并且为这些属性赋值
而且,对象创建出来之后,在创建对象的方法中,还可以畅通无阻的访问对象的属性
当把一个对象的属性拷贝到匿名对象中时,可以不用显示的指定属性的名字,这时原始属性的名字会被“拷贝”到匿名对象中
(3)注意
如果你监视变量obj,你会发现,obj的类型是Anonymous Type类型的
不要试图在创建匿名对象的方法外面去访问对象的属性!
(4)优点
这个特性在网站开发中,序列化和反序列化JSON对象时很有用
3.自动属性
(1)源起
为一个类型定义属性,我们一般都写如下的代码:
public class MyObj2 { private Guid _id; private string _Title; public Guid id { get { return _id; } set { _id = value; } } public string Title { get { return _Title; } set { _Title = value; } } }
但很多时候,这些私有变量对我们一点用处也没有,比如对象关系映射中的实体类。
自C#3.0引入了自动实现的属性,
以上代码可以写成如下形式:
(2)使用
public class MyObj{ public Guid id { get; set; } public string Title { get; set; } }
这个特性也和var关键字一样,是编译器帮我们做了工作,不会影响性能的
4.初始化器
(1)源起
我们创建一个对象并给对象的属性赋值,代码一般写成下面的样子
var myObj = new MyObj(); myObj.id = Guid.NewGuid(); myObj.Title = "allen";
自C#3.0引入了对象初始化器,
代码可以写成如下的样子
(2)使用
var myObj1 = new MyObj() { id = Guid.NewGuid(), Title = "allen" };
如果一个对象是有参数的构造函数
那么代码看起来就像这样
var myObj1 = new MyObj ("allen") { id = Guid.NewGuid(), Title = "allen" };
集合初始化器的样例代码如下:
var arr = new List<int>() { 1, 2, 3, 4, 5, 6 };
(3)优点
我个人认为:这个特性不是那么amazing,
这跟我的编码习惯有关,集合初始化器也就罢了,
真的不习惯用对象初始化器初始化一个对象!
5.委托
(1)使用
我们先来看一个简单的委托代码
delegate Boolean moreOrlessDelgate(int item); class Program { static void Main(string[] args) { var arr = new List<int>() { 1, 2, 3, 4, 5, 6,7,8 }; var d1 = new moreOrlessDelgate(More); Print(arr, d1); Console.WriteLine("OK"); var d2 = new moreOrlessDelgate(Less); Print(arr, d2); Console.WriteLine("OK"); Console.ReadKey(); } static void Print(List<int> arr,moreOrlessDelgate dl) { foreach (var item in arr) { if (dl(item)) { Console.WriteLine(item); } } } static bool More(int item) { if (item > 3) { return true; } return false; } static bool Less(int item) { if (item < 3) { return true; } return false; } }
这段代码中
<1>首先定义了一个委托类型
delegate Boolean moreOrlessDelgate(int item);
你看到了,委托和类是一个级别的,确实是这样:委托是一种类型
和class标志的类型不一样,这种类型代表某一类方法。
这一句代码的意思是:moreOrlessDelgate这个类型代表返回值为布尔类型,输入参数为整形的方法
<2>有类型就会有类型的实例
var d1 = new moreOrlessDelgate(More); var d2 = new moreOrlessDelgate(Less);
这两句就是创建moreOrlessDelgate类型实例的代码,
它们的输入参数是两个方法
<3>有了类型的实例,就会有操作实例的代码
Print(arr, d1);
Print(arr, d2);
我们把前面两个实例传递给了Print方法
这个方法的第二个参数就是moreOrlessDelgate类型的
在Print方法内用如下代码,调用委托类型实例所指向的方法
dl(item)
6.泛型
(1)为什么要有泛型
假设你是一个方法的设计者,
这个方法有一个传入参数,有一个返回值。
但你并不知道这个参数和返回值是什么类型的,
如果没有泛型,你可能把参数和返回值的类型都设定为Object了
那时,你心里肯定在想:反正一切都是对象,一切的基类都是Object
没错!你是对的!
这个方法的消费者,会把他的对象传进来(有可能会做一次装箱操作)
并且得到一个Object的返回值,他再把这个返回值强制类型转化为他需要的类型
除了装箱和类型转化时的性能损耗外,代码工作的很好!
那么这些性能损耗能避免掉吗?
有泛型之后就可以了!
(2)使用
<1>使用简单的泛型
先来看下面的代码:
var intList = new List<int>() { 1,2,3}; intList.Add(4); intList.Insert(0, 5); foreach (var item in intList) { Console.WriteLine(item); } Console.ReadKey();
在上面这段代码中我们声明了一个存储int类型的List容器
并循环打印出了容器里的值
注意:如果这里使用Hashtable、Queue或者Stack等非泛型的容器
就会导致装箱操作,损耗性能。因为这些容器只能存储Object类型的数据
<2>泛型类型
List<T>、Dictionary<TKey, TValue>等泛型类型都是.net类库定义好并提供给我们使用的
但在实际开发中,我们也经常需要定义自己的泛型类型
来看下面的代码:
public static class SomethingFactory<T> { public static T InitInstance(T inObj) { if (false)//你的判断条件 { //do what you want... return inObj; } return default(T); } }
输出的结果为0
这就是一个自定义的静态泛型类型,
此类型中的静态方法InitInstance对传入的参数做了一个判断
如果条件成立,则对传入参数进行操作之后并把它返回
如果条件不成立,则返回一个空值
注意:
[1]
传入参数必须为指定的类型,
因为我们在使用这个泛型类型的时候,已经规定好它能接收什么类型的参数
但在设计这个泛型的时候,我们并不知道使用者将传递什么类型的参数进来
[2]
如果你想返回T类型的空值,那么请用default(T)这种形式
因为你不知道T是值类型还是引用类型,所以别擅自用null
<3>泛型约束
很多时候我们不希望使用者太过自由
我们希望他们在使用我们设计的泛型类型时
不要很随意的传入任何类型
对于泛型类型的设计者来说,要求使用者传入指定的类型是很有必要的
因为我们只有知道他传入了什么东西,才方便对这个东西做操作
让我们来给上面设计的泛型类型加一个泛型约束
代码如下:
public static class SomethingFactory<T> where T:MyObj
这样在使用SomethingFactory的时候就只能传入MyObj类型或MyObj的派生类型啦
注意:
还可以写成这样
where T:MyObj,new()
来约束传入的类型必须有一个构造函数。
(3)泛型的好处
<1>算法的重用
想想看:list类型的排序算法,对所有类型的list集合都是有用的
<2>类型安全
<3>提升性能
没有类型转化了,一方面保证类型安全,另一方面保证性能提升
<4>可读性更好
这一点就不解释了
7.泛型委托
(1)源起
委托需要定义delgate类型
使用起来颇多不便
而且委托本就代表某一类方法
开发人员经常使用的委托基本可以归为三类,
哪三类呢?
请看下面:
(2)使用
<1>Predicate泛型委托
把上面例子中d1和d2赋值的两行代码改为如下:
//var d1 = new moreOrlessDelgate(More); var d1 = new Predicate<int>(More); //var d2 = new moreOrlessDelgate(Less); var d2 = new Predicate<int>(Less);
把Print方法的方法签名改为如下:
//static void Print(List<int> arr, moreOrlessDelgate<int> dl) static void Print(List<int> arr, Predicate<int> dl)
然后再运行方法,控制台输出的结果和原来的结果是一模一样的。
那么Predicate到底是什么呢?
来看看他的定义:
// 摘要: // 表示定义一组条件并确定指定对象是否符合这些条件的方法。 // // 参数: // obj: // 要按照由此委托表示的方法中定义的条件进行比较的对象。 // // 类型参数: // T: // 要比较的对象的类型。 // // 返回结果: // 如果 obj 符合由此委托表示的方法中定义的条件,则为 true;否则为 false。 public delegate bool Predicate<in T>(T obj);
看到这个定义,我们大致明白了。
.net为我们定义了一个委托,
这个委托表示的方法需要传入一个T类型的参数,并且需要返回一个bool类型的返回值
有了它,我们就不用再定义moreOrlessDelgate委托了,
而且,我们定义的moreOrlessDelgate只能搞int类型的参数,
Predicate却不一样,它可以搞任意类型的参数
但它规定的还是太死了,它必须有一个返回值,而且必须是布尔类型的,同时,它必须有一个输入参数
除了Predicate泛型委托,.net还为我们定义了Action和Func两个泛型委托
<2>Action泛型委托
Action泛型委托限制的就不那么死了,
他代表了一类方法:
可以有0个到16个输入参数,
输入参数的类型是不确定的,
但不能有返回值,
来看个例子:
var d3 = new Action(noParamNoReturnAction); var d4 = new Action<int, string>(twoParamNoReturnAction);
注意:尖括号中int和string为方法的输入参数
static void noParamNoReturnAction() { //do what you want } static void twoParamNoReturnAction(int a, string b) { //do what you want }
<3>Func泛型委托
为了弥补Action泛型委托,不能返回值的不足
.net提供了Func泛型委托,
相同的是它也是最多0到16个输入参数,参数类型由使用者确定
不同的是它规定要有一个返回值,返回值的类型也由使用者确定
如下示例:
var d5 = new Func<int, string>(oneParamOneReturnFunc);
注意:string类型(最后一个泛型类型)是方法的返回值类型
static string oneParamOneReturnFunc(int a) { //do what you want return string.Empty; }
8.匿名方法
(1)源起
在上面的例子中
为了得到序列中较大的值
我们定义了一个More方法
var d1 = new Predicate<int>(More);
然而这个方法,没有太多逻辑(实际编程过程中,如果逻辑较多,确实应该独立一个方法出来)
那么能不能把More方法中的逻辑,直接写出来呢?
C#2.0之后就可以了,
请看下面的代码:
(2)使用
var arr = new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8 }; //var d1 = new moreOrlessDelgate(More); //var d1 = new Predicate<int>(More); var d1 = new Predicate<int>(delegate(int item) { //可以访问当前上下文中的变量 Console.WriteLine(arr.Count); if (item > 3) { return true; } return false; }); Print(arr, d1); Console.WriteLine("OK");
我们传递了一个代码块给Predicate的构造函数
其实这个代码块就是More函数的逻辑
(3)好处
<1>代码可读性更好
<2>可以访问当前上下文中的变量
这个用处非常大,
如果我们仍旧用原来的More函数
想要访问arr变量,势必要把arr写成类级别的私有变量了
用匿名函数的话,就不用这么做了。
9.Lambda表达式
(1)源起
.net的设计者发现在使用匿名方法时,
仍旧有一些多余的字母或单词的编码工作
比如delegate关键字
于是进一步简化了匿名方法的写法
(2)使用
List<int> arr = new List<int>() { 1, 2, 3, 4, 5, 6, 7 }; arr.ForEach(new Action<int>(delegate(int a) { Console.WriteLine(a); })); arr.ForEach(new Action<int>(a => Console.WriteLine(a)));
匿名方法的代码如下:
delegate(int a) { Console.WriteLine(a); }
使用lambda表达式的代码如下:
a => Console.WriteLine(a)
这里解释一下这个lambda表达式
<1>
a是输入参数,编译器可以自动推断出它是什么类型的,
如果没有输入参数,可以写成这样:
() => Console.WriteLine("ddd")
<2>
=>是lambda操作符
<3>
Console.WriteLine(a)是要执行的语句。
如果是多条语句的话,可以用{}包起来。
如果需要返回值的话,可以直接写return语句
10.扩展方法
(1)源起
如果想给一个类型增加行为,一定要通过继承的方式实现吗?
不一定的!
(2)使用
来看看这段代码:
public static void PrintString(this String val) { Console.WriteLine(val); }
消费这段代码的代码如下:
var a = "aaa"; a.PrintString(); Console.ReadKey();
我想你看到扩展方法的威力了。
本来string类型没有PrintString方法
但通过我们上面的代码,就给string类型"扩展"了一个PrintString方法
(1)先决条件
<1>扩展方法必须在一个非嵌套、非泛型的静态类中定义
<2>扩展方法必须是一个静态方法
<3>扩展方法至少要有一个参数
<4>第一个参数必须附加this关键字作为前缀
<5>第一个参数不能有其他修饰符(比如ref或者out)
<6>第一个参数不能是指针类型
(2)注意事项
<1>跟前面提到的几个特性一样,扩展方法只会增加编译器的工作,不会影响性能(用继承的方式为一个类型增加特性反而会影响性能)
<2>如果原来的类中有一个方法,跟你的扩展方法一样(至少用起来是一样),那么你的扩展方法奖不会被调用,编译器也不会提示你
<3>扩展方法太强大了,会影响架构、模式、可读性等等等等....
11.迭代器
· (1)使用
我们每次针对集合类型编写foreach代码块,都是在使用迭代器
这些集合类型都实现了IEnumerable接口
都有一个GetEnumerator方法
但对于数组类型就不是这样
编译器把针对数组类型的foreach代码块
替换成了for代码块。
来看看List的类型签名:
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable
IEnumerable接口,只定义了一个方法就是:
IEnumerator<T> GetEnumerator();
(2)迭代器的优点:
假设我们需要遍历一个庞大的集合
只要集合中的某一个元素满足条件
就完成了任务
你认为需要把这个庞大的集合全部加载到内存中来吗?
当然不用(C#3.0之后就不用了)!
来看看这段代码:
static IEnumerable<int> GetIterator() { Console.WriteLine("迭代器返回了1"); yield return 1; Console.WriteLine("迭代器返回了2"); yield return 2; Console.WriteLine("迭代器返回了3"); yield return 3; }
消费这个函数的代码如下:
foreach (var i in GetIterator()) { if (i == 2) { break; } Console.WriteLine(i); } Console.ReadKey();
输出结果为:
迭代器返回了1 1 迭代器返回了2
大家可以看到:
当迭代器返回2之后,foreach就退出了
并没有输出“迭代器返回了3”
也就是说下面的工作没有做。
(3)yield 关键字
MSDN中的解释如下:
在迭代器块中用于向枚举数对象提供值或发出迭代结束信号。
也就是说,我们可以在生成迭代器的时候,来确定什么时候终结迭代逻辑
上面的代码可以改成如下形式:
foreach (var i in GetIterator()) { if (i == 2) { break; } Console.WriteLine(i); } Console.ReadKey();
(4)注意事项
<1>做foreach循环时多考虑线程安全性
在foreach时不要试图对被遍历的集合进行remove和add等操作
任何集合,即使被标记为线程安全的,在foreach的时候,增加项和移除项的操作都会导致异常
(我在这里犯过错)
<2>IEnumerable接口是LINQ特性的核心接口
只有实现了IEnumerable接口的集合
才能执行相关的LINQ操作,比如select,where等
这些操作,我们接下来会讲到。
二、linq常用方法小试牛刀
1. from子句
创建一个LINQ表达式必须要以from子句开头。
1.1 单个from子句
string[] values = { "中国", "日本", "美国", "菲律宾", "越南" }; //查询包含“国”的字符串 var valueQuery = from v in values where v.IndexOf("国") > 0 select v; foreach (var v in valueQuery) { Console.WriteLine("{0,1}", v); }
在这个LINQ表达式的from子句中,v叫做范围变量,values是数据源。v的作用域存在于当前的LINQ表达式,表达式以外不能访问这个变量。where用来筛选元素,select用于输出元素。这里的范围变量v,和foreach语句中得隐式变量v都可以由编译器推断出其类型。
运行的结果如下:
中国 美国
使用LINQ查询List<T>集合
public class CustomerInfo { public string Name { get; set; } public int Age { get; set; } public string Tel { get; set; } } private void formExpDemo2() { //这里用了,对象和集合初始化器 List<CustomerInfo> customers = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="诸葛菲菲", Age=23, Tel ="1380524****"} }; //查询年龄大于20的客户,注意这里的范围变量用了显示类型CustomerInfo var query = from CustomerInfo ci in customers where ci.Age > 20 select ci; foreach (CustomerInfo ci in query) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); } }
结果:
姓名:欧阳晓晓 年龄:35 电话:1330708**** 姓名:诸葛菲菲 年龄:23 电话:1380524****
1.2 复合from子句
在查询数据源中,元素的属性是一个集合时,可以使用复合from子句对这个属性集合查询。比如,一个客户,可能有多个电话。
public class CustomerInfo { public string Name { get; set; } public int Age { get; set; } public List<string> TelTable { get; set; } } private void formExpDemo() { List<CustomerInfo> customers = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, TelTable=new List<string>{"1330708****","1330709****"}}, new CustomerInfo{ Name="上官飘飘", Age=17, TelTable=new List<string>{"1592842****","1592843****"}}, new CustomerInfo{ Name="诸葛菲菲", Age=23, TelTable=new List<string>{"1380524****","1380525****"}} }; //查询包含电话号码1592842****的客户 var query = from CustomerInfo ci in customers from tel in ci.TelTable where tel.IndexOf("1592842****") > -1 select ci; foreach (var ci in query) { Console.WriteLine("姓名:{0} 年龄:{1}", ci.Name, ci.Age); foreach (var tel in ci.TelTable) { Console.WriteLine(" 电话:{0}", tel); } } }
结果:
姓名:上官飘飘 年龄:17 电话:1592842**** 电话:1592843****
1.3 多个from子句
多个from子句查询和复合from子句从字面上看似乎一样,其实是不同的操作。复合from子句查询的是单个数据源中的子元素的集合,而多个from子句,是载入多个数据源进行查询。
private void formExpDemo() { List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="诸葛菲菲", Age=23, Tel ="1380524****"} }; List<CustomerInfo> clist2 = new List<CustomerInfo> { new CustomerInfo{ Name="令狐冲", Age=25, Tel ="1330708****"}, new CustomerInfo{ Name="东方不败", Age=35, Tel ="1592842****"}, new CustomerInfo{ Name="任盈盈", Age=23, Tel ="1380524****"} }; //在clist中查找Age大于20的客户, //在clist2中查找Age小于30的客户 var query = from customer in clist where customer.Age > 20 from customer2 in clist2 where customer2.Age < 30 select new { customer, customer2 }; foreach (var ci in query) { Console.WriteLine("{0} {1}", ci.customer.Name,ci.customer2.Name); } }
在select语句中,我们用了匿名类型来存储筛选出的元素,这样得到的完全是一个交叉联接表,有点类似于SQL中的笛卡尔乘积。
输出的结果:
欧阳晓晓 令狐冲 欧阳晓晓 任盈盈 诸葛菲菲 令狐冲 诸葛菲菲 任盈盈
2、where子句
where子句的作用就是筛选元素,除了开始和结束位置,where子句几乎可以出现在LINQ表达式的任意位置。一个LINQ表达式中可以有where子句,也可以没有;可以有一个,可以有多个;多个where子句之间的关系相当于逻辑“与”,每个where子句可以包含1个或多个逻辑表达式,这些条件成为“谓词”,多个谓词之间用布尔运算符隔开,比如逻辑“与”用&&,逻辑“或”用||,而不是用SQL中的AND或OR。
2.1 常见的where子句查询
List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"} }; //查询名字是3个字或者姓“令”的,但年龄大于20的客户 var query = from customer in clist where (customer.Name.Length == 3 || customer.Name.Substring(0, 1) == "令") && customer.Age > 20 select customer; foreach (var ci in query) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); }
结果:
姓名:令狐冲 年龄:23 电话:1380524****
2.2 在where子句中使用自定义函数
private void whereExpDemo() { List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"} }; //查询名字是3个字并且姓“令”的客户 var query = from customer in clist where (customer.Name.Length == 3 && CheckName(customer.Name)) select customer; foreach (var ci in query) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); } } private bool CheckName(string name) { if (name.Substring(0, 1) == "令") return true; else return false; }
结果:
姓名:令狐冲 年龄:23 电话:1380524****
2.3 动态谓词的筛选
上面的几个例子都是给定了查询谓词然后进行查询,有时候谓词的数量可能并不固定,是随情况变化的。例如:一组名字可能是运行时动态指定的。
List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"} }; //定义动态的谓词数组,这个数组应该由实际运行环境生成 string[] names = { "令狐冲", "任盈盈", "杨过", "小龙女", "欧阳晓晓" }; //查询在给定谓词数组里存在的客户 var query = from customer in clist where names.Contains(customer.Name) select customer; foreach (var ci in query) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); }
结果:
姓名:欧阳晓晓 年龄:35 电话:1330708**** 姓名:令狐冲 年龄:23 电话:1380524****
3. select子句
LINQ表达式的结果是使用select子句获得的。select子句可以对数据进行转换,这个过程称为“投影”。select子句产生的类容,取决于前面的所有子句及其自身表达式执行后的结果。
3.1 输出查询结果
最简单的select就是直接输出from子句建立的那个范围变量:
var query = from customer in clist where names.Contains(customer.Name) select customer;
也可以输出范围变量类型中得某个属性:
select customer.Name;
或者修改一下再输出:
select customer.Name.Replace("gg","mm");
或者干脆使用一个自定义的函数,把范围变量传进去,输出处理后的结果:
select MyFunction(customer.Name);
3.2 对查询结果进行投影
public class MyCustomerInfo { public string Name { get; set; } public string Tel { get; set; } } private void whereExpDemo() { List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="令狐冲", Age=23, Tel ="1380524****"} }; //定义动态的谓词数组,这个数组应该由实际运行环境生成 string[] names = { "令狐冲", "任盈盈", "杨过", "小龙女", "欧阳晓晓" }; //查询在给定谓词数组里存在的客户 var query = from customer in clist where customer.Age < 30 select new MyCustomerInfo { Name = customer.Name, Tel = customer.Tel }; foreach (var ci in query) { Console.WriteLine("姓名:{0} 电话:{1} 类型{2}", ci.Name, ci.Tel,ci.GetType().FullName); } }
上例中,在select子句中用对象初始化器生成了新的数据类型,从而进行了数据转换,使元素变成了MyCustomerInfo类型。
结果:
姓名:上官飘飘 电话:1592842**** 类型LinqDemo.Form1+MyCustomerInfo 姓名:令狐冲 电话:1380524**** 类型LinqDemo.Form1+MyCustomerInfo
4. group子句
按照语法的规定,LINQ表达式必须以from子句开头,以select或group子句结束,所以除了使用select子句外,也可以使用guoup子句来返回元素分组后的结果。group子句返回的是一个IGrouping<TKey,TElement>泛型接口的对象集合,下面先了解下这个接口。
4.1 IGrouping<TKey,TElement>泛型接口
这个接口表示具有公共键的对象集合,它的原型如下:
public interface IGrouping<TKey, TElement> : IEnumerable<TElement>, IEnumerable
TKey是键的对象类型,在用于group子句的时候,数据类型会有编译器推断出来,它一般用于存储分组的键值;TElement是指的对象类型,用于存储分组的结果,变量基于这个接口的类型就是遍历这个值。
4.2 分组查询
分组查询对于关系型数据库是非常常见的一种操作,但在没有LINQ之前,对内存的对象进行分组却是一件非常麻烦的事情。现在,在LINQ表达式中只需要使用group子句就可以轻松完成对内存对象的分组。
List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="欧阳锦鹏", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官无忌", Age=23, Tel ="1380524****"} }; //按照名字的前2个字进行分组 var query = from customer in clist group customer by customer.Name.Substring(0, 2); foreach (IGrouping<string,CustomerInfo> group in query) { Console.WriteLine("分组键:{0}",group.Key); foreach (var ci in group) { Console.WriteLine("姓名:{0} 电话:{1}", ci.Name, ci.Tel); } Console.WriteLine("***************************************"); }
上例代码,按照form子句建立的范围变量customer的Name属性的前两个字作为键值进行分组。所以TKey的类型是一个字符串类型。
输出结果:
分组键:欧阳 姓名:欧阳晓晓 电话:1330708**** 姓名:欧阳锦鹏 电话:1330708**** *************************************** 分组键:上官 姓名:上官飘飘 电话:1592842**** 姓名:上官无忌 电话:1380524**** ***************************************
再看一个分组的例子:
//按照年龄是否大于20分组 var query = from customer in clist group customer by customer.Age > 20; foreach (var group in query) { Console.WriteLine("分组键:{0}",group.Key); foreach (var ci in group) { Console.WriteLine("姓名:{0} 电话:{1}", ci.Name, ci.Tel); } Console.WriteLine("***************************************"); }
group子句用了一个布尔表达式,所以IGrouping<TKey,TElement>的TKey变成了一个bool型。并且循环遍历的时候可以用var代替IGrouping的声明:
foreach (var group in query)
结果如下:
分组键:True 姓名:欧阳晓晓 电话:1330708**** 姓名:欧阳锦鹏 电话:1330708**** 姓名:上官无忌 电话:1380524**** *************************************** 分组键:False 姓名:上官飘飘 电话:1592842**** ***************************************
5. into子句
into子句作为一个临时标识符,用于select,group,join子句中。
List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="欧阳锦鹏", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官无忌", Age=23, Tel ="1380524****"} }; //按照名字的前两个字进行分组,再用分组Key进行排序 var query = from customer in clist group customer by customer.Name.Substring(0, 2) into gpcustomer orderby gpcustomer.Key descending select gpcustomer; Console.WriteLine("into 用于group子句"); foreach (var group in query) { Console.WriteLine("分组键:{0}", group.Key); foreach (var ci in group) { Console.WriteLine("姓名:{0} 电话:{1}", ci.Name, ci.Tel); } Console.WriteLine("***************************************"); } var query2 = from customer in clist select new { NewName = customer.Name, NewAge = customer.Age } into newCustomer orderby newCustomer.NewAge select newCustomer; Console.WriteLine("into 用于select子句"); foreach (var ci in query2) { Console.WriteLine("{0} 年龄:{1}", ci.NewName, ci.NewAge); }
into子句提供了一个临时标识符,它存储了into子句前面的查询内容,使它后面的子句可以方便的使用,对其进行再次查询,投影等操作。
执行结果:
into 用于group子句 分组键:上官 姓名:上官飘飘 电话:1592842**** 姓名:上官无忌 电话:1380524**** *************************************** 分组键:欧阳 姓名:欧阳晓晓 电话:1330708**** 姓名:欧阳锦鹏 电话:1330708**** *************************************** into 用于select子句 上官飘飘 年龄:17 上官无忌 年龄:23 欧阳晓晓 年龄:35 欧阳锦鹏 年龄:35
6. 排序子句
LINQ可以按元素的一个或多个属性对元素进行排序。LINQ表达式的排序方式分为OrderBy、OrderByDescending、ThenBy、ThenByDescending这四种。
6.1 OrderBy和OrderByDescending
OrderBy用于按元素的值进行升序,语法:
orderby 用于排序的元素的表达式
OrderByDescending用于按元素的值进行降序,语法:
orderby 用于排序的元素的表达式 descending
List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="欧阳锦鹏", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官无忌", Age=23, Tel ="1380524****"} }; //按照年龄升序 var query = from customer in clist orderby customer.Age select customer; Console.WriteLine("按年龄升序排列"); foreach (var ci in query) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); } //按照年龄降序 var query2 = from customer in clist orderby customer.Age descending select customer; Console.WriteLine("\n按年龄降序排列"); foreach (var ci in query2) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); }
运行结果:
按年龄升序排列 姓名:上官飘飘 年龄:17 电话:1592842**** 姓名:上官无忌 年龄:23 电话:1380524**** 姓名:欧阳晓晓 年龄:35 电话:1330708**** 姓名:欧阳锦鹏 年龄:35 电话:1330708**** 按年龄降序排列 姓名:欧阳晓晓 年龄:35 电话:1330708**** 姓名:欧阳锦鹏 年龄:35 电话:1330708**** 姓名:上官无忌 年龄:23 电话:1380524**** 姓名:上官飘飘 年龄:17 电话:1592842****
6.2 ThenBy和ThenByDescending
ThenBy和ThenByDescending用于对元素进行次要排序。基本语法:
orderby 用于排序的元素表达式,用于排序的元素表达式 orderby 用于排序的元素表达式,用于排序的元素表达式 descending
List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="郭靖", Age=17, Tel ="1330708****"}, new CustomerInfo{ Name="黄蓉", Age=17, Tel ="1300524****"} }; //按照年龄升序,再按名字的字数次要排序 var query = from customer in clist orderby customer.Age,customer.Name.Length select customer; Console.WriteLine("按年龄排列,按名字字数进行次要排序"); foreach (var ci in query) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); } //按照年龄升序,再按名字的字数降序次要排序 var query2 = from customer in clist orderby customer.Age, customer.Name.Length descending select customer; Console.WriteLine("\n按年龄排列,按名字字数进行降序次要排序"); foreach (var ci in query2) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); } //按照年龄升序,再按名字的字数降序要排序,在按电话号码进行第三条件排序 var query3 = from customer in clist orderby customer.Age, customer.Name.Length,customer.Tel select customer; Console.WriteLine("\n按年龄,名字字数,电话号码排序"); foreach (var ci in query3) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); }
执行结果:
按年龄排列,按名字字数进行次要排序 姓名:郭靖 年龄:17 电话:1330708**** 姓名:黄蓉 年龄:17 电话:1300524**** 姓名:上官飘飘 年龄:17 电话:1592842**** 姓名:欧阳晓晓 年龄:35 电话:1330708**** 按年龄排列,按名字字数进行降序次要排序 姓名:上官飘飘 年龄:17 电话:1592842**** 姓名:郭靖 年龄:17 电话:1330708**** 姓名:黄蓉 年龄:17 电话:1300524**** 姓名:欧阳晓晓 年龄:35 电话:1330708**** 按年龄,名字字数,电话号码排序 姓名:黄蓉 年龄:17 电话:1300524**** 姓名:郭靖 年龄:17 电话:1330708**** 姓名:上官飘飘 年龄:17 电话:1592842**** 姓名:欧阳晓晓 年龄:35 电话:1330708****
7. let子句
let子句用于在LINQ表达式中存储子表达式的计算结果。let子句创建一个范围变量来存储结果,变量被创建后,不能修改或把其他表达式的结果重新赋值给它。此范围变量可以再后续的LINQ子句中使用。
List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="郭靖", Age=17, Tel ="1330708****"}, new CustomerInfo{ Name="黄蓉", Age=17, Tel ="1300524****"} }; //姓“郭”或“黄”的客户 var query = from customer in clist let g = customer.Name.Substring(0,1) where g == "郭" || g == "黄" select customer; foreach (var ci in query) { Console.WriteLine("姓名:{0} 年龄:{1} 电话:{2}", ci.Name, ci.Age, ci.Tel); }
使用let 建立了个范围变量,这个范围变量在后续的where子句中使用,如果不使用let子句,where子句的表达式将写成这样:
where customer.Name.Substring(0, 1) == "郭" || customer.Name.Substring(0, 1) == "黄"
执行结果:
姓名:郭靖 年龄:17 电话:1330708**** 姓名:黄蓉 年龄:17 电话:1300524****
8. join子句
如果一个数据源中元素的某个属性可以跟另一个数据源中元素的属性进行相等比较,那么这两个数据源可以用join子句进行关联。jion子句用equals关键字进行比较,而不是常见的==。
List<CustomerInfo> clist = new List<CustomerInfo> { new CustomerInfo{ Name="欧阳晓晓", Age=35, Tel ="1330708****"}, new CustomerInfo{ Name="上官飘飘", Age=17, Tel ="1592842****"}, new CustomerInfo{ Name="郭靖", Age=17, Tel ="1330708****"}, new CustomerInfo{ Name="黄蓉", Age=17, Tel ="1300524****"} }; List<CustomerTitle> titleList = new List<CustomerTitle> { new CustomerTitle{ Name="欧阳晓晓", Title="歌手"}, new CustomerTitle{ Name="郭靖", Title="大侠"}, new CustomerTitle{ Name="郭靖", Title="洪七公徒弟"}, new CustomerTitle{ Name="黄蓉", Title="才女"}, new CustomerTitle{ Name="黄蓉", Title="丐帮帮主"} }; //根据姓名进行内部联接 Console.WriteLine("内部联接"); var query = from customer in clist join title in titleList on customer.Name equals title.Name select new { Name = customer.Name, Age = customer.Age, Title = title.Title }; foreach (var ci in query) { Console.WriteLine("姓名:{0} 年龄:{1} {2}", ci.Name, ci.Age, ci.Title); } //根据姓名进行分组联接 Console.WriteLine("\n根据姓名进行分组联接"); var query2 = from customer in clist join title in titleList on customer.Name equals title.Name into tgroup select new { Name = customer.Name, Titles = tgroup }; foreach (var g in query2) { Console.WriteLine(g.Name); foreach (var g2 in g.Titles) { Console.WriteLine(" {0}", g2.Title); } } //根据姓名进行 左外部联接 Console.WriteLine("\n左外部联接"); var query3 = from customer in clist join title in titleList on customer.Name equals title.Name into tgroup from subTitle in tgroup.DefaultIfEmpty() select new { Name = customer.Name, Title = (subTitle == null ? "空缺" : subTitle.Title) }; foreach (var ci in query3) { Console.WriteLine("姓名:{0} {1} ", ci.Name, ci.Title); }
要仔细理解上例的,内联接,分组联接,以及左联接。
执行结果:
内部联接 姓名:欧阳晓晓 年龄:35 歌手 姓名:郭靖 年龄:17 大侠 姓名:郭靖 年龄:17 洪七公徒弟 姓名:黄蓉 年龄:17 才女 姓名:黄蓉 年龄:17 丐帮帮主 根据姓名进行分组联接 欧阳晓晓 歌手 上官飘飘 郭靖 大侠 洪七公徒弟 黄蓉 才女 丐帮帮主 左外部联接 姓名:欧阳晓晓 歌手 姓名:上官飘飘 空缺 姓名:郭靖 大侠 姓名:郭靖 洪七公徒弟 姓名:黄蓉 才女 姓名:黄蓉 丐帮帮主
原文地址:https://www.cnblogs.com/wyh19941210/p/8128005.html