编写高质量代码改善C#程序的157个建议[IEnumerable<T>和IQueryable<T>、LINQ避免迭代、LINQ替代迭代]
前言
本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html 。本文主要学习记录以下内容:
建议29、区别LINQ查询中的IEnumerable<T>和IQueryable<T>
建议30、使用LINQ取代集合中的比较器和迭代器
建议31、在LINQ查询中避免不必要的迭代
建议29、区别LINQ查询中的IEnumerable<T>和IQueryable<T>
LINQ查询方法一共提供了两类扩展方法,在System.Linq命名空间下,有两个静态类:
Enumerable类,它针对继承了IEnumerable<T>接口的集合类进行扩展。
Queryable类,它针对继承了IQueryable<T>接口的集合类进行扩展。
稍加观察我们会发现,接口IQueryable<T>实际也是继承了IEnumerable<T>接口的,所以致使这两个接口额方法在很大成都上是一致的。简单的来表述就是:本地数据源用IEnumerable<T>,远程数据源用IQueryable<T>。
LINQ查询从功能上来讲实际上可以分为三类:LINQ to OBJECTS、LINQ to SQL、LINQ to XML。设计Enumerable<T>和Queryable<T>两套接口的原因是为了区别对待LINQ to OBJECTS、LINQ to SQL,两者对于查询的处理在内部使用的是完全不同的机制。针对LINQ to OBJECTS时,使用Enumerable中的扩展方法对本地集合进行排序和查询等操作,查询参数接受的是Func<>。Func<>叫做谓语表达式,相当于一个委托。针对LINQ to SQL时,则使用Queryable中的扩展方法,它接受的参数是Expression<>。Expression<>用于包装Func<>。LINQ to SQL引擎最终会将表达式树转化成为相应的SQL语句,然后在数据库中执行。
那么到底什么时候使用IQueryable<T>,什么时候使用IEnumerable<T>呢?我们来简单的看一个例子:
[Table(Name = "Employees")] public class Employees { [Column(IsPrimaryKey = true,Name="EmployeeID")] public int Id { get; set; } [Column] public string FirstName { get; set; } [Column] public string LastName { get; set; } [Column] public string Title { get; set; } } }
DataContext dataContext = new DataContext(ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString); Table<Employees> employees = dataContext.GetTable<Employees>(); var temp1 = (from p in employees where p.Title.StartsWith("S") select p).AsEnumerable<Employees>(); var temp2 = from p in temp1 where p.FirstName.ToUpper().IndexOf("A") > 0 select p; foreach (var item in temp2) { Console.WriteLine(string.Format("FirstName:{0}\tLastName:{1}\t Title:{2}",item.FirstName,item.LastName,item.Title)); } Console.ReadLine();
通过上面的代码可以发现,虽然我们针对temp1使用的是延迟求值,但是在整个LINQ查询语句的最后对结果使用了AsEnumerable方法,这相当于将远程数组转成了本地数据。通过数据库的见识工具也可以验证这一点。
现在来看另外一个查询,其实还是上面的查询只是做了简单的修改
DataContext dataContext = new DataContext(ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString); Table<Employees> employees = dataContext.GetTable<Employees>(); var temp1 = from p in employees where p.Title.StartsWith("S") select p; var temp2 = from p in temp1 where p.FirstName.ToUpper().IndexOf("A") > 0 select p; foreach (var item in temp2) { Console.WriteLine(string.Format("FirstName:{0}\tLastName:{1}\t Title:{2}",item.FirstName,item.LastName,item.Title)); } Console.ReadLine();
通过监控可以发现它是组合两个查询语句,而生成了一条SQL,如果不理解这一点,那么在编写程序时将会造成性能损耗。在LINQ to SQL的查询中,要尽量始终使用IQueryable<T>。
在使用IQueryable<T>和IEnumerable<T>的时候还需要注意一点,IEnumerable<T>查询的逻辑可以直接用我们自己所定义的方法,IQueryable<T>则不能使用自定义的方法,它必须先生成表达式树,查询由LINQ to SQL引擎处理。在使用IQueryable<T>查询的时候,如果使用自定义的方法,则会抛出异常。
建议30、在查询中使用Lambda表达式
http://www.cnblogs.com/aehyok/p/3631483.html可以查看之前写过的一篇文章中的建议10,来回顾一下比较器。
可以发现以上方式实现的排序至少存在两个问题:
1)可扩展性太低,如果存在新的排序要求,就必须实现新的比较器。
2)对代码的侵入性太高,为类型继承了接口,增加了新的 方法。
那么有没有一种方法,即使类型只存在自动实现的属性,也能满足多方面的排序要求呢?答案是使用LINQ。LINQ提供了类似于SQL的语法来实现遍历、筛选与投影集合的功能。借助于LINQ的强大功能。
来看使用LINQ之后的代码:
public class Salary { /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 基本工资 /// </summary> public int BaseSalary { get; set; } /// <summary> /// 奖金 /// </summary> public int Bouns { get; set; } } static void Main(string[] args) { List<Salary> array = new List<Salary>(); array.Add(new Salary() { Name = "aehyok", BaseSalary = 12000, Bouns = 500 }); array.Add(new Salary() { Name = "Kris", BaseSalary = 11200, Bouns = 400 }); array.Add(new Salary() { Name = "Leo", BaseSalary = 18000, Bouns = 300 }); array.Add(new Salary() { Name = "Niki", BaseSalary = 20000, Bouns = 700 }); Console.WriteLine("根据BaseSalary排序:"); var list=from p in array orderby p.BaseSalary select p; foreach (Salary item in list) { Console.WriteLine("Name={0},\tBaseSalary={1},\tBouns={2}",item.Name,item.BaseSalary,item.Bouns); } Console.WriteLine("根据Bouns排序"); var listBouns=from p in array orderby p.Bouns select p; foreach (Salary item in listBouns) { Console.WriteLine("Name={0},\tBaseSalary={1},\tBouns={2}", item.Name, item.BaseSalary, item.Bouns); } Console.ReadLine(); }
执行结果如下:
我们可以利用LINQ强大的功能来简化自己的编码,但是LINQ功能的实现本身就是借助于FCL泛型集合的比较器、迭代器、索引器的。LINQ相当于封装了这些功能,让我们使用起来更加的方便。在命名空间System.Linq下存在很多静态类,这些静态类存在的意义就是FCL的泛型集合提供扩展方法。
强烈建议你利用LINQ所带来的便捷性,但我们仍需要掌握比较器、迭代器、索引器的原理,以便更好地理解LINQ的思想,写出更高执行的代码。
建议31、在LINQ查询中避免不必要的迭代
无论是SQL查询还是LINQ查询,搜索到结果立刻返回总比搜索完所有的结果再将结果返回的效率要高。现在简单来创建一个自定义的集合类型来说明。
public class Person { public string Name { get; set; } public int Age { get; set; } } public class MyList : IEnumerable<Person> { List<Person> list = new List<Person>() { new Person(){ Name="aehyok",Age=25}, new Person(){ Name="Kris",Age=20}, new Person(){ Name="Leo",Age=25}, new Person(){ Name="Niki",Age=30} }; public int IteratedNum { get; set; } public Person this[int i] { get { return list[i]; } set { this.list[i] = value; } } public IEnumerator<Person> GetEnumerator() { foreach (var item in list) { IteratedNum++; yield return item; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
简单的进行调用
MyList list = new MyList(); var temp = (from c in list where c.Age == 20 select c).ToList(); Console.WriteLine(list.IteratedNum.ToString()); list.IteratedNum = 0; var temp2 = (from c in list where c.Age >= 20 select c).First(); Console.WriteLine(list.IteratedNum.ToString()); Console.ReadLine();
通过结果发现,第二种的性能明显比第一种好很多。第一种查询迭代了4次,而第二种仅有1次。
第二种查询仅仅迭代1次是因为25正好放在list的首位,而查询条件是大于等于20.First方法实际完成的工作就是:搜索到满足条件的第一个元素,就从集合中返回。如果没有符合条件的元素,它也会遍历整个集合。
与First方法类似的还有Take方法,Take方法接收一个整型参数,然后为我们返回该参数指定的元素个数。与First一样,它满足条件以后,会从当前的迭代过程直接返回,而不是等到整个迭代过程完毕再返回。如果一个集合包含了很多的元素,那么这种查询会为我们带来可观的时间效率。
再来看下面的例子,虽然LINQ查询的最后结果都是返回包含了两个元素"Niki"对象,但是实际上,使用Take方法仅仅为我们迭代了2次,而使用where查询方式带来的确实整个集合的迭代,首先修改一下集合类中的元素
List<Person> list = new List<Person>() { new Person(){ Name="Niki",Age=25}, new Person(){ Name="Niki",Age=30}, new Person(){ Name="Kris",Age=20}, new Person(){ Name="Leo",Age=25}, new Person(){ Name="aehyok",Age=30} };
调用
MyList list = new MyList(); var temp = (from c in list select c).Take(2).ToList(); Console.WriteLine(list.IteratedNum.ToString()); list.IteratedNum = 0; var temp2 = (from c in list where c.Name == "Niki" select c).ToList(); Console.WriteLine(list.IteratedNum.ToString()); Console.ReadLine();
结果
在实际的编码过程中,要充分运用First和Take等方法,这样才能为我们的应用带来高效性,而不会让时间浪费在一些无效的迭代中。