Skip to content

LINQ 与性能

LINQ 是什么?

一句话讲清楚

LINQLanguage Integrated Query,意思是语言集成查询。它把“查询数据”的能力集成到 C# 里,让你可以用统一写法处理数组、ListDictionary、XML、数据库等数据。linq-explanation

它能做什么

LINQ 常用来做:

筛选:Where 转换:Select 排序:OrderByOrderByDescending 分组:GroupBy 判断:AnyAll 查找:FirstFirstOrDefault 统计:CountSumMaxMin 转集合:ToListToArray

普通写法和 LINQ 写法

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
public sealed class Player // 定义玩家类
{ // 类开始
    public string Name; // 玩家名字
    public int Level; // 玩家等级
    public int Score; // 玩家分数
} // 类结束
public static class LinqExample // 定义 LINQ 示例类
{ // 类开始
    public static List<string> GetHighLevelPlayerNames(List<Player> players) // 获取高等级玩家名字列表
    { // 方法开始
        List<string> names = players // 从玩家列表开始查询
            .Where(player => player.Level >= 10) // 筛选等级大于等于 10 的玩家
            .OrderByDescending(player => player.Score) // 按分数从高到低排序
            .Select(player => player.Name) // 把 Player 对象转换成玩家名字
            .ToList(); // 真正执行查询并生成 List<string>
        return names; // 返回名字列表
    } // 方法结束
} // 类结束

LINQ 的本质

LINQ 很多方法其实是扩展方法。

比如:

players.Where(...)players.Select(...)players.OrderBy(...)

这些方法大多作用在 IEnumerable<T> 上,所以只要一个集合能被枚举,就能用很多 LINQ 操作。

延迟执行是什么

这是 LINQ 面试最常问的点。

WhereSelect 这类方法通常不会立刻执行。 它们只是先描述一个查询规则。 等你真正遍历它,或者调用 ToList()ToArray()Count()First() 的时候,查询才会执行。

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
public static class DeferredExecutionExample // 定义延迟执行示例类
{ // 类开始
    public static List<int> GetEvenNumbers(List<int> numbers) // 获取偶数列表
    { // 方法开始
        IEnumerable<int> query = numbers.Where(number => number % 2 == 0); // 这里只是创建查询规则,还没有真正遍历
        List<int> result = query.ToList(); // 调用 ToList 时才真正执行筛选
        return result; // 返回偶数结果
    } // 方法结束
} // 类结束

Unity 里要注意什么

LINQ 写起来清晰,但在 Unity 热路径里要小心。

比如 Update、AI Tick、战斗判定、UI 高频刷新里频繁使用 LINQ,可能产生:

迭代器对象。 闭包分配。 临时集合。 装箱。 GC Alloc。

所以 Unity 项目里通常建议:

初始化、编辑器工具、配置处理可以用 LINQ。 战斗、移动、AI、每帧刷新尽量少用 LINQ。 高频逻辑用普通 for 循环更可控。

面试高分回答

NOTE

LINQ 是 C# 的语言集成查询能力,它提供了一套统一的查询方式,可以对集合、数组、XML、数据库等数据源进行筛选、排序、投影、分组和统计。LINQ 常用方法有 WhereSelectOrderByGroupByAnyFirstOrDefaultToList 等。它的优点是表达清晰、代码简洁;但很多 LINQ 查询具有延迟执行特性,只有遍历或调用 ToListCountFirst 等方法时才真正执行。在 Unity 中,LINQ 可能带来额外分配和 GC,所以不建议在 Update、战斗、AI 等热路径频繁使用。

LINQ 延迟执行是什么意思?

一句话讲清楚

LINQ 延迟执行就是:你写下 WhereSelect 时,查询通常不会马上执行,只是先保存一条查询规则;等你真正遍历它,或者调用 ToList()Count()First() 这类方法时,才开始执行。

linq-deferred-execution

先看代码

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
public static class LinqDeferredExample // 定义 LINQ 延迟执行示例类
{ // 类开始
    public static List<int> Run() // 定义运行方法
    { // 方法开始
        List<int> numbers = new List<int> { 1, 2, 3 }; // 创建原始数字列表
        IEnumerable<int> query = numbers.Where(number => number > 1); // 创建查询规则,此时还没有真正筛选
        numbers.Add(4); // 在查询真正执行前,往原始集合里添加 4
        List<int> result = query.ToList(); // 调用 ToList 时才真正执行查询
        return result; // 返回结果,结果是 2、3、4
    } // 方法结束
} // 类结束

这里最关键的是这一行:

c
IEnumerable<int> query = numbers.Where(number => number > 1); // 创建查询规则,此时还没有真正筛选

它不是马上把 2、3 找出来,而是创建了一个“以后怎么查”的规则。

