.NET面試題系列[8] - 泛型

泛型相比反射,委託等較爲抽象的概念要更接地氣得多,並且在日常工做時,咱們幾乎時刻都和泛型有接觸。大部分人對泛型都是比較熟悉的。面試

泛型集合是類型安全的集合。相對於泛型System.Collections.Generic,咱們有類型不安全的集合System.Collections,其中的成員均爲Object類型。一個經典的例子是ArrayList。安全

在使用ArrayList時,咱們能夠插入任意類型的數據,若是插入值類型的數據,其都會裝箱爲Object類型。這形成類型不安全,咱們不知道取出的數據是否是想要的類型。泛型(集合)的數據類型是統一的,是類型安全的,沒有裝箱和拆箱問題,提供了更好的性能。爲泛型變量設置默認值時常使用default關鍵字進行:T temp = default(T)。若是T爲引用類型,則temp爲null,若是T爲值類型,則temp爲0。框架

ArrayList的泛型集合版本爲List<T>。T稱爲類型參數。調用時指定的具體類型叫作實際參數(實參)。less

面試必須知道的泛型三大好處:類型安全,加強性能,代碼複用。函數

泛型集合的使用契機:幾乎任什麼時候候,都不考慮不用泛型集合代替泛型集合。不少非泛型集合也有了本身的泛型版本,例如棧,隊列等。性能

泛型方法

泛型方法的使用契機通常爲傳入類型可能有不少種,但處理方式卻相同的情境。這時咱們能夠不須要寫不少個重載,而考慮用泛型方法達到代碼複用的目的。配合泛型約束,能夠寫出更嚴謹的方法。泛型委託也能夠當作是泛型方法的一種應用。this

例如交換兩個同類型變量的值:spa

static void Swap<T>(ref T lhs, ref T rhs)
{
    T temp;
    temp = lhs;
    lhs = rhs;
    rhs = temp;
}

泛型約束

約束的做用是限制能指定成泛型實參(即T的具體類型)的數量。經過限制類型的數量,能夠對這些類型執行更多的操做。例以下面的方法,T被約束爲必須是實現了IComparable接口的類型。此時,傳入的T除了擁有object類型的方法以外,還額外多了一個CompareTo方法。因爲保證了傳入的T必須是實現了IComparable接口的類型,就能夠確定T類型必定含有CompareTo方法。若是去掉約束,o1是沒有CompareTo方法的。設計

static int Compare<T>(T o1, T o2) where T : IComparable<T>
{
     return o1.CompareTo(o2);
}

此時若是將object類型的數據傳入方法,則會報錯。由於object沒有實現IComparable<T>接口。

泛型約束分爲以下幾類:

  • 接口約束:泛型實參必須實現某個接口。接口約束能夠有多個。
  • 基類型約束:泛型實參必須是某個基類的派生類。特別的,能夠指定T : class / T : struct,此時T分別只能爲引用類型或值類型。基類型約束必須放在其餘約束以前。
  • 構造函數new()約束:泛型實參必須具備可訪問的無參數構造函數(默認的也可)。new()約束出如今where子句的最後。

若是泛型方法沒有任何約束,則傳入的對象會被視爲object。它們的功能比較有限。不能使用 != 和 == 運算符,由於沒法保證具體類型參數能支持這些運算符。

協變和逆變

可變性是以一種類型安全的方式,將一個對象做爲另外一個對象來使用。其對應的術語則是不變性(invariant)。

可變性

可變性是以一種類型安全的方式,將一個對象做爲另外一個對象來使用。例如對普通繼承中的可變性:若某方法聲明返回類型爲Stream,在實現時能夠返回一個MemoryStream。可變性有兩種類型:協變和逆變。

協變性:能夠創建一個較爲通常類型的變量,而後爲其賦值,值是一個較爲特殊類型的變量。例如:

string str = "test";
// An object of a more derived type is assigned to an object of a less derived type. 
object obj = str;

由於string確定是一個object,因此這樣的變化很是正常。 

逆變性:在上面的例子中,咱們沒法將str和一個新的object對象畫等號。若是強行要實現的話,只能這麼幹:

string s = (string) new object();

但這樣仍是會在運行時出錯。這也告訴咱們,逆變性是很不正常的。

 

泛型的協變與逆變

