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#端程序。

image-20260404190204556

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对实现IEnumerableIQueryable接口的类都编写了对应LINQ查询的扩展方法,具体源码放在了EnumerableQueryable两个静态类中。

也就是说,我们可以对实现了IEnumerable的类使用LINQ相关的API,比如数组、字典、列表等等。当然,由于后续我们主要用的是IEnumerable里的数据结构,因此后面我们讲的都是关于IEnumerable的

需要注意的是,由于IEnumerable只有一个GetEnumerator()方法,并没有类似AddRemove等修改集合的方法,因此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操作,优点是某些操作编写起来会比方法语法更简单,比如连接多个表的操作。