C#之LINQ

发布于:2025-09-08 ⋅ 阅读:(28) ⋅ 点赞:(0)

文章目录


前言

LINQ

一、LINQ1

委托->lambda->LINQ
1、委托是可以指向方法的类型,调用委托变量时执行的就是变量指向方法。

在 C# 中,​​委托(Delegate)​​ 是一种类型安全的函数指针,它允许将方法作为参数传递、存储或动态调用。委托是事件(Event)和回调机制的基础,实现了​​松耦合​​的设计模式。
核心概念​​
1.类型安全的方法引用​​

委托定义了方法的签名(参数类型和返回类型),只能绑定匹配签名的方法。

2.类似接口​​

委托类似于只包含一个方法的接口,但更轻量且直接。

3.多播能力​​

一个委托实例可绑定多个方法(+= 添加),调用时按顺序执行所有方法。

委托的声明与使用​

  1. 定义委托类型
// 声明一个委托类型,指定方法签名
public delegate void MyDelegate(string message);
  1. 绑定方法
// 目标方法(签名必须匹配)
public void ShowMessage(string msg)
{
    Console.WriteLine($"Message: {msg}");
}

// 实例化委托并绑定方法
MyDelegate del = new MyDelegate(ShowMessage);
  1. 调用委托
del("Hello, Delegate!"); 
// 输出:Message: Hello, Delegate!

实例

class Program
{
    static void Main(string[] args)
    {
        D1 d = F1;
        d();
        d = F2;
        d();
    }
    static void F1()
    {
        Console.WriteLine("我是F1");
    }
    static void F2()
    {
        Console.WriteLine("我是F2");
    }
}
delegate void D1();

结果
在这里插入图片描述

2、.NET中定义了泛型委托Action(无返回值)和Func(有返回值),所以一般不用自定义委托类型

内置泛型委托​​
C# 提供两种常用泛型委托,无需自定义:

1.​​Action​​

无返回值的方法(支持 0~16 个参数)。

public delegate void Action();                     // 无参数
public delegate void Action<in T>(T obj);          // 1个参数
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2); // 2个参数
// ... 最多支持16个参数 (Action<T1,...,T16>)
Action<string> actionDel = ShowMessage; // void 方法

2.​​Func​​
有返回值的方法(最后一个泛型参数是返回类型)。

public delegate TResult Func<out TResult>();                     // 无参数,有返回值
public delegate TResult Func<in T, out TResult>(T arg);          // 1输入+1输出
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2); // 2输入+1输出
// ... 最多16输入+1输出 (Func<T1,...,T16,TResult>)
Func<int, int, int> add = (a, b) => a + b; 
int result = add(3, 5); // 返回 8

委托变量不仅可以指向普通方法,还可以指向匿名方法。

Func<int, int, string> f1 = delegate (int i1, int i2)
{
    return $"{i1}+{i2}={i1 + i2}";
};

匿名方法可以写成lambda表达式,可以省略参数数据类型,因为编译根据委托类型推断出参数类型,用=>引出方法体

        Func<int, int, string> f2 = (i1, i2) =>
        {
            return $"{i1}+{i2}={i1 + i2}";
        };

lambda表达式

(输入参数) => 表达式或语句块
场景 Lambda 表达式 等效传统写法
无参数 () => Console.WriteLine("Hi") void F() { Console.WriteLine("Hi"); }
单参数 x => x * x int F(int x) { return x * x; }
多参数 (a, b) => a + b int F(int a, int b) { return a + b; }
语句块 s => { Console.WriteLine(s); return s.Length; } int F(string s) { Console.WriteLine(s); return s.Length; }

一、LINQ2

揭秘LINQ方法的背后
LINQ中提供了很多集合扩展方法,配合lambda能简化数据处理。

int[] nums = { 11, 1, 24, 5, 6, 98, 60 };
// Where方法会遍历集合中的每一个元素,对于每一个元素
// 都调用a=> a>10这个表达式判断下一个是否为true
// 如果为true,则把这个放到返回的集合中
IEnumerable<int> result = nums.Where(a => a > 10);
foreach (int i in result)
{
    Console.WriteLine(i);
}

下面手动实现Where功能

int[] nums = { 11, 1, 24, 5, 6, 98, 60 };
//// Where方法会遍历集合中的每一个元素,对于每一个元素
//// 都调用a=> a>10这个表达式判断下一个是否为true
//// 如果为true,则把这个放到返回的集合中
//IEnumerable<int> result = nums.Where(a => a > 10);
IEnumerable<int> result = MyWhere(nums,a=>a>10);
foreach (int i in result)
{
    Console.WriteLine(i);
}

IEnumerable<int> MyWhere(IEnumerable<int> items,Func<int,bool> f)
{
    List<int> result = new List<int>();
    foreach (int item in items)
    {
        if (f(item))
            result.Add(item);

    }
    return result;
}

使用yield实现Where功能

int[] nums = { 11, 1, 24, 5, 6, 98, 60 };
//// Where方法会遍历集合中的每一个元素,对于每一个元素
//// 都调用a=> a>10这个表达式判断下一个是否为true
//// 如果为true,则把这个放到返回的集合中
//IEnumerable<int> result = nums.Where(a => a > 10);
IEnumerable<int> result = MyWhere1(nums,a=>a>10);
foreach (int i in result)
{
    Console.WriteLine(i);
}

IEnumerable<int> MyWhere1(IEnumerable<int> items, Func<int, bool> f)
{
    List<int> result = new List<int>();
    foreach (int item in items)
    {
        if (f(item))
           yield return item;

    }
}

一、LINQ3

LINQ常用扩展方法

(补充)扩展方法
#C# 扩展方法深度解析

一、本质与原理

  1. 核心概念
    扩展方法是一种编译时语法糖,它允许开发者在不修改原始类型、不创建派生类的情况下,为现有类型"添加"新方法。其本质是静态方法,但通过编译器魔法实现了实例方法调用语法。

  2. 实现机制

// 定义扩展方法
public static class StringExtensions {
    public static bool IsValidEmail(this string input) 
        => Regex.IsMatch(input, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}

// 调用代码
var isValid = "test@example.com".IsValidEmail();

// 编译器转换后的实际代码
var isValid = StringExtensions.IsValidEmail("test@example.com");
  1. 关键特性
  • 静态伪装:静态方法伪装成实例方法
  • 非侵入性:不修改原始类型代码
  • 编译时解析:在编译阶段确定方法绑定
  • 命名空间控制:需导入扩展方法所在命名空间

二、技术实现详解

  1. 三大必要条件
public static class Extensions // 条件1:静态类
{
    // 条件2:静态方法 + 条件3:this修饰首参数
    public static string Reverse(this string value)
    {
        char[] chars = value.ToCharArray();
        Array.Reverse(chars);
        return new string(chars);
    }
}
  1. 参数规则
  • 首个参数:必须使用 this 修饰,指定目标类型
  • 附加参数:可添加多个常规参数
public static string Wrap(this string text, string wrapper)
    => $"{wrapper}{text}{wrapper}";

// 使用
"Hello".Wrap("**"); // 输出:**Hello**
  1. 方法重载
// 重载1:默认包装符
public static string Wrap(this string text) => Wrap(text, "[]");

// 重载2:自定义包装符
public static string Wrap(this string text, string wrapper) 
    => $"{wrapper}{text}{wrapper}";

三、高级应用场景

  1. 接口扩展
public static void Log<T>(this IEnumerable<T> collection)
{
    foreach (var item in collection)
        Console.WriteLine(item);
}

// 所有集合类型通用
new List<int>{1,2,3}.Log();
new int[]{4,5,6}.Log();
  1. 链式调用 (Fluent API)
public static StringBuilder AppendFormattedLine(
    this StringBuilder sb,
    string format,
    params object[] args)
{
    sb.AppendFormat(format, args).AppendLine();
    return sb; // 返回自身实现链式调用
}

// 使用
var sb = new StringBuilder()
    .AppendFormattedLine("Date: {0}", DateTime.Now)
    .AppendFormattedLine("User: {0}", "Alice");
  1. 空值处理模式
public static TResult SafeGet<T, TResult>(
    this T obj, 
    Func<T, TResult> selector,
    TResult defaultValue = default)
{
    return obj != null ? selector(obj) : defaultValue;
}

// 安全访问嵌套属性
var city = person?.Address?.City; // 传统方式
var city = person.SafeGet(p => p.Address.City); // 扩展方法方式

四、LINQ风格通用扩展方法

  1. 完整实现示例
public static class EnumerableExtensions
{
    // 通用过滤 (支持所有IEnumerable<T>)
    public static IEnumerable<T> WhereEx<T>(
        this IEnumerable<T> source,
        Func<T, bool> predicate)
    {
        foreach (var item in source)
            if (predicate(item)) 
                yield return item;
    }

    // 通用转换
    public static IEnumerable<TResult> SelectEx<TSource, TResult>(
        this IEnumerable<TSource> source,
        Func<TSource, TResult> selector)
    {
        foreach (var item in source)
            yield return selector(item);
    }

    // 字典键过滤专用
    public static IEnumerable<TKey> KeysWhere<TKey, TValue>(
        this IDictionary<TKey, TValue> source,
        Func<TKey, bool> predicate)
    {
        foreach (var key in source.Keys)
            if (predicate(key))
                yield return key;
    }
}
  1. 多类型兼容使用
// List使用
var numbers = new List<int> {1, 2, 3, 4};
var evens = numbers.WhereEx(n => n % 2 == 0);

// 数组使用
string[] fruits = {"Apple", "Banana"};
var aFruits = fruits.WhereEx(f => f.StartsWith("A"));

// 字典使用
var dict = new Dictionary<int, string> {{1, "A"}, {2, "B"}};
var keys = dict.KeysWhere(k => k > 1); // [2]

Where方法:每一项数据都会进过predicate的测试,如果针对一个元素,predicate执行的返回值为true,那么这个元素就会放到返回值中。

Where参数是一个lambda表达式格式的匿名方法,方法的参数e表示当前判断的元素对象。参数的名字不一定非要叫e,不过一般lambda表达式中的变量名长度都不长。
Count方法:获取数据条数
Any方法:是否至少有一条数据

List<Employee> list = new List<Employee>();
list.Add(new Employee { Id = 1, Name = "jerry", Age = 28, Gender = true, Salary = 5000 });
list.Add(new Employee { Id = 2, Name = "jim", Age = 33, Gender = true, Salary = 3000 });
list.Add(new Employee { Id = 3, Name = "lily", Age = 35, Gender = false, Salary = 9000 });
list.Add(new Employee { Id = 4, Name = "lucy", Age = 16, Gender = false, Salary = 2000 });
list.Add(new Employee { Id = 5, Name = "kimi", Age = 25, Gender = true, Salary = 1000 });
list.Add(new Employee { Id = 6, Name = "nancy", Age = 35, Gender = false, Salary = 8000 });
list.Add(new Employee { Id = 7, Name = "zack", Age = 35, Gender = true, Salary = 8500 });
list.Add(new Employee { Id = 8, Name = "jack", Age = 33, Gender = true, Salary = 8000 });
IEnumerable<Employee> items1 = list.Where(e => e.Age > 30);// 返回符合条件的IEnumerable集合
int items2 = list.Count();// 无条件返回总条数
int items3 = list.Count(e => e.Age > 30);// 返回符合条件的,数量
int items4 = list.Count(e => e.Age > 30 && e.Salary>500);// 返回符合条件的,数量
bool items5 = list.Any();// 有一条数据就返回true否则返回false
bool items6 = list.Any(e => e.Age > 30);// 有一条数据就返回true否则返回false,找到一条符合条件的数据就返回true,且不会继续寻找后面的数据,否则返回false

foreach (Employee item in items1)
{
    Console.WriteLine(item.Name);
}

获取一条数据(是否带参数的两种写法):

Single:有且只有一条满足要求的数据;
SingleOrDefault:最多只有一条满足要求的数据;
First:至少有一条,返回第一条;
FirstOrDefault:返回第一条或者默认值;

C# LINQ 查询方法详解:Single, SingleOrDefault, First, FirstOrDefault

在 C# 的 LINQ 查询中,Single, SingleOrDefault, FirstFirstOrDefault 是常用的元素检索方法,它们有不同的行为和使用场景。下面我将详细解释它们的区别和使用方法。

方法对比表

方法 返回值条件 无匹配时行为 多个匹配时行为 使用场景
Single 有且只有一条满足要求的数据 抛出 InvalidOperationException 抛出 InvalidOperationException 确保只有唯一匹配项时
SingleOrDefault 最多只有一条满足要求的数据 返回默认值(如 null 或 0) 抛出 InvalidOperationException 期望0或1个匹配项时
First 至少有一条,返回第一条 抛出 InvalidOperationException 返回第一个匹配项 需要第一个匹配项且确保存在时
FirstOrDefault 返回第一条或者默认值 返回默认值(如 null 或 0) 返回第一个匹配项 需要第一个匹配项或处理空结果时

详细解释与示例

1. Single

  • 行为:要求序列中有且只有一个元素满足条件
  • 异常情况
    • 如果没有匹配项 → 抛出 InvalidOperationException
    • 如果有多个匹配项 → 抛出 InvalidOperationException
  • 使用场景:当你确定只有一个匹配项时
  • 示例
// 查找唯一ID为3的员工
var employee = list.Single(e => e.Id == 3);
Console.WriteLine(employee.Name); // 输出: lily

// 以下情况会抛出异常:
// var invalid1 = list.Single(e => e.Age > 40); // 无匹配项
// var invalid2 = list.Single(e => e.Age == 35); // 多个匹配项

2. SingleOrDefault

  • 行为:要求序列中最多只有一个元素满足条件
  • 异常情况
    • 如果没有匹配项 → 返回类型的默认值(如 null, 0 等)
    • 如果有多个匹配项 → 抛出 InvalidOperationException
  • 使用场景:当你期望0或1个匹配项时
  • 示例
// 查找唯一ID为10的员工(不存在)
var employee1 = list.SingleOrDefault(e => e.Id == 10);
Console.WriteLine(employee1?.Name ?? "未找到"); // 输出: 未找到

// 查找唯一ID为3的员工(存在)
var employee2 = list.SingleOrDefault(e => e.Id == 3);
Console.WriteLine(employee2.Name); // 输出: lily

// 以下情况会抛出异常:
// var invalid = list.SingleOrDefault(e => e.Age == 35); // 多个匹配项

3. First

  • 行为:返回序列中第一个满足条件的元素
  • 异常情况
    • 如果没有匹配项 → 抛出 InvalidOperationException
    • 如果有多个匹配项 → 返回第一个匹配项
  • 使用场景:当你需要第一个匹配项确保存在
  • 示例
// 查找第一个年龄大于30的员工
var employee = list.First(e => e.Age > 30);
Console.WriteLine(employee.Name); // 输出: jim

// 以下情况会抛出异常:
// var invalid = list.First(e => e.Age > 40); // 无匹配项

4. FirstOrDefault

  • 行为:返回序列中第一个满足条件的元素,或默认值
  • 异常情况
    • 如果没有匹配项 → 返回类型的默认值(如 null, 0 等)
    • 如果有多个匹配项 → 返回第一个匹配项
  • 使用场景:当你需要第一个匹配项或处理空结果
  • 示例
// 查找第一个年龄大于40的员工(不存在)
var employee1 = list.FirstOrDefault(e => e.Age > 40);
Console.WriteLine(employee1?.Name ?? "未找到符合条件的员工"); // 输出: 未找到符合条件的员工

// 查找第一个年龄为35的员工
var employee2 = list.FirstOrDefault(e => e.Age == 35);
Console.WriteLine(employee2.Name); // 输出: lily

性能考虑

  • First/FirstOrDefault 通常比 Single/SingleOrDefault 性能更好,因为它们找到第一个匹配项就返回
  • Single/SingleOrDefault 需要遍历整个序列以确保只有一个匹配项

默认值说明

对于引用类型:

  • 默认值为 null
  • 使用前应检查是否为 null

对于值类型:

  • 默认值为该类型的默认值(如 int 为 0,bool 为 false)
  • 使用前应检查是否等于默认值

最佳实践建议

  1. 优先使用 FirstOrDefault

    • 大多数情况下是最安全的选择
    • 避免异常处理,代码更简洁
  2. 谨慎使用 Single

    • 仅在确保只有一个匹配项时使用
    • 数据库主键查询是典型场景
  3. 避免在可能多个匹配项时使用 SingleOrDefault

    • 多个匹配项会抛出异常
    • 使用前应确认数据唯一性
  4. 处理默认值

var result = list.FirstOrDefault();
if (result != null) // 对于引用类型
{
    // 处理结果
}

var valueResult = intList.FirstOrDefault();
if (valueResult != default) // 对于值类型
{
    // 处理结果
}

总结对比图

       唯一性要求         存在性要求
       ↓               ↓
Single:     有且只有一条满足要求的数据
SingleOrDefault: 最多只有一条满足要求的数据
First:              至少有一条,返回第一条
FirstOrDefault:         返回第一条或者默认值

根据你的具体需求选择合适的方法:

  • 需要唯一结果 → SingleSingleOrDefault
  • 需要第一个结果 → FirstFirstOrDefault
  • 不确定是否存在结果 → ...OrDefault 版本
  • 确保结果存在 → 不带 OrDefault 的版本

排序:

OrderBy() 对数据正序排序;
OrderByDescending()倒序排序;
list.OrderBy(e=>e.Age);
对于简单类型排序,也许不用lambda表达式。特殊案例:按照最后一个字符排序,用Guid或者随机数进行随机排序。

C# LINQ 排序方法详解:OrderBy 与 OrderByDescending

在 C# 的 LINQ 查询中,OrderBy()OrderByDescending() 是用于对数据进行排序的核心方法。下面我将详细解释它们的用法和区别。

基本概念

方法 描述 排序方向
OrderBy() 对序列元素进行升序排序 从小到大 (A→Z, 1→9)
OrderByDescending() 对序列元素进行降序排序 从大到小 (Z→A, 9→1)

基本语法

// 正序排序
IEnumerable<TSource> sortedAsc = source.OrderBy(e => e.Property);

// 倒序排序
IEnumerable<TSource> sortedDesc = source.OrderByDescending(e => e.Property);

示例解释:list.OrderBy(e => e.Age)

// 使用 OrderBy 按年龄正序排序
var sortedByAge = list.OrderBy(e => e.Age);

执行过程:

  1. 遍历 list 中的所有员工
  2. 提取每个员工的 Age 属性值作为排序键
  3. 按照年龄从小到大排序
  4. 返回排序后的新序列(原始列表不会被修改)

排序结果:

假设原始列表年龄为:[28, 33, 35, 16, 25, 35, 35, 33]
排序后变为:[16, 25, 28, 33, 33, 35, 35, 35]

完整排序示例

1. 单属性排序

// 按年龄正序排序
var byAgeAsc = list.OrderBy(e => e.Age);

// 按工资倒序排序
var bySalaryDesc = list.OrderByDescending(e => e.Salary);

2. 多级排序(ThenBy/ThenByDescending)

// 先按性别正序,再按年龄倒序
var multiSort = list
    .OrderBy(e => e.Gender)        // 先按性别排序(false在前,true在后)
    .ThenByDescending(e => e.Age);  // 再按年龄降序

// 先按年龄倒序,再按工资正序
var multiSort2 = list
    .OrderByDescending(e => e.Age)
    .ThenBy(e => e.Salary);

3. 自定义排序逻辑

// 按姓名长度排序
var byNameLength = list.OrderBy(e => e.Name.Length);

// 按工资范围分组排序
var bySalaryRange = list.OrderBy(e => 
{
    if (e.Salary < 3000) return 1;    // 低薪组
    if (e.Salary < 6000) return 2;    // 中薪组
    return 3;                         // 高薪组
});

C# 特殊排序场景详解:简单类型、末位字符与随机排序

在 C# 中,虽然 Lambda 表达式是 LINQ 排序的常见方式,但在某些特殊场景下,我们可以使用更简洁或更灵活的方法进行排序。下面我将详细解释这些特殊排序场景的实现方式。

一、简单类型排序(不使用 Lambda 表达式)

1. 基本排序方法

List<int> numbers = new List<int> { 5, 2, 8, 1, 9 };

// 升序排序(不使用 Lambda)
var sortedAsc = numbers.OrderBy(n => n); // 传统方式
var simpleAsc = numbers.Order();         // C# 11+ 简化方式

// 降序排序(不使用 Lambda)
var sortedDesc = numbers.OrderByDescending(n => n); // 传统方式
var simpleDesc = numbers.OrderDescending();         // C# 11+ 简化方式

2. C# 11+ 的简化语法

在 C# 11 及以上版本中,对于简单类型集合,可以直接使用:

// 升序排序
var sorted = numbers.Order();

// 降序排序
var sortedDesc = numbers.OrderDescending();

3. 字符串集合排序

List<string> fruits = new List<string> { "Apple", "Banana", "Cherry", "Date" };

// 按字母顺序排序
var alphabetical = fruits.Order(); // ["Apple", "Banana", "Cherry", "Date"]

// 按长度排序(仍需使用 Lambda)
var byLength = fruits.OrderBy(f => f.Length); // ["Date", "Apple", "Banana", "Cherry"]

二、特殊案例:按最后一个字符排序

1. 基本实现

List<string> words = new List<string> { "apple", "banana", "cherry", "date" };

// 按最后一个字符升序排序
var byLastChar = words.OrderBy(w => w[1](@ref)); // ^1 表示最后一个字符

// 结果: ["banana"(a), "apple"(e), "date"(e), "cherry"(y)]

2. 处理空字符串和单字符

public static char SafeLastChar(string s)
{
    return string.IsNullOrEmpty(s) ? '\0' : s[1](@ref);
}

// 安全获取最后一个字符并排序
var safeSorted = words
    .Where(w => !string.IsNullOrEmpty(w))
    .OrderBy(w => SafeLastChar(w));

3. 多级排序(先按长度,再按末字符)

var multiSort = words
    .OrderBy(w => w.Length)
    .ThenBy(w => w[1](@ref));

三、随机排序(使用 Guid 或随机数)

1. 使用 Guid 随机排序

// 使用 Guid 生成随机排序键
var randomOrder = list.OrderBy(e => Guid.NewGuid()).ToList();

// 原理:为每个元素分配唯一的随机 Guid,然后排序

优点

