[CLR via C#]16. 数组
數組是允許將多個數據項當作一個集合來處理的機制。CLR支持一維數組、多維數組和交錯數據(即由數組構成的數組)。所有數組類型都隱式地從System.Array抽象類派生,后者又派生自System.Object。這意味著數組始終是引用類型,是在托管堆上分配的。在你應用程序的變量或字段中,包含的是對數組的引用,而不是包含數組本身的元素。下面的代碼更清楚的說明了這一點:
Int32[] myIntegers; //聲明一個數組引用 myIntegers = new int32[100] //創建含有100個Int32的數組在第一行代碼中,myIntegers變量能指向一個一維數組(由Int32值構成)。myIntegers剛開始被設為null,因為當時還沒有分配數組。第二行代碼分配了含有100個Int32值的一個數組,所有Int32都被初始化為0。由于數組是引 用類型,所有托管堆上還包含一個未裝箱Int32所需要的內存塊。實際上,除了數組元素,數字對象占據的內存塊還包含一個類型對象指針、一個同步塊索引和一些額外的成員(overhead)。該數組的內存塊地址被返回并保存到myIntegers變量中。
C#也支持多維數組。下面演示了幾個多維數組的例子:
// 創建一個二維數組,由Double值構成 Double[,] myDoubles = new Double[10,20]; // 創建一個三位數組,由String引用構成 String[,,] myStrings = new String[5,3,10];CLR還支持交錯數組,即由數組構成的數組。下面例子演示了如何創建一個多邊形數組,其中每一個多邊形都由一個Point實例數組構成。
// 創建一個含有Point數組的一維數組 Point[][] myPolygons = new Point[3][]; // myPolygons[0]引用一個含有10個Point實例的數組 myPolygons[0] = new Point[10]; // myPolygons[1]引用一個含有20個Point實例的數組 myPolygons[1] = new Point[20]; // myPolygons[2]引用一個含有30個Point實例的數組 myPolygons[2] = new Point[30]; // 顯示第一個多邊形中的Point for (Int32 x =0 ; x < myPolygons[0].Length; x++) {Console.WriteLine(myPolygons[0][x]); }注意:CLR會驗證數組索引的有效性。換句話說,不能創建一個含有100個元素的數組(索引編號為0到99),又試圖訪問索引為-5或100的元素。
一、始化數組元素
前面展示了如何創建一個數組對象,以及如何初始化數組中的元素。C#允許用一個語句來同時做兩件事。例如:
String[] names = new String[] { "Aidan", "Grant" };大括號中的以逗號分隔的數據成為數組初始化器。每個數據項都可以是一個任意復雜度的表達式;在多維數組的情況下,則可以是一個嵌套的數組初始化器。可利用C#的隱式類型的數組功能讓編譯器推斷數組元素的類型。注意,下面這一行代碼沒有在new和[]之間指定類型:
var names = new[] { "Aidan", "Grant", null};在上一行中,編譯器檢查數組中用于初始化數組元素的表達式的類型,并選擇所有元素最接近的共同基類作為數組的類型。在本例中,編譯器發現兩個String和一個null。由于null可隱式轉型成為任意引用類型(包括String),所以編譯器推斷應該創建和初始化一個由String引用構成的數組。
給定一下代碼:
var names = new[] { "Aidan", "Grant", 123};編譯器是會報錯的,雖然String類和Int32共同基類是Object,意味著編譯器不得不創建Object引用了一個數組,然后對123進行裝箱,并讓最后一個數組元素引用已裝箱的,值為123的一個Int32。但C#團隊認為,隱式對數組?元素進行裝箱是一個代價昂貴的操作,所以要做編譯時報錯。
在C#中還可以這樣初始化數組:
String[] names = { "Aidan", "Grant" };但是C#不允許在這種語法中使用隱式類型的局部變量:
var names = { "Aidan", "Grant" };最后來看下"隱式類型的數組"如何與"匿名類型"和"隱式類型的局部變量"組合使用。
// 使用C#的隱式類型的局部變量、隱式類型的數組和匿名類型 var kids = new[] {new { Name="Aidan" }, new { Name="Grant" }}; // 示例用法 foreach (var kid in kids)Console.WriteLine(kid.Name);輸出結果:
Aidan
Grant
二、數組轉型
對于元素為引用類型的數組,CLR允許將數組元素從一種類型隱式轉型到另一種類型。為了成功轉型,兩個數組類型必須維數相等,而且從源類型到目標類型,必須存在一個隱式或顯示轉換。CLR不允許將值類型元素的數組轉型為其他任何類型。(不過為了模擬實現這種效果,可利用Array.Copy方法創建一個新數組并在其中填充數據)。下面演示了數組轉型過程:
Array.Copy方法的作用不僅僅是將元素從一個數組復制到另一個數組。Copy方法還能正確處理內存的重疊區域。?
Copy方法還能在復制每一個數組元素時進行必要的類型轉換。Copy方法能執行以下轉換:
1)將值類型的元素裝箱為引用類型的元素,比如將一個Int32[]復制到一個Object[]中。
2)將引用類型的元素拆箱為值類型的元素,比如將一個Object[]復制到Int32[]中。
3)加寬CLR基元值類型,比如將一個Int32[]的元素復制到一個Double[]中。
4)在兩個數組之間復制時,如果僅從數組類型證明不了兩者的兼容性。
在某些情況下,將數組從一種類型轉換為另一種類型是非常有用的。這種功能稱為數據協變性。利用數組協變性時,應該清楚由此帶來的性能損失。
注意:如果只需要把數組中某些元素復制到另一個數組,可以選擇System.Buffer的BlockCopy方法,它的執行速度比Array.Copy方法快。不過,Buffer的BlockCopy方法只支持基元類型,不提供像Array的Copy方法那樣的轉型能力。方法的Int32參數代表的是數組中的字節偏移量,而非元素索引。如果需要可靠的將一個數組中的元素復制到另一個數組,應該使用System.Array的ConstrainedCopy方法,該方法能保證不破壞目標數組中的數組的前提下完成復制,或者拋出異常。另外,它不執行任何裝箱、拆箱或向下類型轉換。
三、所有數組都隱式派生自System.Array
如果像下面這樣聲明一個數組變量:
FileStream[] fsArray;CLR會為AppDomain自動創建一個FileStream[]類型。這個類型將隱式派生自System.Array類型;因此,System.Array類型定義的所有實例方法和屬性都將有FileStream[]繼承,使這些方法和屬性能通過fsArray變量調用。
四、所有數組都隱式實現IEnumerable,ICollection和IList
許多方法都能操作各種集合對象,因為在聲明它們時,使用了IEnumerable,ICollection和IList等參數。可以將數組傳給這些方法,因為System.Array也實現了這三個接口。System.Array之所以實現這些非泛型接口,是因為這些接口將所有元素都視為Systm.Object。然而,最好讓System.Array實現這個接口的泛型形式,提供更好的編譯時類型安全性和更好的性能。
五、數組的傳遞和返回
數組作為實參傳給一個方法時,實際傳遞的是對該數組的引用。因此,被調用的方法能修改數組中的元素。如果不想被修改,必須生成數組的一個拷貝,并將這個拷貝傳給方法。注意,Array.Copy方法執行的是淺拷貝。
有的方法返回一個對數組的引用。如果方法構造并初始化數組,返回數組引用是沒有問題的。但假如方法返回的是對一個字段維護的內部數組的引用,就必須決定是否向讓該方法的調用者直接訪問這個數組及其元素。如果是就可以返回數組引用。但是通常情況下,你并不希望方法的調用這獲得這個訪問權限。所以,方法應該構造一個新數組,并調用Array.Copy返回對新數組的一個引用。
如果定義一個返回數組引用的方法,而且該數組不包含元素,那么方法既可以返回null,又可以放回對包含另個元素的一個數組的引用。實現這種方法時,Microsoft強烈建議讓它返回后者,因為這樣做能簡化調用該方法時需要的代碼。
// 這段代碼更容易寫,更容易理解 Appointment[] app = GetAppointmentForToday(); for (Int32 a =0; a< app.Length; a++) { // 對app[a]執行操作 }如果返回null的話:
// 寫起來麻煩,不容易理解 Appointment[] app = GetAppointmentForToday(); if( app !=null ) { for (Int32 a =0; a< app.Length; a++) { // 對app[a]執行操作} }六、創建下限非零的數組
可以調用數組的靜態CreateInstance方法來動態創建自己的數組。該方法有若干個重載版本,允許指定數組元素的類型、數組的維數、每一維的下限和每一維的元素數目。CreateInstance為數組分配內存,將參數信息保存到數組的內存塊的額外開銷(overhead)部分。然后返回對該數組的一個引用。
七、數組的訪問性能
CLR內部實際支持兩種不同的數組
1)下限為0的意味數組。這些數組有時稱為SZ數組或向量。
2)下限未知的一維或多維數組。
可執行一下代碼來實際地查看不同種類的輸出
public static void Go() {
Array a;// 創建一個一維數組的0基數組,其中不包含任何元素 a = new String[0];Console.WriteLine(a.GetType()); // System.String[]// 創建一個一維數組的0基數組,其中不包含任何元素 a = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 0 });Console.WriteLine(a.GetType()); // System.String[]// 創建一個一維數組的1基數組,其中不包含任何元素 a = Array.CreateInstance(typeof(String), new Int32[] { 0 }, new Int32[] { 1 });Console.WriteLine(a.GetType()); // System.String[*] <-- 注意! Console.WriteLine();// 創建一個二維數組的0基數組,其中不包含任何元素 a = new String[0, 0];Console.WriteLine(a.GetType()); // System.String[,]// 創建一個二維數組的0基數組,其中不包含任何元素 a = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 0, 0 });Console.WriteLine(a.GetType()); // System.String[,]// 創建一個二維數組的1基數組,其中不包含任何元素 a = Array.CreateInstance(typeof(String), new Int32[] { 0, 0 }, new Int32[] { 1, 1 });Console.WriteLine(a.GetType()); // System.String[,]} }
對于一維數組,0基數組顯示的類型名稱是System.String[],但1基數組顯示的是System.String[*]。*符號表示CLR知道該數組不是0基的。注意,C#不允許聲明String[*]類型的變量,因此不能使用C#語法來訪問一維的非0基數組。盡管可以調用Array的GetValue和SetValue方法來訪問數組的元素,但速度會比較慢,畢竟有方法調用的開銷。
對于多維數組,0基和1基數組會顯示同樣的類型名稱:System.String[,]。在運行時,CLR將對所有多維數組都視為非0基數組。這自然會人覺得應該顯示為System.String[*,*]。但是,對于多維數組,CLR決定不用*符號,避免開發人員對*產生混淆。
訪問一維0基數組的元素比訪問非0基數組或多維數組的元素稍快一些。首先,有一些特殊的IL指令,比如newarr,ldelem,ldelema等用于處理一維0基數組,這些特殊IL指令會導致JIT編譯器生成優化代碼。其次,JIT編譯器知道for循環要反問0到Length-1之間的數組元素。所以,JIT編譯器生成的代碼會在運行時測試所有數組元素的訪問都在數組有效訪問內。
如果很關系性能,請考慮由數組構成的數組(即交錯數組)來替代矩形數組。
下面C#代碼演示了訪問二維數組的三種方式:
internal static class MultiDimArrayPerformance { private const Int32 c_numElements = 10000;public static void Go() { const Int32 testCount = 10; Stopwatch sw;// 聲明一個二維數組 Int32[,] a2Dim = new Int32[c_numElements, c_numElements];// 將一個二維數組聲明為交錯數組 Int32[][] aJagged = new Int32[c_numElements][]; for (Int32 x = 0; x < c_numElements; x++) aJagged[x] = new Int32[c_numElements];// 1: 用普通的安全技術訪問數組中的所有元素 sw = Stopwatch.StartNew(); for (Int32 test = 0; test < testCount; test++) Safe2DimArrayAccess(a2Dim); Console.WriteLine("{0}: Safe2DimArrayAccess", sw.Elapsed);// 2: 用交錯數組技術訪問數組中的所有元素 sw = Stopwatch.StartNew(); for (Int32 test = 0; test < testCount; test++) SafeJaggedArrayAccess(aJagged); Console.WriteLine("{0}: SafeJaggedArrayAccess", sw.Elapsed);// 3: 用unsafe訪問數組中的所有元素 sw = Stopwatch.StartNew(); for (Int32 test = 0; test < testCount; test++) Unsafe2DimArrayAccess(a2Dim); Console.WriteLine("{0}: Unsafe2DimArrayAccess", sw.Elapsed); Console.ReadLine(); }private static Int32 Safe2DimArrayAccess(Int32[,] a) {Int32 sum = 0;for (Int32 x = 0; x < c_numElements; x++){for (Int32 y = 0; y < c_numElements; y++){sum += a[x, y];}}return sum; }private static Int32 SafeJaggedArrayAccess(Int32[][] a) {Int32 sum = 0;for (Int32 x = 0; x < c_numElements; x++){for (Int32 y = 0; y < c_numElements; y++){sum += a[x][y];} } return sum; }private static unsafe Int32 Unsafe2DimArrayAccess(Int32[,] a) {Int32 sum = 0;fixed (Int32* pi = a){for (Int32 x = 0; x < c_numElements; x++){Int32 baseOfDim = x * c_numElements;for (Int32 y = 0; y < c_numElements; y++){sum += pi[baseOfDim + y];}}}return sum; } }本機結果是:
可以看出,安全二維數組訪問技術最慢。安全交錯數組訪問時間略少于安全二維數組。不過應該注意的是:創建交錯數組所花的時間多于創建多維數組所花的時間,因為創建交錯數組時,要求在堆上為每一維分配一個對象,造成垃圾回收器的周期性活動。所以你可以這樣權衡:如果需要創建大量"多個維的數組",而不會頻繁訪問它的元素,那么創建多維數組就要快點。如果"多個維的數組"只需創建一次,而且要頻繁訪問它的元素,那么交錯數組性能要好點。當然,大多數應用中,后一種情況更常見。
最后請注意,不安全和安全二維數組訪問技術的速度大致相同。但是,考慮到它訪問是單個二維數組(產生一次內存分配),二不像交錯數組那樣需要許多次內存分配。所以它的速度是所有技術中最快的。
八、不安全的數組訪問和固定大小的數組
如果性能是首要目標,請避免在堆上分配托管的數組對象。相反,應該在線程棧上分配數組,這是通過C#的 stackalloc語句來完成的。stackalloc語句只能創建一維0基、由值類型元素構成的數組,而且值類型絕對不能包 含任何引用類型的字段。當然,在棧上分配的內存(數組)會在方法返回時自動釋放。
以下代碼顯示如何使用C#的stackalloc語句:
internal static class StackallocAndInlineArrays { public static void Go() { StackallocDemo(); InlineArrayDemo(); }private static void StackallocDemo() { unsafe { const Int32 width = 20; Char* pc = stackalloc Char[width]; // 在棧上分配數組 String s = "Jeffrey Richter"; // 15 個字符for (Int32 index = 0; index < width; index++) { pc[width - index - 1] = (index < s.Length) ? s[index] : '.'; } //顯示".....rethciR yerffeJ" Console.WriteLine(new String(pc, 0, width)); } }private static void InlineArrayDemo() { unsafe { CharArray ca; // 在棧上分配數組 Int32 widthInBytes = sizeof(CharArray); Int32 width = widthInBytes / 2;String s = "Jeffrey Richter"; // 15 個字符for (Int32 index = 0; index < width; index++) { ca.Characters[width - index - 1] = (index < s.Length) ? s[index] : '.'; } //顯示".....rethciR yerffeJ" Console.WriteLine(new String(ca.Characters, 0, width)); } }private unsafe struct CharArray { // 這個數組以內聯的方式嵌入結構 public fixed Char Characters[20]; } }通常,因為數組是引用類型,所以在一個結構中定義的數組字段實際只是指向數組的一個指針;數組本身在結構的內存的外部。不過,也可以像上述代碼中的CharArray結構那樣,直接將數組嵌入結構中。要在結構中直接嵌入一個數組,需要滿足以下幾個要求:
1)類型必須是結構(值類型);不能在類(引用類型)中嵌入數組。
2)字段或其定義結構必須用unsafe關鍵字標記
3)數組字段必須使用fixed關鍵字標記
4)數組必須是一維0基數組。
5)數組的元素類型必須是一下類型之一:Boolean,Char,SByte,Byte,Int16,Int32,UInt16,UInt32,Int64,UInt64,Single或Double。
內聯(內嵌)數組常用于和非托管代碼進行互操作,而且非托管數據結構也有一個內聯數組。不過,也可用于其他情況。
轉載于:https://www.cnblogs.com/zxj159/p/3569500.html
總結
以上是生活随笔為你收集整理的[CLR via C#]16. 数组的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【LeetCode】Minimum De
- 下一篇: c# char unsigned_dll