2009/9/22

[入門] [Array] .NET 陣列詳論

在 .Net Framework 中,Array 可以說是每個程式設計師最常使用的 Type 之一。不過即便你每天都在使用 Array, 你或許還沒有仔細的把它研究過。其實如果好好使用 Array 物件, 在許多情況之下可以增加我們的生產力。

Array 的宣告


陣列總共可以分為四類:

一維陣列 (Single Dimension Arrays) -

這是我們最常使用的陣列, 其宣告方式如:

C# -

  • string[] str;
  • string[] str = new string[] { };
  • string[] str = new string[3]; // 若已明確指定陣列長度時無需加上 { }
  • string[] str = {"John", "Mary", "Harry"};
  • string[] str = new string[] {"John", "Mary", "Harry"};
  • string[] str = new string[3] {"John", "Mary", "Harry"};

VB -

  • Dim str() As String
  • Dim str() As String = New String() { }
  • Dim str() As String = { "0", "1", "2" }
  • Dim str() As String = New String() { "0", "1", "2" }
  • Dim str() As String = New String(2) { "0", "1", "2" }

如上所述, 你可以在宣告時不指定其值, 也可以指定值; 可以不指定其長度, 也可以指定其長度; 可以使用 new 建構子, 也可以不使用。不過如果你眼尖的話, 你或許已經看出上面指令範例對 VB 和 C# 兩者是並非完全相同的。首先, 你在 C# 中可以使用 string[] str = new string[3]; 這種宣告方式, 但 VB 中這麼宣告 (Dim str() As String = New String() 或 Dim str() As String = New String(3)) 的話會被判定為錯誤 (在 VB 的語法規定中, 你至少必須在後面加上 "{ }")。

其次, 在 VB 和 C# 中對於陣列上限 (Upper Bound) 的宣告方式看起來似乎略有不同。各位可能已經注意到同樣擁有三個元素的陣列, 在 C# 中是以 string[3] 來表示, 在 VB 中則是以 string(2) 來表示。其實更明確的講, 這個陣列的 Upper Bound 仍然是 2, 只不過在宣告陣列大小時, C# 是利用 string[元素個數] 的方式宣告, 而 VB 則是利用 String(上限值) 來宣告。在上述範例中, 不管是 VB 或 C# 的宣告法, 同樣會建立一個上限值為 2 (該值可以使用 str.GetUpperBound() 方法得到), 而元素個數為 3 (該值可以使用 str.Length() 方法得到) 的陣列。所以只要搞清楚這個差異, 就不會造成誤會了。

多維陣列 (Multi Dimension Arrays) -

維度大於一的陣列通稱為多維陣列, 其宣告方式如:

  • string[,] str;
  • // string[,] str = new string[,] { };  這是錯誤的宣告方式; 必須指定長度, 且不需加上 { }, 請見下例
  • string[,] str = new string[3,2] ;
  • string[,] str = { {"A", "B"}, {"C", "D"}, {"E", "F"} };
  • string[,] str = new string[,] { {"A", "B"}, {"C", "D"}, {"E", "F"} };
  • string[,] str = new string[3, 2] { {"A", "B"}, {"C", "D"}, {"E", "F"} };

不規則陣列 (Jagged Arrays) -

此種陣列通常被稱為陣列的陣列 (Array of arrays), 意思就是說陣列中還有陣列的意思, 請看下面的宣告就可以明白了:

  • string[][] str;
  • string[][] str = new string[][] { new string[] { "John", "Mary" }, new string[] { "Robert", "Tom", "Jim" } };
  • string[][] str = new string[4][] { new string[2] { "John", "Mary" }, new string[2] { "Robert", "Tom" }, new string[2]{"A", "B"}, new string[2] {"1","2"} };

混合陣列 (Mixed Arrays) -

多維的不規則陣列 (Jagged Multi-Dimension Arrays) 稱為混合陣列。其宣告方式如下:

  • string[,][] str;
  • string[,][] str = new string[,][] { { new string[] { "A", "B" }, new string[] { "A", "B" } }, { new string[] { "A", "B" }, new string[] { "A", "B" } } };

