面試 | .NET基礎(chǔ)知識快速通關(guān)!
此系列文章為我在2015年發(fā)布于博客園的.NET基礎(chǔ)拾遺系列,它十分適合初中級.NET開發(fā)工程師在面試前進(jìn)行一個系統(tǒng)的復(fù)習(xí),因此我將其搬到公眾號分享與你。
本文為第六篇,我們會對.NET的集合與泛型相關(guān)考點(diǎn)進(jìn)行基礎(chǔ)復(fù)習(xí),全文會以Q/A的形式展現(xiàn),即以面試題的形式來描述。
在.NET中的數(shù)組類型和C++中區(qū)別很大,.NET中無論是存儲值類型對象的數(shù)組還是存儲引用類型的數(shù)組,其本身都是引用類型,其內(nèi)存也都是分配在堆上的。它們的共同特征在于:所有的數(shù)組類型都繼承自System.Array,而System.Array又實(shí)現(xiàn)了多個接口,并且直接繼承自System.Object。不同之處則在于存儲值類型對象的數(shù)組所有的值都已經(jīng)包含在數(shù)組內(nèi),而存儲引用類型對象的數(shù)組,其值則是一個引用,指向位于托管堆中的實(shí)例對象。
下圖直觀地展示了二者內(nèi)存分配的差別(假設(shè)object[]中存儲都是DateTime類型的對象實(shí)例):