協變性和out關鍵字搭配使用,用於向調用者返回某項操做的值。例以下面的接口僅有一個方法,就是生產一個T類型的實例。那麼咱們能夠傳入一個特定類型。如咱們能夠將IFactory<Pizza>視爲IFactory<Food>。這也適用於Food的全部子類型。(即將其視爲一個更通常類型的實現)

    interface IFactory<T>
    {
        T CreateInstance();
    }

逆變性則相反,in關鍵字搭配使用,指的是API將會消費值,而不是生產值。此時通常類型出如今參數中:

    interface IPrint<T>
    {
        void Print(T value);
    }

這意味着若是咱們實現了IPrint<Code>,咱們就能夠將其當作IPrint<CsharpCode>使用。(即將其視爲一個更具體類型的實現)

若是存在雙向的傳遞,則什麼也不會發生。這種類型是不變體(invariant)。

     interface IStorage<T>
    {
        byte[] Serialize(T value);
        T Deserialize(byte[] data);
    }

這個接口是不變體。咱們不能將它視爲一個更具體或更通常類型的實現。

假設有以下繼承關係People –> Teacher,People –> Student。

若是咱們以協變的方式使用(假設你創建了一個IStorage< Teacher >的實例,並將其視爲IStorage<People>)則咱們可能會在調用Serialize時產生異常,由於Serialize方法不支持協變(若是參數是People的其餘子類,例如Student,則IStorage< Teacher >將沒法序列化Student)。

若是咱們以逆變的方式使用(假設你創建了一個IStorage<People>的實例,並將其視爲IStorage< Teacher >),則咱們可能會在調用Deserialize時產生異常,由於Deserialize方法不支持逆變,它只能返回People不能返回Teacher。

使用in和out表示可變性

若是類型參數用於輸出,就使用out,若是用於輸入,就使用in。注意,協變和逆變性體如今泛型類T和T的派生類。目前out 和in 關鍵字只能在接口和委託中使用。

IEnumerable<out T>支持協變性

IEnumerable<T>支持協變性,它容許一個相似下面簽名

void 方法(IEnumerable<T> anIEnumberable)

的方法,該方法傳入更具體的類型(T的派生類),但在方法內部,類型會被當作IEnumerable<T>。注意out關鍵字。

下面的例子演示了協變性。咱們利用IEnumerable<T>的協變性,傳入較爲具體的類型Circle。編譯器會將其當作較爲抽象的類型Shape。

    public class Program
    {
        public static void Main(string[] args)
        {
            var circles = new List<Circle>
            {
                new Circle(new Point(0, 0), 15),
                new Circle(new Point(10, 5), 20),
            };
            var list = new List<IShape>();

            //泛型的協變:
            //AddRange傳入的是特殊的類型List<Circle>,但要求是通常的類型List<IShape>
            //AddRange方法簽名:void AddRange(IEnumerable<T> collection)
            //IEnumerable<out T>容許協變(對於LINQ來講,協變尤爲重要,由於不少API都表示爲IEnumerable<T>)
            list.AddRange(circles);

            //C# 4.0以前只能這麼作
            list.AddRange(circles.Cast<IShape>());
        }
    }

    public sealed class Circle : IShape
    {
        private readonly Point center;
        public Point Center { get { return center; } }

        private readonly double radius;
        public double Radius { get { return radius; } }

        public Circle(Point center, int radius)
        {
            this.center = center;
            this.radius = radius;
        }

        public double Area
        {
            get { return Math.PI * radius * radius; }
        }
    }

    public interface IShape
    {
        double Area { get; }
    }

IComparer<in T>支持逆變性

