數組使用---進階編程篇(五)


本篇文章講解數組的使用,先是介紹下幾種不同的數組,在說明下各自的區別和使用場景,然后注意細節,廢話不多說,趕緊上代碼。


在.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第五個值的時候,就會發生很多事情

  1. 判斷原來的數組容量夠不夠?因為數量已經超了,所以不夠
  2. 因為原數組不夠,所以需要進行擴充,擴充多少呢?原數組長度*2!!!!!,此處為8
  3. 確定好了擴充的數據,重新生成一個8長度的數組
  4. 准備復制數據,值類型復制數值本身,而引用類型僅僅復制引用,此處把原來4個舊的數據復制到8個新數組的前四個位置上
  5. 最后的最后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"之類,每個數據關聯了一個對象,那我們想要快速直達對象最好的方式就是詞典了。

 

 

這么點東西一個晚上居然還沒寫好。。。。。下次想到再補充吧。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM