.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稱為類型參數。調用時指定的具體類型叫做實際參數(實參)。

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

泛型集合的使用契機:幾乎任何時候,都不考慮不用泛型集合代替泛型集合。很多非泛型集合也有了自己的泛型版本,例如棧,隊列等。

泛型方法

泛型方法的使用契機一般為傳入類型可能有很多種,但處理方式卻相同的情境。這時我們可以不需要寫很多個重載,而考慮用泛型方法達到代碼復用的目的。配合泛型約束,可以寫出更嚴謹的方法。泛型委托也可以看成是泛型方法的一種應用。

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM