文章目录
- 前言
- LINQ
- 一、LINQ1
- 一、LINQ2
- 一、LINQ3
- Where方法:每一项数据都会进过predicate的测试,如果针对一个元素,predicate执行的返回值为true,那么这个元素就会放到返回值中。
- 获取一条数据(是否带参数的两种写法):
- C# LINQ 查询方法详解:Single, SingleOrDefault, First, FirstOrDefault
- 方法对比表
- 详细解释与示例
- 1. Single
- 2. SingleOrDefault
- 3. First
- 4. FirstOrDefault
- 性能考虑
- 默认值说明
- 最佳实践建议
- 总结对比图
- 排序:
- C# LINQ 排序方法详解:OrderBy 与 OrderByDescending
- 基本概念
- 基本语法
- 示例解释:`list.OrderBy(e => e.Age)`
- 执行过程:
- 排序结果:
- 完整排序示例
- 1. 单属性排序
- 2. 多级排序(ThenBy/ThenByDescending)
- 3. 自定义排序逻辑
- C# 特殊排序场景详解:简单类型、末位字符与随机排序
- 一、简单类型排序(不使用 Lambda 表达式)
- 1. 基本排序方法
- 2. C# 11+ 的简化语法
- 3. 字符串集合排序
- 二、特殊案例:按最后一个字符排序
- 1. 基本实现
- 2. 处理空字符串和单字符
- 3. 多级排序(先按长度,再按末字符)
- 三、随机排序(使用 Guid 或随机数)
- 1. 使用 Guid 随机排序
- 限制结果集,获取部分数据:
- C# LINQ 分页操作详解:Skip 与 Take
- 基本概念
- 基本语法
- 方法详解
- 1. Skip(n)
- 2. Take(n)
- 组合使用:分页实现
- 基本分页公式
- 完整分页示例
- 分页辅助方法
- 集合函数:
- C# LINQ 聚合方法与链式调用详解
- LINQ 聚合方法概述
- 链式调用原理
- 链式调用示例
- 代码解析:`list.Where(e=>e.Age>30).Min(e=>e.Age);`
- 执行步骤
- 等效传统代码
- 注意事项
- 其他聚合方法链式调用示例
- 1. 计算平均值
- 2. 求和统计
- 3. 计数统计
- 4. 多级聚合
- 链式调用的高级应用
- 1. 条件聚合
- 2. 组合使用
- 3. 空值处理技巧
- 分组:
- C# LINQ GroupBy 分组方法详解
- GroupBy 方法核心概念
- 方法签名
- 关键特性
- IGrouping 接口解析
- 核心特性
- 基本用法示例
- 1. 简单分组
- 2. 分组后聚合计算
- 高级分组技巧
- 1. 复合键分组
- 2. 分组后元素转换
- 3. 自定义结果选择器
- IGrouping 的实际应用
- 1. 直接访问分组键
- 2. 分组嵌套处理
- 3. 转换为字典
- 性能注意事项
- 投影:
- C# LINQ 投影操作详解
- 投影的本质
- C# LINQ 投影操作详解
- 投影的本质
- 核心方法:Select()
- 方法签名
- 投影的基本用法
- 1. 提取属性值
- 2. 创建新对象
- 3. 转换类型
- 高级投影技巧
- 1. 带索引的投影
- 2. 嵌套投影
- 3. 条件投影
- 4. 计算字段投影
- 实际应用场景
- 1. 数据转换(Entity → DTO)
- 2. 数据简化
- 3. 计算字段
- 4. 组合数据
- 性能考虑
- 与 SelectMany() 的区别
- 最佳实践
- 集合转换:
前言
LINQ
一、LINQ1
委托->lambda->LINQ
1、委托是可以指向方法的类型,调用委托变量时执行的就是变量指向方法。
在 C# 中,委托(Delegate) 是一种类型安全的函数指针,它允许将方法作为参数传递、存储或动态调用。委托是事件(Event)和回调机制的基础,实现了松耦合的设计模式。
核心概念
1.类型安全的方法引用
委托定义了方法的签名(参数类型和返回类型),只能绑定匹配签名的方法。
2.类似接口
委托类似于只包含一个方法的接口,但更轻量且直接。
3.多播能力
一个委托实例可绑定多个方法(+= 添加),调用时按顺序执行所有方法。
委托的声明与使用
- 定义委托类型
// 声明一个委托类型,指定方法签名
public delegate void MyDelegate(string message);
- 绑定方法
// 目标方法(签名必须匹配)
public void ShowMessage(string msg)
{Console.WriteLine($"Message: {msg}");
}// 实例化委托并绑定方法
MyDelegate del = new MyDelegate(ShowMessage);
- 调用委托
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# 扩展方法深度解析
一、本质与原理
-
核心概念
扩展方法是一种编译时语法糖,它允许开发者在不修改原始类型、不创建派生类的情况下,为现有类型"添加"新方法。其本质是静态方法,但通过编译器魔法实现了实例方法调用语法。 -
实现机制
// 定义扩展方法
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");
- 关键特性
- 静态伪装:静态方法伪装成实例方法
- 非侵入性:不修改原始类型代码
- 编译时解析:在编译阶段确定方法绑定
- 命名空间控制:需导入扩展方法所在命名空间
二、技术实现详解
- 三大必要条件
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);}
}
- 参数规则
- 首个参数:必须使用
this
修饰,指定目标类型 - 附加参数:可添加多个常规参数
public static string Wrap(this string text, string wrapper)=> $"{wrapper}{text}{wrapper}";// 使用
"Hello".Wrap("**"); // 输出:**Hello**
- 方法重载
// 重载1:默认包装符
public static string Wrap(this string text) => Wrap(text, "[]");// 重载2:自定义包装符
public static string Wrap(this string text, string wrapper) => $"{wrapper}{text}{wrapper}";
三、高级应用场景
- 接口扩展
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();
- 链式调用 (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");
- 空值处理模式
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风格通用扩展方法
- 完整实现示例
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;}
}
- 多类型兼容使用
// 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,且不会继续寻找后面的数据,否则返回falseforeach (Employee item in items1)
{Console.WriteLine(item.Name);
}
获取一条数据(是否带参数的两种写法):
Single:有且只有一条满足要求的数据;
SingleOrDefault:最多只有一条满足要求的数据;
First:至少有一条,返回第一条;
FirstOrDefault:返回第一条或者默认值;
C# LINQ 查询方法详解:Single, SingleOrDefault, First, FirstOrDefault
在 C# 的 LINQ 查询中,Single
, SingleOrDefault
, First
和 FirstOrDefault
是常用的元素检索方法,它们有不同的行为和使用场景。下面我将详细解释它们的区别和使用方法。
方法对比表
方法 | 返回值条件 | 无匹配时行为 | 多个匹配时行为 | 使用场景 |
---|---|---|---|---|
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)
- 使用前应检查是否等于默认值
最佳实践建议
-
优先使用 FirstOrDefault:
- 大多数情况下是最安全的选择
- 避免异常处理,代码更简洁
-
谨慎使用 Single:
- 仅在确保只有一个匹配项时使用
- 数据库主键查询是典型场景
-
避免在可能多个匹配项时使用 SingleOrDefault:
- 多个匹配项会抛出异常
- 使用前应确认数据唯一性
-
处理默认值:
var result = list.FirstOrDefault();
if (result != null) // 对于引用类型
{// 处理结果
}var valueResult = intList.FirstOrDefault();
if (valueResult != default) // 对于值类型
{// 处理结果
}
总结对比图
唯一性要求 存在性要求↓ ↓
Single: 有且只有一条满足要求的数据
SingleOrDefault: 最多只有一条满足要求的数据
First: 至少有一条,返回第一条
FirstOrDefault: 返回第一条或者默认值
根据你的具体需求选择合适的方法:
- 需要唯一结果 →
Single
或SingleOrDefault
- 需要第一个结果 →
First
或FirstOrDefault
- 不确定是否存在结果 →
...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);
执行过程:
- 遍历
list
中的所有员工 - 提取每个员工的
Age
属性值作为排序键 - 按照年龄从小到大排序
- 返回排序后的新序列(原始列表不会被修改)
排序结果:
假设原始列表年龄为:[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),这得益于:
- 几乎所有 LINQ 方法都是针对
IEnumerable<T>
接口的扩展方法 - 大多数方法返回
IEnumerable<T>
或IOrderedEnumerable<T>
- 每个方法操作前一个方法返回的结果集
链式调用示例
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);
执行步骤
-
过滤阶段:
var filtered = list.Where(e => e.Age > 30);
- 遍历原始集合
list
- 筛选出年龄大于30的元素
- 返回
IEnumerable<Employee>
类型的结果集
- 遍历原始集合
-
聚合阶段:
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("序列不包含任何元素");
}
注意事项
-
空集合处理:
- 如果
Where
过滤后没有元素,Min()
会抛出InvalidOperationException
- 安全处理方式:
var minAge = list.Where(e => e.Age > 30).Select(e => e.Age).DefaultIfEmpty(0).Min();
- 如果
-
性能优化:
- 对于大型集合,考虑使用更高效的算法:
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; }
}
核心特性
-
继承自 IEnumerable
- 每个分组本身是一个可枚举集合
- 可以遍历分组内的所有元素
-
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"];
性能注意事项
-
延迟执行:
GroupBy()
是延迟执行方法- 实际分组操作在枚举结果时发生
-
内存占用:
- 分组操作需要将整个数据集加载到内存
- 大数据集考虑使用数据库分组
-
数据库优化:
- 在 Entity Framework 中,
GroupBy()
会转换为 SQL 的GROUP BY
- 确保分组字段有索引
- 在 Entity Framework 中,
// 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
});
性能考虑
-
延迟执行:
Select()
是延迟执行的,只有在实际枚举结果时才会执行投影
-
高效转换:
- 在数据库查询中(如 EF Core),
Select()
会转换为 SQL 的SELECT
子句 - 只选择需要的字段可以减少数据传输量
- 在数据库查询中(如 EF Core),
-
避免重复计算:
// 低效:重复计算 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));
最佳实践
-
明确目标类型:
- 使用具体类型而非
var
提高可读性
List<string> names = employees.Select(e => e.Name).ToList();
- 使用具体类型而非
-
避免过度投影:
- 只选择真正需要的字段
- 避免选择整个对象再丢弃不需要的字段
-
结合过滤:
// 先过滤再投影,提高效率 var activeUsers = users.Where(u => u.IsActive).Select(u => u.Email);
-
使用查询语法:
// 与方法语法等效 var results = from e in employeesselect new { e.Name, e.Age };
集合转换:
有一些地方需要数组类型或者List类型的变量,我们可以用ToArray()方法和ToList()分别把IEnumerable<T>
转换为数组类型和List<T>
类型。