.NET基礎知識快速通關(6)

語言: CN / TW / HK

【.NET 總結 /Edison Zhou

此係列文章為我在2015年釋出於部落格園的 .NET基礎拾遺系列 ,它十分適合初中級.NET開發工程師在面試前進行一個系統的複習,因此我將其搬到公眾號分享與你。

本文為第六篇,我們會對.NET的集合與泛型相關考點進行基礎複習,全文會以Q/A的形式展現,即以面試題的形式來描述。

1 int[]是值型別還是應用型別?

在.NET中的陣列型別和C++中區別很大, .NET中無論是儲存值型別物件的陣列還是儲存引用型別的陣列,其本身都是引用型別,其記憶體也都是分配在堆上的 。它們的共同特徵在於:所有的陣列型別都繼承自System.Array,而System.Array又實現了多個介面,並且直接繼承自System.Object。不同之處則在於儲存值型別物件的陣列所有的值都已經包含在陣列內,而儲存引用型別物件的陣列,其值則是一個引用,指向位於託管堆中的例項物件。

下圖直觀地展示了二者記憶體分配的差別(假設object[]中儲存都是DateTime型別的物件例項):

在.NET中CLR會檢測所有對陣列的訪問,任何檢視訪問陣列邊界以外的程式碼都會產生一個IndexOutOfRangeException異常。

2 你知道陣列之間如何轉換的嗎?

陣列型別的轉換需要遵循以下兩個原則:

(1)包含值型別的陣列不能被隱式轉換成其他任何型別;

(2)兩個陣列型別能夠相互轉換的一個前提是兩者維數相同;

我們可以通過以下程式碼來看看陣列型別轉換的機制:

// 編譯成功
string[] sz = { "a", "a", "a" };
object[] oz = sz;
// 編譯失敗,值型別的陣列不能被轉換
int[] sz2 = { 1, 2, 3 };
object[] oz2 = sz;
// 編譯失敗,兩者維數不同
string[,] sz3 = { { "a", "b" }, { "a", "c" } };
object[] oz3 = sz3;

除了型別上的轉換,我們平時還可能會遇到內容轉換的需求。例如,在一系列的使用者介面操作之後,系統的後臺可能會得到一個DateTime的陣列,而現在的任務則是將它們儲存到資料庫中,而資料庫訪問層提供的介面只接受String[]引數,這時我們要做的就是把DateTime[]從內容上轉換為String[]物件。當然,慣常做法是遍歷整個源陣列,逐一地轉換每個物件並且將其放入一個目標陣列型別容器中,最後再生成目標陣列。But,這裡我們推薦使用Array.ConvertAll方法,它提供了一個簡便的轉換陣列間內容的介面,我們只需指定源陣列的型別、物件陣列的型別和具體的轉換演算法,該方法就能高效地完成轉換工作。

下面的程式碼清楚地展示了普通的陣列內容轉換方式和使用Array.ConvertAll的陣列內容轉換方式的區別:

public class Program
{
public static void Main(string[] args)
{
String[] times ={"2008-1-1",
"2008-1-2",
"2008-1-3"};


// 使用不同的方法轉換
DateTime[] result1 = OneByOne(times);
DateTime[] result2 = ConvertAll(times);


// 結果是相同的
Console.WriteLine("手動逐個轉換的方法:");
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();
}


// 逐個手動轉換
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);
}
}

從上述程式碼可以看出,二者實現了相同的功能,但是Array.ConvertAll不需要我們手動地遍歷陣列,也不需要生成一個臨時的容器物件,更突出的優勢是它可以接受一個動態的演算法作為具體的轉換邏輯。當然,明眼人一看就知道,它是以一個委託的形式作為引數傳入,這樣的機制保證了Array.ConvertAll具有較高的靈活性。

3 能說說泛型的基本原理嗎?

泛型的語法和概念類似於C++中的template(模板),它是.NET 2.0中推出的眾多特性中最為重要的一個, 方便我們設計更加通用的型別,也避免了容器操作中的裝箱和拆箱操作

假如我們要實現一個排序演算法,要求能夠針對各種型別進行排序。按照以前的做法,我們需要對int、double、float等型別都實現一次,但是我們發現除了資料型別,其他的處理邏輯完全一致。這時,我們便可以考慮使用泛型來進行實現:

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--)
{
// 對兩個元素進行交換
if (array[j].CompareTo(array[j - 1]) < 0)
{
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}

Tips:Microsoft在產品文件中建議所有的泛型引數名稱都以T開頭,作為一箇中編碼的通用規範,建議大家都能遵守這樣的規範,類似的規範還有所有的介面都以I開頭。

泛型型別和普通型別有一定的區別,通常泛型型別被稱為開放式型別,.NET中規定開放式型別不能例項化,這樣也就確保了開放式型別的泛型引數在被指定前,不會被例項化成任何物件(事實上,.NET也沒有辦法確定到底要分配多少記憶體給開放式型別)。為開放式的型別提供泛型的例項導致了一個新的封閉型別的生成,但這並不代表新的封閉型別和開放型別有任何繼承關係,它們在類結構圖上是處於同一層次,並且兩者之間沒有任何關係。下圖展示了這一概念:

此外,在.NET中的System.Collections.Generic名稱空間下提供了諸如List<T>、Dictionary<T>、LinkedList<T>等泛型資料結構,並且在System.Array中定義了一些靜態的泛型方法, 我們應該在編碼實踐時充分使用這些泛型容器,以提高我們的開發和系統的執行效率

4 泛型的主要約束和次要約束是什麼?

當一個泛型引數沒有任何約束時,它可以進行的操作和運算是非常有限的,因為不能對實參進行任何型別上的保證,這時候就需要用到泛型約束。泛型的約束分為:主要約束和次要約束,它們都使實參必須滿足一定的規範,C#編譯器在編譯的過程中可以根據約束來檢查所有泛型型別的實參並確保其滿足約束條件。

(1)主要約束

一個泛型引數至多擁有一個主要約束,主要約束可以是一個引用型別、class或者struct。如果指定一個引用型別(class),那麼實參必須是該型別或者該型別的派生型別。相反,struct則規定了實參必須是一個值型別。下面的程式碼展示了泛型引數主要約束:

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是引用型別,可以置null
myT = null;
}
}


public class ClassT3<T> where T : struct
{
private T myT;
public override string ToString()
{
// T是值型別,不會發生NullReferenceException異常
return myT.ToString();
}
}

泛型引數有了主要約束後,也就能夠在型別中對其進行一定的操作了。

(2)次要約束

次要約束主要是指實參實現的介面的限定。對於一個泛型,可以有0到無限的次要約束,次要約束規定了實參必須實現所有的次要約束中規定的介面。次要約束與主要約束的語法基本一致,區別僅在於提供的不是一個引用型別而是一個或多個介面。例如我們為上面程式碼中的ClassT3增加一個次要約束:

public class ClassT3<T> where T : struct, IComparable
{
......
}

End 總結

本文總結複習了.NET的集合與泛型處理相關的重要知識點,下一篇會總結.NET中流與序列化處理相關的重要知識點,歡迎繼續關注!

參考資料(全是經典)

朱毅 ,《進入IT企業必讀的200個.NET面試題》

張子陽,《.NET之美:.NET關鍵技術深入解析》

王濤,《你必須知道的.NET》

:point_down:掃碼關注EdisonTalk