• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

十四、C#支持标准查询运算符的集合接口

原作者: [db:作者] 来自: [db:来源] 收藏 邀请
支持标准查询运算符的集合接口。
System.Linq.Enumeralbe类提供的一些常用的API 来执行集合处理
1、匿名类型
2、隐匿类型的局部变量
3、集合初始化器
4、集合
5、标准查询运算符
 
本章主要讨论泛型集合接口。
非泛型的集合类,待查。
 
一、匿名类型和隐式类型的局部变量声明
C#3.0增强。
1、匿名类型
一种特殊的数据类型,它最终是由编译器声明的,而非通过已定义好的类来声明的。
和匿名函数相似,当编译器看到一个匿名类型时,会自动执行一些后台操作,生成必要的代码,
允许像显式声明的那样使用它。
 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             var patent1 = new
 6             {
 7                 Title = "xxm1",
 8                 YearOfPublication = "1977"
 9             };
10             var patent2 = new
11             {
12                 Title = "xxm2",
13                 YearOfPublication = "1978"
14             };
15             var patent3 = new
16             {
17                 patent1.Title,
18                 //重新命名属性
19                 Year = patent2.YearOfPublication
20             };
21  
22             Console.WriteLine(patent1.Title + ":" + patent1.YearOfPublication);
23             Console.WriteLine(patent2.Title + ":" + patent2.YearOfPublication);
24             Console.WriteLine(patent3.Title + ":" + patent3.Year);
25  
26             Console.WriteLine();
27             Console.WriteLine(patent1);
28             Console.WriteLine(patent2);
29             Console.WriteLine(patent3);
30  
31             Console.ReadLine();
32  
33  
34  
35         }
36     }
37  
38 输出:
39 xxm1:1977
40 xxm2:1978
41 xxm1:1978
42  
43 { Title = xxm1, YearOfPublication = 1977 }
44 { Title = xxm2, YearOfPublication = 1978 }
45 { Title = xxm1, Year = 1978 }

 

 
 
匿名类型完全是由C#编译器实现的,而不会在"运行时"内有显式实现。
具体地说,当编译器遇到匿名类型的语法时,会自动生成一个CIL类,
其属性和匿名类型声明中命名的值和数据类型是对应的。
 
2、隐式类型的局部变量
由于根据定义,匿名类型是没有名称的,所以不可能将一个局部变量显式声明为匿名类型。
相反,局部变量的类型要替换成var。
假如将一个匿名类型赋给一个隐式类型的变量,那么在为局部变量生成的CIL代码中,它的数据类型就是
编译器生成的类型,类似地,如果将一个string赋给隐式类型的变量,那么在最终生成的CIL中,它的数据类型就是
string。事实上,对于隐式类型的变量来说,假如赋给它的是一个非匿名的类型如:string,那么最终生成的CIL代码和
直接声明为string类型并无区别。
 
1 string text=" this is a test of the ...";
2 //<====>
3 var text="this is a test of the ...";

 

这两个语句最终生成的CIL代码是完全一样的。
 
虽然C#的匿名类型没有可用的名称,但它仍然是强类型的。
比如:类型的属性是完全 可以访问的。
对于匿名类型来说,是不可能指定数据类型的,所以必须使用var。
 
3、匿名类型和隐式局部变量的更多注意事项
在匿名类型进行声明时,如果所赋值的是一个属性或者字段调用,名称就无需要指定(也可以指定)。
1             var patent3 = new
2             {
3                 patent1.Title,
4                 //重新命名属性
5                 Year = patent2.YearOfPublication
6             };

 

 
如果两个匿名类型的属性名称和顺序以及数据类型都完全匹配的话,系统在编译时只为这两个
匿名类型声明生成一个数据类型。
 
所以,只有属性名、数据类型和属性顺序完全匹配,才类型兼容。
 
匿名类型是不可变的,一经实例化,再更改它的某个属性,会生成编译错误。
 
