C#Linq查询(七):集合转换与创建(纯干货)
七、集合转换与创建
用于转换序列类型(如将IEnumerable<T>转为List<T>)或创建新序列(如连续整数、重复元素)。这类操作是连接 “查询逻辑” 与 “具体使用场景” 的桥梁,能根据需求灵活调整序列的存储和访问方式。
1. ToArray() / ToList() / ToDictionary() / ToHashSet()
将序列转换为具体集合类型(数组、列表、字典、哈希集合),便于后续操作(如索引访问、快速查找)。转换后得到的是独立集合(修改原序列不会影响转换结果)。
(1)ToArray() / ToList():基础类型转换
将序列转为T[](数组)或List<T>(列表),解决IEnumerable<T>“延迟执行” 的问题(转换时立即执行查询并缓存结果)。
性能特性:O (n) 复杂度(需遍历序列生成新集合),转换后支持索引访问(数组和列表均为 O (1) 索引)。
典型场景:
- 需要多次遍历序列(避免重复执行查询逻辑)。
- 需要索引访问(如按位置获取元素)。
- 需要修改集合(如添加 / 删除元素,
IEnumerable<T>本身不可修改)。
示例 1:基础转换与延迟执行处理
// 定义一个延迟执行的序列(仅在遍历/转换时执行)
IEnumerable<int> numbers = Enumerable.Range(1, 3).Select(x => x * 2); // {2,4,6}(未执行)
// 转为数组(立即执行查询,缓存结果)
int[] arr = numbers.ToArray(); // 执行查询,结果:[2,4,6]
// 转为列表(立即执行查询,缓存结果)
List<int> list = numbers.ToList(); // 结果:{2,4,6}
// 修改原序列的数据源(不影响已转换的集合)
numbers = Enumerable.Range(1, 2).Select(x => x * 2); // 新序列 {2,4}
Console.WriteLine(arr.Length); // 仍为 3(转换结果已缓存)
示例 2:解决 “多次遍历重复计算” 问题
// 模拟一个耗时的查询(每次遍历都会重新计算)
IEnumerable<int> expensiveQuery = GetExpensiveNumbers();
// 错误:多次遍历导致重复计算
int count1 = expensiveQuery.Count(); // 第一次计算
int sum1 = expensiveQuery.Sum(); // 第二次计算
// 正确:先转换为列表,后续操作使用缓存结果
List<int> cached = expensiveQuery.ToList(); // 仅计算一次
int count2 = cached.Count; // 直接访问属性(O(1))
int sum2 = cached.Sum(); // 使用缓存数据
(2)ToDictionary(keySelector):转换为字典
将序列转为Dictionary<TKey, TSource>,通过 “键选择器” 指定键(键必须唯一),支持 O (1) 复杂度的键查找。
核心参数:
keySelector:从元素中提取键的逻辑(如x => x.Id)。- 可选
elementSelector:指定字典的值(默认值为元素本身)。 - 可选
comparer:键的比较器(如忽略大小写的字符串比较)。
异常场景:键重复时抛 ArgumentException(需确保键唯一)。
典型场景:按唯一键(如 ID)快速查询元素(比 FirstOrDefault(x => x.Id == id) 更高效)。
示例 1:基础字典转换
public class Book {
public int Id { get; set; }
public string Title { get; set; }
}
var books = new List<Book> {
new Book { Id = 1, Title = "C# 入门" },
new Book { Id = 2, Title = "LINQ 实战" }
};
// 按 Id 转为字典(键:Id,值:Book 对象)
Dictionary<int, Book> bookDict = books.ToDictionary(b => b.Id);
// 通过键快速查找(O(1) 复杂度)
if (bookDict.TryGetValue(2, out Book book)) {
Console.WriteLine(book.Title); // "LINQ 实战"
}
示例 2:自定义键和值,处理键重复
var users = new List<User> {
new User { Id = 1, Name = "张三" },
new User { Id = 2, Name = "李四" }
};
// 键:Id,值:Name(仅存储名称)
Dictionary<int, string> nameDict = users
.ToDictionary(u => u.Id, u => u.Name); // 值选择器:u => u.Name
// 处理可能的键重复(先去重)
var duplicateKeys = new List<User> {
new User { Id = 1, Name = "A" },
new User { Id = 1, Name = "B" } // 重复键
};
// 安全转换:取第一个重复键的元素
Dictionary<int, User> safeDict = duplicateKeys
.GroupBy(u => u.Id)
.ToDictionary(g => g.Key, g => g.First()); // 按组取第一个元素
(3)ToHashSet():转换为哈希集合
将序列转为HashSet<T>,去重并支持 O (1) 复杂度的Contains检查(比List.Contains的 O (n) 更高效)。
性能特性:去重(自动忽略重复元素),查找性能优异(适合频繁检查包含性的场景)。
典型场景:去重、快速元素检查(如 “判断用户是否在黑名单中”)。
示例:去重与快速查找
var numbers = new List<int> { 1, 2, 2, 3, 3, 3 };
// 转为 HashSet(自动去重)
HashSet<int> uniqueNumbers = numbers.ToHashSet(); // {1,2,3}
// 快速检查元素是否存在(O(1))
bool has2 = uniqueNumbers.Contains(2); // true
// 与 List.Contains 对比(大集合时性能差异明显)
List<int> bigList = Enumerable.Repeat(1, 1000000).ToList();
HashSet<int> bigSet = bigList.ToHashSet();
// List.Contains 是 O(n)(慢)
bool listCheck = bigList.Contains(2);
// HashSet.Contains 是 O(1)(快)
bool setCheck = bigSet.Contains(2);
2. AsEnumerable() / AsQueryable():切换查询类型
切换序列的编译时类型(IEnumerable<T> ↔ IQueryable<T>),控制查询执行的位置(客户端 vs 数据库)。
核心作用:
AsEnumerable():将IQueryable<T>转为IEnumerable<T>,后续操作在客户端内存执行(而非数据库)。AsQueryable():将IEnumerable<T>转为IQueryable<T>,后续操作可被转换为数据库查询(如 EF Core 的 SQL)。
示例 1:AsEnumerable() 控制查询执行位置
// 假设 dbContext.Users 是 IQueryable<User>(数据库查询)
var dbUsers = dbContext.Users;
// 场景:需在客户端执行数据库不支持的逻辑(如自定义方法)
var result = dbUsers
.Where(u => u.Age > 18) // 这部分在数据库执行(转为 SQL:WHERE Age > 18)
.AsEnumerable() // 后续操作切换到客户端执行
.Where(u => IsValidUser(u)) // 客户端执行(数据库不认识 IsValidUser 方法)
.ToList();
示例 2:AsQueryable() 支持数据库查询
// 内存集合(IEnumerable)
List<User> memoryUsers = GetUsersFromMemory();
// 转为 IQueryable,支持类似数据库的查询转换
IQueryable<User> queryableUsers = memoryUsers.AsQueryable();
// 后续操作可被解析为表达式树(适合模拟数据库查询测试)
var adults = queryableUsers
.Where(u => u.Age > 18)
.Select(u => u.Name)
.ToList();
3. Range() / Repeat() / Empty<T>():创建新序列
直接创建特定规则的序列(无需手动初始化集合)。
(1)Range(start, count):创建连续整数序列
生成从start开始的count个连续整数(如Range(1,3) → 1,2,3)。
示例:
// 创建 1~5 的整数序列
IEnumerable<int> numbers = Enumerable.Range(1, 5); // {1,2,3,4,5}
// 扩展:创建日期序列(连续 7 天)
DateTime startDate = DateTime.Today;
IEnumerable<DateTime> dates = Enumerable
.Range(0, 7)
.Select(i => startDate.AddDays(i)); // 今天到第6天的日期
(2)Repeat(element, count):创建重复元素序列
生成包含count个重复element的序列(如Repeat("a",3) → "a","a","a")。
示例:
// 创建 3 个相同的默认用户
IEnumerable<User> defaultUsers = Enumerable
.Repeat(new User { Name = "Guest" }, 3);
// 注意:引用类型重复的是同一个对象(修改一个会影响所有)
var sharedUser = new User { Name = "Shared" };
var users = Enumerable.Repeat(sharedUser, 2).ToList();
users[0].Name = "Modified";
Console.WriteLine(users[1].Name); // "Modified"(同个对象)
(3)Empty<T>():创建空序列
返回空的IEnumerable<T>(比new List<T>()更轻量,无需初始化集合)。
示例:
// 返回空序列(避免返回 null)
IEnumerable<User> GetUsersIfExists(bool exists) {
if (exists) return new List<User> { new User() };
return Enumerable.Empty<User>(); // 空序列(非 null)
}
// 安全处理:空序列可直接调用 LINQ 方法(不会抛异常)
var empty = Enumerable.Empty<int>();
int count = empty.Count(); // 0(安全)
(4). 序列创建方法对比
| 方法 | 作用 | 适用场景 | 注意事项 |
|---|---|---|---|
Range(start, count) |
创建连续整数序列 | 生成有序数字(如页码、日期偏移) | 仅支持整数,count 必须 ≥0 |
Repeat(element, count) |
创建重复元素序列 | 生成默认值集合、测试数据 | 引用类型重复的是对象引用 |
Empty<T>() |
创建空序列 | 作为默认返回值(避免 null) | 比 new List<T>() 更轻量 |
核心对比与注意事项
| 转换方法 | 目标类型 | 核心优势 | 关键注意事项 |
|---|---|---|---|
ToArray() |
T[] |
索引访问、固定长度 | 不可动态添加元素 |
ToList() |
List<T> |
动态增删、索引访问 | 可修改长度(Add/Remove) |
ToDictionary() |
Dictionary<TKey, T> |
键值查找(O (1)) | 键必须唯一,否则抛异常 |
ToHashSet() |
HashSet<T> |
去重、包含检查(O (1)) | 元素需重写 GetHashCode |
AsEnumerable() |
IEnumerable<T> |
切换到客户端执行 | 不修改数据,仅改变编译类型 |
AsQueryable() |
IQueryable<T> |
支持数据库查询转换 | 需要表达式树支持 |
关键注意事项
-
转换的即时性:
ToArray()/ToList()等方法会立即执行查询(触发延迟执行的序列),并缓存结果(后续原序列变化不影响转换结果)。 -
键的唯一性:
ToDictionary()要求键唯一,否则抛异常(可先GroupBy去重)。 -
性能权衡:
- 频繁查找用
ToHashSet()/ToDictionary(); - 动态增删用
ToList(); - 固定长度用
ToArray()(内存占用略低)。
- 频繁查找用
-
查询类型切换:
AsEnumerable()和AsQueryable()不修改数据,仅控制后续操作的执行位置(客户端 vs 数据库),避免 “将复杂逻辑推给数据库” 或 “在客户端执行大量数据过滤”。
更多推荐
所有评论(0)