System.Collections.ArrayList(一)

来源:互联网 发布:给大一新生的建议知乎 编辑:程序博客网 时间:2024/06/05 21:56

System.Collections.ArrayList是一个动态数组,与C++ STL的std::vector的行为很相似,特别是数组的动态扩充方式上,两者基本上一摸一样。不同的是ArrayList使用的是接口方式实现枚举器,而std::vector使用模板+typedef手法。以下本文就将主要在这两点上,对ArrayList进行一些分析。某些地方可能会提到STL相应的做法,以示比较。
    本文分析的代码来源于http://www.123aspx.com/rotor/RotorSrc.aspx?rot=39810,这是微软公开的代码,完全合法。当然,这部分代码与微软实际发布的.Net Framework可能小有差异,如果您不放心,可以下载Reflector反编译.Net Franework。根据我自己的观察,ArrayList在这两个版本中是完全一样的。
    
1.  成员变量:
      // Fields
      private Object[] _items;
      private int _size;
      private int _version;
    
      private const int _defaultCapacity = 16;

      _size表示当前数组中的数据项的数目,注意,不是容器可以容纳的数据项的数目,容器实际上有一个缓冲手法,请参考下面的分析。    
  
      _items是容器的数据项集合,这里有一个缓冲设计。每次插入一个数据项时,便会检查当前_items的容量Length,如果发现Length,比_size大,则说明当前_items中还有一些空闲的空间,那么直接把数据项插入数组即可。如果发现_items的长度与_size相同,则说明_items中已经没有了空闲空间,要在数组中插入数据项,必须先扩充数据_items的空间。
      扩充_items空间的算法分为3个步骤,首先,分配一个更大的空间_items_new,一般是Length*2的大小.然后,把原来_items中的数据全部复制到_items_new上来。最后,用_items_new替换调_items.ArrayList的内存扩充算法是EnsureCapacity,实际上会调用到capacity的Length属性的set方法,在set方法中真正实现扩展功能。

      再来看下一个变量,_version是一个比较特殊的变量,用来表示当前数组被修改过的次数。每次调用Insert,Remove之类的改变数组的函数时,都会导致该变量的值增加。那么,为什么要这样设计呢?其实完全是为迭代器IEnumerator来服务的。考虑如下一个场景,在一个聊天软件的服务器中,聊天室用类class TalkRoom表示,而每个用户用类User表示.
      class User
      {
          //服务器向用户发送消息。
           public  void  SendMsg(String msg){...}
      }

      class TalkRoom
      {
          //当前聊天室的成员
          private ArrayList  _peoples; 
          
          //向聊天室的所有用户广播消息。
          public  void          BroadCast(String msg)
          {
              User        user = null;
              IEnumerator it = _peoples.GetEnumerator();
              while( it.MoveNext() )
              {
                  user = (User)it;
                  it.SendMsg(msg);
              }
          }
      }    
      这两段短小的代码描叙了服务器向聊天室的所有用户广播消息的流程,看上去很合理,但是,是不是真的完美呢?假设在TalkRoom::BroadCast函数中,it.MoveNext()之后,User = (User)it之前,另外一个线程恰好因为某些原因,从_peoples中删去了it所指向的User对象(比如用户下线了),那会出现什么情况呢?更为严重的情况是,如果被删除的User 正好是该数组的最后一个对象,那么此时it将变成一个null指针,对null实施强制类型转换的后果,就不用我多做解释了。

      其实STL的枚举器也有同样的问题,所以在STL的一些参考手册中指出,push_back,Pop_front之类的操作可能导致容器的迭代器失效,就是这种情况了。
      针对这种情况,微软的解决方案是使用一个_version变量,每次改变容器数据,该变量递增。同时在GetEnumerator()时把当前容器_version传递给迭代器,每次执行迭代器的MoveNext()函数时,先检查容器的_version和迭代器的_versin是否相等,一旦发现不相等,说明容器已经改变,迭代器已经失效,马上抛出一个异常。具体可以参看代码:
      class ArrayListEnumerator 
      {
          ...
          public virtual bool MoveNext()
          {
              if (this.version != this.list._version)
              {
                   throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumFailedVersion"));
              }
              ...
          }
      }

      最后一个变量是_defaultCapacity,用于指定创建容器时,默认预先分配的数组大小,默认是16。