后面你加了 4,再 ToList(),结果会包含 4

什么时候会真正执行

这些通常是延迟执行:

WhereSelectTakeSkipOrderBy

这些通常会触发执行:

foreachToList()ToArray()Count()First()Any()Sum()

简单记: 中间操作多半只是描述查询,最终要结果的时候才执行。

延迟执行的好处

它可以按需处理数据,不一定一开始就把所有结果算出来。 如果你只取前几个结果,它可能不用处理完整个集合。 多个 LINQ 操作可以串起来,最后统一执行。

比如:

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
public static class LinqTakeExample // 定义 Take 示例类
{ // 类开始
    public static List<int> GetFirstThreeEvenNumbers(List<int> numbers) // 获取前三个偶数
    { // 方法开始
        List<int> result = numbers // 从原始列表开始查询
            .Where(number => number % 2 == 0) // 延迟筛选偶数
            .Take(3) // 延迟限制最多取 3 个
            .ToList(); // 这里才真正执行查询
        return result; // 返回前三个偶数
    } // 方法结束
} // 类结束

延迟执行的坑

第一个坑:查询变量不是结果快照。

c
IEnumerable<int> query = numbers.Where(number => number > 1); // query 保存的是查询规则,不是固定结果

如果 numbers 后面变了,最终结果也可能变。

第二个坑:重复枚举会重复执行。

如果你对同一个 query 遍历两次,它可能执行两次筛选逻辑。 如果筛选里有复杂计算、日志、网络请求、随机数,就会出现意料之外的问题。

第三个坑:ToList() 会立即执行并分配新集合。

它能固定结果,但会产生新 List。Unity 里如果频繁这么做,可能产生 GC Alloc。

Unity 里怎么用

初始化、编辑器工具、配置筛选,用 LINQ 很舒服。 Update、战斗判定、AI Tick、UI 高频刷新里要谨慎。 如果结果要重复用,可以初始化时 ToList() 缓存。 如果是每帧逻辑,普通 for 循环通常更可控。

面试高分回答

NOTE

LINQ 延迟执行指的是很多 LINQ 方法,比如 WhereSelectTake,调用时不会立刻遍历集合,而是返回一个保存查询规则的 IEnumerable。只有在真正枚举它,比如 foreach,或者调用 ToListCountFirst 等终止操作时,查询才会执行。它的好处是按需计算、表达灵活;坑是查询结果不是快照,数据源变化会影响最终结果,而且重复枚举会重复执行。在 Unity 中,延迟执行加上 ToList、闭包、迭代器可能带来 GC 和性能开销,所以热路径要谨慎使用。

WhereSelectToList 分别什么时候执行?

一句话讲清楚

WhereSelect 通常不会立刻执行,它们只是创建查询规则;ToList 会立刻执行整条查询,把结果真正算出来并放进一个新的 List 里。

linq-where-select-tolist-execution

分别什么时候执行

Where:定义筛选规则,真正遍历时才逐个判断。 Select:定义转换规则,真正遍历时才把元素转换成新结果。 ToList:立即执行,遍历数据源,执行前面的 WhereSelect,最后生成一个新的 List

看代码最清楚

c
using System.Collections.Generic; // 引入 List 和 IEnumerable
using System.Linq; // 引入 LINQ 扩展方法
public static class LinqExecutionExample // 定义 LINQ 执行时机示例类
{ // 类开始
    public static List<int> Run() // 定义运行方法
    { // 方法开始
        List<int> numbers = new List<int> { 1, 2, 3 }; // 创建原始数据源
        IEnumerable<int> query = numbers // 从原始集合开始创建查询
            .Where(number => number > 1) // 定义筛选规则,此时还没有真正筛选
            .Select(number => number * 10); // 定义转换规则,此时还没有真正转换
        numbers.Add(4); // 在查询执行前修改原始集合
        List<int> result = query.ToList(); // ToList 触发执行,结果是 20、30、40
        return result; // 返回真正生成好的 List
    } // 方法结束
} // 类结束

重点是这段:

c
IEnumerable<int> query = numbers.Where(...).Select(...); // 这里只是创建查询规则

这时候没有真正遍历 numbers

真正执行发生在:

c
List<int> result = query.ToList(); // 这里才真正执行 Where 和 Select

执行顺序

对每个元素来说,大概是这样:

先从 numbers 取一个元素。 先执行 Where 判断要不要。 如果 Where 返回 false,这个元素直接丢掉。 如果 Where 返回 true,再执行 Select 转换。 转换后的结果加入新 List

所以不是先把所有元素全部 Where 完,再全部 Select。很多 LINQ 查询是边遍历边处理。

ToList 的意义

ToList() 会把延迟查询变成立即结果。

调用 ToList() 前,query 是规则。 调用 ToList() 后,result 是快照。

也就是说,如果 ToList() 之后你再修改原始集合,已经生成的 result 不会跟着变。

面试高分回答

WARNING

WhereSelect 是 LINQ 的中间操作,通常是延迟执行的。调用它们时只是返回一个 IEnumerable 查询对象,并不会马上遍历集合。真正执行发生在枚举结果时,比如 foreach,或者调用 ToListToArrayCountFirst 这类终止操作。ToList 会立即遍历数据源,执行前面的 WhereSelect,并把结果物化成一个新的 List。所以 Where 决定元素要不要,Select 决定元素变成什么,ToList 决定现在立刻执行并保存结果。

Unity 中为什么慎用 LINQ?

一句话讲清楚

Unity 中慎用 LINQ,不是因为 LINQ 不能用,而是因为它在高频逻辑里可能产生额外分配、闭包、迭代器对象、临时集合和重复遍历,最终造成 GC Alloc 和卡顿。

unity-linq-caution

为什么 Unity 里要慎用

Unity 游戏逻辑是按帧运行的。

如果你在 Update 里每帧都用 LINQ,看起来一行代码很优雅,但它可能在背后创建对象。一次分配可能很小,但每秒 60 帧持续分配,就会增加 GC 压力。GC 一旦触发,就可能造成明显卡顿。

容易产生开销的地方

WhereSelect 可能创建迭代器对象。 Lambda 如果捕获外部变量,可能产生闭包对象。 ToList()ToArray() 会创建新的集合。 OrderBy() 通常需要额外排序结构。 重复枚举同一个查询,可能导致查询逻辑重复执行。

不推荐写法

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
using UnityEngine; // 引入 UnityEngine 命名空间
public sealed class EnemyFinderBad : MonoBehaviour // 定义不推荐写法的敌人查找组件
{ // 类开始
    private List<Enemy> enemies = new List<Enemy>(); // 保存敌人列表
    private Transform player; // 保存玩家 Transform
    private void Update() // Unity 每帧调用 Update
    { // 方法开始
        List<Enemy> nearEnemies = enemies // 从敌人列表开始查询
            .Where(enemy => Vector3.Distance(enemy.transform.position, player.position) < 5f) // 每帧用 LINQ 筛选附近敌人,可能产生迭代器和闭包开销
            .ToList(); // 每帧创建新的 List,容易产生 GC Alloc
    } // 方法结束
} // 类结束

这段代码在功能上没问题,但如果敌人很多、每帧执行,就不适合。

更推荐的写法

c
using System.Collections.Generic; // 引入 List 集合
using UnityEngine; // 引入 UnityEngine 命名空间
public sealed class EnemyFinderGood : MonoBehaviour // 定义推荐写法的敌人查找组件
{ // 类开始
    private readonly List<Enemy> enemies = new List<Enemy>(); // 保存所有敌人
    private readonly List<Enemy> nearEnemies = new List<Enemy>(); // 复用附近敌人列表,避免每帧 new
    private Transform player; // 保存玩家 Transform
    private void Update() // Unity 每帧调用 Update
    { // 方法开始
        nearEnemies.Clear(); // 清空复用列表,不重新分配 List
        Vector3 playerPosition = player.position; // 缓存玩家位置,避免循环中重复取
        float sqrRange = 5f * 5f; // 使用平方距离,避免 Vector3.Distance 开根号
        for (int i = 0; i < enemies.Count; i++) // 用 for 循环遍历敌人列表
        { // for 开始
            Enemy enemy = enemies[i]; // 取出当前敌人
            Vector3 offset = enemy.transform.position - playerPosition; // 计算敌人到玩家的偏移
            if (offset.sqrMagnitude > sqrRange) continue; // 超出范围就跳过
            nearEnemies.Add(enemy); // 在范围内就加入复用列表
        } // for 结束
    } // 方法结束
} // 类结束

这段更啰嗦,但更可控,适合热路径。

什么时候可以用 LINQ

编辑器工具可以用。 初始化阶段可以用。 配置表加载可以用。 低频 UI 查询可以用。 调试代码和 Demo 可以用。

这些地方追求表达清晰,LINQ 很舒服。

什么时候慎用 LINQ

UpdateFixedUpdateLateUpdate 战斗判定 怪物 AI Tick 技能目标筛选 UI 高频刷新 大量对象循环处理

这些地方追求稳定帧率和低 GC,普通 for 循环更适合。

面试高分回答

WARNING

