枚舉類型(enumerated types)定義了一組"符號名稱/值"配對。例如,如下Color類型定義了一組符號,每一個符號都標識一種顏色:c++
internal enum Color { While, //賦值0 Red, //賦值1 Green, //賦值2 Blue, //賦值3 Orange //賦值4 }
使用枚舉類型的好處:程序員
1)枚舉類型使程序更容易編寫、閱讀和維護。有了枚舉類型,符號名稱可在代碼中隨便使用,開發人員不須要記住每一個硬編碼的含義。並且,一旦與符號名稱對應的值發生變化,代碼也能夠簡單的從新編譯,不須要對源代碼作出任何修改。除此以外,文檔工具和其餘實用程序能向開發人員顯示有意義的符號名稱。c#
2)枚舉類型是強類型的。數組
在Microsoft .NET Framework中,枚舉類型不僅是編譯器所關心的符號,它在類型系統中還具備"一等公民"的地位,能實現很是強大的操做。而在其餘環境(好比非託管c++)中,枚舉類型是沒有這個特色的。緩存
枚舉類型都直接從System.Enum派生,後者從System.ValueType派生,而System.ValueType用從 System.Object派生。因此,枚舉類型是值類型,可表示成未裝箱和已裝箱形式。然而,有別於其餘值類型,枚舉類型不能定義任何方法、屬性和事件。不過可利用C#的"擴展方法"功能模擬向枚舉類型添加方法。安全
編譯枚舉類型時,C#編譯器會把每一個符號轉換成爲類型的一個常量字段。例如,編譯器會把前面的Color枚舉類型當作如下代碼數據結構
internal struct Color : System.Enum { //如下是一些公共常量,它們定義了Color的符號和值 public const Color While = (Color)0; public const Color Red = (Color)1; public const Color Green = (Color)2; public const Color Bule = (Color)3; public const Color Orange = (Color)4; //如下是一個公共實例字段,它包含一個Color變量的值, //不能寫代碼來直接引用這個實例字段 public Int32 value__; }
C#編譯器實際上並不編譯這段代碼,由於它禁止定義從System.Enum這一特殊類型派生的類型。不過,能夠經過上述僞類型定義瞭解內部工做方式。簡單的說,枚舉類型只是一個結構,其中定義了一組常量字段和一個實例字段。常量字段會嵌入程序集的元數據中,並能夠經過反射來訪問。這意味着能夠在運行時得到與枚舉類型關聯的全部符號及其值。還意味着能夠將一個字符串符號轉換成對應的數值。這些操做是經過System.Enum基類型來提供的,該類型提供了幾個靜態和實例方法,可利用它們操做枚舉類型的一個實例,從而避免了必須使用反射的麻煩。app
提示:枚舉類型定義的符號是常量值。因此當編譯器發現代碼引用了一個枚舉類型的符號,就會在編譯時用數值替換符號,代碼將再也不引用定義了符號的枚舉類型。這意味着在運行時可能不須要定義了枚舉類型的程序集,在編譯時須要。ide
例如,System.Enum類型有一個GetUnderlyingType的靜態方法,而System.Type類型有一個 GetEnumUnderlyingType的實例方法。函數
public static Type GetUnderlyingType (Type enumType); //System.Enum中定義 public Type GetEnumUnderlyingType (Type enumType); //System.Type中定義
這些方法返回用於容納一個枚舉類型的值的基礎類型。每一個枚舉類型都有一個基礎類型,它能夠是byte,sbyte, short,ushort,int(最經常使用,也是C#默認的),uint,long,或ulong。雖然這些C#基元類型都有都有對象的FCL 類型,但C#編譯器爲了簡化自己的實現,要求只能指定基元類型名稱。若是使用FCL類型名稱(如Int32),就會報錯。
如下代碼演示瞭如何聲明一個基礎類型爲byte(System.Byte)的枚舉類型:
interal enum Color :byte { While, Red, Green, Blue, Orange }
基於這個Color枚舉類型,一下代碼顯示了GetUnderlyingType 的返回結果:
//如下代碼會顯示"System.Byte" Console.WriteLine(Enum.GetUnderlyingType(typeof(Color)));
C#編譯器將枚舉類型視爲基元類型。因此,能夠運用許多操做符(==,!=,<,>等等)來操做枚舉類型的實例。全部這些操做符實際做用於枚舉類型實例內部的value__實例字段。此外,C#編譯器還容許將枚舉類型的實例顯式轉型爲一個不一樣的枚舉類型。也能夠顯式將一個枚舉類型實例轉型爲一個數值類型。
給定一個枚舉類型的實例,能夠調用System.Enum的靜態方法GetValue或者System.Type的實例方法GetEnumValue獲取一個數組,該數組的每個元素都對應枚舉類型中的一個符號名稱,每一個元素都包含符號名稱的數值:
public static Array GetValues(Type enumType); //System.Enum中定義 public Array GetEnumValues(Type enumType); //System.Type中定義
這個方法結合ToString方法使用,可顯示枚舉類型中全部符號名稱及其對應的數值,以下所示:
Color[] colors = (Color[])Enum.GetValues(typeof(Color)); Console.WriteLine("Number of symbols defined: " + colors.Length); Console.WriteLine("Value\tSymbol\n-----\t------"); foreach (Color color in colors) { // 以十進制和常規格式顯示每一個符號 Console.WriteLine("{0,5:D}\t{0:G}", color);
以上代碼產生的輸出以下:
Number of symbols defined: 5 Value Symbol ----- ------ 0 While 1 Red 2 Green 3 Blue 4 Orange
程序員常常要與位標識(bit flag)集合打交道。調用System.IO.File類型的GetAttributes方法,會返回FileAttributes類型的一個實例。FileAttributes類型是基本類型爲Int32的枚舉類型,其中每一位都反映了文件的一項屬性。FileAttibutes類型在FCL中的定義以下:
[Flags] [Serializable] [ComVisible (true)] public enum FileAttributes { Archive = 0x00020, Compressed = 0x00800, Device = 0x00040, // Reserved for future use (NOT the w32 value). Directory = 0x00010, Encrypted = 0x04000, // NOT the w32 value Hidden = 0x00002, Normal = 0x00080, NotContentIndexed = 0x02000, Offline = 0x01000, ReadOnly = 0x00001, ReparsePoint = 0x00400, SparseFile = 0x00200, System = 0x00004, Temporary = 0x00100, #if NET_4_5 IntegrityStream = 0x8000, NoScrubData = 0x20000, #endif }
爲了判斷一個文件是否隱藏,可執行下面這樣的代碼:
String file = Assembly.GetEntryAssembly().Location; FileAttributes attributes = File.GetAttributes(file); Console.WriteLine("Is {0} hidden? {1}",file,(attributes & FileAttributes.Hidden) !=0);
如下代碼演示瞭如何將一個文件的屬性改成只讀和隱藏:
File.SetAttributes(file,FileAttributes.ReadOnly | FileAttribute.Hidden);
正如FileAttributes類型展現的那樣,常常都要用枚舉類型來表示一組能夠組合的位標誌。不過,雖然枚舉類型和位標誌類似,但它們的語義不盡相同。例如,枚舉類型表示單個數值,而位標識表示一組位,其中有些位是1,有些位是0.
定義用於標識位標誌的枚舉類型時,固然應該顯式爲每一個符號分配一個數值。一般,每一個符號都有單獨的一個位處於on(1)狀態.此外,常常都要定義一個值爲0的None符號。還能夠定義一些表明經常使用位組合的符號。另外,強烈建議向枚舉類型應用System.Flags.Attribute這個定製的attribute類型,以下所示
[Flags] public enum Actions { Read = 0x0001, Write = 0x0002, ReadWrite = Actions.Read | Actions.Write, Delete = 0x0004, Query = 0x0008, Sync = 0x0010 }
由於Actions是枚舉類型,因此在操做位標誌枚舉類型時,可使用上一節描述的全部方法。
Actions actions = Actions.Read | Actions.Delete; //0x0005 Console.WriteLine(actions.ToString()); //"Read,Delete"
調用ToString時,它會視圖將數值轉換爲對應的符號。如今的數值是0x0005,它沒有對應的符號。不過,ToString方法檢測到Actions類型上存在[Flags]這個attribute,因此ToString方法如今不會將該數值視爲單獨的值。相反,會將它視爲一組位標誌。因爲0x0005有0x0001和0x0004組合而成,因此ToString會生成字符串"Read,Delete", 若是從Actions類型中刪除[Flags]這個attribute,ToString方法返回"5"。
ToString方法啊容許以3種方式格式化輸出G(常規)、D(十進制)和X(十六進制)。使用常規格式來格式化枚舉類型的實例時,首先會檢查類型,看它是否應用了[Flags]這個特性。沒有應用就查找與該數值匹配的符號並返回符號。ToString方法的工做過程以下
1 獲取枚舉類型定義的數值集合,降序排列這些數值。
2 每一個數值都和枚舉實例中的值進行「按位與」計算(「&」,參與運算的兩數各對應的二進位相與。只有對應的兩個二進位都爲1時,結果位才爲1),假如結果等於數值,與該數值關聯的字符串就附加到輸出字符串上,對應的位會被認爲已經考慮過了,會被關閉(設爲0)。這一步不斷重複,知道檢查完全部數值,或知道枚舉實例的全部位都被關閉。
3 檢查完全部數值後,若是枚舉枚舉實例仍然不爲0,代表枚舉實例中一些處於on狀態的位不對應任何已定義的符號。在這種狀況下,ToString將枚舉實例中的原始值做爲字符串返回。
4 若是枚舉實例原始值不爲0,返回符號之間以逗號分隔的字符串。
5 若是枚舉實例原始值爲0,並且枚舉類型定義的一個符號對應的是0值,就返回這個符號。
6 若是到達這一步,就返回0
永遠不要對位標誌枚舉類型使用IsDefined方法,理由以下:
1)若是向IsDefined方法傳遞一個字符串,它不會將這個字符串拆分爲單獨的token來進行查找,而是視圖查找整個字符串,把它當作是包含逗號的一個更大的符號。因爲不能在枚舉類型中定義含有逗號的符號,因此這個符號永遠找不到。
2)若是向IsDefined方法傳遞一個數值,它會檢查枚舉類型是否認義了一個其對應數值和傳入數值匹配的符號。因爲位標誌不能這樣簡單匹配,因此IsDefined一般會返回flase。
如今,可使用C#的擴展方法功能向枚舉類型模擬添加方法。
若是想爲FileAttributes枚舉類型添加一些方法,能夠定義一個包含了擴展方法的靜態類,以下所示:
public static Boolean Set(this FileAttributes flags, FileAttributes testFlags) { return flags | testFlags; }
從表面看,我彷佛真的在枚舉類型上調用這些方法:
FileAttributes fa = FileAttributes.System; fa = fa.Set(FileAttributes.ReadOnly);
數組是容許將多個數據項做爲集合來處理的機制。clr支持一維、多維和交錯數組(即數組構成的數組)。全部數組類型都隱式地從System.Array抽象類派生,後者又派生自system.Object。這意味着數組始終是引用類型,是在託管堆上分配的。在應用程序的變量或字段中,包含的是對數組的引用,而不是包含數組自己的元素。以下例子
Int32[] myIntegers; //聲明一個數組引用 myIntegers = new int32[100] //建立含有100個Int32的數組
在第一行代碼中,myIntegers變量能指向一個一維數組(由Int32值構成)。myIntegers剛開始被設爲null,由於當時尚未分配數組。第二行代碼分配了含有100個Int32值的一個數組,全部Int32都被初始化爲0。因爲數組是引用類型,全部託管堆上還包含一個未裝箱Int32所須要的內存塊。實際上,除了數組元素,數字對象佔據的內存塊還包含一個類型對象指針、一個同步塊索引和一些額外的成員(overhead)。該數組的內存塊地址被返回並保存到myIntegers變量中。
爲了符common Language Specification,CLS 的要求,全部數組都必須是0基數組(即最小索引爲0)。這樣就能夠用C#的方法建立數組,並將該數組的引用傳給其餘語言。此外,因爲0基數組是最經常使用的數組,因此Microsoft花了很大力氣優化性能。
每一個數組都關聯了一些額外的開銷信息。這些信息包括數組的秩、數組每一維的下限和每一維的長度。開銷信息還包含數組元素類型。
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" };
大括號中的以逗號分隔的數據項稱爲數組初始化器(array initializer)。每一個數據項均可以是一個任意複雜度的表達式;在多維數組的狀況下,則能夠是一個嵌套的數組初始化器。可利用C#的「隱式類型的局部變量」的數組功能來簡化代碼:
//利用c#的隱式類型的局部變量公告呢 var names = new string[] { "Aidan", "Grant", null};
編譯器推斷局部變量names是string[]類型,由於那是賦值操做符(=)右側的表達式的類型。
可利用c#的隱式類型的數組功能讓編譯器推斷數組元素的類型。注意,下面這行代碼沒有在new和[]指定類型
//利用c#的隱式類型的局部變量和隱式類型的數組功能 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
能夠調用數組的靜態CreateInstance方法來動態建立本身的數組。該方法有若干個重載版本,容許指定數組元素的類型、數組的維數、每一維的下限和每一維的元素數目。CreateInstance爲數組分配內存,將參數信息保存到數組的內存塊的額外開銷(overhead)部分。而後返回對該數組的一個引用。
對於元素爲引用類型的數組,CLR容許將數組元素從一種類型隱式轉型到另外一種類型。爲了成功轉型,兩個數組類型必須維數相等,並且從源類型到目標類型,必須存在一個隱式或顯示轉換。CLR不容許將值類型元素的數組轉型爲其餘任何類型。(不過爲了模擬實現這種效果,可利用Array.Copy方法建立一個新數組並在其中填充數據)。下面演示了數組轉型過程:
// 建立一個二維FileStream數組 FileStream[,] fs2dim = new FileStream[5, 10]; // 隱式轉型爲一個二維Object數組 Object[,] o2dim = fs2dim; // 不能從二維數組轉型爲一維數組 //Stream[] s1dim = (Stream[]) o2dim; // 顯式轉型爲二維Stream數組 Stream[,] s2dim = (Stream[,]) o2dim; // 顯式轉型爲二維String數組 // 能經過編譯,但在運行時會拋出異常 String[,] st2dim = (String[,]) o2dim; // 建立一個意味Int32數組(元素是值類型) Int32[] i1dim = new Int32[5]; // 不能將值類型的數組轉型爲其餘任何類型 // Object[] o1dim = (Object[]) i1dim; // 建立一個新數組,使用Array.Copy將元數組中的每個元素 // 轉型爲目標數組中的元素類型,並把它們複製過去 // 下面的代碼建立一個元素爲引用類型的數組, // 每一個元素都是對已裝箱的Int32的引用 Object[] o1dim = new Object[i1dim.Length]; Array.Copy(i1dim, o1dim, 0);
Array.Copy方法的做用不只僅是將元素從一個數組複製到另外一個數組。Copy方法還能正確處理內存的重疊區域。 Copy方法還能在複製每個數組元素時進行必要的類型轉換。Copy方法能執行如下轉換:
1)將值類型的元素裝箱爲引用類型的元素,好比將一個Int32[]複製到一個Object[]中。
2)將引用類型的元素拆箱爲值類型的元素,好比將一個Object[]複製到Int32[]中。
3)加寬CLR基元值類型,好比將一個Int32[]的元素複製到一個Double[]中。
4) 在兩個數組之間複製時,若是僅從數組類型證實不了二者的兼容性,好比從Object[]轉型爲IFormattable[],就根據須要對元素進行鄉下類型轉換。若是Object[]中的每一個對象都實現了IFormattable,Copy方法就能執行成功。
有時確實須要將數組從一種類型轉換爲另外一種類型。這種功能稱爲數據協變性。利用數組協變性時,應該清楚由此帶來的性能損失。
String[] sa =new stirng [100]; Object[] oa=sa; oa[5]=」JEFF」;//性能損失:clr檢查oa的元素類型是否是string。檢查經過 oa[3]=5;// 性能損失:clr檢查oa的元素類型是否是string。發現有錯,拋出異常
注意:若是隻須要把數組中某些元素複製到另外一個數組,能夠選擇System.Buffer的BlockCopy方法,它的執行速度比Array.Copy方法快。不過,Buffer的BlockCopy方法只支持基元類型,不提供像Array的Copy方法那樣的轉型能力。方法的Int32參數表明的是數組中的字節偏移量,而非元素索引。若是須要可靠的將一個數組中的元素複製到另外一個數組,應該使用System.Array的ConstrainedCopy方法,該方法能保證不破壞目標數組中的數組的前提下完成複製,或者拋出異常。另外,它不執行任何裝箱、拆箱或向下類型轉換。
若是像下面這樣聲明一個數組變量:
FileStream[] fsArray;
CLR會爲AppDomain自動建立一個FileStream[]類型。這個類型將隱式派生自System.Array類型;所以,System.Array類型定義的全部實例方法和屬性都將有FileStream[]繼承,使這些方法和屬性能經過fsArray變量調用。
許多方法都能操做各類集合對象,由於他們聲明爲容許獲取IEnumerable,ICollection和IList等參數。可將數組傳給這些方法,由於System.Array也實現了這三個接口。System.Array之因此實現這些非泛型接口,是由於這些接口將全部元素都視爲Systm.Object。然而,最好讓System.Array實現這個接口的泛型形式,提供更好的編譯時類型安全性和更好的性能。
不過,因爲涉及多維數組和非0基數組的問題,clr團隊不但願system.Array實現IEnumerable<T>,ICollection<t>和IList<T>。若在System.Array上定義這些接口,就會爲全部數組類型啓用這些接口。因此,clr沒有那麼作,而是耍了一個小花招;建立一維0基數組類型時,clr自動使數組類型實現IEnumerable<T>,ICollection<t>和IList<T>。同時,還爲數組類型的全部基類型實現這三個接口,只要他們是引用類型。
若是數組包含值類型的元素,數組類型不會爲元素的基類型實現接口。這是由於值類型的數組在內存中的佈局與引用類型不一樣。
數組做爲實參傳給一個方法時,實際傳遞的是對該數組的引用。所以,被調用的方法能修改數組中的元素。若是不想被修改,必須生成數組的一個拷貝,並將這個拷貝傳給方法。注意,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]執行操做 } }
將方法設計爲返回對含有0個元素的一個數組的引用,而不是返回null,該方法的調用者就能更清爽地使用該方法。順便提一句,對字段也應如此。若是類型中有一個字段是數組引用,應考慮讓這個字段始終引用數組,即便數組中不包含任何元素。
CLR內部實際支持兩種不一樣的數組
1)下限爲0的數組。這些數組有時稱爲SZ數組或向量。
2)下限未知的一維或多維數組。
可執行一下代碼來實際地查看不一樣種類的輸出
public static void Main() { 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編譯器生成的代碼假定數組是0基的,因此在訪問元素時不須要從指定索引中減去一個偏移量。其次,通常狀況下,jit編譯器能將索引範圍檢查代碼從循環中拿出,致使它只執行一次。
其次,JIT編譯器知道for循環要訪問0到Length-1之間的數組元素。因此,JIT編譯器生成的代碼會在運行時測試全部數組元素的訪問都在數組有效訪問內,這個檢查會在循環以前發生。而非零基數組,檢查會在循環內部執行。
若是很關心性能,請考慮由數組構成的數組(即交錯數組)來替代矩形數組。
c#和clr還容許使用unsafe(不可驗證)代碼訪問數組,這種技術實際能在訪問數組時關閉索引上下限檢查。這種不安全的數組訪問技術適合不少值類型數組。這個功能很強大,可是使用需謹慎,由於它容許直接內存訪問,訪問越界不會拋出異常。
下面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語句來完成的(它很大程度上相似於c的alloca函數)。stackalloc語句只能建立一維0基、由值類型元素構成的數組,並且值類型絕對不能包含任何引用類型的字段。實際上,應該把它的做用當作是分配一個內存塊,這個內存塊可使用不安全的指針來操縱。因此,不能將這個內存緩存區的地址傳給大部分FCL方法。固然,在棧上分配的內存(數組)會在方法返回時自動釋放:這對加強性能起到必定做用。使用這個功能要求爲c#編譯器指定/unsafe開關。
如下代碼顯示如何使用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。
內聯(內嵌)數組經常使用於和非託管代碼進行互操做,並且非託管數據結構也有一個內聯數組。不過,也可用於其餘狀況。