管理器的陷阱|列表遍歷時修改了列表

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

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);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章