Unity 中慎用 LINQ,主要是因为游戏逻辑很多都在每帧执行,而 LINQ 在某些情况下会创建迭代器对象、闭包、临时集合,比如 ToListToArray 会直接分配新集合,Lambda 捕获外部变量可能产生闭包,频繁使用会造成 GC Alloc。一次分配很小,但在 60 FPS 的热路径里持续发生,就可能触发 GC,造成卡顿。所以 LINQ 可以用于初始化、编辑器工具、配置处理等低频场景,但在 Update、战斗、AI、UI 高频刷新里更推荐使用普通 for 循环、缓存列表和对象池来控制分配。

LINQ 会不会产生 GC?

一句话讲清楚

LINQ 可能产生 GC,但不是所有 LINQ 都一定产生 GC。Unity 里要特别警惕在 Update、战斗、AI、UI 高频刷新中使用 LINQ,因为它可能创建迭代器、闭包、临时集合,最终造成 GC Alloc

linq-gc-alloc

常见 GC 来源

WhereSelect:可能创建迭代器对象。 Lambda 捕获外部变量:可能产生闭包对象。 ToList()ToArray():一定会创建新的集合。 OrderBy()GroupBy():通常需要额外缓存结构。 非泛型集合或值类型转 object:可能发生装箱。

容易产生 GC 的写法

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
using UnityEngine; // 引入 UnityEngine 命名空间
public sealed class LinqGcBadExample : MonoBehaviour // 定义不推荐的 LINQ 示例组件
{ // 类开始
    private readonly List<Transform> enemies = new List<Transform>(); // 保存敌人 Transform 列表
    private Transform player; // 保存玩家 Transform
    private void Update() // Unity 每帧调用 Update
    { // 方法开始
        float range = 5f; // 定义查找范围
        List<Transform> nearEnemies = enemies // 从敌人列表开始查询
            .Where(enemy => Vector3.Distance(enemy.position, player.position) < range) // Lambda 捕获 range 和 player,且 Where 可能创建迭代器
            .ToList(); // ToList 每帧创建新 List,容易产生 GC Alloc
    } // 方法结束
} // 类结束

这段的问题不是“写法错”,而是每帧跑就很危险。

更推荐的热路径写法

c
using System.Collections.Generic; // 引入 List 集合
using UnityEngine; // 引入 UnityEngine 命名空间
public sealed class LinqGcGoodExample : MonoBehaviour // 定义推荐的无 LINQ 示例组件
{ // 类开始
    private readonly List<Transform> enemies = new List<Transform>(); // 保存敌人 Transform 列表
    private readonly List<Transform> nearEnemies = new List<Transform>(); // 复用结果列表,避免每帧创建新 List
    private Transform player; // 保存玩家 Transform
    private void Update() // Unity 每帧调用 Update
    { // 方法开始
        nearEnemies.Clear(); // 清空复用列表,不产生新 List
        Vector3 playerPosition = player.position; // 缓存玩家位置
        float sqrRange = 5f * 5f; // 使用平方距离,避免开根号
        for (int i = 0; i < enemies.Count; i++) // 用 for 循环遍历敌人
        { // for 开始
            Transform enemy = enemies[i]; // 取出当前敌人
            Vector3 offset = enemy.position - playerPosition; // 计算敌人与玩家的偏移
            if (offset.sqrMagnitude > sqrRange) continue; // 超出范围就跳过
            nearEnemies.Add(enemy); // 在范围内就加入复用列表
        } // for 结束
    } // 方法结束
} // 类结束

什么时候可以用 LINQ

初始化阶段可以用。 编辑器工具可以用。 配置表处理可以用。 低频业务逻辑可以用。 调试代码、Demo 可以用。

什么时候要慎用

UpdateFixedUpdate 战斗目标筛选 怪物 AI Tick UI 高频刷新 大量对象循环处理 移动端性能敏感逻辑

面试高分回答

IMPORTANT

LINQ 不一定每次都会产生 GC,但很多常见写法会产生额外分配。比如 WhereSelect 可能创建迭代器对象,Lambda 捕获外部变量可能产生闭包,ToListToArray 会创建新集合,OrderByGroupBy 也会有额外缓存结构。在 Unity 中,因为很多逻辑每帧执行,小分配会持续累积,最终触发 GC 导致卡顿。所以 LINQ 可以用于初始化、编辑器工具、配置处理等低频场景,但在 Update、战斗、AI、UI 高频刷新中要慎用,最好用 Profiler 看 GC Alloc,必要时改成 for 循环和复用集合。

如何不用 LINQ 写高性能筛选?

一句话讲清楚

不用 LINQ 写高性能筛选,核心就是:复用结果 List,用 for 循环遍历,用 continue 提前过滤,用平方距离代替开根号,避免每帧 new 和 ToList。

high-performance-filter-no-linq

