LINQ

C# LINQ

一、Lambda 表达式:从匿名方法到语法糖

1. 委托与匿名方法

Lambda 表达式本质是简化写法的匿名方法,最终都会被编译为委托实例。在 Lambda 语法出现之前,使用 delegate 关键字定义匿名方法:

// 匿名方法写法
Func<int, int, int> f1 = delegate (int i, int j)
{
    return i + j;
};
Console.WriteLine(f1(3, 5)); // 输出 8
  • Func<in T1, in T2, out TResult>:有返回值的泛型委托,最后一个泛型参数为返回值类型。
  • 对应的无返回值泛型委托为 Action<T1, T2>

2. Lambda 表达式语法

Lambda 使用 => 运算符连接参数列表与方法体,编译器可根据委托类型自动推断参数类型。

完整写法

Func<int, int, int> f2 = (int i, int j) =>
{
    return i + j;
};
Console.WriteLine(f2(3, 4)); // 输出 7

简化规则

  1. 参数类型可省略,由编译器推断:(i, j) => { return i + j; }
  2. 方法体只有一行代码且有返回值时,可省略 {}return
Func<int, int, string> f3 = (i1, i2) => $"{i1}+{i2}={i1 + i2}";
Console.WriteLine(f3(1, 2)); // 输出 1+2=3
  1. 单个参数时,参数外的括号可省略:
Func<int, bool> isEven = x => x % 2 == 0;

二、LINQ 概述与底层原理

1. 什么是 LINQ

LINQ(Language Integrated Query,语言集成查询)是 C# 中用于统一处理各类集合数据的语法体系,核心是为 IEnumerable<T> 提供了一系列扩展方法,配合 Lambda 表达式可以简洁地完成数据筛选、排序、投影、聚合等操作。

  • 核心命名空间:using System.Linq;
  • 支持数据源:数组、List<T>、EF Core 数据库、XML 等所有实现 IEnumerable<T> 的类型

2. LINQ 方法的本质:扩展方法 + 迭代器

LINQ 的筛选、投影等方法本质是扩展方法,内部通过迭代器模式实现。我们可以手写一个简易版 Where 来理解其原理:

版本1:立即执行(一次性生成所有结果)

static IEnumerable<int> MyWhere1(IEnumerable<int> items, Func<int, bool> f)
{
    List<int> result = new List<int>();
    foreach (int i in items)
    {
        if (f(i))
            result.Add(i);
    }
    return result;
}

特点:调用时立即执行所有循环,将结果全部存入 List 后返回,属于立即执行

版本2:延迟执行(yield return 迭代器)

static IEnumerable<int> MyWhere2(IEnumerable<int> items, Func<int, bool> f)
{
    foreach (int i in items)
    {
        if (f(i))
            yield return i;
    }
}

特点:使用 yield return 构建迭代器,调用方法时不会立即执行,只有在遍历(foreach、ToList、Count 等)时才会逐个生成元素,即延迟执行(惰性求值)

系统内置的 LINQ 方法(Where、Select、OrderBy 等)几乎都采用延迟执行机制,这是 LINQ 高效的核心原因之一。

3. var 与类型推断

LINQ 中常用 var 简化类型声明,由编译器自动推断变量类型:

int[] nums = { 3, 99, 88, 77, 7, 8, 9, 66, 15, 7 };
// 等同于 IEnumerable<int> items = nums.Where(i => i > 10);
var items = nums.Where(i => i > 10);

注意:C# 的 var强类型,编译时就已确定类型,和 JavaScript 的弱类型 var 完全不同。C# 中的弱类型是 dynamic


三、LINQ 常用扩展方法详解

以下示例统一使用测试数据:

List<Student> students = new List<Student>
{
    new Student { Id = 1, Name = "张三", Age = 18, Score = 95, ClassId = 1 },
    new Student { Id = 2, Name = "李四", Age = 19, Score = 82, ClassId = 1 },
    new Student { Id = 3, Name = "王五", Age = 17, Score = 60, ClassId = 2 },
    new Student { Id = 4, Name = "赵六", Age = 20, Score = 78, ClassId = 2 },
    new Student { Id = 5, Name = "孙七", Age = 18, Score = 90, ClassId = 1 },
};

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public int Score { get; set; }
    public int ClassId { get; set; }
}