不是想潑你冷水, 但是在這四類陣列裡, 若論實際生活中的情況, 大部份人只會用到一維陣列跟二維陣列;真正用到三維以上陣列的情況實在少之又少。不過,如果你想使用二維陣列,我恐怕會建議你視情況改用 DataTable 物件。DataTable 物件可以結合資料庫,又有各種 ADO.NET 指令可以使用,說起來還蠻實用的。當然, 如果你不需要考慮到跟資料庫的結合, 或是你必須運用在某些特殊的狀況 (例如處理 2D 圖形),那麼二維陣列還是可以用。

至於一維陣列,.NET 另外提供了許多其它的選擇,包括 HashTable、SortedList、Dictionary 和 List 等衍生自 System.Collections 的結構, 在實際用途上一點都不輸給 Array。尤其是支援泛型的 List 物件,在使用上提供了蠻大的便利性,所以以我個人來講,List 一向是我優先考慮的一維型別。

此外, Array 是所謂 Tabular 或 Rectanble 資料結構的典型代表。所以如果你要使用陣列去處理樹狀結構,只能使用不規則陣列 (Jagged Array) 結構,但是它的方便性與擴充性卻又遠不如使用 XML 結構。

然而在許多情形之下還是值得使用陣列的。簡單和效率是它最大的優點,而且也幾乎是大家所熟悉的 (絕大部份語言都支援陣列結構)。所以我相信花點時間好好研究一下陣列仍然有其價值。所以請耐心的繼續看下去吧!

Array 的值的指定與複製


所有的陣列型別都衍生自 System.Array 類別, 而 System.Array 又是衍生自 System.Object。而 String 型別雖然也衍生自 System.Object,但是在複製行為方面兩者卻有很大的差異。

我們先來看看 String 的行為:

string strSource = "ABC";
string strDest = strSource;
MessageBox.Show(strDest);
strSource = "DEF";
MessageBox.Show(strDest);

在把上述程式拿去執行之前,各位不妨先猜猜看結果是什麼?事實上,當你使用 strDest = strSource 指令之後,系統會產生一個 strSource 字串的複本,再把這個複本指定給 strDest,所以當你修改 strSource 的內容之後,strDest 的內容並不會隨之修改。

但同樣的狀況發生在 Array 物件時,其情況是迴異的。請看以下程式:

string[] src = new[] { "6", "2", "3" };
//string[] tar = (string[]) src.Clone();
string[] tar = src;
MessageBox.Show("tar = "+string.Join(",", tar));
Array.Sort(src);
MessageBox.Show("src = " + string.Join(",", src));
MessageBox.Show("tar = " + string.Join(",", tar));

當你執行到 tar = src 這道指令時,系統其實是把指向 src 的記憶體位址 (Pointer, 或 Location, 而非實際內容) 複製給了 tar 物件,導致兩者會指向相同的一個物件,所以當你對 src 執行到 Array.Sort 指令之後,tar 會指向變更後的物件。

如果你沒辦法體會上面所描述的意義, 那麼你可以想像一下, 假設你以為你「複製」了一幢房子, 那麼你必須搞清楚你到底是依照原來房子的樣子和傢俱另外「蓋」了一模一樣的房子, 還是只是把原來房子的「地址」影印一份而已。若是前者, 那麼原來的房子裡面的擺設不會永遠一樣; 若是後者, 那麼原來的「地址」和影印的「地址」所指向的還是同一幢房子, 裡面的擺設不管怎麼改變, 其結果一定是一樣的。

換句話說,對 string 物件和 string[] 物件而言 (其實對 int 與 int[] 及其它型別亦然),你對它們使用 "=" 指令的意義並不一樣。

那麼,如果你真的是要複製一個陣列的值而非指標,應該怎麼做呢?在這種情況下,你可以把上面 //string[] tar = (string[]) src.Clone(); 前面的註解標示拿掉,亦即使用 Clone() 方法,那麼就會真正複製一份陣列物件了。