不推荐写法

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
using UnityEngine; // 引入 UnityEngine 命名空间
public sealed class BadFilterExample : MonoBehaviour // 定义不推荐的筛选示例
{ // 类开始
    private readonly List<Enemy> enemies = new List<Enemy>(); // 保存所有敌人
    private Transform player; // 保存玩家 Transform
    private void Update() // Unity 每帧调用
    { // 方法开始
        List<Enemy> result = enemies // 从敌人列表开始查询
            .Where(enemy => enemy.IsAlive && Vector3.Distance(enemy.transform.position, player.position) < 5f) // 每帧使用 LINQ,可能产生迭代器和闭包开销
            .ToList(); // 每帧创建新的 List,容易产生 GC Alloc
    } // 方法结束
} // 类结束

推荐写法

c
using System.Collections.Generic; // 引入 List 集合
using UnityEngine; // 引入 UnityEngine 命名空间
public sealed class HighPerformanceFilterExample : MonoBehaviour // 定义高性能筛选示例
{ // 类开始
    private readonly List<Enemy> enemies = new List<Enemy>(); // 保存所有敌人
    private readonly List<Enemy> result = new List<Enemy>(64); // 复用结果列表,提前给一点容量减少扩容
    private Transform player; // 缓存玩家 Transform
    private const float SearchRange = 5f; // 定义搜索范围
    public List<Enemy> FilterAliveEnemiesInRange() // 筛选范围内存活敌人
    { // 方法开始
        result.Clear(); // 清空结果列表但不释放内部数组,避免每次 new List
        if (player == null) return result; // 如果玩家为空,直接返回空结果
        Vector3 playerPosition = player.position; // 缓存玩家位置,避免循环里重复访问属性
        float sqrRange = SearchRange * SearchRange; // 计算平方距离阈值,避免开根号
        for (int i = 0; i < enemies.Count; i++) // 使用 for 循环遍历敌人列表
        { // for 开始
            Enemy enemy = enemies[i]; // 取出当前敌人
            if (enemy == null) continue; // 空对象直接跳过
            if (!enemy.IsAlive) continue; // 死亡敌人直接跳过
            Vector3 offset = enemy.transform.position - playerPosition; // 计算敌人到玩家的偏移
            if (offset.sqrMagnitude > sqrRange) continue; // 超出范围直接跳过
            result.Add(enemy); // 满足条件的敌人加入结果列表
        } // for 结束
        return result; // 返回复用的结果列表
    } // 方法结束
} // 类结束
public sealed class Enemy : MonoBehaviour // 定义敌人组件
{ // 类开始
    public bool IsAlive = true; // 表示敌人是否存活
} // 类结束

为什么这样更快

result.Clear() 不会重新创建 List,只是把数量清零。 for 循环没有 LINQ 查询对象和 ToList() 分配。 continue 可以让不符合条件的对象尽早跳过。 offset.sqrMagnitude 避免 Vector3.Distance 的开根号。 提前缓存 player.position,避免循环中重复取属性。

更进一步的优化

敌人数量少,用上面的 for 循环就够了。 敌人数量很多,可以先做空间划分,比如网格、四叉树、BVH。 如果不是每帧必须筛选,可以分帧处理。 如果结果列表会暴露给外部修改,要注意调用方不要长期持有这个复用 List。

面试高分回答

WARNING

Unity 热路径里不用 LINQ 做高性能筛选时,我会用 for 循环和复用列表。结果 List 做成成员变量,每次筛选前 Clear,避免 ToList 创建新集合。筛选条件按便宜到昂贵排序,比如先判断 null、死亡、阵营,再算距离。距离判断用 sqrMagnitude 和平方半径,避免 Vector3.Distance 开根号。对象数量很多时再考虑空间划分或分帧处理。最后用 Profiler 看 GC Alloc 和 CPU 耗时,确认优化确实有效。

IEnumerable 多次遍历有什么坑?

一句话讲清楚

IEnumerable 多次遍历的坑是:它可能不是一个已经算好的集合,而是一条延迟执行的查询规则。你遍历几次,它就可能重新执行几次。

ienumerable-multiple-enumeration-pitfalls

常见坑

第一,重复计算。

c
using System.Collections.Generic; // 引入 List 和 IEnumerable
using System.Linq; // 引入 LINQ 扩展方法
public static class MultipleEnumerationBad // 定义多次遍历问题示例类
{ // 类开始
    public static int SumHighScores(List<int> scores) // 计算高分总和
    { // 方法开始
        IEnumerable<int> query = scores.Where(score => score >= 60); // 创建延迟查询,此时还没有真正筛选
        int count = query.Count(); // 第一次遍历 query,执行一次 Where
        int sum = query.Sum(); // 第二次遍历 query,又执行一次 Where
        return sum; // 返回总和
    } // 方法结束
} // 类结束

