C# 泛型
正如类型不是对象而是对象的模板,泛型也不是类型而是类型的模板,泛型允许我们声明类型参数化
的代码,可以用不同的类型进行实例化,也就是说我们可以用“类型占位符”来写代码,然后在创建类的实例时指明真实的类型。
C#提供了5种泛型:类
,结构
,接口
,委托
和方法
。前四个是类型,最后一个是成员。
泛型类
声明泛型类
- 在类名后放置一组尖括号
<>
- 在尖括号中用逗号分隔的占位符来表示希望提供的类型,这就是
类型参数
。 - 在泛型类声明的主体中使用类型参数来表示应代替的类型
class SomeClass<T1,T2>{
public T1 SomeVar=new T1();
public T2 OtherVar=new T2();
}
创建构造类型
列出类名并在尖括号中提供真实类型来代替类型参数,要代替类型参数的真实类型叫做类型实参
。
SomeClass<short,int>
创建变量和实例
SomeClass<short,int> mySc1=new SomeClass<short,int>();
var mySc2=new SomeClass<short,int>();
SomeClass<int,short> myInt;
myInt=new SomeClass<int,short>();
SomeClass<int,long> mySc3=new SomeClass<int,long>();
使用泛型的栈示例
namespace ConsoleApp1
{
class MyStack<T>
{
T[] StackArray;
int StackPointer = 0;
const int MaxStack = 10;
bool IsStackFull { get { return StackPointer >= MaxStack; } }
bool IsStackEmpty { get { return StackPointer <= 0; } }
public void PushT(T x)
{
if (!IsStackFull)
{
StackArray[StackPointer++] = x;
}
}
public T Pop()
{
return (!IsStackEmpty)
? StackArray[--StackPointer]
: StackArray[0];
}
public MyStack()
{
StackArray = new T[MaxStack];
}
public void Print()
{
for (int i = StackPointer - 1; i >= 0; i--)
{
Console.WriteLine("Value:{0}", StackArray[i]); //先入后出
}
}
}
class Program
{
static void Main()
{
MyStack<int> StackInt = new MyStack<int>();
MyStack<string> StackString = new MyStack<string>();
StackInt.PushT(3);
StackInt.PushT(5);
StackInt.PushT(7);
StackInt.PushT(9);
Console.WriteLine("StackInt After PushT:");
StackInt.Print();
StackInt.Pop();
StackInt.Pop();
Console.WriteLine("StackInt After Pop");
StackInt.Print();
StackString.PushT("This is fun");
StackString.PushT("Hi there!");
StackString.Print();
}
}
}
类型参数的约束
从上例可以看出,泛型栈除了保存和显示它包含的一些项之外没有做任何事情,因为它不知道它保存项的类型是什么,它不会知道这些类型实现的成员。
然而所有C#对象都从object
继承,因此,栈可以确认的是,这些保存的项都实现了object
类的成员,它们包含ToString,Equals,GetType
等。除此之外,它不知道还有哪些成员可用。
只要我们的代码不访问它处理的一些类型的对象(或者只要它始终是object
类型的成员),泛型类就可以处理任何类型。符合约束的类型参数叫做未绑定的类型参数。
要让泛型变得更有用,我们需要提供额外的信息让编译器知道参数可以接受哪些类型,这些额外的信息就叫做**约束
**,只有符合约束的类型才能替代给定的类型参数,来产生构造类型。
Where子句
约束用where
子句给出,每一个有约束的类型参数都有自己的where子句。如果形参有多个约束,它们在where子句中使用逗号分隔。
语法如下:
where TypeParam:constraint,constraint...
有关类型参数约束要点如下:
- 在类型参数列表的关闭尖括号之后列出
- 它们不适用逗号或其他符号分隔
- 可以 以任意次序列出
- where是上下文关键字,所以可以在其他上下文中使用
例:
class MyCalss<T1,T2,T3> //T1未绑定约束
where T2:Customer //T2的约束,只有Customer类型或者从Customer继承的类才能作为T2类型实参
where T3:IComparable //T3的约束,只有实现了IComparable接口的类才能用于类型实参
约束的类型和次序
where子句可以以任意次序列出,然而,where子句中的约束必须有特定的顺序。
约束类型 | 描述 |
---|---|
类名 | 只有这个类型的类或从它继承的类才能用作类型实参 |
class | 任何引用类型,包括类,数组,委托,接口都可以用作类型实参 |
struct | 任何值类型都可以用作类型实参 |
接口名 | 只有这个接口或实现这个接口的类型才能用作类型实参 |
new() | 任何带有无参公共构造函数的类型都可以用作类型实参,这叫做构造函数约束 |
特定的顺序为:
[ClassName,class,struct], [interfaceName], [new()]
第一个主约束,包括类名,class,struct,最多只有一个主约束,必须放第一位;
可以有任意多的接口约束;
如果存在构造函数约束,必须放最后,最多一个。
泛型方法
与其他泛型不一样,方法是成员,不是类型,可以在泛型和非泛型类,以及结构和接口中声明
声明泛型方法
泛型方法具有类型参数列表和可选的约束,其中,方法参数
列表封闭在圆括号内,类型参数
列表封闭在尖括号内。在方法名称后和方法参数列表之前放置类型参数列表,在方法参数列表后放置可选的约束子句。
public void PrintData<S,T>(S p, T t) where S:Person{
....
}
调用泛型方法
void DoStuff<T1,T2>(T1 t1,T2 t2){
T1 someVar=t1;
T2 OtherVar=t2;
....
}
DoStuff<short,int>(sVal,iVal);
DoStuff<int,short>(iVal,sVal);
如果我们为方法传入参数,编译器有时可以从方法参数列表中推断出泛型方法的类型形参中用到的那些类型,这样就可以使调用更简单,可读性更强。
int myInt=5;
MyMethod<int>(myInt);
//上述代码的快捷用法如下:
MyMethod(myInt);
示例:
class Test
{
public static void Print<T,S>(T t,S s)
{
Console.WriteLine(t.ToString());
Console.WriteLine(s.ToString());
}
}
class Program
{
static void Main()
{
Test.Print(15, 3.232);
}
扩展方法和泛型类
扩展方法
允许编写的方法和声明它的类之外的类关联,扩展方法的重要要求如下:
- 声明扩展方法的类必须是
static
- 扩展方法本身也必须是
static
- 扩展方法必须包含关键字
this
作为它的第一个参数类型,并在后面跟着它所扩展的类的名称
实例:
sealed class MyData //不允许继承
{
private double D1, D2, D3;
public MyData(double d1,double d2,double d3)
{
D1 = d1;D2 = d2;D3 = d3;
}
public double Sum()
{
return D1 + D2 + D3;
}
}
static class ExtendMyData //扩展方法所在的static 类
{
public static double Average(this MyData md) //扩展方法,以this作为第一个参数类型,后面跟着所扩展的类名MyData
{
return md.Sum() / 3;
}
}
class Program
{
static void Main()
{
MyData md = new MyData(3, 4, 5);
Console.WriteLine("Average:{0}", md.Average()); // 实例可以直接调用扩展方法,就好像Average方法是MyData的实例成员那样!!
}
}
扩展方法可以和泛型类结合使用,允许我们将扩展方法应用到不同的泛型类上,我们就可以像调用类构造实例的实例方法一样来调用方法。
实例:
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; }
}
static class ExtendHolder
{
public static void Print<T>(this Holder<T> h) //泛型扩展方法,泛型要求带<T>,扩展方法要求参数是this Holder<T> h
{
T[] vals = h.GetValues();
Console.WriteLine("{0},\t{1},\t{2}", vals[0], vals[1], vals[2]);
}
}
class Program
{
static void Main()
{
var intHolder = new Holder<int>(3, 5, 6);
var stringHolder = new Holder<string>("a1", "a2", "a3");
intHolder.Print();
stringHolder.Print();
}
}
泛型结构
与泛型类相似,泛型结构可以有类型参数和约束。规则与泛型类是一样的
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()
{
var intData = new PieceOfData<int>(10);
var stringData = new PieceOfData<string>("Hi there");
Console.WriteLine("intData={0}", intData.data);
Console.WriteLine("stringData={0}", stringData.data);
}
}
泛型委托
委托例:
对于有返回值的委托,委托返回的是最后一个函数的返回值。
class MyClass1
{
int IntValue = 5;
public int Add2() { IntValue += 2;return IntValue; }
public int Add3() { IntValue += 3;return IntValue; }
}
class Program
{
static void Main()
{
MyClass1 mc1 = new MyClass1();
MyClass1 mc2 = new MyClass1();
MyDel mDel = mc1.Add2;
mDel += mc1.Add3;
mDel += mc2.Add2;
Console.WriteLine($"{mDel()}");
}
}
delegate void MyDelegate<T>(T value);
class Simple
{
static public void PrintString(string s)
{
Console.WriteLine(s);
}
static public void PrintUpperString(string s)
{
Console.WriteLine("{0}", s.ToUpper());
}
}
class Program
{
static void Main()
{
var myDel = new MyDelegate<string>(Simple.PrintString);
myDel += Simple.PrintUpperString;
myDel("Hi there!");
}
}
泛型接口
接口例子:
class Myclass1 : IComparable
{
public int TheValue;
public int CompareTo(object obj)
{
Myclass1 mc = obj as Myclass1;
if (this.TheValue < mc.TheValue) return 1;
if (this.TheValue > mc.TheValue) return -1;
return 0;
}
public Myclass1(int value)
{
TheValue = value;
}
}
class Program
{
static void PrintOut(string s,Myclass1[] mc)
{
Console.WriteLine(s);
foreach (var m in mc)
{
Console.Write($"{m.TheValue} ");
}
Console.WriteLine(" ");
}
static void Main()
{
var myInt = new[] { 20, 4, 16, 9, 2 };
Myclass1[] mcArr = new Myclass1[5];
for (int i = 0; i < 5; i++)
{
mcArr[i] = new Myclass1(myInt[i]);
}
PrintOut("Initial order:", mcArr);
Array.Sort(mcArr);
PrintOut("Sorted order:", mcArr);
}
}
泛型接口例子:
interface IMyIfc<T> //泛型接口
{
T ReturnIt(T invalue);
}
class Simple<T>:IMyIfc<T> //泛型接口使用,需要在接口后仍然加上<T>
{
public T ReturnIt(T invalue)
{
return invalue;
}
}
class Program
{
static void Main()
{
var triInt = new Simple<int>();
var triString = new Simple<string>();
Console.WriteLine($"{triInt.ReturnIt(30)}");
Console.WriteLine($"{triString.ReturnIt("Hi There!")}");
}
}
- 与其他泛型类似,实现不同类型参数的泛型接口是不同的接口;我们可以在非泛型类型中实现泛型接口。
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()
{
Simple trivial = new Simple();
Console.WriteLine($"{trivial.ReturnIt(5)}");
Console.WriteLine($"{trivial.ReturnIt("Hi there")}");
}
}
- 实现泛型类接口时,必须保证类型实参组合不会在类型中产生两个重复的接口。
如下例:
上述代码,Simple类使用了两个IMyIfc接口的实例化,第一个是构造类型,使用类型int进行实例化,第二个有一个类型参数但不是实参。
对于泛型接口,使用两个相同接口本身没有错,问题在于这么做会的话会产生一个潜在的冲突,如果把int作为类型参数来代替第二个接口中的s的话,Simple可能会有两个相同类型的接口,这是不允许的。
协变
首先看一段代码:
class Animals {
public int legs = 4;
public void Print()
{
Console.WriteLine("This is animal");
}
}
class Dogs : Animals {
new public void Print()
{
Console.WriteLine("This is Dog");
}
}
delegate T Factory<T>();
class Program
{
static Dogs MakeDog()
{
return new Dogs();
}
static void Main()
{
Factory<Dogs> dogMaker = MakeDog;
Factory<Animals> animalMaker = dogMaker;
Console.WriteLine(animalMaker().legs.ToString());
animalMaker().Print();
}
}
可以发现,我们试图将派生类构造的委托赋值给由基类构造的委托,编译器报错了,原因是两个委托对象是同级的,都从delegate
派生得到,所以两者没有相互派生的关系,因此赋值兼容性不适用。
现在我们希望的是执行animalMaker
委托,调用代码就返回的是一个Animal
对象引用,仔细分析下这种情况,如果类型参数只用作输出值
,则派生类创建的委托就可以隐式转换为基类创建的委托,这就是协变的概念。
所以,只需要在上述代码中修改委托为delegate T Factory<out T>
即可。
class Animals {
public int legs = 4;
public void Print()
{
Console.WriteLine("This is animal");
}
}
class Dogs : Animals {
new public void Print()
{
Console.WriteLine("This is Dog");
}
}
delegate T Factory<out T>();
class Program
{
static Dogs MakeDog()
{
return new Dogs();
}
static void Main()
{
Factory<Dogs> dogMaker = MakeDog;
Factory<Animals> animalMaker = dogMaker;
Console.WriteLine(animalMaker().legs.ToString());
animalMaker().Print();
}
}
逆变
class Animals {
public int legs = 4;
public int x = 0;
public virtual void Print()
{
Console.WriteLine("This is animal");
}
}
class Dogs : Animals {
public int x = 1;
public override void Print()
{
Console.WriteLine("This is Dog");
}
}
delegate void Factory<in T>(T a);
class Program
{
static void ActionOnAnimal(Animals a)
{
a.Print();
Console.WriteLine(a.x.ToString());
}
static void Main()
{
Factory<Animals> act1 = ActionOnAnimal;
Factory<Dogs> act2 = act1;
act2(new Dogs());
}
}