動態改變 Array 的大小


使用陣列結構的一個很大的優點, 就是因為它的大小是可以隨時調整的。如果你想以動態 Linked List 的結構來操作陣列, 那是行不通的 (在這種情況下你應該改用 Collection 或 List 結構), 因為你最好一開始就指定陣列的大小, 否則動不動就會出現「索引在陣列的界限之外」之類的錯誤。

如果你是舊版 VB 的愛用者, 我相信你一定知道可以使用 ReDim 指令來改變陣列大小。在 VB.NET 中, 你仍然可以使用這個熟悉的指令; 不過你也可以使用 Array.Resize() 方法, 其範例如下 (請同時留意二者在使用上的差異):

VB -

Dim str() As String = { }
ReDim str(2) '使用這個指令會讓 str 具有三個元素; 換句話說, 你可以指定或取得 str(2) 的值
Array.Resize(str, 2) '使用這個指令會讓 str 具有兩個元素; 換句話說, 你不可以指定或取得 str(2) 的值

至於 C#, 它就單純多了, 因為只有一種方法:

C# -

string[] str = new string[2];
str = new string[3];

換句話說, 你不管在什麼時候, 都可以使用 new string[3] 這種指令以重新定義陣列的大小。不過請注意, 無論 C# 或 VB, 當你重新定義陣列大小的同時, 原陣列裡面的值都會被清掉

不過如果你使用 VB 的話, 它倒是有個 Preserve 關鍵字, 可以保留原陣列的值, 如下例:

VB -

Dim str() As String = { "0", "1", "2" }
ReDim Preserve str(8)

在上例中, 我們使用 ReDim 指令將陣列大小從三個改成九個, 但因為我們加上了 Preserve 關鍵字, 所以該陣列前三個元素的值會與變動陣列大小前一樣。如果你忘了加上 Preserve 關鍵字, 那麼整個陣列的值都會被清空。

對於 C# 使用者, 如果你想在變動陣列大小時保留舊資料, 那麼你並沒有 Preserve 關鍵字可以用, 必須自己複製及拷貝。

Array 的列舉與繫結


System.Array 和許多同類的型別一樣實作了 IEnumerable, 所以你可以很容易的將其內容進行列舉。當然,既然可以列舉,也就很容易作為控制項的繫結來源。此外,要列舉實作 IEnumerable 的型別的方式,最簡單而直覺的方法當然就是 foreach, 如下例:

string[] nations = { "Taiwan", "USA", "Japan", "China", "Korea", "Franch" };
DropDownList1.DataSource = nations;
DropDownList1.DataBind();
foreach (string nation in nations) {
   Response.Write(nation + ", ");
}

請特別注意 foreach() 方法對於多維陣列的列舉方式。如下例:

int[,] intNumbers =  { {1, 2, 3}, {4, 5, 6} };
foreach (int intNum in intNumbers) {
   Response.Write(intNum.ToString() + ", ");
}

上面程式的執行結果會是 1, 2, 3, 4, 5, 6。

如果你想對陣列進行更進一步的處理, 那麼你可以使用 LINQ 陳述式:

        int[] intSource = { 1, 1, 2, 2, 4, 3, 2 };
        var intSrc=(from int intItem in intSource
                    orderby intItem
                    select intItem).Distinct();
        DropDownList1.DataSource = intSrc;
        DropDownList1.DataBind();

透過 LINQ, 你可以對陣列物件 (其實對其它同樣實作 IEnumerable 的型別都一樣) 做類似 SQL 查詢的動作,為原來的型別潻加許多使用上的彈性。在上例中, 我們已將陣列做過排序 (使用 orderby 子句), 並擷取出陣列的唯一值 (使用 Distinct() 方法), 所以擊結到 DropDownList 之後會得到 1, 2, 3, 4 這四個數字。

對 Array 的處理與加工


我們除了可以對陣列進行列舉和控制項的資料繫結之外, 我們還可以使用 Array 類別來對陣列進行其它的處理。例如對陣列進行排序:

