本篇文章講解數組的使用,先是介紹下幾種不同的數組,在說明下各自的區別和使用場景,然后注意細節,廢話不多說,趕緊上代碼。
在.Net 3.5之中,我們常用的數組基本就是如下的幾種方式(詞典Dictionary<TKey,TValue>比較特殊,下面單獨解釋):
- ArrayList 方式的數組
- T[] 方式的數組
- List<T> 方式數組
- Queue<T> 先進先出的數組
- Stack<T> 后進先出的數組
ArrayList:
簡單的說,ArrayList是一個微軟早期提供的數組類型,在那個時代還沒有泛型的時候,我們需要一個可變數組的時候,就會采用這個數組,現在來說,已經很少使用了,不排除有些場景特別適用這個數組,如果要說這個數組的優點,恐怕只有一個,還是最大的一個優點就是對數組成員類型不確定,所以我們可以這么寫代碼:
1 private void button1_Click(object sender, EventArgs e) 2 { 3 ArrayList arrayList = new ArrayList(); 4 arrayList.Add(false); 5 arrayList.Add(5); 6 arrayList.Add(5.3); 7 arrayList.Add("測試數據"); 8 arrayList.Add(Guid.NewGuid()); 9 arrayList.Add(DateTime.Now); 10 11 12 foreach(var m in arrayList) 13 { 14 textBox1.AppendText(m.GetType().ToString() + Environment.NewLine); 15 } 16 17 }
顯示結果為:
這種方式和我們的一般思維不太一樣,我們一般定義一組數據時,肯定是一樣的類型的呀,如果按照ArrayList類型來看,我們每次調用中間一個成員的時候,還得判斷下什么類型,因為它很可能不是簡單的Object,如果我們在代碼中都是Add同一種類型,那么是否就意味着使用的時候不需要進行類型判定了嗎?肯定不行,因為你一旦在代碼的其他地方使用了Add方法,並添加了一個不是你期望的類型對象(編譯器根本不會提示錯誤),這會對以后你查找問題產生極大的阻礙。
這種數組還有巨大的缺陷。就是頻繁的拆箱裝箱,如果只有100長度以內的數據,對性能的損耗也許會看不出來,但是數組長度達上萬的時候,絕對會成為性能的瓶頸,關於拆箱裝箱的描述,在其他很多的書上都會有描述,如果需要說清楚,又得回到區分值類型和引用類型的區別,又會扯出線程棧和托管堆的區別,暫時就不深入了。所以基本不選擇這個數組。
T[] 數組
這是一個最常用的數組類型了,其實這種數組非常的好用和擴展功能,我們非常習慣於下面的定義方式:
1 private void button2_Click(object sender, EventArgs e) 2 { 3 int[] temp = new int[100]; 4 Button[] buttones = new Button[100]; 5 6 // 正常訪問 7 foreach (var m in temp) 8 { 9 textBox1.AppendText(m.ToString() + Environment.NewLine); 10 } 11 12 // 異常,拋出NullReferenceException 13 foreach (var m in buttones) 14 { 15 textBox1.AppendText(m.Text + Environment.NewLine); 16 } 17 18 }
在上述的例子中,存在一個細微的差別,如果你定義的是非空的值類型數據,會自動的賦初值,也即是default(int),也即是0,而引用類型的初值是NULL,但無論怎么說,buttones確實一個長度為100的數組,只是里面的數據都為空罷了,所以我們可以這么寫:
1 Button[] buttones = new Button[100]; 2 buttones[99] = new Button();
這樣我們只實例化數組的最后一個對象。
List<T> 數組對象
這個數組對象厲害了,這個數組自從C#引入了泛型以來就立即被廣泛引用,它繼承了ArrayList的優良傳統,又解決掉了ArrayList留下來的所有弊端,基本上可以說一勞永逸了,它甚至更強大的是帶來了Lambada表達式,這真是一個巨大的進步,下面就來詳細的介紹下C#中大名鼎鼎的List<T>數組了,先從實例化開始好了,List<T>的數組實例化小個小細節,如果我們要實例化一個包含了100萬個int的數組,應該怎做,和int[] 初始化有什么區別:
1 private void button3_Click(object sender, EventArgs e) 2 { 3 int[] list1 = new int[1000000]; 4 // 可以直接按下面這么用 5 list1[10000] = 10; 6 7 List<int> list = new List<int>(1000000); 8 // 下面的代碼會引發異常ArgumentOutOfRangeException 9 list[10000] = 10; 10 }
如下兩種方式實例化一個長度1000000個0的List<int>實例:
1 private void button5_Click(object sender, EventArgs e) 2 { 3 // 方式一 4 List<int> list1 = new List<int>(); 5 for(int i=0;i<1000000;i++) 6 { 7 list1.Add(0); 8 } 9 10 // 方式二 11 List<int> list2 = new List<int>(new int[1000000]); 12 }
以下展示一個Lambada表達式的巨大好處,假設有個List<int>數組,包含了10000個數據,我們需要篩選出里面所有大於100的數據並重新生成一個數組,以下展示2中寫法,你就能明白中間的區別在哪里了
1 private void button6_Click(object sender, EventArgs e) 2 { 3 // 生成一萬個隨機數據(0-200)的數組 4 List<int> list = new List<int>(); 5 Random r = new Random(); 6 for(int i=0;i<10000;i++) 7 { 8 list.Add(r.Next(200)); 9 } 10 11 // 方式一 12 List<int> result1 = new List<int>(); 13 for (int i = 0; i < list.Count; i++) 14 { 15 if (list[i] > 100) 16 { 17 result1.Add(list[i]); 18 } 19 } 20 21 // 方式二 22 List<int> result2 = list.Where(m => m > 100).ToList(); 23 }
無論從代碼的工作量上來說還是理解度來說,都是第二種方式完勝,而且兩者的性能相差無幾,所以極度推薦大家學好委托和Lambada表達式。
講完了List<T>類型的幾種用法,來說說原理的東西,我剛學習並用了一段時間的List<T>后,就特別好奇微軟怎么實現了List<T>對象,所謂的動態長度的數組原理是什么,為什么可以使用Add方法來新增Item,帶着這些疑問,后來看到了微軟開源出來的代碼,List<T>的開源地址為http://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,2765070d40f47b98
源代碼非常的長,這里就不全復制了,有興趣的可以看看,上面有很多細節可以學習,我就大致講一下List<T>的根本原理,有助於大家理解,首先List<T>內部使用的底層數據仍然是T[]類型的,並且聲明了一個初始容量4,在執行了List<int> list = new List<int>(1000000)這段代碼后,部分真的有一個 _items 為int[10000]的對象,但是我們為什么在使用list[10000]=10的時候異常了呢?因為里面還有個數據容量 _size,在初始化的時候並沒有賦值;而當你使用list[10000]時,下面的代碼足以說明問題:
1 // Sets or Gets the element at the given index. 2 // 3 public T this[int index] { 4 get { 5 // Following trick can reduce the range check by one 6 if ((uint) index >= (uint)_size) { 7 ThrowHelper.ThrowArgumentOutOfRangeException(); 8 } 9 Contract.EndContractBlock(); 10 return _items[index]; 11 } 12 13 set { 14 if ((uint) index >= (uint)_size) { 15 ThrowHelper.ThrowArgumentOutOfRangeException(); 16 } 17 Contract.EndContractBlock(); 18 _items[index] = value; 19 _version++; 20 } 21 }
源代碼里還有細節值得注意,當我們調用了Add(1000)方法時,發生了什么事情,如下的源代碼來源於微軟
1 public void Add(T item) { 2 if (_size == _items.Length) EnsureCapacity(_size + 1); 3 _items[_size++] = item; 4 _version++; 5 } 6 private void EnsureCapacity(int min) { 7 if (_items.Length < min) { 8 int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2; 9 // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow. 10 // Note that this check works even when _items.Length overflowed thanks to the (uint) cast 11 if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength; 12 if (newCapacity < min) newCapacity = min; 13 Capacity = newCapacity; 14 } 15 } 16 public int Capacity { 17 get { 18 Contract.Ensures(Contract.Result<int>() >= 0); 19 return _items.Length; 20 } 21 set { 22 if (value < _size) { 23 ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity); 24 } 25 Contract.EndContractBlock(); 26 27 if (value != _items.Length) { 28 if (value > 0) { 29 T[] newItems = new T[value]; 30 if (_size > 0) { 31 Array.Copy(_items, 0, newItems, 0, _size); 32 } 33 _items = newItems; 34 } 35 else { 36 _items = _emptyArray; 37 } 38 } 39 } 40 }
源代碼中的兩個方法加一個屬性已經表示的很清楚了,假設list原來初始容量為4,我們剛好add了4個值,當我們add第五個值的時候,就會發生很多事情
- 判斷原來的數組容量夠不夠?因為數量已經超了,所以不夠
- 因為原數組不夠,所以需要進行擴充,擴充多少呢?原數組長度*2!!!!!,此處為8
- 確定好了擴充的數據,重新生成一個8長度的數組
- 准備復制數據,值類型復制數值本身,而引用類型僅僅復制引用,此處把原來4個舊的數據復制到8個新數組的前四個位置上
- 最后的最后list[5]=10;因為這時候的長度已經足夠了,允許安全的賦值
所以這個操作還是非常恐怖的,假設這時候數組的長度已經100萬了,再新增一個數據,將要生成200萬長度的數據,再挪一百萬長度的數據,雖然說挪一次的性能還是非常高的,這里為什么要按兩倍擴充呢,估計也是為了性能考慮,假設你的list使用Add方法調用了100萬次,實際只是擴充了19次(可能18次,沒具體算),所以這么看下來如果確定數組的長度是固定的情況,使用 T[] 的性能最好。
Queue<T> 數組類型
這個數組對象和List<T>非常的像,在絕大多數情況下都可以用List<T>來替代實現,如下的代碼演示了同時添加一個數據,和移除一個數據時的操作:
1 private void button7_Click(object sender, EventArgs e) 2 { 3 Queue<int> queue = new Queue<int>(); 4 List<int> list = new List<int>(); 5 6 // 兩者等同 7 queue.Enqueue(1000); 8 list.Add(1000); 9 10 // 兩者有一點微小的區別 11 int i = queue.Dequeue(); 12 int j = list[0]; 13 list.RemoveAt(0); 14 }
如果此處不需要獲取被移除數據的數值的話,這里的代碼就幾乎等同了,所以這里適用的場景其實也很明顯了,比如我們有一個消息隊列,存放了消息對象,每個對象包含了發送人,發送時間,接收人,發送內容等等,來自各個地方的消息統一壓入隊列,專門由一個線程出處理這些消息,處理的模型就是拿一個處理一個,肯定是先發先處理了,這種情況就特別適合使用Queue<T>隊列了。但是這時候又該考慮另一個問題了,那就是同步,在本篇下面將介紹。
Stack<T> 數組類型
不得不說,這個對象和List<T>, Queue<T> 都是非常的像,無非就是先入后出罷了,只要前兩個理解了,這個就什么難度,至於實際中的應用場景,暫時還沒想到,不過有一個技術真的超級適合先入后出的方式,就是指針,指針在執行到調用方法時,會將當前的位置壓入堆棧,然后跳轉到其他方法執行,執行完畢后,取出堆棧的值,跳到相應的地址繼續執行。即使方法里再跳方法,方法里再跳方法,執行的步驟也不會亂。
使用注意:
ArrayList妙用:
如果你獲取到一個數據Object 對象,但是知道它是一個數組,可能是bool[], 也可能是int[], double[], sting[]等等,現在要在表格中顯示,就特別適合使用ArrayList類型
1 private void button8_Click(object sender, EventArgs e) 2 { 3 // 獲取到的對象 4 Object obj = new object(); 5 6 if(obj is ArrayList list) 7 { 8 foreach(object m in list) 9 { 10 // 可以進行顯示一些操作 11 } 12 } 13 }
排序性能:
有時候我們需要對一個數組進行排序,從小到大也好,從大到小也好,大學里教的都是冒泡法之類的,其實根本不要去使用,性能超差。
1 private void button9_Click(object sender, EventArgs e) 2 { 3 // 生成100000個數據長度的數組 4 int[] data = new int[100000]; 5 // 隨機賦值 6 Random r = new Random(); 7 for (int i = 0; i < data.Length; i++) 8 { 9 data[i] = r.Next(1000000); 10 } 11 12 DateTime start = DateTime.Now; 13 14 // 開始排序 15 for (int i = 0; i < data.Length; i++) 16 { 17 for (int j = i + 1; j < data.Length; j++) 18 { 19 if (data[j] < data[i]) 20 { 21 int temp = data[j]; 22 data[j] = data[i]; 23 data[i] = temp; 24 } 25 } 26 } 27 28 textBox1.AppendText((DateTime.Now - start).TotalMilliseconds + Environment.NewLine); 29 30 // 重新賦值 31 for (int i = 0; i < data.Length; i++) 32 { 33 data[i] = r.Next(1000000); 34 } 35 36 start = DateTime.Now; 37 // 第二種從小到大的排序 38 Array.Sort(data); 39 40 textBox1.AppendText((DateTime.Now - start).TotalMilliseconds + Environment.NewLine); 41 ; 42 }
實際消耗的時間差別巨大,冒泡法居然用了整整34秒鍾,而系統自帶排序算法只用了15毫秒,整個時間竟然差了2200倍以上。
同步問題:
這個問題算是最難也是最容易出問題的地方,而且很多問題還出的莫名其妙,有時出問題,有時又不出,關鍵的是在於對多線程的理解錯誤,在絕大多數的單線程應用程序中,幾乎沒有同步問題,比如你在窗口類中定義了一個數組,在窗口類中其他地方可以隨意的使用數組,更改值也好,取出數值也好,求取平均值也好。程序都可以很好的工作,但是對於多線程的應用程序,我們很容易想到這樣的處理模型。
我們建一個緩存的中間對象,比如int[] temp=new int[100],然后有一個線程從其他對方獲取數據,可能來自設備,可能來自數據庫,網絡等等,獲取數據后,對數組進行更新數據,然后其他地方對數組數據進行處理,獲取值進行顯示,計算平均數顯示等等,所以我們很容易這么寫代碼:
1 private int[] arrayData = new int[100]; // 緩存數組 2 System.Windows.Forms.Timer timer = null; // 定時器 3 private void button10_Click(object sender, EventArgs e) 4 { 5 // 開線程更新數據 6 Thread thread = new Thread(UpdateArray); 7 thread.IsBackground = true; 8 thread.Start(); 9 10 // 在主界面開定時器訪問數組顯示或計算等等 11 timer = new System.Windows.Forms.Timer(); 12 timer.Tick += Timer_Tick; 13 timer.Interval = 1000; // 每秒更新一次 14 timer.Start(); // 啟動定時器 15 } 16 17 18 private void UpdateArray() 19 { 20 // 每隔200ms更新一次數據,先全部置1,然后全部置2 21 int jj = 1; 22 while (true) 23 { 24 Thread.Sleep(200); 25 for (int i = 0; i < arrayData.Length; i++) 26 { 27 arrayData[i] = jj; 28 } 29 jj++; 30 } 31 } 32 33 34 private void Timer_Tick(object sender, EventArgs e) 35 { 36 textBox1.Text = "總和:" + arrayData.Sum() + " 平均值:" + arrayData.Average(); 37 }
咋一看,沒什么問題,然后運行調試,也沒什么問題,就可以讓它一直在現場跑程序,24小時不間斷,然后突然有一天就掛了,拋出了異常,這個異常是新手經常忽略的地方,也比較難以找到。究其根本原因是,Sum()和Average()操作是需要對數組進行迭代的,而在迭代操作的同時是不允許更改數組的,那我們可以繞過迭代嗎?,答案當然是可以的:
1 private void Timer_Tick(object sender, EventArgs e) 2 { 3 textBox1.Text = "總和:" + SumArrayData() + " 平均值:" + AverageArrayData(); 4 } 5 6 private int SumArrayData() 7 { 8 int sum = 0; 9 for (int i = 0; i < arrayData.Length; i++) 10 { 11 sum += arrayData[i]; 12 } 13 return sum; 14 } 15 16 private double AverageArrayData() 17 { 18 int sum = SumArrayData(); 19 return sum * 1.0 / arrayData.Length; 20 }
將需要迭代的代碼改用for循環來獲取計算,這樣就不會發生異常了,即時24運行程序,也不會拋出異常,但是回過頭來思考,為什么迭代會拋出異常?
為了數據安全!
假設我們需要計算總和,我們需要將數組的項目一個個相加,當我加到一半的時候,更改了數據,然后我們相加得到的總和中就會有一半的數據是舊的,另一半的數據是新的,最終獲取到的數據就不是一次正常的數據,如果你的程序僅僅用來顯示,那么問題不大,如果用來處理一些重要的邏輯業務,那么問題就大了,會直接帶來安全漏洞。帶來數據的不准確,以至於后面根據該數據做出的決策全部都錯誤,比如說根據平均值進行報警,操作設備。所以進行迭代操作拋出異常是合情合理的。
這是一個多么值得深思的問題,這個問題不僅僅是針對T[] 數組類型的,還針對了上述所有的數組類型,因為他們都不是線程安全的。
所以,但凡碰到一個數組需要進行多線程操作的時候,必然加鎖,來進行線程間的同步問題,一般我們比較能想到的就是lock語法糖,更高級的鎖我們以后的文章再提及,所以上述的代碼,我們可以將數組改造成線程安全的方式:
1 private int[] arrayData = new int[100]; // 緩存數組 2 System.Windows.Forms.Timer timer = null; // 定時器 3 private object lock_array = new object(); // 添加的鎖 4 private void button10_Click(object sender, EventArgs e) 5 { 6 // 開線程更新數據 7 Thread thread = new Thread(UpdateArray); 8 thread.IsBackground = true; 9 thread.Start(); 10 11 // 在主界面開定時器訪問數組顯示或計算等等 12 timer = new System.Windows.Forms.Timer(); 13 timer.Tick += Timer_Tick; 14 timer.Interval = 1000; // 每秒更新一次 15 timer.Start(); // 啟動定時器 16 } 17 18 19 private void UpdateArray() 20 { 21 // 每隔200ms更新一次數據,先全部置1,然后全部置2 22 int jj = 1; 23 while (true) 24 { 25 Thread.Sleep(200); 26 lock (lock_array) 27 { 28 for (int i = 0; i < arrayData.Length; i++) 29 { 30 arrayData[i] = jj; 31 } 32 jj++; 33 } 34 } 35 } 36 37 38 private void Timer_Tick(object sender, EventArgs e) 39 { 40 textBox1.Text = "總和:" + SumArrayData() + " 平均值:" + AverageArrayData(); 41 } 42 43 private int SumArrayData() 44 { 45 lock (lock_array) 46 { 47 return arrayData.Sum(); 48 } 49 } 50 51 private double AverageArrayData() 52 { 53 lock (lock_array) 54 { 55 return arrayData.Average(); 56 } 57 }
如果你定義的數組多了,鎖多了,就顯得代碼比較亂,可以將數組,鎖,和數組提供的訪問接口提煉成一個安全的數組類,這樣的話你的代碼也會變得好看很多,自己的能力也上去了。如果你只是對一個單線程的數組進行操作,那么可以隨意的進行迭代。
微軟在.Net 4.X中提供了上述數組的線程安全版本,當然,我們在.Net 3.5中也是可以自己擴充實現的,有興趣的小伙伴可以去嘗試嘗試。
Dictionary<TKey,TValue> 類型數據
怎么說呢,總感覺詞典類型和數組很相似,都可以用來存儲一組數據,當然里面的存儲機制肯定完全不一致。它和數組最大的區別在於,詞典不是基於索引訪問的,不存在你直接訪問第100個數據,然后MoveNext訪問,(不過詞典仍然支持迭代遍歷),而數組基本支持索引訪問的(先入先出和后入先出只支持頭尾訪問),所以詞典的使用場景就是針對不是索引訪問的情況,而是根據唯一的鍵來訪問的,故名思議,詞典是最適合的,比如我們需要有一堆的文本,“Find”, "AAAA", "BBBB"之類,每個數據關聯了一個對象,那我們想要快速直達對象最好的方式就是詞典了。
這么點東西一個晚上居然還沒寫好。。。。。下次想到再補充吧。