  • 实现简单,一行代码
  • 分布均匀,随机性好

缺点

  • 性能较差(生成 Guid 开销大)
  • 不适用于大数据集

限制结果集,获取部分数据:

Ship(n)跳过n条数据,Take(n) 获取n条数据。
案例:获取从第2条开始获取3条数据 var orderedItems1 = list.Skip(2).Take(3);
Skip()、Take()也可以单独使用。

C# LINQ 分页操作详解:Skip 与 Take

在 C# 的 LINQ 查询中,Skip()Take() 是两个用于数据分页和子集选择的核心方法。它们通常结合使用来实现高效的分页功能。

基本概念

方法 描述 行为
Skip(n) 跳过序列中的前 n 个元素 返回剩余元素的序列
Take(n) 从序列开头获取前 n 个元素 返回包含前 n 个元素的序列

基本语法

// 跳过前 n 个元素
IEnumerable<T> skipped = source.Skip(n);

// 获取前 n 个元素
IEnumerable<T> taken = source.Take(n);

// 组合使用(分页)
IEnumerable<T> page = source.Skip(pageIndex * pageSize).Take(pageSize);

方法详解

1. Skip(n)

  • 功能:跳过序列中的前 n 个元素
  • 参数:要跳过的元素数量
  • 返回值:包含源序列中跳过指定数量元素后的剩余元素
  • 边界情况
    • 如果 n ≤ 0:返回整个序列
    • 如果 n ≥ 序列长度:返回空序列
List<int> numbers = new List<int> {1, 2, 3, 4, 5};

// 跳过前 2 个元素
var skipped = numbers.Skip(2); // [3, 4, 5]

// 跳过 0 个元素
var skipZero = numbers.Skip(0); // [1, 2, 3, 4, 5]

// 跳过超过序列长度
var skipLarge = numbers.Skip(10); // 空序列

2. Take(n)

  • 功能:从序列开头获取指定数量的元素
  • 参数:要获取的元素数量
  • 返回值:包含源序列前 n 个元素的序列
  • 边界情况
    • 如果 n ≤ 0:返回空序列
    • 如果 n ≥ 序列长度:返回整个序列
List<int> numbers = new List<int> {1, 2, 3, 4, 5};

// 获取前 3 个元素
var taken = numbers.Take(3); // [1, 2, 3]

// 获取 0 个元素
var takeZero = numbers.Take(0); // 空序列

// 获取超过序列长度
var takeLarge = numbers.Take(10); // [1, 2, 3, 4, 5]

组合使用:分页实现

基本分页公式

int pageIndex = 2; // 第3页(从0开始计数)
int pageSize = 3;  // 每页3条

var page = source
    .Skip(pageIndex * pageSize)
    .Take(pageSize);

完整分页示例

List<Employee> employees = GetEmployees(); // 假设有100名员工

int pageSize = 10; // 每页10条

// 获取第3页数据(索引从0开始)
var page3 = employees
    .OrderBy(e => e.LastName) // 先排序
    .Skip(2 * pageSize)       // 跳过前20条
    .Take(pageSize);          // 取10条

Console.WriteLine($"第3页数据(共{page3.Count()}条):");
foreach (var emp in page3)
{
    Console.WriteLine($"{emp.LastName}, {emp.FirstName}");
}

分页辅助方法

public static class PagingExtensions
{
    public static IEnumerable<T> Page<T>(
        this IEnumerable<T> source, 
        int pageIndex, 
        int pageSize)
    {
        return source
            .Skip(pageIndex * pageSize)
            .Take(pageSize);
    }
    
