[入門文章] .NET 陣列詳論

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

在 .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"};
  • int BOUND = 20;
    string[] str = new string[BOUND]; // 可以通過編譯,且 str 所有元素會自動賦予初始值  null
  • int BOUND = 20;
    string[] str = new string[BOUND]  { }; // 無法通過編譯 - 你必須將 BOUND 宣告為 const

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() 方法得到) 的陣列。所以只要搞清楚這個差異, 就不會造成誤會了。

請注意 C# 對陣列的宣告方式和 C++ 有很大的差異。C++ 的宣告方式如下:

C++ -

  • int num[2]; // 宣告一個未賦予初始值的陣列
  • int num[2] = {1, 2}; // 賦予初始值的語法
  • int num[2] = {1}; // 未被初始化的第二個元素將以 0 做為初始值
  • int num[2] = { }; // 若全部元素都沒有設定特定的初始值,將全部以 0 取代
  • char station[3] = { 'a', 'b', 'c' };
  • const int BOUND = 5;
    int[BOUND ] num = { }; // 陣列長度只能接受常數值

像 int num[2]; 這種未設定初始值的宣告中,這個 num 陣列的內容將是一組亂七八糟的值 - 被分配到的記憶體中原本是什麼就是什麼。所以我們最好養成習慣,將陣列給予初始值;即使像 int num[2] = { }; 這樣的宣告也比未指定初始值來得保險一點。這一點跟 C# 有蠻大的差異;在 C# 中,如果你並未賦予陣列初始值,你根本無法存取它,在 C++ 卻可以。因此,我們可以說 C# 在這方面的嚴謹度勝過 C++,因為在 C++ 程式中你可能不小心取得未經初始化的陣列中垃圾數字,在 C# 中卻沒有這種可能性。

C++ 與 C# 在使用變數定義陣列長度的方式也有差異 (請參考上面二者的宣告方式)。C++ 無論如何都不能接受使用變數以宣告陣列長度,C# 卻可以 (只要你不要在宣告時同時強制賦予初始值)。在這方面,C# 確實比較具有彈性。

此外,如下的 C++/CLI 宣告是錯誤的:

  • String ^hStr[3];

由於 String^ 屬於 Managed 型別,原生陣列裡面不可包含 Managed  型別,只能包含原生型別 (char, int, double 等等)。你必須使用  Managed Array。

C++/CLI 提供了 Managed Array,它和傳統 C++ 的陣列宣告方式不太一樣。Managed Array 必須以 Handle 變數宣告 (如上例中的 String^ 。如果你不知道什麼是 Handle 變數,請參考「從 C#/VB 開發者的角度解析 C++ 中的指標」一文),如下例:

  • array<int> ^hIntArray = gcnew array<int>(3) { 2, 4, 6 };
    for (int i = 0; i < hIntArray -> Length; i++)
    Console::Write("{0, 3}", hIntArray[i] - 1);

至於 C++ 的相關細節在本文中就不做過多說明了。

多維陣列 (Multi Dimension Arrays) -

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

C# -

  • 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"} };

以上是 C# 的語法。相較之下,C++ 的多維陣列的宣告方式是非常不一樣的:

C++ -

  • int data[2][4] = { {1, 3, 5, 7}, {2, 4, 6, 8} };
  • char data[2][50] = { "Johnny", "Worker" };

C++/CLI -

  • array<int, 2> ^hTwoDimensionArray = gcnew array<int, 2>(3, 4); // 宣告一個三乘四 (三列、四欄) 的二維陣列
    for (int i = 0; i < 3; i++)
       for (int j = 0; j < 4; j++)
          hTwoDimensionArray[i, j] = (i + 1) * (j + 1);
    for (int i = 0; i < 3; i++)
       for (int j = 0; j < 4; j++)
          Console::Write("{0, 3}", hTwoDimensionArray[i, j]);

千萬不要跟以下的不規則陣列搞混了。到這裡為止,我們討論的都是規則陣列。

不規則陣列 (Jagged Arrays) -

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

C# -

  • 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"} };

在 C++ 中沒有不規則陣列這種東西,至少沒有原生的不規則陣列。但是如果有需要的話,我們還是可以經由 pointer 做出類似的東西,但是過程比較複雜,不列入本文討論範圍。

混合陣列 (Mixed Arrays) -

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

C# -

  • 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 或 Rectangle 資料結構的典型代表。所以如果你要使用陣列去處理樹狀結構,只能使用不規則陣列 (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() 方法,那麼就會真正複製一份陣列物件了。

此外, 我們必須特別注意 .Net 語言中對於所謂 Array Covariance 的特性。例如, 以下程式在 .Net 中是正確的:

object[] arr = new string[3];

照理說, 這個陣列的型別是 object, 但是你卻可以指派宣告為 string 型別的物件給它。如果對調過來就不行了:

string[]  = new object[3];

上面這行程式在編譯時就會被擋下來。

.Net 這種 Array Covariance (陣列的共變異性) 的特性允許你把衍生型別的物件指派給原型別物件而無需明確轉換。在上例中, string 型別是衍生自 (derived from) object 型別, 所以像 object[] arr = new string[3]; 這樣的陣述式是可以成立的。

這種 Covariance 特性並不只對原生型別有效, 對於自訂型別也一樣有效。請看以下的範例:

public class employee
{
    public int employeeId;
    public string name;
}

public class engineer : employee // 請留意我在這裡並未實作隱含或明確的型別轉換
{
    public int profession;
    public int department;
}

static void Main(string[] args)
{
    employee[] eng = new engineer[5];
}

其實這種特性在 .Net 中是普遍存在的, 並不只針對陣列。仿上例, 對於一般變數, 我們同樣也可以進行此種指派方式:

employee lee = new engineer() { employeeId=5, name="Johnny", profession=1, department=2 };

由於此種特性, 當我們在指派變數給這些變數時, 最好能特別留意型別間的繼承關係, 以免掉進難以除錯的錯誤陷阱。什麼意思呢? 假設我們在engineer 之外又加入了一個同樣是繼承了 employee 的新類別 financial:

public class financial : employee
{
    public int role;
}

那麼, 如果我們不小心把程式寫成如下的樣子:

employee[] emp = new engineer[5];
emp[1] = new financial() { employeeId = 7, name = "Susan", role = 2 };

它在編譯的時候是正確的。但是當程式執行到第二行時, 就會出現 runtime 錯誤了。要避免這種錯誤, 你就應該在撰寫程式時避過 Array Covariance, 把上述程式中的 engineer[5] 還原成 employee[5]。像以下這種寫法才是合理而且不會錯誤的:

employee[] emp = new employee[5];
emp[0] = new engineer() { employeeId = 5, name = "Johnny", profession = 1, department = 2 };
emp[1] = new financial() { employeeId = 7, name = "Grace", role = 2 };

 

動態改變 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];

也可以使用 Array.Resize 方法:

string[] str = new string[2];
 Array.Resize<string>(ref str, 3);:

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

VB -

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

在上例中, 我們使用 ReDim 指令將陣列大小從三個改成九個, 但因為我們加上了 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 換成你自己提供的型別轉換方法。


Dev 2Share @ 點部落