在声明一个方法时,不可能装饰它的某个参数声明为隐式数据类型(var)。
在创建匿名类型的那个方法的内部,只能以及两种方式将匿名类型的实例传到方法外部。
首先,如果方法的参数是object类型,则匿名类型的实例可以传到方法外部,因为匿名类型会隐匿地转换。
第二种方式是使用方法类型推导,在这种情况下,匿名类型的实例以一个方法的“类型参数”的形式来传递,
编译器能成功推导出具体的类型。
 
所以,使用Function(patent1)调用void Method<T>(T parameter)会成功地通过编译,尽管在Function()内部,
parameter允许的操作仅限于object支持的那些操作。
 
匿名类型是C#3.0支持“投射”的关键。
 
匿名类型的生成:
虽然Console.WriteLine(patent1) 默认调用了ToString(),但匿名类型的ToString()已经得到重写,
编译器在生成匿名类型的代码时,重写了ToString()方法。类似地,在生成的类型中也重要了Equals()和GetHashCode()的实现。
所以,一旦属性的顺序发生了变化,就会生成最终生成一个全新的数据类型。
假如不是这样设置,而是为属性顺序不同的两个匿名类型生成同一个类型,
那么一个实现在属性顺序上发生的变化,就会对另一个实现的ToString()输出造成显式 的、甚至可能是让人不可接受的影响。
 
除此之外,在程序执行的时候,有可能反射一个类型,并检查类型的成员-----甚至动态调用其中一个成员(所谓“动态调用成员 ”,是指在
程序运行期间,根据具体的情况来决定调用哪个成员)。如果两个貌似相同的类型在成员顺序上有所区别,就可能造成出乎预料的结果。
为了避免这些问题,C#的设计者最终决定:假如属性的顺序不同,就生成两个不同的类型。
 
二、集合初始化器
C#3.0新增的另一个特性是集合初始化器。
使用集合初始化器,程序员可以采取和数组声明相似的方式,在一个集合的实例化期间用一套初始的成员来构造这个集合。
如果没有集合初始化器,就只有在集合实例化好之后,才能将成员显式 添加到集合中。
 
 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5  
 6             List<string> sevenWorldBlunders = new List<string>();
 7             sevenWorldBlunders = new List<string>()
 8             {
 9                 "Wealth without work",
10                 "Pleasure without conscience",
11                 "Knowledge without character",
12             };
13  
14  
15             Print(sevenWorldBlunders);
16  
17         }
18         private static void Print<T>(IEnumerable<T> items)
19         {
20             foreach (T item in items)
21             {
22                 Console.WriteLine(item);
23             }
24  
25         }
26     }

 

 
这个语法不仅和数组初始化的语法相似,也和对象初始化器语法相似。
 
集合初始化器要想成功编译,需满足几个基本条件。理想情况下,集合类型应该实现了
System.Collections.Generic.ICollection<T>接口。
或者在实现了IEnumerable<T>的类型上存在一个或多个Add方法。
 
匿名类型不可以使用集合初始化器。有几种解决方法。待查。
 
