稍微有點經驗的程序員都知道一件時,在遍歷列表的時候最好不要修改列表,包括添加和刪除(特別是刪除),因爲很可能會造成不可知的錯誤,嚴重甚至會導致奔潰,如下面這段代碼:
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);
}
}