    public static IQueryable<T> Page<T>(
        this IQueryable<T> source, 
        int pageIndex, 
        int pageSize)
    {
        return source
            .Skip(pageIndex * pageSize)
            .Take(pageSize);
    }
}

// 使用
var page = employees.Page(2, 10); // 获取第3页,每页10条

集合函数:

Max()、Min()、Average()、Sum()、Count()。
LINQ中所有的扩展方法几乎都是针对IEnumerable接口的,而几乎所有能返回集合的都返回IEnumerable,所以是可以把几乎所有方法“链式使用”的。list.Where(e=>e.Age>30).Min(e=>e.Age);

C# LINQ 聚合方法与链式调用详解

LINQ 聚合方法概述

方法 描述 返回值类型 空集合行为
Max() 返回序列中的最大值 数值类型 抛出异常
Min() 返回序列中的最小值 数值类型 抛出异常
Average() 返回序列的平均值 数值类型 抛出异常
Sum() 返回序列的总和 数值类型 返回0
Count() 返回序列的元素数量 int 返回0

链式调用原理

LINQ 的核心设计理念是链式调用(Method Chaining),这得益于:

  1. 几乎所有 LINQ 方法都是针对 IEnumerable<T> 接口的扩展方法
  2. 大多数方法返回 IEnumerable<T>IOrderedEnumerable<T>
  3. 每个方法操作前一个方法返回的结果集

链式调用示例

var result = employees
    .Where(e => e.Department == "IT")  // 返回 IEnumerable<Employee>
    .OrderBy(e => e.LastName)          // 返回 IOrderedEnumerable<Employee>
    .Select(e => new { e.Name, e.Salary }) // 返回 IEnumerable<匿名类型>
    .Take(10);                         // 返回 IEnumerable<匿名类型>

代码解析:list.Where(e=>e.Age>30).Min(e=>e.Age);

执行步骤

  1. 过滤阶段

    var filtered = list.Where(e => e.Age > 30);
    
    • 遍历原始集合 list
    • 筛选出年龄大于30的元素
    • 返回 IEnumerable<Employee> 类型的结果集
  2. 聚合阶段

    var minAge = filtered.Min(e => e.Age);
    
    • 遍历过滤后的结果集 filtered
    • 提取每个元素的 Age 属性
    • 找出这些年龄值中的最小值
    • 返回 int 类型的最小年龄值

等效传统代码

int minAge = int.MaxValue;
bool found = false;

foreach (var employee in list)
{
    if (employee.Age > 30)
    {
        found = true;
        if (employee.Age < minAge)
        {
            minAge = employee.Age;
        }
    }
}

if (!found)
{
    throw new InvalidOperationException("序列不包含任何元素");
}

注意事项

  1. 空集合处理

    • 如果 Where 过滤后没有元素,Min() 会抛出 InvalidOperationException
    • 安全处理方式:
      var minAge = list.Where(e => e.Age > 30)
                      .Select(e => e.Age)
                      .DefaultIfEmpty(0)
                      .Min();
      
  2. 性能优化

    • 对于大型集合,考虑使用更高效的算法:
      int? minAge = null;
      foreach (var e in list)
      {
          if (e.Age > 30 && (minAge == null || e.Age < minAge))
          {
              minAge = e.Age;
          }
      }
      

其他聚合方法链式调用示例

1. 计算平均值

double avgSalary = employees
    .Where(e => e.Department == "Sales")
    .Average(e => e.Salary);

2. 求和统计

decimal totalSales = salesRecords
    .Where(s => s.Year == 2023)
    .Sum(s => s.Amount);

3. 计数统计

int highEarners = employees
    .Where(e => e.Salary > 100000)
    .Count();

4. 多级聚合

var stats = employees
    .GroupBy(e => e.Department)
    .Select(g => new {
        Department = g.Key,
        MinSalary = g.Min(e => e.Salary),
        MaxSalary = g.Max(e => e.Salary),
        AvgSalary = g.Average(e => e.Salary)
    });

链式调用的高级应用

1. 条件聚合

var result = products
    .Where(p => p.Category == "Electronics")
    .Select(p => p.Price)
    .DefaultIfEmpty(0) // 处理空集合
    .Average();

2. 组合使用

var analysis = orders
    .Where(o => o.Date.Year == 2023)
    .GroupBy(o => o.CustomerId)
    .Select(g => new {
        CustomerId = g.Key,
        TotalOrders = g.Count(),
        TotalAmount = g.Sum(o => o.Amount),
        AvgOrderValue = g.Average(o => o.Amount)
    })
    .OrderByDescending(x => x.TotalAmount)
    .Take(10);

3. 空值处理技巧

decimal? maxDiscount = customers
    .Where(c => c.IsPremium)
    .Select(c => c.DiscountPercentage)
    .Where(d => d.HasValue)
    .DefaultIfEmpty(0)
    .Max();

分组:

GroupBy()方法参数是分组条件表达式,返回值为IGrouping<TKey,TSource>类型的泛型IEnumerable,也就是每一组以一个IGrouping对象的形式返回。IGrouping是一个继承自IEnumerable的接口,IGrouping中Key属性表示这一组的分数的值。例子:根据年龄分组,获取每组人数、最高工资、平均工资。

C# LINQ GroupBy 分组方法详解

GroupBy 方法核心概念

GroupBy() 是 LINQ 中最强大的数据分组方法,它允许您根据指定的键将数据集合划分为多个逻辑组。

方法签名

IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector
)

关键特性