三、是什么使用类成为一个集合:IEnumberable<T>
按照定义,.NET中的一个集合本质上是一个类,它最起码实现了IEnumerable<T>(从技术角度说应该是非泛型类型
IEnumberable。
要想支持对集合执行遍历,最起码的要求就是实现由IEnumberable<T>规定的方法。
 注:“运行时”根本不知foreach语句为何物。
1、foreach和数组
1             int[] arr = new[] { 1, 2, 3, 4, 5 };
2  
3             foreach (int item in arr)
4             {
5                 Console.WriteLine(item);
6             }

 

 
基于这段代码,C#编译器会用CIL来创建一个等价的for循环。
foreach在这个例子中,要依赖于Length属性和数组索引运算符[]的支持。
 
2、foreach和IEnumerable<T>
 
不是所有类型的集合都包含已知数量的元素。
除此之外,许多集合类,包括Stack<T>、Queue<T>以及 Dictionary<Tkey,Tvalue>,都不支持按照索引来获取元素。
因此,需要一种更常规的方式来遍历元素集合。迭代器(iterator)模式提供了这个能力。
只要你能确定第一个元素、下一个元素和最后一个元素,就不需要事先知道元素总数,也不需要按照索引来获取元素。
 
System.Collections.Generic.IEnumerator<T>和非泛型System.Collections.Generic.IEnumerator接口的设计目标就是允许用
迭代器模式来遍历元素集合,同时放弃使用:以上使用的长度---索引(length---index)模式。
 
IEnumerator<T>从IEnumerator派生,后者包含3个成员。
第一个成员是bool MoveNext()。
第二个成员是只读属性Current(返回当前元素。
利用这两个成员,只需要用一个while循环就可以遍历集合。
 
但是有两个重要的实现细节:交错和错误处理
 
2.1 状态共享
假如同时有两个循环交错遍历同一个集合(一个foreach中嵌套了另一个foreach),
那么集合必须维持当前元素的一个状态指示器,确保当调用MoveNext()时,能正确定位下一个元素。
现在的问题是,交错的循环可能相互干扰(假如循环由多个线程执行,那么会发生同样的问题)。
 
为了解决这个问题,集合类不直接支持IEnumerator<T>和IEnumerator接口。
还有第二个接口,它名为IEnumerable<T>,它唯一的方法就是GetEnumerator()。
这个方法的作用是返回支持IEnumerator<T>的一个对象。在这里,不是由集合类维持状态。
相反,是由一个不同的类(通常是一个嵌套类,以便访问到集合内部)来支持IEnumerator<T>接口,
并负责维护循环遍历的状态。
枚举数相当于一个"游标“或者”书签“。
可以有多个书签,移动每个书签 都可独立于其他书签来遍历集合。
 
2.2 清理状态
由于是由实现了IEnumerator<T>接口的类来维持状态,所以在退出循环之后,有时需要对状态进行清理。
为此IEnumerator<T>接口派生于IDisposable。实现IEnumerator的枚举数不一定要实现IDisposable。
但是,假如实现了IDisposable,就一样会调用Dispose()方法。
 
没有IEnumerable的foreach:
从技术上说,编译器为了用foreach在一个数据类型上迭代,并不要求一定要支持IEnumerator<T>/IEnumerator。
相反,编译器采用一个称为"Duck typing"的概念。
Duck typing待查。
 
3、foreach循环内不要修改集合
 
第3章讲过,编译器禁止对foreach变量标识符进行赋值。
 
假如在foreach循环期间对集合进行了修改,重新访问枚举数就会引发System.
InvalidOperationException类型的异常,指出在枚举数实例化之后,集合已经发生了变化。
迭代器也会受影响。
 
四、标准查询运算符
 
如果将System.Object定义的方法排除在外,那么实现IEnumerable<T>的任何类型都只有一个方法,
即GetEnumerator()。
事实上,任何类型在实现IEnumerable<T>之后,都有超过50个方法可供使用。
其中还不包括重载的版本。
只需要显式实现接口中的GetEnumerator()方法。
其它的附加的功能是由C#3.0的扩展方法来提供的。
所有方法都在System.Linq.Enumerable类中定义。
所以,为了用到这些方法,只需要简单地添加以下语句:
using System.Linq;
 
IEnumerable<T>上的每个方法都是一个标准查询运算符(standard query operator)。
它提供了对它操作的集合进行查询的能力。
 
示例类:
  1     class Program
  2     {
  3         static void Main(string[] args)
  4         {
  5             IEnumerable<Patent> patents = PatentData.Patents;
  6  
  7             Print(patents);
  8  
  9             Console.WriteLine();
 10  
 11             IEnumerable<Inventor> inventors = PatentData.Inventors;
 12  
 13             Print(inventors);
 14  
 15             Console.ReadLine();
 16  
 17  
 18  
 19  
 20         }
 21         public static void Print<T>(IEnumerable<T> items)
 22         {
 23             foreach (T item in items)
 24             {
 25                 Console.WriteLine(item);
 26             }
 27         }
 28         //专利类
 29         public class Patent
 30         {
 31             public string Title { get; set; }
 32  
 33             public string YearOfPublication { get; set; }
 34             public string ApplicationNumber { get; set; }
 35             public long[] InventorIds { get; set; }
 36             public override string ToString()
 37             {
 38                 return string.Format("{0}({1})", Title, YearOfPublication);
 39             }
 40         }
 41         //发明者类
 42         public class Inventor
 43         {
 44             public long Id { get; set; }
 45             public string Name { get; set; }
 46             public string City { get; set; }
 47             public string State { get; set; }
 48             public string Country { get; set; }
 49             public override string ToString()
 50             {
 51                 return string.Format("{0}({1},{2})", Name, City, State);
 52             }
 53         }
 54  
 55         //实际数据
 56         public static class PatentData
 57         {
 58             public static readonly Inventor[] Inventors = new Inventor[] {
 59                 new Inventor(){
 60                     Name="Benjamin Franklin",City="Philadelphia",
 61                     State="PA",Country="USA",Id=1
 62                 },
 63                 new Inventor(){
 64                     Name="Orville Wright",City="Kitty Hawk",
 65                     State="NC",Country="USA",Id=2
 66                 },
 67                 new Inventor(){
 68                     Name="Wilbur Wright",City="Kitty Hawk",
 69                     State="NC",Country="USA",Id=3
 70                 },
 71                 new Inventor(){
 72                     Name="Samuel Morse",City="New York",
 73                     State="NY",Country="USA",Id=4
 74                 },
 75                 new Inventor(){
 76                     Name="George Stephenson",City="Wylam",
 77                     State="Northumberland",Country="UK",Id=5
 78                 },
 79                 new Inventor(){
 80                     Name="John Michaelis",City="Chicago",
 81                     State="IL",Country="USA",Id=6
 82                 },
 83                 new Inventor(){
 84                     Name="Mary Phelps Jacob",City="New York",
 85                     State="NY",Country="USA",Id=7
 86                 },
 87             };
 88  
 89             public static readonly Patent[] Patents = new Patent[] {
 90                 new Patent(){
 91                     Title="Bifocals",YearOfPublication="1784",
 92                     InventorIds=new long[]{1}
 93                 },
 94                 new Patent(){
 95                     Title="Phonograph",YearOfPublication="1877",
 96                     InventorIds=new long[]{1}
 97                 },
 98                 new Patent(){
 99                     Title="Kinetoscope",YearOfPublication="1888",
100                     InventorIds=new long[]{1}
101                 },
102                 new Patent(){
103                     Title="Electrical Telegraph",YearOfPublication="1837",
104                     InventorIds=new long[]{4}
105                 },
106                 new Patent(){
107                     Title="Flying machine",YearOfPublication="1903",
108                     InventorIds=new long[]{2,3}
109                 },
110                 new Patent(){
111                     Title="Steam Locomotive",YearOfPublication="1815",
112                     InventorIds=new long[]{5}
113                 },
114                 new Patent(){
115                     Title="Droplet deposition apparatus",YearOfPublication="1989",
116                     InventorIds=new long[]{6}
117                 },
118                 new Patent(){
119                     Title="Backless Brassiere",YearOfPublication="1914",
120                     InventorIds=new long[]{7}
121                 },
122             };
123         }
124     }

 

 
以下的的Lambda表达的形式参数的类型都与集合中的元素类型一致。
1、使用Where()来筛选
 
为了从集合中筛选出某些数据,需要提供一个筛选器方法返回true或false,从而表明
一个特定的元素是否应该被包含进来。
获取一个实参,并返回一个布尔值的委托表达式称为一个“谓词”。
 
1             IEnumerable<Patent> patents = PatentData.Patents;
2  
3             patents = patents.Where(
4                 patent => patent.YearOfPublication.StartsWith("18")
5                 );
6  
7             Print(patents);

 

 
注意:代码将Where()的输出结果赋还给IEnumerable<T>。
IEnumerable<T>.Where()输出的是一个新的IEnumerable<T>的集合。以上的代码返回的是IEnumerable<Patent>。
 
Where()方法的表达式实参并非一定是在赋值时求值的。这一点适用于许多标准查询运算符。
在Where()的情况下,表达式传给集合,“保存”起来但不马上执行。
相反,只有在需要遍历集合中的项时,才会真正对表达式进行求值。
 
应该将Where()方法理解为只是描述了集合中应该出现什么,它没有涉及更实际的工作。
 
2、使用Select()来投射
由于IEnumerable<T>.Where()输出的是一个新的
IEnumerable<T>集合,所以可以在这个集合的基础上再调用另一个标准查询运算符
使用Select()进行转换。
1             IEnumerable<Patent> patents = PatentData.Patents;
2  
3             IEnumerable<Patent> patents1800 = patents.Where(
4                 patent => patent.YearOfPublication.StartsWith("18")
5                 );
6             IEnumerable<string> items = patents1800.Select(item => item.ToString());
7  
8             //Print(patents);
9             Print(items);

 

在此代码中,创建了一个新的IEnumerable<string>集合。虽然添加了一个Select()调用,但并未
造成输出有任何改变。
显然,针对每一个数据项,都会发生一次转换:从原始集合的Patent类型转换成items集合的string类型。
 
 1             IEnumerable<string> filelist = Directory.GetFiles("D:\\");
 2             IEnumerable<FileInfo> files = filelist.Select(file => new FileInfo(file));
 3             Print(files);
 4             //注:以上的Lambda表达的形式参数的类型都与集合中的元素类型一致。
 5 匿名类型:
 6             IEnumerable<string> filelist = Directory.GetFiles("D:\\");
 7             var items = filelist.Select(file =>
 8             {
 9                 FileInfo fileInfo = new FileInfo(file);
10                 return new { FileName = fileInfo.Name, Size = fileInfo.Length };
11             });
12             Print(items);

 

 
使用Where()标准查询运算符,在“垂直”方向上筛选一个集合(减少集合项目中元素的数量)。
现在使用Select()标准查询运算符,还可以在“水平”方向上缩减集合的规模(减少列的数量)或者对数据进行彻底的转换。
 
综合运用Where()和Select(),可以获得原始集合的一个子集,从而满足当前算法的要求。
 
LINQ查询的并行运行:
修改程序支持多线程。
1             IEnumerable<string> filelist = Directory.GetFiles("D:\\");
2             var items = filelist.AsParallel().Select(file =>
3             {
4                 FileInfo fileInfo = new FileInfo(file);
5                 return new { FileName = fileInfo.Name, Size = fileInfo.Length };
6             });
7             Print(items);

 

 
代码中发生的变化使并行 支持变得轻而易举。
唯一要做就是利用.NET Framework 4引入的标准查询运算符AsParallel()。
这是静态类System.Linq.ParallelEnumerable的一个成员。
使用这个简单的扩展方法,“运行时”一边遍历fileList中的数据项,一边返回结果对象,这两个操作是并行发生的。
 
3、用Count()对元素进行计数。
 
使用Count()来统计所有元素的数量,或者获取一个谓词作为参数,只对谓词表达式指明的数据项进行计数。
 
1             IEnumerable<Patent> patents = PatentData.Patents;
2             Console.WriteLine("Patent Count:{0}", patents.Count());
3             Console.WriteLine("Patent Count in 1800s:{0}",
4                 patents.Count(
5                 patent => patent.YearOfPublication.StartsWith("18")
6                 ));

 

 
虽然Count()语句写起来很简单,但IEnumerable<T>没有改变,所以真正执行的代码仍然会遍历集合中的所有项。
如果集合直接提供了一个Count属性,就应首选这个属性,而不要用LINQ的Count()方法。
幸好,ICollection<T>包含了Count属性,所以如果一个集合支持ICollection<T>,那么在它上面调用Count()方法,会对集合
进行转型,并直接调用Count。然后,如果不支持ICollection<T>,Enumerable.Count()就会枚举集合中的所有项,而不是调用内
建的Count机制。
如果计数的目的只是为了看这个计数是否大于0,那么首选的做法是使用Any()运算符
1 if(patents.Any())
2 {  }

 

Any()只尝试遍历集合中的一个项,如果成功就返回true。它不会遍历整个序列。
 
4、推迟执行
使用LINQ时,一个重要概念就是推迟执行。
 1             IEnumerable<Patent> patents = PatentData.Patents;
 2             bool result;
 3             patents = patents.Where(patent =>
 4                 {
 5                     if (result = patent.YearOfPublication.StartsWith("18"))
 6                     {
 7                         Console.WriteLine(" 
                       
                    
                    

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap