稍微有点经验的程序员都知道一件时,在遍历列表的时候最好不要修改列表,包括添加和删除(特别是删除),因为很可能会造成不可知的错误,严重甚至会导致奔溃,如下面这段代码:
List<int> list = new List<int>(){1,0,0,1,2,0};
for(int i=0;i<list.Count;i++)
{
if(list[i]==0)
{
list.RemoveAt(i);
}
}
//这里本意是删除列表里所有为0的元素, 但是实际执行完这段代码后
//list中的元素为{1,0,1,2}
这么明显的错误,除了新手,一般人也不会犯。
然而有一种情况,还是会让很多人掉到坑里。
那就是管理器中的列表遍历。
笔者遇到的需求:因为要做帧同步,所以对游戏中的逻辑帧需要自己管理。这里很简单就想到用观察者模式做帧的管理器:
public interface ITick
{
void Tick(int tickCount);
}
public class TickerManager
{
private List<ITick> m_lsKeyTicker= new List<ITick>();
private int m_iTickCount = 0;
public void AddKeyTicker(ITick item)
{
m_lsKeyTicker.Add(item);
}
public void RemoveKeyTicker(ITick item)
{
m_lsKeyTicker.Remove(item);
}
public void Signal(float detalTime)
{
m_iTickCount ++;
for (int i = 0; i < m_lsKeyTicker.Count; ++i)
{
m_lsKeyTicker[i].Tick(m_iTickCount);
}
}
}
貌似一切看起来很完美。但是,要注意,这里的ITick是一个接口,你没法控制实现的Tick函数会干什么,而这里很可能就会对m_lsKeyTicker做了修改。比如一次Tick中,一个buff发现生效时间到了,于是需要把自己从角色身上和各种管理器中删掉,当然,这也包括这个TickerManager。于是,就这么掉入了遍历列表时修改列表的大坑。
比起从Tick函数实现上约束不能修改list,从管理器上入手明显更为合理。(当陷入具体需求的时候,很容易忘了各种潜规则,而修改列表都是需要通过管理器的add和remove函数,可以在这里对修改列表行为做限制。)
根据需求,我这里要做到的是:删除行为是立即生效的,而添加行为应该延时生效。于是有了下面的实现:
public enum InListFlag
{
FLAG_NONE,
FLAG_IN_ADD_BUFFER,
FLAG_IN_LIST,
}
public interface ITick
{
InListFlag inListFlag//标志位属性,标识当前对象是否处于更新列表中,由管理器维护状态,ITick本身并不需要关注这个值
{
get;
set;
}
void Tick(int tickCount);
}
//由于需要新增添加缓冲列表,
//所以这里把列表的相关操作单独抽出一个类,
//这样可以让管理器看起来更为清晰
class TickList
{
private List<ITick> m_stList = new List<ITick>();
//为实现延时添加,特意加的缓冲列表
private List<ITick> m_lsAddBuffer = new List<ITick>();
public void Add(ITicker item)
{
switch(item.inListFlag)
{
case InListFlag.FLAG_NONE:
item.inListFlag = InListFlag.FLAG_IN_ADD_BUFFER;
m_lsAddBuffer.Add(item);
break;
case InListFlag.FLAG_IN_ADD_BUFFER:
//已经在添加缓冲中, 说明是重复添加,是非法行为,抛出异常
//当然,根据需求,也可以是忽略本次操作
throw new Exception("add tick twice!");
break;
case InListFlag.FLAG_IN_LIST:
//已经在更新列表中, 说明是重复添加,是非法行为,抛出异常
//当然,根据需求,也可以是忽略本次操作
throw new Exception("add tick twice!");
break;
}
}
public void Remove(ITicker item)
{
switch(item.inListFlag)
{
case InListFlag.FLAG_NONE:
break;
case InListFlag.FLAG_IN_ADD_BUFFER:
//在添加缓冲中,所以可以从缓冲中直接删除
//(添加缓冲中的元素不会执行Tick)
item.inListFlag = InListFlag.FLAG_NONE;
m_lsAddBuffer.Remove(item);
break;
case InListFlag.FLAG_IN_LIST:
//在更新列表的元素不能直接删除,
//只能做标记,等到更新结束在删除
item.inListFlag = InListFlag.FLAG_NONE;
break;
}
}
private void LaterOperation()
{//延时操作,在更新结束后执行的操作,
//包括真正删除列表中被标记为要删除的元素,
//和真正把添加缓冲中元素加到更新列表
//注意这里必须是先删除后添加的操作顺序
//因为元素有可能同时存在list和AddBuffer中(已经在列表中的元素先删除在添加的情况)
for (int i = m_stList.Count - 1; i >= 0; --i)
{//从后往前删除可以保证删除的正确性
if (m_stList[i].inListFlag != InListFlag.FLAG_IN_LIST)
{
m_stList[i].inListFlag = InListFlag.FLAG_NONE;
m_stList.RemoveAt(i);
}
}
for (int i = 0; i < m_lsAddBuffer.Count; ++i)
{
m_lsAddBuffer[i].inListFlag = InListFlag.FLAG_IN_LIST;
m_stList.Add(m_lsAddBuffer[i]);
}
m_lsAddBuffer.Clear();
}
public void DoTick(int tickCount)
{
int count = m_stList.Count;
for (int i = 0; i < count; ++i)
{//只有标记为在列表中的才更新,可以做到删除是即时生效
if (m_stList[i].inListFlag == InListFlag.FLAG_IN_LIST)
{
m_stList[i].Tick(tickCount);
}
}
LaterOperation();
}
}
public class TickerManager
{
private TickList m_lsKeyTicker= new TickList();
private int m_iTickCount = 0;
public void AddKeyTicker(ITick item)
{
m_lsKeyTicker.Add(item);
}
public void RemoveKeyTicker(ITick item)
{
m_lsKeyTicker.Remove(item);
}
public void Signal(float detalTime)
{
m_iTickCount ++;
m_lsKeyTicker.DoTick(m_iTickCount);
}
}