IComparer支持逆變性。咱們能夠簡單的實現一個能夠比較任何圖形面積的方法,傳入的輸入類型(in是最General的類型IShape。以後,在使用時,咱們得到的結果是較爲具體的類型Circle。由於任何圖形均可以比較面積,圓形固然也能夠。

注意IComparer的簽名是public interface IComparer<in T>。

    public class Program
    {
        public static void Main(string[] args)
        {
            var circles = new List<Circle>
            {
                new Circle(new Point(0, 0), 15),
                new Circle(new Point(10, 5), 20),
            };

            //泛型的逆變:
            //AreaComparer能夠比較任意圖形的面積,但咱們能夠傳入具體的圖形例如圓或正方形
                //Compare方法簽名:Compare(IShape x, IShape y)
            //IComparer<in T>支持逆變
            //傳入的是圓形Circle,但要求的輸入是IShape
            circles.Sort(new AreaComparer());
        }
    }

    class AreaComparer : IComparer<IShape>
    {
        public int Compare(IShape x, IShape y)
        {
            return x.Area.CompareTo(y.Area);
        }
    }

C#中泛型可變性的限制

1. 不支持類的類型參數的可變性。只有接口和委託能夠擁有可變的類型參數。in out 修飾符只能用來修飾泛型接口和泛型委託。

2. 可變性只支持引用轉換。可變性只能用於引用類型,禁止任何值類型和用戶定義的轉換,以下面的轉換是無效的:

  • 將 IEnumerable<int> 轉換爲 IEnumerable<object> ——裝箱轉換
  • 將 IEnumerable<short> 轉換爲 IEnumerable<int> ——值類型轉換
  • 將 IEnumerable<string> 轉換爲 IEnumerable<XName> ——用戶定義的轉換

3. 類型參數使用了 out 或者 ref 將禁止可變性。對於泛型類型參數來講,若是要將該類型的實參傳給使用 out 或者 ref 關鍵字的方法,便不容許可變性,如:

delegate void someDelegate<in T>(ref T t)

這段代碼編譯器會報錯。

4. 可變性必須顯式指定。從實現上來講編譯器徹底能夠本身判斷哪些泛型參數可以逆變和協變,但實際卻沒有這麼作,這是由於C#的開發團隊認爲:必須由開發者明確的指定可變性,由於這會促使開發者考慮他們的行爲將會帶來什麼後果,從而思考他們的設計是否合理。

5. 多播委託與可變性不能混用。下面的代碼可以經過編譯,可是在運行時會拋出 ArgumentException 異常:

Func<string> stringFunc = () => "";
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + stringFunc;

這是由於負責連接多個委託的 Delegate.Combine方法要求參數必須爲相同的類型,而上面的兩個泛型委託的輸出一個爲字符串,另外一個爲object。上面的示例咱們能夠修改爲以下正確的代碼:

Func<string> stringFunc = () => "";
Func<object> defensiveCopy = new Func<object>(stringFunc);
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + defensiveCopy;

此時兩個泛型委託的輸出均爲object。

 

協變與逆變的相互做用

如下的代碼中,接口IBar中有一個方法,其接受另外一個接口IFoo做爲參數。IFoo是支持協變的。這樣會出現一個問題。

    interface IFoo<in T>
    {

    }

    interface IBar<in T>
    {
        void Test(IFoo<T> foo);
    }

假設T爲字符串類型。則若是有一類Bar <T>: IBar<T>,另外一類Foo<T>:IFoo<T>,則Bar的某個實例應該能夠這樣調用方法:aBar.Test (foo)。

    class Bar<T> : IBar<T>
    {
        public void Test(IFoo<T> foo)
        {
            throw new NotImplementedException();
        }
    }

    class Foo<T> : IFoo<T>
    {
        
    }

    class Program
    {
        public static void Main()
        {
            Bar<string> aBar = new Bar<string>();
            Foo<object> foo = new Foo<object>();
            aBar.Test(foo);
        }
    }

當調用方法以後,傳入的參數類型是Foo<object>。咱們再看看方法的簽名:

    interface IBar<in T>
    {
        void Test(IFoo<T> foo);
    }

如今咱們的aBar的類型參數T是string,因此,咱們期待的Test方法的傳入類型也應該是IFoo<string>,或者可以變化成IFoo<string>的類型,但傳入的倒是一個object。因此,這兩個接口的方法的寫法是有問題的。

    interface IFoo<out T>
    {

    }

當把IFoo接口的簽名改用out修飾以後,問題就解決了。此時因爲容許逆變,Foo<object>就能夠變化成IFoo<string>了。不過本人眼光短淺,目前還沒發現這個特色在實際工做中有什麼應用。

參考資料

http://www.cnblogs.com/LoveJenny/archive/2012/03/13/2392747.html

http://www.cnblogs.com/xinchufa/p/3524452.html

http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

相關文章
相關標籤/搜索