《CLR Via C# 第3版》筆記之(十四) - 泛型高級
為了更好的利用泛型,現將泛型的一些高級特性總結一下。
主要內容:
- 泛型的協變和逆變
- 泛型的參數的約束
1. 泛型的協變和逆變
對于泛型參數(一般用T表示),指定了類型之后。就只能識別此類型,面向對象中的繼承并不適用泛型參數,比如T指定為ClassA,盡管ClassB是ClassA的子類,也不能代替ClassA來作為泛型參數。
但是,利用泛型的協變和逆變之后,我們可以寫出更加靈活的泛型代碼,避免不必要的強制轉型操作。
首先看下面的示例代碼:
using System;
class CLRviaCSharp_14
{
// 泛型委托,其中委托的參數和返回值都是泛型
public delegate TResult Print<T, TResult>(T arg);
static void Main(string[] args)
{
ClassA a = new ClassA();
ClassB b = new ClassB();
ClassC c = new ClassC();
Print<ClassB, ClassB> p1 = new Print<ClassB, ClassB>(Show);
// 此處無法賦值,會報錯
Print<ClassC, ClassB> p2 = p1;
Console.WriteLine(p2(c).ToString());
// 此處無法賦值,會報錯
Print<ClassB, ClassA> p3 = p1;
Console.WriteLine(p3(b).ToString());
Console.ReadKey();
}
static ClassB Show(ClassB b)
{
return (ClassB)b;
}
}
class ClassA
{
public override string ToString()
{
return "This is Class A!";
}
}
class ClassB : ClassA
{
public override string ToString()
{
return "This is Class B!";
}
}
class ClassC : ClassB
{
public override string ToString()
{
return "This is Class C!";
}
}
上面有兩處地方無法編譯通過,分別是
1. p2的參數類型ClassC無法轉換為p1的參數類型ClassB
2. p1的返回值類型ClassB無法轉換為p3的返回值類型ClassA
上面這2點其實都是 子類=>父類 的過程,在C#中是很自然的轉換。
通過泛型的協變和逆變,也可以實現上面的轉換。
上面的代碼只需改動一行就可以編譯成功,即改變其中委托的定義,加入協變和逆變的關鍵字in和out
// 泛型委托,其中委托的參數和返回值都是泛型
// in表示逆變, 即輸入參數的類型可由基類改為派生類
// out表示協變,即返回值類型可以由派生類改為基類
public delegate TResult Print<in T, out TResult>(T arg);
這里需要強調一點的是,不管協變和逆變,其本質都是子類代替父類,并沒有違反面向對象的Liscov原則。
首先看逆變,因為參數類型由基類變成了派生類,那么函數內部的使用基類完成的操作都可以用派生類來替換。
再看協變,返回值由派生類變成了基類,那么函數內部原有返回派生類的操作都可以隱式轉換為基類再返回。
通過協變和逆變,我們就可以不用修改函數(即上例中的Show函數)的前提下,使其支持多種泛型委托。
2. 泛型的參數的約束
泛型的約束不僅不會限制泛型的靈活性,反而會由于限制了泛型的類型,從而寫出更有針對性的代碼。
泛型的約束主要有3種:主要約束,次要約束,構造器約束。
2.1 主要約束
類型參數可以指定零個或一個主要約束。主要約束可以是一個引用類型,連個特殊的主要約束是class和struct
指定一個主要約束,相當于通知編譯器:一個指定的類型實參要么是與約束類型相同的類型,要么是從約束類型派生的類型。
using System;
using System.IO;
class CLRviaCSharp_14
{
static void Main(string[] args)
{
GenericClassA<string> ga = new GenericClassA<string>(); // 正確
GenericClassA<int> ga1 = new GenericClassA<int>(); // 錯誤
GenericClassB<int> gb = new GenericClassB<int>(); // 正確
GenericClassB<string> gb1 = new GenericClassB<string>(); // 錯誤
GenericClassC<int> gc = new GenericClassC<int>(); // 錯誤
GenericClassC<string> gc1 = new GenericClassC<string>(); // 錯誤
GenericClassC<Stream> gc2 = new GenericClassC<Stream>(); // 正確
GenericClassC<FileStream> gc3 = new GenericClassC<FileStream>(); // 正確
Console.ReadKey();
}
}
// T必須是引用類型
class GenericClassA<T> where T : class
{
}
// T必須是值類型
class GenericClassB<T> where T : struct
{
}
// T必須是Stream類型或者Stream類型的派生類型
class GenericClassC<T> where T : Stream
{
}
2.2 次要約束
類型參數可以指定零個或多個次要約束。主要約束代表一個接口類型。
指定一個次要約束,相當于通知編譯器:一個指定的類型實參要么是實現了指定接口的一個類型。
using System;
using System.IO;
class CLRviaCSharp_14
{
static void Main(string[] args)
{
// 錯誤,string實現了IComparable但是沒有實現IDisposable
GenericClassD<string> gd = new GenericClassD<string>();
// 正確,ClassD既實現了IDisposable也實現了IComparable
GenericClassD<ClassD> gd1 = new GenericClassD<ClassD>();
// 錯誤,Stream實現了IDisposable但是沒有實現IComparable
GenericClassD<Stream> gd2 = new GenericClassD<Stream>();
Console.ReadKey();
}
}
class GenericClassD<T> where T : IDisposable, IComparable
{
}
class ClassD : IDisposable, IComparable
{
#region IDisposable Members
public void Dispose()
{
throw new NotImplementedException();
}
#endregion
#region IComparable Members
public int CompareTo(object obj)
{
throw new NotImplementedException();
}
#endregion
}
3.3 構造器約束
類型參數可以指定零個或一個構造器約束。
指定一個構造器約束,相當于通知編譯器:一個指定的類型實參是實現了公共無參構造器的非抽象類型。
using System;
using System.IO;
class CLRviaCSharp_14
static void Main(string[] args)
{
// 錯誤,Stream是抽象類型
GenericClassE<Stream> ge = new GenericClassE<Stream>();
// 錯誤,FileStream沒有公共無參構造函數
GenericClassE<FileStream> ge1 = new GenericClassE<FileStream>();
// 正確,ClassE有公共默認無參構造函數,并且也是非抽象類型
GenericClassE<ClassE> ge2 = new GenericClassE<ClassE>();
Console.ReadKey();
}
static ClassB Show(ClassB b)
{
return (ClassB)b;
}
}
class GenericClassE<T> where T : new()
{
}
class ClassE
{
}

浙公網安備 33010602011771號