泛型(Generic)
使用场景
很多时候,如果我们可以把类的行为提取或重构出来,使之不仅能应用到它们编码的数据类型上,而且还能应用到其他类型上。有了泛型,我们可以从后代码并且二外的增加一个抽象层,对于这样的代码来说,数据类型就不用编码,这是专门为多段代码在不同的数据类型上执行相同指令的情况专门设计的。
泛型(generic)特性是提供了一种更为优雅的方式,可以让多个类型共享一组代码。泛型允许我们声明类型参数化(type-parameterized)的代码,可以用不同的类型进行实例化。也就是说我们可以用“类型占位符”来写代码,然后在创建类的实例时指明真实的类型。
类型不是对象而是对象的模板,泛型类型不是类型,是类型的模板。
C# 提供了5种泛型:类、结构、接口、委托和方法。前面4个是类型,而方法是成员。
泛型由尖括号和T构成,T是类型的占位符。
泛型类
泛型类不是实际的类,而是类的模板,所以我们必须先从它们构建实际的类类型,耳后创建这个构建后的类类型的实例。
●在某些类型上使用占位符来声明一个类。
●为占位符提供真实类型。这样就有了真实类的定义,填补了所有的“空缺”。该类型称为构造类型(constructed type)。
●创建构造类型的实例。
声明泛型类型
声明一个简单的泛型类型和声明普通类型差不多,区别如下:
在类名之后放置一组尖括号。
●在尖括号中用逗号分隔的占位符字符串来表示希望提供类型,这叫类型参数(type parameter)。
●在泛型类声明的主体中使用类型参数来表示应该替代的类型。
在泛型类型声明中并没有特殊关键字,取而代之的是尖括号中的类型参数列表。它可以区分泛型类与普通类的声明。
创建构造类型
一旦创建了泛型类型,我们就需要告诉编译器能使用那些真实类型来替代占位符(类型参数)。编译器获取这些真实类型并创建构造类型(用来创建真实类对象的模板)。
创建构造类型的语法如下,包括列出类名并在尖括号中提供真实类型来替代类型参数。要替代类型参数的真实类型叫做类型实参(tupe argument)。
编译器接受了类型实参并且替换泛型类主体中的相应类型参数,产生了构造类型——从它创建真实类型的实例。
类型参数和参数实参的区别
●泛型类声明上的类型参数用做类型的占位符。
●在创建构造类型时提供的真实类型时类型实参。
创建变量和实例
、
和非泛型类一样,引用和实例可以分开创建。内存中出现的情况与泛型类是一样的。
类型参数的约束
复合约束的类型参数叫做未绑定的类型参数(unbounded type parameter)。要让泛型变得更有用,我们需要提供额外的信息让编译器知道参数可以接受那些类型。这些额外的信息叫做约束(constranin)。只要符合约束的类型才能替代给定的类型参数,来产生构造类型。
Where子句
●每一个有约束参数有自己的where子句。
●如果形参有多个约束,它们在where子句中使用逗号分隔。
where子句的要点如下。
●它们在类型参数列表的关闭尖括号之后列出。
●它们不适用逗号或其他符号分隔。
●它们可以以任何次序列出。
●where是上下文关键字,所以可以在其他上下文中使用。
约束类型和次序
where子句可以以任何次序列出,然而,wher子句中的约束必须有特定的顺序。
●最多只能有一个主约束,如果有则必须放在第一位。
●可以有任意多的接口名约束。
●如果存在的构造函数约束,则必须放在最后。
泛型方法
与其他泛型不一样, 方法是成员,不是类型。泛型方法可以在泛型和非泛型类以及结构和接口中声明。
声明泛型方法
泛型方法具有类型参数列表和可选的约束。
●泛型方法有两个参数列表。
■封闭在圆括号内的方法参数列表。
■封闭在尖括号内的类型参数列表。
●要声明泛型方法,需要:
■在方法名称之后和方法参数列表之前放置类型参数列表。
■在方法参数列表后放置可选的约束子句。
调用泛型方法
要调用泛型方法,应该在方法调用时提供类型实参。如下图所示:
class Program
{
public void DoStuff<T1, T2>(T1 t1, T2 t2)
{
T1 someVar = t1;
T2 otherVar = t2;
Console.WriteLine(typeof(T1).Name+"-"+typeof(T2));
}
static void Main(string[] args)
{
int a = 555;
string b = "bbb";
new Program().DoStuff(a,b);
Console.ReadKey();
}
}
推断类型
如果我们为方法传入参数,编译器有时可以从方法参数中推断出泛型的参数形参中用到的那些类型。
示例
class Simple{
public static void ReverseAndPrint<T>(T[] arr)
{
Array.Reverse(arr);
foreach(var s in arr){
Console.Write("{0} ",s.ToString());
}
Console.WriteLine();
}
}
class Program
{
static void Main(string[] args)
{
var intArray = new int[] {1,2,3,4,5,6 };
var stringArray = new string[] { "a","b","c","d","e"};
var doubleArray = new double[] { 1.1,1.2,1.3,1.4};
Simple.ReverseAndPrint(intArray);
Simple.ReverseAndPrint(stringArray);
Simple.ReverseAndPrint(doubleArray);
Console.ReadKey();
}
}
扩展方法和泛型类
泛型类的扩展方法:
●必须声明为static;
●必须是静态类的成员;
●第一个参数中必须有this,后面是扩展发的泛型累的名字
static class ExtendHolder
{
public static void Print<T>(this Holder<T> h)
{
T[] vals = h.GetValues();
Console.WriteLine("{0},\t{1},\t{2}",vals[0],vals[1],vals[2]);
}
}
class Holder<T> {
T[] Vals = new T[3];
public Holder(T v0,T v1,T v2) {
Vals[0] = v0;
Vals[1] = v1;
Vals[2] = v2;
}
public T[] GetValues() { return Vals; }
}
class Program
{
static void Main(string[] args)
{
var intHolder = new Holder<int>(3,5,7);
var stringHolder = new Holder<string>("a1","b2","c3");
intHolder.Print();
stringHolder.Print();
Console.ReadLine();
}
}
泛型结构
与泛型类相似,泛型结构可以有泛型参数和约束,泛型结构的规则和条件与泛型类是一样的
struct PieceOfData<T>{
private T _data;
public T Data
{
get { return _data; }
set { _data = value; }
}
public PieceOfData(T value) { _data = value; }
}
class Program
{
static void Main(string[] args)
{
var intData = new PieceOfData<int>(10);
var stringData = new PieceOfData<string>("Hi thre.");
Console.WriteLine("intData ={0}",intData.Data);
Console.WriteLine("stringData ={0}",stringData.Data);
Console.ReadLine();
}
}
泛型委托
泛型委托和非泛型委托非常相似,不过类型参数决定 了能接受什么样的方法。
●要声明泛型委托,在委托名称后、委托参数列表之前的尖括号中放置类型参数列表
●注意,在这里有两个参数列表:委托形参列表和类型参数列表。
●类型参数的范围包括:
■返回值;
■形参列表;
■约束子句。
示例1
delegate void MyDelegate<T>(T value);
delegate T MyDelegate1<T>(T value);
class Simple
{
public static void PrintString(string s) {
Console.WriteLine(s);
}
public static void PringtUpperString(string s) {
Console.WriteLine("{0}",s.ToUpper());
}
public static string PrintContent(string s) {
return s;
}
}
class Program
{
static void Main(string[] args)
{
var printString = new MyDelegate<string>(Simple.PrintString);
printString("aaa");
var printUpperString = new MyDelegate<string>(Simple.PringtUpperString);
printUpperString("bbb");
var printContent = new MyDelegate1<string>(Simple.PrintContent);
Console.WriteLine(printContent("返回值"));
Console.ReadLine();
}
}
示例2
public delegate TR Func<T1,T2,TR>(T1 p1,T2 p2);//泛型委托
class Simple
{
public static string PrintString(int p1, int p2)//方法匹配委托
{
int total = p1 + p2;
return total.ToString();
}
}
class Program
{
static void Main(string[] args)
{
var myDel = new Func<int ,int, string>(Simple.PrintString);//创建委托
Console.WriteLine("Total:{0}",myDel(15,13));//调用委托
Console.ReadKey();
}
}
泛型接口
泛型接口允许我们编写参数和接口成员返回类型是泛型类型参数的接口,泛型接口的声明和非泛型接口的什么差不多,但是需要在接口名称之后的尖括号里放置类型参数。
示例1:
interface IMyIfc<T>
{
T ReturnIt(T inValue);
}
class Simple<S> : IMyIfc<S>
{
public S ReturnIt(S inValue)
{
return inValue;
}
}
class Program
{
static void Main(string[] args)
{
var trivInt = new Simple<int>();
var trivString = new Simple<string>();
Console.WriteLine("{0}",trivInt.ReturnIt(5));
Console.WriteLine("{0}",trivString.ReturnIt("Hi there."));
Console.ReadKey();
}
}
使用泛型接口的示例
●与其他泛型相似,实现不同类型参数的泛型接口是不同的接口;
●我们可以在非泛型类型中实现泛型接口。
interface IMyIfc<T>
{
T ReturnIt(T inValue);
}
class Simple : IMyIfc<int>, IMyIfc<string>
{
public int ReturnIt(int inValue)
{
return inValue;
}
public string ReturnIt(string inValue)
{
return inValue;
}
}
class Program
{
static void Main(string[] args)
{
Simple trivial = new Simple();
Console.WriteLine("{0}",trivial.ReturnIt(5));
Console.WriteLine("{0}", trivial.ReturnIt("Hi there."));
Console.ReadKey();
}
}
泛型接口的实现必须唯一
实现泛型接口时,必须保证类型实参组合会不会在类型中产生两个重复的接口。
//泛型接口的名字不会和非泛型冲突,例如,我们在前面的代码中可以声明一个名称为ImyIfc的非泛型接口。interface IMyIfc
{
string ReturnIt(string inValue);
}
interface IMyIfc<T> {
T ReturnIt(T inValue);
}
//对于泛型接口,使用两个相同接口本身并没有错,问题在于这么做会产生一个潜在的冲突,因为如果把int作为类型参数来替换第二个接口中的S的话,Simple可能会有两个相同的类型的接口,这是不允许的。
class Simple<S> :IMyIfc, IMyIfc<int>, IMyIfc<S>
{
public int ReturnIt(int inValue)
{
return inValue;
}
public string ReturnIt(string inValue)
{
return inValue;
}
}
class Program
{
static void Main(string[] args)
{
}
}
协变
可变性(variance):分为三种——协变(convariance)、逆变(contravariance)和不变(invariance)
/// <summary>
/// 协变
/// </summary>
class Animal { public int Legs = 4;}//基类
class Dog : Animal { }
delegate T Factory<out T>();
class Program
{
static Dog MakeDog()
{
return new Dog();
}
static void Main(string[] args)
{
//这样很正常,赋值兼容性
Animal a = new Dog();
//这样出现错误,原因:委托Factory<Dog>没有从委托Factory<Animal>派生。相反,两个委托对象是同级的,它们都从delegate类型派生。后者又派生自object类型。两者没有相互之间的派生关系,因此赋值不适用。
Factory<Dog> dogMaker = MakeDog;
Factory<Animal> animalMaker = dogMaker;
Console.WriteLine(animalMaker().Legs);
Console.ReadKey();
}
如果派生类只是用于输出值, 那么这种结构化的委托有效性之间的常数关系叫做协变。为了能够正常工作,必须使用out关键字标记委托声明中的类型参数。
逆变
/// <summary>/// 逆变
/// </summary>
class Animal { public int NumberOfLegs = 4;}
class Dog : Animal { }
class Program
{
delegate void Action1<in T>(T a);
static void ActionAnimal(Animal a)
{
Console.WriteLine(a.NumberOfLegs);
}
static void Main(string[] args)
{
Action1<Animal> act1 = ActionAnimal;
Action1<Dog> dog1 = act1;
dog1(new Dog());
Console.ReadKey();
}
}
这种在期望传入基类时允许传入派生对象的特性叫做逆变。可以在类型参数显示使用in关键字来使用。
接口的协变和逆变
接口使用协变的例子
●代码使用类型参数T声明了泛型接口。out关键字指定了类型参数是协变的。
●泛型类SimpleReturn实现了泛型接口。
●方法DoSomething演示了方法如何接受一个接口作为参数。这个方法接受由Animal类型构建的泛型接口IMyIfc作为参数。
示例:
class Animal { public string Name;}
class Dog : Animal { }
interface IMyIfc<out T>
{
T GetFirst();
}
class SimpleReturn<T> : IMyIfc<T>
{
public T[] items = new T[2];
public T GetFirst() { return items[0];}
}
class Program
{
static void DoSimething(IMyIfc<Animal> returner)
{
Console.WriteLine(returner.GetFirst().Name);
}
static void Main(string[] args)
{
SimpleReturn<Dog> dogReturner = new SimpleReturn<Dog>();
dogReturner.items[0] = new Dog() { Name="Avonlea"};
IMyIfc<Animal> animalReturner = dogReturner;
DoSimething(dogReturner);
Console.ReadLine();
}
}
有关可变性的更多内容
有些情况下编译器可以自动识别某个已构建的委托是协变或逆变并且自动进行类型强制转换。这通常发生在没有为对象的类型赋值的时候。
class Animal { public int Legs = 4;}//基类
class Dog : Animal { }//派生类
class Program
{
delegate T Factory<out T>();
static Dog MakeDog() {
return new Dog();
}
static void Main(string[] args)
{
Factory<Animal> animalMarker1 = MakeDog;//隐式强制转换
Factory<Dog> dogMaker = MakeDog;
Factory<Animal> animalMarker2 = dogMaker;//需要out标识符
Factory<Animal> animalMarker3 = new Factory<Dog>(MakeDog);//需要out标识符
Console.ReadLine();
}
}
有关可变性的其他一些重要的事项如下
1.变化处理的是使用派生类替换基类的安全情况,反之亦然。因此变化只适用于引用类型,因为不能从值类型派生其他类型。
2.显示变化使用in和out关键字只适用于委托和接口,不适用于类、结构和方法。
3.不包括in和out关键字的委托和接口类参数叫不变,这些类型参数不能用于协变或逆变。