C#之LINQ——概述(壹)
一、概述
1、LINQ简介
LINQ全称Language Integrated Query,即语言集成查询 。
LINQ是一组技术,允许对不同种类的数据进行简单高效的查询。
LINQ的所有方法都位于System.Linq命名空间下,它允许我们执行各种操作,比如过滤、排序、转换集合中的元素等等。
2、LINQ原理
LINQ除了与C#集合(列表、数组等)搭配使用,还可以与其他类型的数据集合配合使用,如数据库或XML文件等等。当然还可以支持自定义的数据,只需要有对应的提供类Provider即可。
比如下图,我们可以使用统一的Linq语法来对数据进行操作,无需关心数据源。之后通过Linq提供类LINQ Providers将统一的Linq语法翻译成对应数据源能懂的语言。
以SQL数据库为例,我们编写的Linq语句会被LINQ to SQL Provider转换成对应的SQL语句。之后在数据库端处理完数据后返回给C#端程序。

3、LINQ的优点
接下来我们将通过处理C#的数据集合来学习LINQ语法。学习LINQ前需要理解lambda、拓展方法、迭代器等知识。
以查找数组是否包含数字0为例,若不使用LINQ,以前我们是这样做的:
static void Main(string[] args)
{
List<int> numbers = new List<int>() { 1, 2, 3, 4, 5 };
bool isAnyZero = IsAnyZero(numbers); // false
}
public static bool IsAnyZero(List<int> numbers)
{
foreach (int number in numbers)
{
if (number == 0)
{
return true;
}
}
return false;
}
而使用LINQ,只需调用Any方法即可:
List<int> numbers = new List<int>() { 1, 2, 3, 4, 5 };
bool isAnyZero = numbers.Any(number => number == 0); // false
就算没有学习对应API,也能大概看懂大概是什么意思,就是判断numbers数组中是否有为0的数字。当然这些API我们会在后面讲解,因此可以先不用管这些API的具体用法。
可以发现for循环的使用很麻烦,不可维护和可读性差;而LINQ则有更少的代码、更具可读性,使用起来更加方便。
二、LINQ API
我们可以为实现IEnumerable <T>或IQueryable <T>接口的类编写LINQ查询,因为LINQ对实现IEnumerable或IQueryable接口的类都编写了对应LINQ查询的扩展方法,具体源码放在了Enumerable和Queryable两个静态类中。
也就是说,我们可以对实现了IEnumerable的类使用LINQ相关的API,比如数组、字典、列表等等。当然,由于后续我们主要用的是IEnumerable里的数据结构,因此后面我们讲的都是关于IEnumerable的。
需要注意的是,由于IEnumerable只有一个GetEnumerator()方法,并没有类似Add、Remove等修改集合的方法,因此LINQ永远不会修改输入的集合。你可能会发现LINQ有Append这个方法,但实际上它不会修改原集合,而是创建一个新的集合。
三、LINQ的链式调用
我们关注LINQ的一些方法,比如Where,会发现LINQ的两个特性:一是这些LINQ方法以IEnumerable作为参数,另一个是这些LINQ方法以IEnumerable作为结果返回。这意味着我们可以在一个LINQ方法后再执行另一个LINQ方法,即链式调用。
比如提取numbers数组中的奇数并排序:
List<int> numbers = new List<int>() { 1, 2, 3, 4, 5 };
var orderedOddNumbers = numbers
.Where(number => number % 2 == 1)
.OrderBy(number => number); // 1 3 5
只要是类似Where这样的方法,我们可以无限的在后面进行链式调用。
四、LINQ延迟执行
延迟执行意味着LINQ表达式的求值会延迟到实际需要该值时才进行,作用就是通过不必要的执行提高程序性能。
比如这个例子,我们需要查询所有单词中长度小于3的。而这里LINQ表达式的求值并不会在第二行执行,这只是创建了查询。实际上LINQ的执行会延迟到真正需要结果时才执行,即for循环这里。
var words = new List<string> { "a", "bb", "ccc", "dddd" };
var shortWords = words.Where(word => word.Length < 3); // 并不是在这行执行
// 延迟到需要结果时才执行,即这里的foreach循环
foreach (var word in shortWords)
{
Console.WriteLine(word);
}
好,我们再来看一个例子,这次我们为words添加一个新单词并再次查询。正常来看,由于for循环遍历的shortWords不变,我们只是修改了words,所以两次结果应该相同:
var words = new List<string> { "a", "bb", "ccc", "dddd" };
var shortWords = words.Where(word => word.Length < 3);
Console.WriteLine("第一次循环");
foreach (var word in shortWords)
{
Console.WriteLine(word);
}
words.Add("e");
Console.WriteLine("第二次循环");
foreach (var word in shortWords)
{
Console.WriteLine(word);
}
而实际上看输出结果,发现第二次打印shortWords竟然包含了words添加的e!
第一次循环
a
bb
第二次循环
a
bb
e
这意味着shortWords也被修改了吗?其实不是的,我们之前说过LINQ的延迟执行,所以下面这一句其实是创建了一个查询,而并非进行了一次结果计算。我们应该将LINQ查询视为查询本身,而非数据。
也就是说,下面这句我们应当看作声明了一个可以过滤输入数据的查询,因此,即使后面的words集合发生了改变,我们仍然可以通过这个查询来获取最新的结果。
var shortWords = words.Where(word => word.Length < 3);
不过,我们可以强制使数据具体化,即LINQ的立即执行,比如使用ToList()、ToLookUp等方法。这样,两次循环打印的结果就相同了,这是因为这使得查询具体化了,shortWords不再是一个查询,而是具体的数据。
var shortWords = words.Where(word => word.Length < 3).ToList();
通过上面的例子,我们可以总结一下LINQ延迟执行的作用:
- 提高性能,因为查询仅在实际需要时才具体化
- 延迟执行使得我们可以处理到最新的数据
五、方法语法和查询语法
LINQ的查询有两种方法,分别是方法语法和查询语法。其中查询语法类似于数据库的SQL,而方法语法则是调用LINQ拓展方法。
下面举个例子就很容易懂了,首先准备一个数组:
var numbers = new[] { 4, 2, 7, 10, 12, 5 };
然后选择数组中小于10的数字,并按升序排列。
- 方法语法
var smallOrderedNumbersMethodSyntax = numbers
.Where(number => number < 10)
.OrderBy(number => number);
- 查询语法
var smallOrderedNumbersQuerySyntax = from number in numbers
where number < 10
orderby number
select number;
两种方法的优缺点
- 方法语法:存粹的C#代码,没有新内容,学习起来简单。
- 查询语法:需要学习新语言,熟悉SQL语法会很容易学习。缺点是不支持所有LINQ操作,优点是某些操作编写起来会比方法语法更简单,比如连接多个表的操作。