这段代码里 Where 可能执行两遍。数据少看不出来,数据多就会浪费性能。

第二,副作用重复。

如果查询里有日志、随机数、计数器、状态修改,多次遍历会让这些副作用重复发生。

第三,结果可能变化。

因为 IEnumerable 可能是延迟查询,不是快照。 如果数据源在两次遍历之间变了,第二次结果可能和第一次不一样。

第四,远程查询可能重复。

如果这个 IEnumerable 来自数据库、网络、文件流,多次遍历可能导致多次 SQL、网络请求或文件读取。

正确做法:需要多次用就 ToList

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
public static class MultipleEnumerationGood // 定义避免多次遍历的示例类
{ // 类开始
    public static int SumHighScores(List<int> scores) // 计算高分总和
    { // 方法开始
        List<int> highScores = scores.Where(score => score >= 60).ToList(); // 执行一次查询并保存成 List 快照
        int count = highScores.Count; // 直接读取 List 的 Count,不重新筛选
        int sum = 0; // 初始化总分
        for (int i = 0; i < highScores.Count; i++) // 遍历缓存好的 List
        { // for 开始
            sum += highScores[i]; // 累加当前分数
        } // for 结束
        return sum; // 返回总分
    } // 方法结束
} // 类结束

什么时候该收 IEnumerable

如果你的方法只遍历一次,收 IEnumerable<T> 很灵活。

什么时候不该只收 IEnumerable

如果你的方法需要多次遍历、随机访问、拿 Count、按下标取值,更适合用:

IReadOnlyList<T>List<T>T[]

这样接口本身就告诉别人:我需要一个已经准备好的集合。

Unity 里注意

在 Unity 热路径里,多次遍历 IEnumerable 可能重复计算。 但直接 ToList() 又会创建新集合,可能产生 GC。

所以热路径里更推荐:

提前缓存成 List。 复用结果容器。 用 for 循环。 避免每帧构造 LINQ 查询。

面试高分回答

CAUTION

IEnumerable 多次遍历的坑在于它不一定是一个已经存好的集合,可能是一个延迟执行的查询。每次 foreachCountSumToList 都可能重新枚举它,导致查询逻辑重复执行。如果查询里有计算、副作用、数据库请求、随机数或依赖可变数据源,就可能出现性能浪费、结果不一致或副作用重复的问题。解决方式是如果结果需要多次使用,就先 ToListToArray 物化成快照;如果方法本身需要多次遍历,接口上可以改收 IReadOnlyList<T>List<T>,表达得更清楚。

ToArrayToList 的代价是什么?

一句话讲清楚

ToArray()ToList() 都会把延迟查询立刻执行,并分配新的容器保存结果。代价就是:遍历一次数据、分配内存、复制元素,频繁调用会产生 GC 压力。

toarray-tolist-cost

它们共同的代价

ToArray()ToList() 都会触发 LINQ 查询立即执行。

比如:

c
using System.Collections.Generic; // 引入 List 和 IEnumerable
using System.Linq; // 引入 LINQ 扩展方法
public static class ToListCostExample // 定义 ToList 代价示例类
{ // 类开始
    public static List<int> Run(List<int> numbers) // 定义运行方法
    { // 方法开始
        IEnumerable<int> query = numbers.Where(number => number > 10); // 创建延迟查询,此时还没有真正筛选
        List<int> result = query.ToList(); // ToList 触发遍历,分配新 List,并把结果复制进去
        return result; // 返回生成好的 List
    } // 方法结束
} // 类结束

这句:

c
List<int> result = query.ToList(); // ToList 触发遍历,分配新 List,并把结果复制进去

会让前面的 Where 真正执行。

ToArray 的代价

ToArray() 会生成一个新的数组。

特点:

数组长度固定。 内存比较紧凑。 适合只读遍历,或者 API 需要数组时使用。 结果是一个快照,后续源集合变化不会影响这个数组。

代价:

分配新数组。 遍历并复制元素。 如果源集合数量一开始不知道,内部可能需要临时缓存,再复制到精确长度数组。

ToList 的代价

ToList() 会生成一个新的 List<T>

特点:

可以继续 AddRemoveSort。 适合后续还要修改结果。 结果也是快照。

代价:

分配一个 List<T> 对象。 分配 List<T> 内部数组。 遍历并复制元素。 容量可能比实际元素数量略大。

怎么选

只需要固定结果、只读遍历:用 ToArray()。 后续还要增删改:用 ToList()。 只是遍历一次:不要急着 ToArray()ToList(),直接 foreach 可能就够了。 Unity 热路径:尽量都别频繁用,优先复用已有 List

Unity 中的问题

如果你在 Update 里这样写:

