CLR支持一維/多維/交錯數組。
兩種聲明方式:
Array a; a = new String[0, 1]; String[] s = new String[5];
注意,聲明不要給與數組長度,因為此時還不分配內存;new時要指定長度。
將數組聲明為Array和像String[]這樣帶中括號的,效果是一樣的,只是前者更靈活,當然類型不安全 ,可能會有運行期錯誤。
所有數組都隱式派生於System.Array,後者派生自System.Object,所以數組是引用類型。
值類型的數組,new時,會創建實際的對象,所有元素都初始化為0;
引用類型的數組,new時,只是創建引用,不會創建實際的對象。
CLS要求數組必須是0基數組。
每個數組都關聯了一些額外的信息,稱為開銷信息(Overhead)。
JITer只在執行一次循環之前檢查一次數組邊界,而不是每次循環都這麼做。
13.1 數組的類型轉換
對於引用類型的數組,兩個數組類型必須維數相同,並且從源元素類型到目標元素類型,必須存在一 個隱式(向父類轉)或顯示(向子類轉)的類型轉換。
對於值類型的數組,不能將其轉換為其他任何一種類型——使用Array.Copy方法替代:
Int32[] ildim = new Int32[5]; Object[] obldim = new Object[ildim.Length]; Array.Copy(ildim, obldim, ildim.Length);
Array.Copy還可以再復制每個數組元素時進行必要的類型轉換:
*將值類型的元素裝箱為引用類型的元素
*將引用類型的元素拆箱為值類型的元素
*加寬CLR基元值類型,Int32到Double
*如果兩個數組的類型不兼容(從Object[]轉型為IFormattable[]),就對元素進行向下類型轉換。
數組的協變性:將數組從一種類型轉換為另一種類型,——會有性能損失,如下代碼,會在運行期報 錯:
String[] sa = new String[100]; Object[] oa = sa; oa[5] = "Jax"; oa[3] = 1; //運行期錯誤
如果只是需要將數組中的某些元素復制到另一個數組,可以使用System.Buffer的BlockCopy()方法, 執行速度比Array.Copy快,但是只支持基元類型,不具有轉型能力。
13.3 所有數組都隱式實現IEnumerable,ICollection,IList
對於泛型接口,因為多維數組和非0基數組的問題,System.Array並不完全實現。
只有一維0基的引用類型的數組,在創建時,會自動實現了IEnumerable<T>, ICollection<T>,IList<T>,T為這個數組的類型;還為T的所有基類型B實現了 IEnumerable<B>,ICollection<B>,IList<B>接口。
如FileStream[] fsArray,會自動實現了IEnumerable<FileStream>, ICollection<FileStream>,IList<FileStream>,同時會實現IEnumerable<Stream> ,ICollection<Stream>,IList<Stream>,IEnumerable<Object>, ICollection<Object>,IList<Object>,所以fsArray可以作為參數傳遞給以下方法:
void M1(IList<FileStream> fsList) { } void M2(ICollection<Stream> sCollection) { } void M3(IEnumerable<Object> oEnumerable) { }
但是,對於一維0基的值類型的數組,在創建時,會自動實現了IEnumerable<T>, ICollection<T>,IList<T>,T為這個數組的類型;但是不會為其基類實現接口。如 DateTime[] dtArray;這個值類型dtArray不能傳遞給上面的M3方法。
13.4 數組的傳遞和返回
Array.Copy返回的是淺Copy。
數組最好是0長度,而不是null
13.5 非0基數組
使用Array.CreateInstance方法,可以指定數組中元素類型,數組維數,數組下界,數組每一維中元 素個數,有若干重載,如下:
//重載1,數組長度 Decimal[] fsArray = (Decimal[])Array.CreateInstance(typeof(Decimal), 2); //重載2,數組長度用一個數組表示,指定多維數組的維數和各維上的長度4和5 Int32[] lengths = { 4, 5 }; Decimal[,] fsArray = (Decimal[,])Array.CreateInstance(typeof (Decimal), lengths); //重載3,最後一個參數指定數組下界 Decimal[] fsArray = (Decimal[])Array.CreateInstance(typeof(Decimal), 2, 1); //重載4,最後一個數組參數,指定數組各維上的下界 Int32[] lengths = { 4, 5 }; Int32[] lowerBounds = { 1, 2 }; Decimal[,] fsArray = (Decimal[,])Array.CreateInstance(typeof (Decimal), lengths, lowerBounds); //重載5,創建一個三維數組,後三個參數分別指定各維的長度 Decimal[,,] fsArray = (Decimal[,,])Array.CreateInstance(typeof (Decimal), 2, 3, 4);
可以使用GetLowerBound(維數);與GetUpperBound(維數);獲取數組的邊界
在一位數組fsArray中,可以使用fsArray數組對象的GetValue()和SetValue()方法來操作數組,但是 比較慢。
13.6 數組訪問性能
對於1維數組,0基的typeof為String.String[];非零基為String.String[*]
對於多維數組,都會顯示String.String[,]。在運行時,CLR將多維數組視為非零基數組。
訪問1維零基數組 比 非零基1維數組 和 多維數組 速度快很多。這是因為:
1.有特殊的IL指令,用於處理1維零基數組,而不用在索引中減去偏移量
2.JIT會將索引范圍檢查代碼從循環中取出,從而只執行一次檢查。
關於for循環遍歷數組:
Int32 a = new Int32[5]; for (Int32 index = 0; index < a.Length; a++) { //對a[index]進行操作 }
Length是一個數組屬性,調用一次後,JIT會將結果存入一個臨時變量,以後每次循環迭代訪問的都是 這個變量,而不是再次計算長度,從而速度更快。
在循環前,JIT編譯器會自動生成代碼檢測有效范圍:if((Length-1) < a.GetUpperBound(0)),只 會檢測一次。
如果將Length保存在一個本地變量,在for循環時每次都會比較該變量,反而會損害性能。
以上只適用於0基數組。非零基數組中,JIT編譯器在循環中,每次都要檢查制指定索引范圍是否有效 ,此外還要從指定索引減去數組下界,從而降低了性能。
提升性能的兩個辦法:
使用交錯數組(數組的數組)來代替多維數組。
使用非安全數組代替非零基數組,在訪問數組時關閉索引邊界檢查——只適用於基元值類型。
使用非安全代碼訪問2維數組:
//用不安全代碼訪問2維數組中所有元素 public static unsafe void Unsafe2DimArrayAccess(Int32[,] a) { Int32 dim0LowIndex = 0; //等價於a.GetLowerBound(0) Int32 dim0HighIndex = a.GetUpperBound(0); Int32 dim1LowIndex = 0; //等價於a.GetLowerBound(1) Int32 dim1HighIndex = a.GetUpperBound(1); Int32 dim0Elements = dim0HighIndex - dim0LowIndex; fixed (Int32* pi = &a[0, 0]) { for (Int32 x = dim0LowIndex; x <= dim0HighIndex; x++) { Int32 baseOfDim = x * dim0Elements; for (Int32 y = dim1LowIndex; y <= dim1HighIndex; y++) { Int32 element = pi[baseOfDim + y]; } } } }
13.7 非安全數組和內嵌數組
非安全數組可以訪問以下元素:
托管堆上的數組
非托管堆的數組
線程堆棧上的數組
在性能第一的前提下,避免在堆上分配數組對象,應該在線程堆棧上分配——使用stackalloc
stackalloc只適用於創建1維0基值類型數組,而且值類型中決不能包括任何引用類型。見以下方法, 將一個字符串倒置:
public static void StackallocDemo() { unsafe { const Int32 width = 20; //在堆棧上分配數組 Char* pc = stackalloc Char[width]; String s = "Jax Bao"; for (Int32 index = 0; index < width; index++) { pc[width - index - 1] = (index < s.Length) ? s [index] : ' '; } String newS = new String(pc, 0, width); Console.WriteLine(newS.Trim()); } }
在struct中定義數組,一般來說,數組本身在struct的外部。
要使數組內嵌在struct中,要遵從:
1.struct要用unsafe標記
2.數組字段要用fixed標記
3.數組必須是1維0基的
4.數組類型必須是基元值類型
采用內嵌數組實現的方法,將一個字符串倒置:
public static void InlineArrayDemo() { unsafe { CharArray ca; //在堆棧上分配數組 Int32 widthInBytes = sizeof(CharArray); Int32 width = widthInBytes / 2; String s = "Jax Bao"; for (Int32 index = 0; index < width; index++) { ca.Characters[width - index - 1] = (index < s.Length) ? s[index] : ' '; } String newS = new String(pc, 0, width); Console.WriteLine(newS.Trim()); } } public unsafe struct CharArray { public fixed Char Characters[20]; }