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];
}
}
}
}
對ArrayList的分析就到此為止,其實真正有意思的地方就三點,首先,動態記憶體配置演算法,這一點其實可以參考侯捷先生的著作<<STL 源碼分析>>中關於std::vector變長數組的分析章節,圖文結合,想不看懂都不容易。其次,使用_version變數確保迭代器有效。最後,發現一段永遠不會執行到的代碼。
另外,微軟的.Net Framework是一個源碼寶藏,遍及各個方面的各種實現代碼,本人一直在學習中,以後將不斷寫下一些分析的心得和體會,本人水平有限,如有錯誤,歡迎指正,也歡迎鄙視。