把章节变成可单步观察的过程
分类:CSharp 基础数据结构
先说结论
排序改变整个集合的顺序,查找返回一个元素,筛选产生多个匹配元素,判断只返回真假,删除则会修改原集合。它们可能使用相似的 lambda,但结果类型和副作用完全不同。
用同一组敌人比较结果
| ID | HP | 距离 |
|---|---|---|
| 1 | 80 | 6 |
| 2 | 20 | 9 |
| 3 | 20 | 4 |
按 HP、距离排序 -> [3, 2, 1]
Find(HP < 30) -> 2(取决于当前顺序)
FindAll(HP < 30)-> [2, 3]
Exists(距离 < 5)-> true
RemoveAll(HP < 30) 后只剩 [1]
Find 返回谁取决于调用时的集合顺序,所以“找最小值”不能随便用 Find 代替比较过程。
说明
这一节重点记录适合 Unity 运行时和战斗热路径的集合写法:
Sort
Find
FindAll
Exists
RemoveAll
手写 for 循环
LINQ 写法虽然简洁,但在性能和分配上不够友好,尤其不适合高频运行逻辑,所以本节不作为重点推荐。
为什么把排序、查找和删除放在同一篇里:实际战斗代码通常不是只调用一个 API,而是先整理候选顺序,再找出目标,最后清理无效对象。分清每一步是否修改原集合,才能避免前一步悄悄改变后一步的输入。
用同一组敌人贯穿所有操作
先准备一组具体数据:
using System;
using System.Collections.Generic;
List<(string Name, int Hp, float Distance)> enemies = new()
{
new("Slime", 30, 6f),
new("Knight", 80, 3f),
new("Archer", 50, 3f),
new("Ghost", 0, 8f)
};
第一步:先按距离排序
enemies.Sort((a, b) => a.Distance.CompareTo(b.Distance));
foreach ((string Name, int Hp, float Distance) enemy in enemies)
Console.WriteLine($"{enemy.Name}: {enemy.Distance}m");
输出:
Knight: 3m
Archer: 3m
Slime: 6m
Ghost: 8m
Sort 会直接改变原列表。Knight 和 Archer 距离相同,这时只比较距离不足以表达我们希望谁排在前面。
第二步:距离相同时,血量低的优先
enemies.Sort((a, b) =>
{
int byDistance = a.Distance.CompareTo(b.Distance);
if (byDistance != 0)
return byDistance;
return a.Hp.CompareTo(b.Hp);
});
比较器返回值只表达顺序:
小于 0 -> a 排在 b 前面
等于 0 -> 当前规则认为两者相等
大于 0 -> a 排在 b 后面
现在 Archer(50 HP) 会排在 Knight(80 HP) 前面。第二条件只在第一条件相等时执行。
第三步:排序后寻找第一个可攻击目标
(string Name, int Hp, float Distance)? target = enemies.Find(enemy =>
enemy.Hp > 0 && enemy.Distance <= 5f);
Console.WriteLine(target?.Name ?? "没有目标"); // Archer
Find 从列表开头向后检查。因为前一步已经按“近距离、低血量”排序,第一个满足“存活且 5 米内”的敌人就是当前优先目标。
第四步:清掉死亡对象
int removed = enemies.RemoveAll(enemy => enemy.Hp <= 0);
Console.WriteLine(removed); // 1
Console.WriteLine(enemies.Count); // 3
这条完整链的状态变化是:
原列表 4 个敌人
-> Sort 改变遍历顺序,但数量仍为 4
-> Find 返回一个引用,但不修改列表
-> RemoveAll 删除 Ghost,数量变成 3
后面各 API 的独立例子,都可以对照这条运行链判断:它是原地修改集合、只返回查询结果,还是创建一个新集合。
Sort:排序 List
数字排序:
List<int> nums = new() { 3, 1, 5, 2 };
nums.Sort();
结果:
1, 2, 3, 5
字符串排序:
List<string> names = new() { "Bob", "Alice", "Cindy" };
names.Sort();
结果:
Alice, Bob, Cindy
自定义排序
假设有一个物品类:
public class Item
{
public int Id;
public int Quality;
public int Count;
}
按品质从低到高:
items.Sort((a, b) => a.Quality.CompareTo(b.Quality));
按品质从高到低:
items.Sort((a, b) => b.Quality.CompareTo(a.Quality));
这里:
(a, b) => a.Quality.CompareTo(b.Quality)
意思是:
给 Sort 两个元素 a 和 b;
返回比较结果,告诉 Sort 谁应该排前面。
CompareTo 返回值大概含义:
负数:a 排在 b 前面
0:两者相等
正数:a 排在 b 后面
多条件排序
例如先按品质降序,再按 ID 升序:
items.Sort((a, b) =>
{
int qualityCompare = b.Quality.CompareTo(a.Quality);
if (qualityCompare != 0)
{
return qualityCompare;
}
return a.Id.CompareTo(b.Id);
});
含义:
先比较 Quality。
如果 Quality 不同,就按 Quality 排。
如果 Quality 相同,再比较 Id。
这种“主排序键 + 次排序键”的写法很常见。
Find:找第一个满足条件的元素
Item item = items.Find(item => item.Id == 1001);
意思是:
从 items 里找第一个 Id 等于 1001 的物品。
如果 Item 是 class,找不到会返回 null。
所以要判断:
Item item = items.Find(item => item.Id == 1001);
if (item != null)
{
Debug.Log(item.Count);
}
FindAll:找所有满足条件的元素
List<Item> highQualityItems = items.FindAll(item => item.Quality >= 4);
意思是:
找出所有品质 >= 4 的物品,返回一个新的 List。
注意:
FindAll 会创建新列表。
如果在高频逻辑里使用,要注意分配。
Exists:判断有没有满足条件的元素
bool hasRareItem = items.Exists(item => item.Quality >= 5);
意思是:
只要有一个满足条件,就返回 true。
等价于手写:
bool hasRareItem = false;
for (int i = 0; i < items.Count; i++)
{
if (items[i].Quality >= 5)
{
hasRareItem = true;
break;
}
}
RemoveAll:删除所有满足条件的元素
int removedCount = items.RemoveAll(item => item.Count <= 0);
意思是:
删除所有数量小于等于 0 的物品。
返回删除了几个。
高频逻辑更推荐手写 for
如果在每帧、战斗、AI、资源热路径里,推荐手写 for:
Item found = null;
for (int i = 0; i < items.Count; i++)
{
if (items[i].Id == 1001)
{
found = items[i];
break;
}
}
优点:
没有 LINQ 临时分配风险
逻辑完全可控
方便 break / continue
方便倒序删除
倒序删除多个元素:
for (int i = items.Count - 1; i >= 0; i--)
{
if (items[i].Count <= 0)
{
items.RemoveAt(i);
}
}
最重要的收获
Sort:排序。
Find:找第一个。
FindAll:找多个,但会创建新列表。
Exists:判断有没有。
RemoveAll:删除所有满足条件的。
手写 for:高频逻辑最稳。
常用语法:
items.Sort((a, b) => b.Quality.CompareTo(a.Quality));
Item item = items.Find(item => item.Id == 1001);
bool exists = items.Exists(item => item.Count > 0);
int removed = items.RemoveAll(item => item.Count <= 0);