c
List<Enemy> targets = enemies.Where(enemy => enemy.IsAlive).ToList(); // 每帧筛选并创建新 List,容易产生 GC Alloc

每帧都会创建新 List。 60 FPS 下持续分配,就可能触发 GC,造成卡顿。

更推荐:

c
using System.Collections.Generic; // 引入 List 集合
public static class ReuseListExample // 定义复用列表示例类
{ // 类开始
    public static void FilterAliveEnemies(List<Enemy> enemies, List<Enemy> result) // 把存活敌人筛选到复用结果列表
    { // 方法开始
        result.Clear(); // 清空结果列表,复用内部数组
        for (int i = 0; i < enemies.Count; i++) // 遍历敌人列表
        { // for 开始
            Enemy enemy = enemies[i]; // 取出当前敌人
            if (!enemy.IsAlive) continue; // 非存活敌人直接跳过
            result.Add(enemy); // 存活敌人加入结果列表
        } // for 结束
    } // 方法结束
} // 类结束
public sealed class Enemy // 定义敌人类
{ // 类开始
    public bool IsAlive; // 表示敌人是否存活
} // 类结束

TIP

面试高分回答

ToArrayToList 都是 LINQ 的终止操作,会立即枚举数据源并执行前面的查询,然后把结果物化成新的容器。ToArray 会分配新数组,结果长度固定,适合只读和需要数组的场景;ToList 会分配 List 对象和内部数组,适合后续还要增删改。它们的共同代价是遍历、复制和内存分配,在 Unity 的 Update、战斗筛选、AI Tick、UI 高频刷新里频繁调用会产生 GC Alloc。热路径更推荐复用 List,用 for 循环把结果写进去。

闭包和 LINQ 结合有什么风险?

一句话讲清楚

闭包和 LINQ 结合的风险是:闭包会捕获外部变量,而 LINQ 又常常延迟执行,所以查询真正执行时,外部变量可能已经变了;同时闭包还可能产生额外对象,导致 Unity 中出现 GC Alloc

closure-linq-risks

风险 1:变量变化导致结果变了

c
using System.Collections.Generic; // 引入 List 和 IEnumerable
using System.Linq; // 引入 LINQ 扩展方法
public static class ClosureLinqRiskExample // 定义闭包和 LINQ 风险示例类
{ // 类开始
    public static List<int> Run() // 定义运行方法
    { // 方法开始
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; // 创建数字列表
        int min = 3; // 定义筛选下限
        IEnumerable<int> query = numbers.Where(number => number > min); // Lambda 捕获 min,但 Where 此时还没有真正执行
        min = 4; // 修改被闭包捕获的变量
        List<int> result = query.ToList(); // ToList 时才执行查询,此时使用的是 min = 4
        return result; // 返回结果,结果是 5,不是 4 和 5
    } // 方法结束
} // 类结束

重点是:闭包捕获的是变量,不是当时的值。 所以 min 后来变成 4,查询执行时就用 4

风险 2:产生闭包对象和 GC

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
using UnityEngine; // 引入 UnityEngine
public sealed class ClosureLinqUnityBad : MonoBehaviour // 定义不推荐的 Unity 示例
{ // 类开始
    private readonly List<Enemy> enemies = new List<Enemy>(); // 保存敌人列表
    private int minHp = 50; // 定义外部变量
    private void Update() // Unity 每帧调用
    { // 方法开始
        List<Enemy> result = enemies.Where(enemy => enemy.Hp > minHp).ToList(); // Lambda 捕获 minHp,且 ToList 每帧创建新 List
    } // 方法结束
} // 类结束
public sealed class Enemy // 定义敌人类
{ // 类开始
    public int Hp; // 敌人血量
} // 类结束

这段在 Update 里很危险,因为可能同时有:

闭包对象。 迭代器对象。 新 List。 每帧 GC Alloc。

风险 3:循环变量捕获

c
using System; // 引入 Action 委托
using System.Collections.Generic; // 引入 List 集合
public static class LoopClosureBad // 定义循环闭包错误示例
{ // 类开始
    public static List<Action> BuildActions() // 创建一组 Action
    { // 方法开始
        List<Action> actions = new List<Action>(); // 创建 Action 列表
        for (int i = 0; i < 3; i++) // 循环创建 3 个 Action
        { // for 开始
            actions.Add(() => Console.WriteLine(i)); // Lambda 捕获同一个 i,最后可能都输出 3
        } // for 结束
        return actions; // 返回 Action 列表
    } // 方法结束
} // 类结束

更安全的写法是拷贝一份局部变量:

