MyFramework:Unity SafeList0 的延迟压缩设计

项目地址:

GitHub - ZHOURUIH/MyFramework: Unity 商用级别开发框架,经过了多年经验沉淀.一个在unity上使用的网络游戏客户端开发框架,为unity所有使用方式提供完善的封装和管理,只需要专注于游戏逻辑的编写 · GitHub

上一篇讲了 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 是值类型,例如 intdefault(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> 没有每次复制列表。

它依靠 mForeachDepthcompact() 处理嵌套遍历。

成本更低。

代价是删除元素在遍历期间会变成 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> 是一个更直接、更低成本的选择。

相关推荐
_zhourui_h_1 天前
MyFramework:Unity SafeList 如何支持遍历中修改
unity3d
SmalBox1 天前
【节点】[SmoothWave节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox2 天前
【节点】[RoundedRectangle节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_2 天前
MyFramework:AssetBundle 延迟卸载与依赖保护
unity3d
_zhourui_h_3 天前
MyFramework:safe() 扩展函数的空集合设计
unity3d·游戏开发
SmalBox3 天前
【节点】[RoundedPolygon节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox4 天前
【节点】[Rectangle节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox5 天前
【节点】[Polygon节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_5 天前
MyFramework:整体代码结构与热更新分层解析
unity3d·游戏开发