特性 说明
分组条件 通过 keySelector 函数指定分组依据
返回值 IEnumerable<IGrouping<TKey, TSource>>
分组对象 每个分组是一个 IGrouping<TKey, TSource> 对象
分组访问 可以通过 Key 属性访问分组键
元素访问 分组本身是可枚举的,包含该组的所有元素

IGrouping 接口解析

IGrouping<TKey, TSource> 接口定义如下:

public interface IGrouping<out TKey, out TElement> : IEnumerable<TElement>
{
    TKey Key { get; }
}

核心特性

  1. 继承自 IEnumerable

    • 每个分组本身是一个可枚举集合
    • 可以遍历分组内的所有元素
  2. Key 属性

    • 表示该分组的键值
    • 类型为 TKey,由分组条件决定

基本用法示例

1. 简单分组

List<Employee> employees = new List<Employee>
{
    new Employee { Name = "Alice", Department = "HR", Salary = 50000 },
    new Employee { Name = "Bob", Department = "IT", Salary = 60000 },
    new Employee { Name = "Charlie", Department = "HR", Salary = 55000 },
    new Employee { Name = "David", Department = "IT", Salary = 70000 }
};

// 按部门分组
var groups = employees.GroupBy(e => e.Department);

foreach (var group in groups)
{
    Console.WriteLine($"部门: {group.Key}");
    foreach (var emp in group)
    {
        Console.WriteLine($" - {emp.Name}: {emp.Salary}");
    }
}

输出结果

部门: HR
 - Alice: 50000
 - Charlie: 55000
部门: IT
 - Bob: 60000
 - David: 70000

2. 分组后聚合计算

var departmentStats = employees
    .GroupBy(e => e.Department)
    .Select(g => new {
        Department = g.Key,
        EmployeeCount = g.Count(),
        AverageSalary = g.Average(e => e.Salary),
        MaxSalary = g.Max(e => e.Salary)
    });

foreach (var stat in departmentStats)
{
    Console.WriteLine($"{stat.Department}部门: " +
        $"人数={stat.EmployeeCount}, " +
        $"平均工资={stat.AverageSalary}, " +
        $"最高工资={stat.MaxSalary}");
}

输出结果

HR部门: 人数=2, 平均工资=52500, 最高工资=55000
IT部门: 人数=2, 平均工资=65000, 最高工资=70000

高级分组技巧

1. 复合键分组

// 按部门和薪资范围分组
var groups = employees.GroupBy(e => new {
    e.Department,
    SalaryRange = e.Salary / 10000 * 10000 // 按万为单位分组
});

foreach (var group in groups)
{
    Console.WriteLine($"部门: {group.Key.Department}, " +
        $"薪资范围: {group.Key.SalaryRange}-{group.Key.SalaryRange + 9999}");
    foreach (var emp in group)
    {
        Console.WriteLine($" - {emp.Name}: {emp.Salary}");
    }
}

2. 分组后元素转换

// 分组后只保留员工姓名
var nameGroups = employees
    .GroupBy(e => e.Department, 
             e => e.Name); // 元素选择器

foreach (var group in nameGroups)
{
    Console.WriteLine($"部门: {group.Key}");
    Console.WriteLine($"员工: {string.Join(", ", group)}");
}

3. 自定义结果选择器

var results = employees.GroupBy(
    keySelector: e => e.Department,
    resultSelector: (key, elements) => new {
        Department = key,
        Employees = elements.Select(e => e.Name),
        TotalSalary = elements.Sum(e => e.Salary)
    });

foreach (var result in results)
{
    Console.WriteLine($"{result.Department}部门: " +
        $"总薪资={result.TotalSalary}, " +
        $"员工={string.Join(", ", result.Employees)}");
}

IGrouping 的实际应用

1. 直接访问分组键

var groups = employees.GroupBy(e => e.Department);

// 获取所有部门列表
var departments = groups.Select(g => g.Key).ToList();
// ["HR", "IT"]

2. 分组嵌套处理

foreach (var group in groups)
{
    Console.WriteLine($"--- {group.Key} 部门员工详情 ---");
    
    // 分组内排序
    var sortedEmployees = group.OrderByDescending(e => e.Salary);
    
    foreach (var emp in sortedEmployees)
    {
        Console.WriteLine($"{emp.Name}: {emp.Salary}");
    }
}

3. 转换为字典

// 将分组转换为字典
Dictionary<string, List<Employee>> departmentDict = groups
    .ToDictionary(g => g.Key, g => g.ToList());

// 访问特定部门
var hrEmployees = departmentDict["HR"];

性能注意事项

  1. 延迟执行

    • GroupBy() 是延迟执行方法
    • 实际分组操作在枚举结果时发生
  2. 内存占用

    • 分组操作需要将整个数据集加载到内存
    • 大数据集考虑使用数据库分组
  3. 数据库优化

    • 在 Entity Framework 中,GroupBy() 会转换为 SQL 的 GROUP BY
    • 确保分组字段有索引
// EF Core 中的分组
var departmentStats = dbContext.Employees
    .GroupBy(e => e.Department)
    .Select(g => new {
        Department = g.Key,
        Count = g.Count()
    })
    .ToList();

投影:

把集合中的每一项转换为另外一种类型。
IEnumerable names = list.Select(e=> e.Gender?“男”:“女”);
var dogs = list.Select(p => new Dog { NickName = e.Name, Age = e.Age });