c
using System; // 引入 Action 委托
using System.Collections.Generic; // 引入 List 集合
public static class LoopClosureGood // 定义循环闭包正确示例
{ // 类开始
    public static List<Action> BuildActions() // 创建一组 Action
    { // 方法开始
        List<Action> actions = new List<Action>(); // 创建 Action 列表
        for (int i = 0; i < 3; i++) // 循环创建 3 个 Action
        { // for 开始
            int index = i; // 拷贝当前循环值,避免所有 Lambda 捕获同一个 i
            actions.Add(() => Console.WriteLine(index)); // Lambda 捕获 index,每个 Action 都有自己的值
        } // for 结束
        return actions; // 返回 Action 列表
    } // 方法结束
} // 类结束

怎么避免

如果变量后面会变,先拷贝局部变量:

c
int snapshot = min; // 把当前值保存成快照
IEnumerable<int> query = numbers.Where(number => number > snapshot); // Lambda 捕获 snapshot,而不是后续会变化的 min

如果结果要固定,马上 ToList()ToArray()。 如果在 Unity 热路径里,尽量不用闭包 LINQ,改用 for 循环和复用 List。 如果必须用 LINQ,避免捕获外部变量,避免每帧 ToList()

面试高分回答

WARNING

闭包和 LINQ 结合的风险主要有两个。第一,闭包捕获的是变量本身,而 LINQ 的 WhereSelect 又常常延迟执行,所以查询真正执行时,捕获变量可能已经被修改,导致结果和预期不一致。第二,Lambda 捕获外部变量时可能生成闭包对象,再加上 LINQ 的迭代器和 ToList 分配,在 Unity 的 Update、战斗、AI 等热路径里容易产生 GC Alloc。解决方式是对会变化的变量做局部快照,需要固定结果时及时物化,循环里捕获变量要拷贝局部变量,热路径优先使用 for 循环和复用集合。

面试中如何评价 LINQ 的优缺点?

一句话讲清楚

面试中评价 LINQ,最好不要说“LINQ 不能用”,而是说:LINQ 的优点是表达清晰、开发效率高;缺点是隐藏了遍历、分配、延迟执行和 GC 成本。低频逻辑可以用,Unity 热路径要慎用。

linq-pros-cons-interview

优点

LINQ 的最大优点是可读性强。

比如筛选、排序、转换、分组,用 LINQ 写出来很接近人的思考方式:

c
using System.Collections.Generic; // 引入 List 集合
using System.Linq; // 引入 LINQ 扩展方法
public sealed class Player // 定义玩家类
{ // 类开始
    public string Name; // 玩家名字
    public int Level; // 玩家等级
    public int Score; // 玩家分数
} // 类结束
public static class LinqReadableExample // 定义 LINQ 可读性示例
{ // 类开始
    public static List<string> GetTopPlayerNames(List<Player> players) // 获取高等级玩家名字
    { // 方法开始
        return players // 从玩家列表开始查询
            .Where(player => player.Level >= 10) // 筛选等级大于等于 10 的玩家
            .OrderByDescending(player => player.Score) // 按分数从高到低排序
            .Select(player => player.Name) // 只取玩家名字
            .ToList(); // 生成结果列表
    } // 方法结束
} // 类结束

这段代码表达很清楚:筛选高等级玩家,按分数排序,取名字。

缺点

LINQ 的问题是隐藏成本。

WhereSelect 可能创建迭代器对象。 Lambda 捕获外部变量可能产生闭包。 ToList()ToArray() 会创建新集合。 OrderBy()GroupBy() 通常会创建额外结构。 延迟执行会导致查询不是结果快照。 多次枚举同一个 IEnumerable 可能重复执行。

所以在 Unity 里,如果这段代码在 Update、战斗目标筛选、AI Tick、UI 高频刷新里每帧执行,就可能出现 GC Alloc 和卡顿。

Unity 项目里的态度

编辑器工具、初始化、配置表处理、低频业务逻辑:可以用 LINQ。 战斗、移动、AI、技能判定、UI 高频刷新:慎用 LINQ。 移动端性能敏感逻辑:优先普通 for 循环、复用 List、对象池。 是否真的有问题:用 Profiler 看 GC Alloc 和 CPU 时间。

面试高分回答

TIP

LINQ 的优点是表达力强,能用统一的方式对集合做筛选、投影、排序、分组和统计,代码可读性高,适合编辑器工具、配置处理、初始化和低频业务逻辑。缺点是它隐藏了执行成本,比如延迟执行、多次枚举、迭代器对象、闭包、ToList 分配和排序缓存。在 Unity 中,因为很多逻辑每帧执行,频繁使用 LINQ 可能造成 GC Alloc 和性能问题。所以我的原则是:低频逻辑用 LINQ 提升开发效率,热路径用显式 for 循环和复用容器保证性能,并用 Profiler 验证。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0