管理器的陷阱|列表遍历时修改了列表

稍微有点经验的程序员都知道一件时,在遍历列表的时候最好不要修改列表,包括添加和删除(特别是删除),因为很可能会造成不可知的错误,严重甚至会导致奔溃,如下面这段代码:

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