在.NET中CLR會檢測所有對數(shù)組的訪問,任何視圖訪問數(shù)組邊界以外的代碼都會產(chǎn)生一個IndexOutOfRangeException異常。
數(shù)組類型的轉(zhuǎn)換需要遵循以下兩個原則:
(1)包含值類型的數(shù)組不能被隱式轉(zhuǎn)換成其他任何類型;
(2)兩個數(shù)組類型能夠相互轉(zhuǎn)換的一個前提是兩者維數(shù)相同;
我們可以通過以下代碼來看看數(shù)組類型轉(zhuǎn)換的機(jī)制:
// 編譯成功string[] sz = { "a", "a", "a" };object[] oz = sz;// 編譯失敗,值類型的數(shù)組不能被轉(zhuǎn)換int[] sz2 = { 1, 2, 3 };object[] oz2 = sz;// 編譯失敗,兩者維數(shù)不同string[,] sz3 = { { "a", "b" }, { "a", "c" } };object[] oz3 = sz3;
除了類型上的轉(zhuǎn)換,我們平時還可能會遇到內(nèi)容轉(zhuǎn)換的需求。例如,在一系列的用戶界面操作之后,系統(tǒng)的后臺可能會得到一個DateTime的數(shù)組,而現(xiàn)在的任務(wù)則是將它們存儲到數(shù)據(jù)庫中,而數(shù)據(jù)庫訪問層提供的接口只接受String[]參數(shù),這時我們要做的就是把DateTime[]從內(nèi)容上轉(zhuǎn)換為String[]對象。當(dāng)然,慣常做法是遍歷整個源數(shù)組,逐一地轉(zhuǎn)換每個對象并且將其放入一個目標(biāo)數(shù)組類型容器中,最后再生成目標(biāo)數(shù)組。But,這里我們推薦使用Array.ConvertAll方法,它提供了一個簡便的轉(zhuǎn)換數(shù)組間內(nèi)容的接口,我們只需指定源數(shù)組的類型、對象數(shù)組的類型和具體的轉(zhuǎn)換算法,該方法就能高效地完成轉(zhuǎn)換工作。
下面的代碼清楚地展示了普通的數(shù)組內(nèi)容轉(zhuǎn)換方式和使用Array.ConvertAll的數(shù)組內(nèi)容轉(zhuǎn)換方式的區(qū)別:
public class Program{public static void Main(string[] args){String[] times ={"2008-1-1","2008-1-2","2008-1-3"};// 使用不同的方法轉(zhuǎn)換DateTime[] result1 = OneByOne(times);DateTime[] result2 = ConvertAll(times);// 結(jié)果是相同的Console.WriteLine("手動逐個轉(zhuǎn)換的方法:");foreach (DateTime item in result1){Console.WriteLine(item.ToString("yyyy-MM-dd"));}Console.WriteLine("使用Array.Convert方法:");foreach (DateTime item2 in result2){Console.WriteLine(item2.ToString("yyyy-MM-dd"));}Console.ReadKey();}// 逐個手動轉(zhuǎn)換private static DateTime[] OneByOne(String[] times){List<DateTime> result = new List<DateTime>();foreach (String item in times){result.Add(DateTime.Parse(item));}return result.ToArray();}// 使用Array.ConertAll方法private static DateTime[] ConvertAll(String[] times){return Array.ConvertAll(times,new Converter<String, DateTime>(DateTimeToString));}private static DateTime DateTimeToString(String time){return DateTime.Parse(time);}}
從上述代碼可以看出,二者實(shí)現(xiàn)了相同的功能,但是Array.ConvertAll不需要我們手動地遍歷數(shù)組,也不需要生成一個臨時的容器對象,更突出的優(yōu)勢是它可以接受一個動態(tài)的算法作為具體的轉(zhuǎn)換邏輯。當(dāng)然,明眼人一看就知道,它是以一個委托的形式作為參數(shù)傳入,這樣的機(jī)制保證了Array.ConvertAll具有較高的靈活性。
泛型的語法和概念類似于C++中的template(模板),它是.NET 2.0中推出的眾多特性中最為重要的一個,方便我們設(shè)計(jì)更加通用的類型,也避免了容器操作中的裝箱和拆箱操作。
假如我們要實(shí)現(xiàn)一個排序算法,要求能夠針對各種類型進(jìn)行排序。按照以前的做法,我們需要對int、double、float等類型都實(shí)現(xiàn)一次,但是我們發(fā)現(xiàn)除了數(shù)據(jù)類型,其他的處理邏輯完全一致。這時,我們便可以考慮使用泛型來進(jìn)行實(shí)現(xiàn):
public static class SortHelper<T> where T : IComparable{public static void BubbleSort(T[] array){int length = array.Length;for (int i = 0; i <= length - 2; i++){for (int j = length - 1; j >= 1; j--){// 對兩個元素進(jìn)行交換if (array[j].CompareTo(array[j - 1]) < 0){T temp = array[j];array[j] = array[j - 1];array[j - 1] = temp;}}}}}
Tips:Microsoft在產(chǎn)品文檔中建議所有的泛型參數(shù)名稱都以T開頭,作為一個中編碼的通用規(guī)范,建議大家都能遵守這樣的規(guī)范,類似的規(guī)范還有所有的接口都以I開頭。
泛型類型和普通類型有一定的區(qū)別,通常泛型類型被稱為開放式類型,.NET中規(guī)定開放式類型不能實(shí)例化,這樣也就確保了開放式類型的泛型參數(shù)在被指定前,不會被實(shí)例化成任何對象(事實(shí)上,.NET也沒有辦法確定到底要分配多少內(nèi)存給開放式類型)。為開放式的類型提供泛型的實(shí)例導(dǎo)致了一個新的封閉類型的生成,但這并不代表新的封閉類型和開放類型有任何繼承關(guān)系,它們在類結(jié)構(gòu)圖上是處于同一層次,并且兩者之間沒有任何關(guān)系。下圖展示了這一概念:

此外,在.NET中的System.Collections.Generic命名空間下提供了諸如List<T>、Dictionary<T>、LinkedList<T>等泛型數(shù)據(jù)結(jié)構(gòu),并且在System.Array中定義了一些靜態(tài)的泛型方法,我們應(yīng)該在編碼實(shí)踐時充分使用這些泛型容器,以提高我們的開發(fā)和系統(tǒng)的運(yùn)行效率。
當(dāng)一個泛型參數(shù)沒有任何約束時,它可以進(jìn)行的操作和運(yùn)算是非常有限的,因?yàn)椴荒軐?shí)參進(jìn)行任何類型上的保證,這時候就需要用到泛型約束。泛型的約束分為:主要約束和次要約束,它們都使實(shí)參必須滿足一定的規(guī)范,C#編譯器在編譯的過程中可以根據(jù)約束來檢查所有泛型類型的實(shí)參并確保其滿足約束條件。
(1)主要約束
一個泛型參數(shù)至多擁有一個主要約束,主要約束可以是一個引用類型、class或者struct。如果指定一個引用類型(class),那么實(shí)參必須是該類型或者該類型的派生類型。相反,struct則規(guī)定了實(shí)參必須是一個值類型。下面的代碼展示了泛型參數(shù)主要約束:
public class ClassT1<T> where T : Exception{private T myException;public ClassT1(T t){myException = t;}public override string ToString(){// 主要約束保證了myException擁有source成員return myException.Source;}}public class ClassT2<T> where T : class{private T myT;public void Clear(){// T是引用類型,可以置nullmyT = null;}}public class ClassT3<T> where T : struct{private T myT;public override string ToString(){// T是值類型,不會發(fā)生NullReferenceException異常return myT.ToString();}}
泛型參數(shù)有了主要約束后,也就能夠在類型中對其進(jìn)行一定的操作了。
(2)次要約束
次要約束主要是指實(shí)參實(shí)現(xiàn)的接口的限定。對于一個泛型,可以有0到無限的次要約束,次要約束規(guī)定了實(shí)參必須實(shí)現(xiàn)所有的次要約束中規(guī)定的接口。次要約束與主要約束的語法基本一致,區(qū)別僅在于提供的不是一個引用類型而是一個或多個接口。例如我們?yōu)樯厦娲a中的ClassT3增加一個次要約束:
public class ClassT3<T> where T : struct, IComparable{......}
本文總結(jié)復(fù)習(xí)了.NET的集合與泛型處理相關(guān)的重要知識點(diǎn),下一篇會總結(jié).NET中流與序列化處理相關(guān)的重要知識點(diǎn),歡迎繼續(xù)關(guān)注!


谷歌靈魂插件,98%的程序員都好評!

再見Vip,免費(fèi)看網(wǎng)飛影視大片!