2.  构造函数:
    public ArrayList() {
            _items = new Object[_defaultCapacity]; //参考_defaultCapacity的分析,可以发现其用途。
        }

    public ArrayList(int capacity) {                    //如果制定了初始分配的大小,则用capacity替换_defaultCapacity。
            if (capacity < 0) throw new ArgumentOutOfRangeException("capacity", Environment.GetResourceString("ArgumentOutOfRange_SmallCapacity"));
            _items = new Object[capacity];
        }

    还有两个构造函数,但是没有特别值得分析的地方。

3.  添加和删除函数:

    //添加value到数组的最后
    public virtual int Add(Object value) {
            if (_size == _items.Length) EnsureCapacity(_size + 1); //如果有必要,扩充容量
            _items[_size] = value;
            _version++;    //版本号改变
            return _size++;
        }

    //删除某个元素,先定位,然后调用RemoveAt。
    public virtual void Remove(Object obj) {
            int index = IndexOf(obj);
            BCLDebug.Correctness(index >= 0 || !(obj is Int32), "You passed an Int32 to Remove that wasn't in the ArrayList./r/nDid you mean RemoveAt?  int: "+obj+"  Count: "+Count);
            if (index >=0) 
                RemoveAt(index);
        }

    //真正删除元素的函数。
    public virtual void RemoveAt(int index) {
            if (index < 0 || index >= _size) throw new ArgumentOutOfRangeException("index", Environment.GetResourceString("ArgumentOutOfRange_Index"));
            _size--;
            if (index < _size) {
                Array.Copy(_items, index + 1, _items, index, _size - index);
            }
            _items[_size] = null;
            _version++;                         //版本号改变
        }

4.  Capacity的属性:
    
    Capacity属性决定这整个容器的内存分配,get函数非常简单,主要分析set函数。
    public virtual int Capacity
    {
      get                                       //get,返回数组大小。
      {
            return this._items.Length;  
      }
      //set 方法主流程有3步
      //1.  比较需要的数组大小和当前的数组大小,如果相等,则直接退出。
      //2.  比较需要的数组大小和当前的数组中数据数目,如果是要截断数组,则抛出异常。
      //3.  扩充数组,首先分配一个更大的数组,然后将原数组拷贝到新数组,再用新数组替换原数组,与上面的分析完全一致。      
      set
      {
            if (value != this._items.Length)       
            {
                  if (value < this._size)       //不允许截断数组。
                  {
                        throw new ArgumentOutOfRangeException("value", Environment.GetResourceString("ArgumentOutOfRange_SmallCapacity"));
                  }
                  if (value > 0)
                  {
                        object[] objArray1 = new object[value];  //分配需要的数组大小
                        if (this._size > 0)
                        {                                               //讲原有数据拷贝到新的数组
                              Array.Copy(this._items, 0, objArray1, 0, this._size);
                        }
                        this._items = objArray1;                        //用新数组替换原有的_items对象
                  }
                  /*有意思的是,居然可以指定新数组大小为0或者负数,但是,认真分析一下,你好发现<0的情况是永远不会出现的。因为如果新的数组大小小于0,那么,value < this._size必然为true,成了在第二步就好抛出异常。进一步分析,value=0也是永远不会出现的情况。如果等于0,分两种情况分析:如果原容器中没有数据,那么在第一步判断if (value != this._items.Length)时,就已经失败;如果容器中有数据,那么if (value < this._size)又会导致抛出异常。由以上分析,可见这个else实在是一段死代码。*/
                  else
                  {
                        this._items = new object[0x10];
                  }
            }
      }
    }

原创粉丝点击