1. 筛选:Where

按条件筛选集合元素,返回满足条件的元素集合。

// 筛选分数大于80的学生
var highScoreStudents = students.Where(s => s.Score > 80);

2. 数量判断:Count 与 Any

  • Count():返回满足条件的元素总数
int count = students.Count(s => s.Age >= 18); // 结果:4
  • Any():判断是否至少存在一条满足条件的数据,返回 bool
bool hasFail = students.Any(s => s.Score < 60); // 结果:false

性能提示:只需要判断“有没有”时,优先用 Any(),比 Count() > 0 效率更高,因为找到第一条匹配项就会停止遍历。

3. 获取单个元素

方法 适用场景 异常情况
Single() 有且只有一条满足条件的数据 0条或多条都会抛出异常
SingleOrDefault() 最多一条满足条件的数据 多条时抛异常;0条返回默认值
First() 至少有一条满足条件,取第一条 0条时抛异常
FirstOrDefault() 取第一条,没有则返回默认值 不会因数量不足抛异常

代码示例:

// 有且只有一个Id=1的学生,正常返回
var s1 = students.Single(s => s.Id == 1);

// 不存在Id=10的学生,抛出异常
// var s2 = students.Single(s => s.Id == 10); 

// 不存在则返回 null
var s3 = students.SingleOrDefault(s => s.Id == 10); 

// 取班级1的第一个学生
var first = students.First(s => s.ClassId == 1);

4. 排序

  • OrderBy():按指定字段升序排序
  • OrderByDescending():按指定字段降序排序
  • ThenBy() / ThenByDescending():多规则次级排序,必须跟在 OrderBy 之后
// 先按班级升序,同班再按分数降序
var sorted = students
    .OrderBy(s => s.ClassId)
    .ThenByDescending(s => s.Score);

5. 分页:Skip 与 Take

  • Skip(n):跳过前 n 条数据
  • Take(n):只取前 n 条数据

组合实现分页(第2页,每页2条):

var page2 = students
    .OrderBy(s => s.Id)
    .Skip(2) // 跳过前2条
    .Take(2); // 取2条

6. 聚合函数

对集合进行数值统计计算:

int maxScore = students.Max(s => s.Score);    // 最高分 95
int minScore = students.Min(s => s.Score);    // 最低分 60
double avgScore = students.Average(s => s.Score); // 平均分 81
int totalScore = students.Sum(s => s.Score);  // 总分 405
int count = students.Count();                 // 总人数 5

7. 分组:GroupBy

按指定字段对集合分组,返回 IGrouping<TKey, TElement> 集合,每个分组包含 Key 和组内元素。

// 按班级分组
var classGroups = students.GroupBy(s => s.ClassId);

foreach (var group in classGroups)
{
    Console.WriteLine($"班级 {group.Key}:");
    foreach (var s in group)
    {
        Console.WriteLine($"  {s.Name} - {s.Score}分");
    }
}

8. 投影:Select 与匿名类型

投影就是把集合中的每一项转换成另一种类型,常用 Select 实现。

基础投影

// 只提取学生姓名集合
IEnumerable<string> names = students.Select(s => s.Name);

匿名类型投影

可以使用 new {} 生成匿名类型,自定义字段名和内容,无需提前定义类:

var studentInfo = students.Select(s => new
{
    XingMing = s.Name,
    NianLing = s.Age,
    ScoreLevel = s.Score >= 80 ? "优秀" : "及格"
});

foreach (var item in studentInfo)
{
    Console.WriteLine($"{item.XingMing} - {item.NianLing}岁 - {item.ScoreLevel}");
}

9. 链式调用

LINQ 方法大多返回 IEnumerable<T>,因此可以链式拼接多个操作,形成完整的数据处理流水线:

