七、集合转换与创建

用于转换序列类型(如将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> 支持数据库查询转换 需要表达式树支持

关键注意事项

  1. 转换的即时性ToArray()/ToList()等方法会立即执行查询(触发延迟执行的序列),并缓存结果(后续原序列变化不影响转换结果)。

  2. 键的唯一性ToDictionary()要求键唯一,否则抛异常(可先GroupBy去重)。

  3. 性能权衡:

    • 频繁查找用ToHashSet()/ToDictionary()
    • 动态增删用ToList()
    • 固定长度用ToArray()(内存占用略低)。
  4. 查询类型切换AsEnumerable()AsQueryable()不修改数据,仅控制后续操作的执行位置(客户端 vs 数据库),避免 “将复杂逻辑推给数据库” 或 “在客户端执行大量数据过滤”。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