在C# 3.0之前,我们对不同的数据源(数据集合、SQL 数据库、XML 文档等等)进行操作(查询、筛选、投影等等),会使用不同的操作方式。
C# 3.0中提出了LINQ(Language Integrated Query)这个非常重要的特性, LINQ定义了一组标准查询操作符用于在所有基于.NET平台的编程语言中更加直接地声明跨越、过滤和投射操作的统一方式。
关于LINQ中标准操作符的介绍和使用,园子里有很多很好的文章了,所以这里就不介绍LINQ的操作符使用了,主要通过一些概念和例子介绍LINQ是怎么工作的。
LINQ to Objects
首先我们看看LINQ to Objects:
- LINQ to Objects是指直接对任意实现 IEnumerable 或 IEnumerable<T> 接口集合使用 LINQ 查询
- Enumerable静态类封装了对查询IEnumerable或 IEnumerable<T>接口类型的静态扩展方法
- 从Enumerable类的代码可以看到所有的扩展方法中的逻辑表达式都是Func泛型委托,也就是直接使用委托去执行逻辑操作
从上面的概况可以看到,对于实现IEnumerable 或 IEnumerable<T> 接口的集合,我们都可以使用Enumerable中的扩展方法对集合使用LINQ查询。
为了进一步理解这些概念,下面例子中创建了Where和Select扩展方法,模拟了Enumerable中的标准操作符Where和Select:
namespace LINQtoObject { public static class DummyLINQ { public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate) { if (source == null || predicate == null) { throw new ArgumentNullException(); } return WhereImpl<T>(source, predicate); } public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) { if (source == null || selector == null) { throw new ArgumentNullException(); } return SelectImpl<TSource, TResult>(source, selector); } private static IEnumerable<T> WhereImpl<T>(IEnumerable<T> source, Func<T, bool> predicate) { foreach (var item in source) { if (predicate(item)) { Console.WriteLine(" Where: {0} matches where", item); yield return item; } else { Console.WriteLine(" Where: {0} doesn‘t match where", item); } } } private static IEnumerable<TResult> SelectImpl<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, TResult> selector) { foreach (var item in source) { Console.WriteLine(" Selcet: select {0}", item); yield return selector(item); } } } class Student { public string Name { get; set; } public int Age { get; set; } public override string ToString() { return String.Format("(Name:{0}, Age:{1})", this.Name, this.Age); } } class Program { static void Main(string[] args) { List<Student> school = new List<Student> { new Student{Name = "Wilber", Age = 28}, new Student{Name = "Will", Age = 25}, new Student{Name = "July", Age = 23}, new Student{Name = "Lucy", Age = 24}, new Student{Name = "Jean", Age = 22}, }; var stus = school.Where(s => s.Age >= 24).Select(s => s.Name); foreach (var stu in stus) { Console.WriteLine(stu); } Console.Read(); } } }
代码的输出为:
上面的代码简单的模拟了LINQ to Objects的工作方式及实现,自定义的Where和Select操作符也按照了我们预期的方式工作。
通过IL代码可以看到,在LINQ to Objects中,所有扩展方法中的逻辑表达式都被编译器转换成了匿名方法。
延迟执行
如果通过单步调试查看上面代码,可以看到当下面语句执行的时候,并没有处理任何数据。
var stus = school.Where(s => s.Age >= 24).Select(s => s.Name);
只有当访问结果IEnumerable<Student>的时候,上面的查询才会被真正的执行,这个就是LINQ的延迟执行,其实这个延迟执行是基于我们前面介绍过的yield return创建的迭代器块。
虽然我们看到了前面例子的输出,但是下面的序列图可以更加清楚的表示上面查询执行的过程:
方法语法(Fluent Syntax)和查询表达式(Query Expression)
有了Enumerable静态类中所有扩展方法的支持,对于所有实现IEnumerable或 IEnumerable<T>接口的集合,我们可以通过扩展方法组成的方法链完成集合的操作。
var stus = school.Where(s => s.Age >= 24).Select(s => s.Name);
在LINQ中,我们还可以通过查询表达是的方式完成LINQ查询:
var stus = from s in school where s.Age >= 24 select s.Name;
CLR并不具有查询表达式的概念,编译器会在程序编译时把查询表达式转换为方法语法,从而进行扩展方法的调用。
OfType 和Cast
前面例子中我们使用的是List<T>集合,当我们对弱类型的集合使用LINQ的时候,我们就需要用到OfType和Cast两个操作符了。
- OfType 和Cast共同点:
- 都可以处理任意非类型化的序列,并返回强类型的序列
- 不同点:
- Cast把每个元素都转换成目标类型(遇到类型转换错误的时候,就会抛出异常)
- OfType会跳过错误类型的元素,然后对序列进行转换
下面看一个简单的例子:
static void Main(string[] args) { ArrayList list = new ArrayList { "Frist", "Second", "Third" }; IEnumerable<string> stringList1 = list.Cast<string>(); foreach (var str in stringList1) { Console.WriteLine(str); } IEnumerable<string> stringList2 = list.OfType<string>(); foreach (var str in stringList2) { Console.WriteLine(str); } list = new ArrayList { "Frist", "Second", "Third", 1, 2, 3}; //IEnumerable<string> stringList3 = list.Cast<string>(); //foreach (var str in stringList3) //{ // Console.WriteLine(str); //} IEnumerable<int> stringList4 = list.OfType<int>(); foreach (var str in stringList4) { Console.WriteLine(str); } Console.Read(); }
注释掉的代码会产生异常,因为对ArrayList中的int类型数据进行Cast<string>时候会失败。
LINQ to Others
前面看到,对于实现IEnumerable 或 IEnumerable<T> 接口的集合,我们可以直接使用LINQ to Objects。那么当我们碰到其他数据源的时候,我们就需要使用这种数据源特有的LINQ查询了。
如果要对特殊的数据源实现LINQ查询,我们需要知道Queryable类,以及IQueryable<T>和IQueryableProvider<T>接口。
IQueryable<T>和IQueryableProvider<T>
从下面的类图可以看到,IQueryable<T>继承自IEnumerable,IEnumerable<T>和非泛型的IQueryable。
对于IQueryable仅有三个属性:Provider、ElementType和Expression。
对于一个特殊的数据源,如果我们想要使用LINQ进行数据源查询,我们就要自己实现IQueryable<T>、IQueryProvider两个接口的。当使用LINQ查询表达式来查询System.Linq.IQueryable<T>类型对象的话,编辑器会认为我们要查询自定的数据源对象,在执行的时候会调用我们实现的System.Linq.IQueryableProvider<T>接口实现类,该类提供对表达式树的解析和执行。
自己实现的provider把表达式树转换成目标平台的特定类型的查询语句,例如LINQ to SQL的provider就是把表达式树转换成SQL语句,然后有SQL server执行SQL语句返回结果。
Queryable
LINQ to Objects中的数据源总是实现IEnumerable<T>(可能在调用OfType或Cast之后),然后使用Enumerable类中的扩展方法。
类似的,对于实现了IQueryable<T>的数据源,就可以使用Queryable静态类(在System.Linq命名空间中)中的扩展方法。
如果查看代码会发现,System.Linq.Queryable静态类中的所有扩展方法与System.Linq.Enumerable类中的扩展方法的区别是所有的Func类型都被System.Linq.Expressions.Expression<T>类型包装着,也就是说我们的查询表达式(Lambda表达式,匿名方法)在LINQ to Objects中转换成了Func类型的委托,在LINQ to Others中转换成了表达式树。
有了Queryable类中的扩展方法,相当于自定义的数据源也有了统一的操作符,也就是说不同的数据源也可以使用统一的查询方式。当我们使用ILSpy查看"Where"操作符的代码,可以发现这写扩展方法都使用了我们实现的provider。
下面看一个简单的例子,假设我们有一个图书系统,可以通过特定类型的方式(QueryString)进行检索。如果我们想要通过LINQ实现对这种数据源的查询,我们就需要实现一个porvider可以把LINQ查询转换成QueryString。
namespace LINQtoOthers { public class QueryableSource<T> : IQueryable<T> { public IQueryProvider Provider { get; private set; } public Expression Expression { get; private set; } public Type ElementType { get { return typeof(T); } } public QueryableSource() { Provider = new QueryableSourceProvider(); Expression = Expression.Constant(this); } public QueryableSource(QueryableSourceProvider provider, Expression expression) { if (provider == null) { throw new ArgumentNullException("provider"); } if (expression == null) { throw new ArgumentNullException("expression"); } if (!typeof(IQueryable<T>).IsAssignableFrom(expression.Type)) { throw new ArgumentOutOfRangeException("expression"); } Provider = provider; Expression = expression; } public IEnumerator<T> GetEnumerator() { return (Provider.Execute<IEnumerable<T>>(Expression)).GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } } public class QueryableSourceProvider : IQueryProvider { public IQueryable<T> CreateQuery<T>(Expression expression) { return new QueryableSource<T>(this, expression); } public IQueryable CreateQuery(Expression expression) { try { return (IQueryable)Activator.CreateInstance( typeof(QueryableSource<>).MakeGenericType(expression.Type), new object[] { this, expression }); } catch { throw new Exception(); } } public T Execute<T>(Expression expression) { BookInfoExpressionVisitor visitor = new BookInfoExpressionVisitor(); visitor.Visit(expression); Console.WriteLine("QueryString is {0}", visitor.QueryString); return (T)((IEnumerable<BookInfo>)new List<BookInfo> { }); } public object Execute(Expression expression) { return null; } } public class BookInfoExpressionVisitor : ExpressionVisitor { public StringBuilder QueryString { get; set; } public BookInfoExpressionVisitor() { this.QueryString = new StringBuilder(); } protected override Expression VisitMethodCall(MethodCallExpression node) { if (node.Method.Name == "Where") { Console.WriteLine("parsing --- {0}", node); this.Visit((UnaryExpression)node.Arguments[1]); return node; } Console.WriteLine(string.Format("!!!!!Method {0} is not supported", node.Method.Name)); return node; } protected override Expression VisitUnary(UnaryExpression node) { Console.WriteLine("parsing --- {0}", node); this.Visit(node.Operand); return node; } protected override Expression VisitBinary(BinaryExpression node) { Console.WriteLine("parsing --- {0}", node); this.Visit(node.Left); switch (node.NodeType) { case ExpressionType.AndAlso: this.QueryString.Append(" AND "); break; case ExpressionType.OrElse: this.QueryString.Append(" OR "); break; case ExpressionType.Equal: this.QueryString.Append(" = "); break; case ExpressionType.NotEqual: this.QueryString.Append(" <> "); break; case ExpressionType.LessThan: this.QueryString.Append(" < "); break; case ExpressionType.LessThanOrEqual: this.QueryString.Append(" <= "); break; case ExpressionType.GreaterThan: this.QueryString.Append(" > "); break; case ExpressionType.GreaterThanOrEqual: this.QueryString.Append(" >= "); break; default: Console.WriteLine(string.Format("!!!!!Operation type {0} is not supported", node.NodeType)); break; } Console.WriteLine("parsing --- {0}", node.NodeType); this.Visit(node.Right); return node; } protected override Expression VisitMember(MemberExpression node) { Console.WriteLine("parsing --- {0}", node); this.QueryString.Append("[" + node.Member.Name); return node; } protected override Expression VisitConstant(ConstantExpression node) { Console.WriteLine("parsing --- {0}", node); if (node.Type.Name == "String") { this.QueryString.Append("\""); this.QueryString.Append(node.Value); this.QueryString.Append("\"]"); } else { this.QueryString.Append(node.Value + "]"); } return node; } } public class BookInfo { public string BookName { get; set; } public string Author { get; set; } public int SelledNumber { get; set; } public int year { get; set; } } class Program { static void Main(string[] args) { QueryableSource<BookInfo> queryableSource = new QueryableSource<BookInfo>(); var result = from book in queryableSource where book.BookName == "C# in Depth" && book.year > 2012 select book; foreach (var item in result) { Console.WriteLine(item); } Console.Read(); } } }
通过程序的输出可以看到provider一步步解析表达式树的过程,最终生成了一个QueryString。
总结
本文通过一些概念和例子介绍了LINQ to Objects和LINQ to Others,能够对LINQ有一些基本的认识。
C# 3.0中出现的LINQ极大程度的简化了数据操作的代码,通过LINQ实现的数据操作代码会更加的直观。同时,有了LINQ,即使是不同的数据源,我们也可以使用统一的数据操作方式。
LINQ有很多操作符,这些操作符的使用就不介绍了,请自己在园子中搜搜吧。