var result = students
    .Where(s => s.Score > 60)               // 筛选及格的
    .GroupBy(s => s.ClassId)                // 按班级分组
    .OrderBy(g => g.Key)                    // 按班级号排序
    .Take(2)                                // 取前2个班
    .Select(g => new                        // 投影统计结果
    {
        BanJi = g.Key,
        RenShu = g.Count(),
        PingJunFen = g.Average(s => s.Score)
    });

四、LINQ 的两种语法:方法语法 vs 查询语法

LINQ 有两种书写风格,最终编译结果完全一致。

1. 方法语法(Method Syntax)

上面所有示例都属于方法语法,通过扩展方法 + Lambda 调用,是最灵活、最常用的写法。

2. 查询语法(Query Syntax)

类似 SQL 的书写风格,使用 fromwhereorderbyselect 等关键字,编译器会自动转换为方法语法。

var query = from s in students
            where s.Score > 80
            orderby s.Score descending
            select new
            {
                s.Name,
                s.Score
            };
  • 适用场景:复杂的多表连接、分组查询时,查询语法可读性更好。
  • 限制:并非所有 LINQ 方法都有对应的查询语法关键字(如 Skip、Take、Any 等),复杂场景仍需混合方法语法。

五、LINQ 核心特性:延迟执行

1. 什么是延迟执行

WhereSelectOrderByGroupBy 这类返回 IEnumerable<T> 的方法,调用时不会立即执行查询逻辑,只是构建了查询计划。只有当数据被真正使用时(遍历、转为集合、聚合计算),查询才会执行。

int[] nums = { 1, 2, 3, 4 };
var query = nums.Where(x => x > 2); // 此时并未执行筛选

nums[2] = 100; // 修改原数组

foreach (var n in query) // 遍历的时候才真正执行筛选
{
    Console.WriteLine(n); // 输出 100, 4
}

结果输出 1004,证明查询是在遍历的时候才执行的,会读取最新的数据源。

2. 立即执行的方法

以下操作会触发查询立即执行:

  • 转为集合:ToList()ToArray()ToDictionary()
  • 聚合计算:Count()Sum()Max()First()Any()
  • 遍历:foreach 循环

3. 注意事项

  1. 重复枚举会重复执行:每次 foreach 都会重新执行一次查询,数据源变化时结果可能不同。
  2. 复杂查询可缓存:如果需要多次使用结果,建议调用 ToList() 立即执行并缓存结果,避免重复计算。

六、补充:更多常用 LINQ 方法

1. All

判断集合中所有元素是否都满足条件:

bool allPass = students.All(s => s.Score >= 60); // true

2. Distinct

去除集合中的重复元素:

int[] nums = { 1, 2, 2, 3, 3, 3 };
var distinctNums = nums.Distinct(); // 1, 2, 3

3. SelectMany

将集合的每个子集合“扁平化”成一个单一集合:

// 例如每个班级有多个学生,把所有班级的学生合并成一个列表
List<Class> classes = ...;
var allStudents = classes.SelectMany(c => c.Students);

4. TakeWhile / SkipWhile

  • TakeWhile:只要满足条件就取元素,遇到不满足的立即停止
  • SkipWhile:只要满足条件就跳过,遇到不满足的开始取后面所有元素
int[] nums = { 1, 2, 3, 4, 5, 1, 2 };
var takeWhile = nums.TakeWhile(x => x < 4); // 1, 2, 3

七、使用建议与常见坑点

  1. 优先使用方法语法:功能完整,链式调用清晰,是行业主流写法。
  2. 注意延迟执行副作用:不要在循环中反复枚举同一个 LINQ 查询,必要时先 ToList 缓存。
  3. EF Core 场景注意:LINQ to Entities 中,Where 条件会被翻译为 SQL,不能使用无法翻译的自定义方法,否则会抛出异常。
  4. Single 慎用:不确定数据数量时,优先用 FirstOrDefault 或 SingleOrDefault,避免不必要的异常。
posted @ 2026-08-16 10:55  古月秋筠  阅读(6)  评论(0)    收藏  举报