文章目錄
- 前言
- 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>
類型。