int[] intSource = { 1, 1, 2, 2, 4, 3, 2 };
Array.Sort(intSource);

做過 Array.Sort 指令之後, intSource 的內容會變成 { 1, 1, 2, 2, 2, 3, 4 }。

Array 類別還提供了 Binary Search 的功能, 可以讓我們做陣列元素的搜尋, 如下例:

int[] intSource = { 1, 1, 2, 2, 4, 3, 2 };
Array.Sort(intSource);
int Result = Array.BinarySearch(intSource, 4);

在上例中, Result 會得出一個正值, 表示搜尋的元素出現在陣列中的位置。當然, 如果你知道 Binary Search 的邏輯, 你就應該知道為什為我在 Array.BinarySearch 指令之前非得擺上一個 Array.Sort 指令不可。如果你把 Array.Sort 這個指令給忘了, 那麼上面的結果將是 -8 (傳回值若小於0, 表示找不到), 即使 4 這個元素確實在陣列裡面。

如果你不想先對陣列排序, 或是你不想使用 Binary Search, 那麼你可以使用 Array.IndexOf 方法以對陣列進行搜尋:

int[] intSource = { 1, 1, 2, 2, 4, 3, 2 };
int Result = Array.IndexOf(intSource, 4);

在上例中, 你會得到 4 這個正確的結果。那麼, 即然 BinarySearch() 和 IndexOf() 都可以搜尋陣列中的元素, 到底兩者的差異在哪裡? 顧名思義, BinarySearch() 方法採用 Binary Search 邏輯, 而 IndexOf() 則採用 Linear Search 邏輯。Binary Search 適合用來對大量資料進行搜尋, 其執行效率為 O(logN), 而 Linear Search 的效率僅為 O(N)。但是在陣列容量不大的情形下, 使用 IndexOf() 反而會比較快 (除非你本來就需要執行 Sort() 指令)。

你或許會想知道如何做反向排序。其實很簡單; Array 類別提供了 Reverse() 方法, 可以將陣列反向, 所以你可以在做過 Sort 之後再做 Reverse 即可:

int[] intSource = { 1, 1, 2, 2, 4, 3, 2 };
Array.Sort(intSource);
Array.Reverse(intSource);

我想你可能會覺得好奇, .NET 有沒有支援像矩陣相加、乘積或轉置矩陣等功能呢? 據我所知在這方面應該是沒有太多現成的功能可用, 如果你有需要, 可能得自己寫程式, 或是求助於 3rd Party 廠商。

System.Array 類別另外還提供了一些其它的函式和屬性, 你可以在 MSDN 網站上找到正式說明文件。

Array 間的型別轉換


有時候我們需要快速的將某一型別的陣列轉換為另一型別, 通常我們會寫一個迴圈來把每一個項目個別轉換。但是遇到 Redim 陣列時會有資料遺失的問題而必須特別尋找暫時性儲存空間, 這可能會讓人覺得麻煩。

如果你使用 .Net Framework 3.0 以上, 倒是有個簡單的方法可以做到這件事情, 那就是使用 Array.ConvertAll 指令, 範例如下:

string[] s = new string[] {"1", "2", "3"};
int[] i = Array.ConvertAll<string, int>(s, int.Parse);

在這裡我們用到了 Lambda 運算式, 而這是 .Net Framework 3.0 以上才有的。

如果你要轉換的型別是自訂型別, 那麼你必須把上面範例中的 int.Parse 換成你自己提供的型別轉換方法。

補充說明 -

自 .Net Framework 2.0 之後, 事實上不但 C# 有支援 Array.Resize, 而且無論 C# 或 VB, 在進行陣列大小的 resizing 之後, 並不會清掉既有的內容, 詳情可以參考: http://msdn.microsoft.com/zh-tw/library/1ffy6686(VS.80).aspx

此外, 對於 Array.Rezie 方法, 請留意 C# 的語法在參數部份和 VB 略有不同。

在我對上文進行修改之前, 請讀者自行注意一下上文中部份過時與謬誤之處。

Technorati 的標籤:,,

沒有留言:

張貼留言