项目地址:
上一篇讲了 SafeList<T>。
SafeList<T> 用三个列表解决遍历中修改问题:
bash
mMainList
mUpdateList
mModifyList
SafeList0<T> 的实现更轻。
它没有维护遍历快照,也没有维护修改记录。
它只保留一个主列表:
bash
protected List<T> mMainList = new();
删除时不一定马上删除。
正在遍历时,只把元素标记成 default。
等所有遍历结束后,再统一压缩列表。
一、定位
SafeList0<T> 的注释已经说明了定位:
bash
// 非线程安全
// 效率更高的可在遍历中再次开始遍历的列表
// 不过由于删除不是立即删除的,所以在部分情况下使用时需要注意
public class SafeList0<T> : ClassObject
{
protected List<T> mMainList = new();
protected int mForeachDepth;
protected bool mNeedCompact;
}
核心特点:
bash
只维护一个列表
支持嵌套遍历
遍历中删除只做标记
最后一层遍历结束后统一 compact
它适合需要频繁遍历、删除量不大、可以接受遍历中出现 default 占位的场景。
二、遍历深度
SafeList0<T> 支持嵌套遍历。
开始遍历时,只增加深度:
bash
public List<T> startForeach()
{
++mForeachDepth;
return mMainList;
}
结束遍历时,减少深度:
bash
public void endForeach()
{
if (--mForeachDepth == 0 && mNeedCompact)
{
compact();
}
}
这里的重点是:
bash
mForeachDepth > 0
表示当前仍在遍历中
mForeachDepth == 0
表示所有遍历都结束
只有最后一层遍历结束时,才允许真正压缩列表。
三、删除逻辑
删除元素时,先查找下标:
bash
public bool remove(T value)
{
int index = mMainList.IndexOf(value);
if (index < 0)
{
return false;
}
if (mForeachDepth > 0)
{
// 标记删除
mMainList[index] = default;
mNeedCompact = true;
}
else
{
mMainList.removeAt(index);
}
return true;
}
如果当前没有遍历,直接 removeAt。
如果正在遍历,不改变列表长度,只把元素设置成 default。
这样当前遍历不会因为列表长度变化而出问题。
四、clear 逻辑
clear() 也是同样思路:
bash
public void clear()
{
if (mForeachDepth > 0)
{
for (int i = 0; i < mMainList.Count; ++i)
{
mMainList[i] = default;
}
mNeedCompact = true;
}
else
{
mMainList.Clear();
}
}
正在遍历时,不直接 Clear()。
它把所有元素标记成 default。
遍历结束后再统一压缩。
没有遍历时,直接清空列表。
五、压缩逻辑
真正删除发生在 compact():
bash
protected void compact()
{
int write = 0;
int count = mMainList.Count;
for (int i = 0; i < count; ++i)
{
if (!equal(mMainList[i], default))
{
mMainList[write++] = mMainList[i];
}
}
mMainList.RemoveRange(write, count - write);
mNeedCompact = false;
}
这个函数使用双指针方式压缩列表。
i 负责遍历旧数据。
write 负责写入保留数据。
遇到不是 default 的元素,就移动到前面。
最后一次性移除尾部无效区间。
流程类似:
bash
原列表:
[A, default, B, default, C]
compact 后:
[A, B, C]
六、为什么不立即删除
遍历中立即删除会改变列表结构。
例如:
bash
[A, B, C, D]
遍历到 B 时删除 B,列表变成:
bash
[A, C, D]
后续下标会整体移动。
如果外部还在按原列表遍历,就会出现跳过元素、重复元素或越界问题。
SafeList0<T> 的处理方式是:
bash
删除时不改变列表长度
只把位置标记为空
遍历结束后再真正删除
这样遍历过程中的列表结构保持稳定。
七、嵌套遍历
SafeList<T> 不支持嵌套遍历。
SafeList0<T> 支持。
例如:
bash
List<int> outer = list.startForeach();
List<int> inner = list.startForeach();
list.endForeach();
list.endForeach();
第一次 startForeach() 后:
bash
mForeachDepth = 1
第二次 startForeach() 后:
bash
mForeachDepth = 2
第一次 endForeach() 后:
bash
mForeachDepth = 1
此时不会 compact。
第二次 endForeach() 后:
bash
mForeachDepth = 0
如果 mNeedCompact 为 true,才执行 compact。
这保证了内层遍历结束不会破坏外层遍历。
八、遍历中删除示例
初始列表:
bash
[1, 2, 3]
开始遍历:
bash
List<int> iter = list.startForeach();
删除 2:
bash
list.remove(2);
此时:
bash
mMainList = [1, default, 3]
count = 3
列表长度没有变化。
遍历仍然可以继续。
结束遍历:
bash
list.endForeach();
执行 compact() 后:
bash
mMainList = [1, 3]
count = 2
删除真正生效。
九、遍历中的 default
SafeList0<T> 的限制也在这里。
遍历中删除后,列表里会出现 default。
如果 T 是引用类型,就是 null。
所以遍历时通常需要处理:
bash
foreach (var item in list.startForeach())
{
if (item == null)
{
continue;
}
// ...
}
list.endForeach();
如果 T 是值类型,例如 int,default(int) 是 0。
这种情况下要确保 0 不会被当成有效元素,或者不要用 SafeList0<int> 存储可能等于默认值的数据。
这也是 SafeList0<T> 的使用边界。
它用更低成本换来了一个限制:
bash
遍历中删除会留下 default 占位
十、和 SafeList 的区别
SafeList<T> 的特点:
bash
维护 mMainList
维护 mUpdateList
维护 mModifyList
遍历的是快照
不支持嵌套遍历
遍历中不会看到 default 占位
SafeList0<T> 的特点:
bash
只有 mMainList
遍历的是主列表
支持嵌套遍历
删除时标记 default
最后一层遍历结束后压缩
两者适合不同场景。
SafeList<T> 更稳,适合希望遍历列表保持干净的场景。
SafeList0<T> 更轻,适合高频列表和嵌套遍历场景。
十一、和 DeepSafeList 的区别
如果需要嵌套遍历,也可以使用 DeepSafeList。
但 DeepSafeList 的做法通常是每次遍历都复制一个列表。
这样实现简单,但性能成本更高。
SafeList0<T> 没有每次复制列表。
它依靠 mForeachDepth 和 compact() 处理嵌套遍历。
成本更低。
代价是删除元素在遍历期间会变成 default。
十二、适用场景
SafeList0<T> 适合这些场景:
bash
事件监听列表
临时回调列表
可嵌套遍历的运行时列表
删除不频繁的列表
可以跳过 null/default 的列表
希望减少快照复制成本的列表
不适合这些场景:
bash
元素可能等于 default 的值类型列表
遍历中不能接受 null/default 的列表
需要严格保持列表内容干净的列表
需要线程安全的列表
它不是通用容器。
它是一个针对框架运行时场景优化的小型安全列表。
十三、设计取舍
优点:
bash
实现简单
只有一个主列表
支持嵌套遍历
删除时不改变当前列表长度
最后统一压缩
没有遍历快照复制成本
限制:
bash
非线程安全
遍历中删除会产生 default 占位
值类型需要注意 default 值冲突
遍历代码可能需要跳过 null/default
这个设计适合 MyFramework 中大量主线程运行时列表。
它不是为了完全替代 List<T>。
它只解决一个明确问题:
bash
低成本支持遍历中删除,并支持嵌套遍历。
总结
SafeList0<T> 的核心逻辑很短:
bash
startForeach
mForeachDepth++
remove
如果正在遍历,元素置为 default
否则直接删除
endForeach
mForeachDepth--
如果深度为 0 且需要压缩,执行 compact
它的精巧点在于:
bash
不复制列表
不维护修改记录
不在遍历中改变列表长度
用 foreachDepth 支持嵌套遍历
用 compact 在最后统一清理无效元素
SafeList<T> 更完整。
SafeList0<T> 更轻。
在能够接受 default 占位的场景中,SafeList0<T> 是一个更直接、更低成本的选择。