C# LINQ 投影操作详解

投影的本质

投影(Projection)是 LINQ 中的核心概念,指的是将集合中的每个元素转换为另一种形式或类型的操作。这类似于数学中的映射函数,将输入集合中的每个元素映射到输出集合中的新元素。

C# LINQ 投影操作详解

投影的本质

投影(Projection)是 LINQ 中的核心概念,指的是将集合中的每个元素转换为另一种形式或类型的操作。这类似于数学中的映射函数,将输入集合中的每个元素映射到输出集合中的新元素。

核心方法:Select()

Select() 方法是 LINQ 中实现投影的主要方式,它允许您:

  • 提取对象的特定属性
  • 创建新的对象结构
  • 执行计算并返回结果
  • 转换数据类型

方法签名

IEnumerable<TResult> Select<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TResult> selector
)

投影的基本用法

1. 提取属性值

List<Employee> employees = new List<Employee>
{
    new Employee { Name = "Alice", Age = 30 },
    new Employee { Name = "Bob", Age = 25 }
};

// 投影到名字列表
IEnumerable<string> names = employees.Select(e => e.Name);
// 结果: ["Alice", "Bob"]

2. 创建新对象

// 投影到匿名对象
var employeeInfos = employees.Select(e => new {
    Name = e.Name,
    BirthYear = DateTime.Now.Year - e.Age
});
// 结果: [{Name="Alice", BirthYear=1993}, {Name="Bob", BirthYear=1998}]

3. 转换类型

// 转换为DTO对象
List<EmployeeDTO> dtos = employees.Select(e => new EmployeeDTO {
    EmployeeName = e.Name,
    Age = e.Age
}).ToList();

高级投影技巧

1. 带索引的投影

// 包含元素索引
var indexed = employees.Select((e, index) => new {
    Index = index,
    e.Name
});
// 结果: [{Index=0, Name="Alice"}, {Index=1, Name="Bob"}]

2. 嵌套投影

// 嵌套集合投影
var departments = new List<Department>
{
    new Department {
        Name = "Dev",
        Employees = new List<Employee> { /* ... */ }
    }
};

var employeeNamesByDept = departments.Select(d => new {
    DeptName = d.Name,
    EmployeeNames = d.Employees.Select(e => e.Name)
});

3. 条件投影

// 根据条件返回不同投影
var mixed = employees.Select(e => e.Age > 25 ? 
    new { e.Name, Category = "Senior" } : 
    new { e.Name, Category = "Junior" });

4. 计算字段投影

// 计算年薪(月薪*12)
var annualSalaries = employees.Select(e => new {
    e.Name,
    AnnualSalary = e.MonthlySalary * 12
});

实际应用场景

1. 数据转换(Entity → DTO)

// 数据库实体转视图模型
var viewModels = dbContext.Products
    .Where(p => p.Price > 100)
    .Select(p => new ProductViewModel {
        Id = p.Id,
        Name = p.Name,
        Price = p.Price * 1.1 // 添加增值税
    })
    .ToList();

2. 数据简化

// 只选择需要的字段
var lightweights = bigList.Select(item => new {
    item.Id,
    item.CreatedDate
});

3. 计算字段

// 计算BMI
var bmiData = persons.Select(p => new {
    p.Name,
    BMI = p.Weight / (p.Height * p.Height)
});

4. 组合数据

// 组合多个来源的数据
var combined = employees.Select(e => new {
    e.Name,
    DepartmentName = departments.First(d => d.Id == e.DeptId).Name
});

性能考虑

  1. 延迟执行

    • Select() 是延迟执行的,只有在实际枚举结果时才会执行投影
  2. 高效转换

    • 在数据库查询中(如 EF Core),Select() 会转换为 SQL 的 SELECT 子句
    • 只选择需要的字段可以减少数据传输量
  3. 避免重复计算

    // 低效:重复计算
    var inefficient = list.Select(x => new {
         Value = HeavyCalculation(x)
     });
     
    // 高效:预计算
    var efficient = list.Select(x => {
         var result = HeavyCalculation(x);
         return new { Value = result };
     });
    

与 SelectMany() 的区别

特性 Select SelectMany
输入 单个元素 元素集合
输出 转换后的单个元素 展平的集合
嵌套集合 返回嵌套集合 展平嵌套集合
使用场景 简单转换 处理一对多关系
// Select 返回嵌套集合
var nested = departments.Select(d => d.Employees.Select(e => e.Name));

// SelectMany 展平嵌套集合
var flat = departments.SelectMany(d => d.Employees.Select(e => e.Name));

最佳实践

  1. 明确目标类型

    • 使用具体类型而非 var 提高可读性
    List<string> names = employees.Select(e => e.Name).ToList();
    
  2. 避免过度投影

    • 只选择真正需要的字段
    • 避免选择整个对象再丢弃不需要的字段
  3. 结合过滤

    // 先过滤再投影,提高效率
    var activeUsers = users
        .Where(u => u.IsActive)
        .Select(u => u.Email);
    
  4. 使用查询语法

    // 与方法语法等效
    var results = from e in employees
                 select new { e.Name, e.Age };
    

集合转换:

有一些地方需要数组类型或者List类型的变量,我们可以用ToArray()方法和ToList()分别把IEnumerable<T>转换为数组类型和List<T>类型。


网站公告

今日签到

点亮在社区的每一天
去签到