泛型是为了代码能够跨类型复用设计的
如果没有泛型,想让一个类支持多种数据结构,那么只能硬编码不同的版本,或者使用Object作为元素类型,但是两个都有对应的问题,前者会有大量的重复代码和维护问题,后者会需要装箱拆箱,而且不能在编译时检查是否合法
泛型里面有一个类型参数(占位符类型),需要由泛型的消费者填充
static void Swap<T>(ref t a, ref t b)
{
t temp = a;
a= b;
b = temp;
}
int x =5;int y = 10;
Swap(ref x ,ref y);
Swap<int> (ref x,ref y);
只有方法,委托,类,接口,结构体,才能引入类型参数(用尖括号标出),其他属性,索引器,事件,字段,构造器,运算符等都只能使用已经引入的类型参数,但是不能引入
某个泛型如果还未填充(stack<T>),则称为开放类型,如果已经填充了(stack<int>),则称为封闭类型
运行时所有泛型都是封闭的
default
可以用default关键字在泛型里面指定默认值
static void SetEmptyArray<T>(T[] array)
{
for(int i=0;i< array.Length;i++)
{
array[i] = default(T);
}
}
约束
可以在类型参数上添加约束,添加了对应的约束之后,就可以使用对应约束的特性
static T Max<T> (T a ,T b) where T:IComparable<T>
{
return a.CompareTo(b)>0?a:b;
}
约束列表如下
- where T : class
引用类型 - where T : class?
可空引用类型,包含引用类型加上可空引用类型,不包含可空值类型 - where T : struct
值类型约束,不包含可空值类型 - where T : unmanaged
非托管类型约束,指不受CLR管理的类型,不受垃圾回收管理,包含所有值类型,指针,枚举类型或者自定义的上面的组合 - where T : new()
有一个无参的构造函数,意味着定义里面可以使用new关键字 - where T : not null
非空值类型,或者非空引用类型 - where T : base-class
某个类的子类 - where T : interface
实现某个接口 - where U : T
T是一个类型参数,U是T类型的子类
private bool UpdateArray<T>(T[] existArray, T[] readArray) where T : struct, IEquatable<T>
{
if (existArray.Length != readArray.Length)
return false;
bool anyChange = false;
for (int i = 0; i < existArray.Length; i++)
{
//这里添加了IEquatable接口才能使用,并且不能使用==判断
if (!existArray[i].Equals(readArray[i]))
{
existArray[i] = readArray[i];
anyChange = true;
}
}
return anyChange;
}
类型转换
常规的类型转换,在编译时就决定了类型转换的方式,但是泛型由于编译时类型还未确定,所以就有可能产生二义性
协变和逆变
// Assignment compatibility.
string str = "test";
// An object of a more derived type is assigned to an object of a less derived type.
object obj = str;
// Covariance.
IEnumerable<string> strings = new List<string>();
// An object that is instantiated with a more derived type argument
// is assigned to an object instantiated with a less derived type argument.
// Assignment compatibility is preserved.
IEnumerable<object> objects = strings;
// Contravariance.
// Assume that the following method is in the class:
static void SetObject(object o) { }
Action<object> actObject = SetObject;
// An object that is instantiated with a less derived type argument
// is assigned to an object instantiated with a more derived type argument.
// Assignment compatibility is reversed.
Action<string> actString = actObject;
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
public delegate void Action<in T>(T obj);
协变
协变,指子类变成父类 只能out
考虑以下例子
有个IList <Dog> 和IList <Animal>
将IList<Dog>转换成IList<Animal>,这个时候,实际类型是Dog的列表,但是转换成了Animal,从语法上,往里面塞一只动物是合法的,比如放一只cat,但是运行时就会出错,因为实际引用的是Dog的列表,不能放cat
但是,从里面取是合法的,因为我从里面取出来一个animal,而列表里面只有dog,也就是实际取了一个Dog,Dog属于Animal,所以合法
···
public interface IEnumerable<out T>
{
IEnumerator<T> GetEnumerator();
}
IEnumerable<Dog> dogs = new List<dog>();
IEnumerable<Animal> animals = dogs;
animals.GetEnumerator();
···
上面的例子里面,animals里面存的实际上还是dogs,调用的也是dogs上面的相关方法,但是由于参数里面只能作为输出,所以运行时可以将方法返回的结果Dog转换成Animal,符合里氏替换原则
如果想往转换结束的IEnumerable<Animal>塞数据,是不行的,因为里面实际是Dogs,而实际上,编译器也只允许你从里面拿数据
interface I<out R>
这里定义了泛型接口后,R类型就只能用作里面方法的返回值,不能作为参数了
interface ICovariant<out R>
{
R GetSomething();
// The following statement generates a compiler error.
// void SetSomething(R sampleArg);
}
唯一可以放在参数里面的是作为泛型委托,本质上是传递一个方法传入进来,还是并非作为真正的参数传递进来
interface ICovariant<out R>
{
void DoSomething(Action<R> callback);
}
协变类型不能用作接口方法的泛型约束,但是逆变可以
interface ICovariant<out R>
{
// The following statement generates a compiler error
// because you can use only contravariant or invariant types
// in generic constraints.
// void DoSomething<T>() where T : R;
}
逆变
逆变,指父类变成子类 只能in
将iList<Animal>转换成iList<Dog>,这个时候,实际类型是Animal的列表,但是转换成了Dog,从语法上,往外取一只Dog是合法的,但是运行时就会出错,因为实际引用的是Animal的列表,可能取出来的不是Dog,也就是不能返回
但是,往里面放是合法的,因为我从实际是animal的列表,我放一只dog进去,dog也是animal啊,不管从语法还是实际都合法
逆变类型只能用作方法参数的类型,不能用作接口方法的返回类型。
interface IContravariant<in A>
{
void SetSomething(A sampleArg);
void DoSomething<T>() where T : A;
// The following statement generates a compiler error.
// A GetSomething();
}
interface IShout<in A>
{
void Shout(A sampleArg);
}
class Dog :Animal, IShout<Dog>
{
public void Shout(Dog D)
{
Console.WriteLine("i am dog "+ D.Name);
}
}
class Animal : IShout<Animal>
{
public string Name { get; set; }
public void Shout(Animal a)
{
Console.WriteLine("i am animal "+ a.Name);
}
}
static void Main(string[] args)
{
var animal = new Animal() { Name = "animal1"};
IShout<Dog> d = animal;
d.Shout(new Dog() { Name = "dog1"});
//i am animal dog1
}
上面的例子里面,d里面存储的实际上是一个animal对象,而animal对象的方法接受一个animal参数,dog属于animal,所以这样调用是合法的。反过来取是不行的,因为取出来的animal不一定是dog,所以编译器也不允许这样的操作。还是符合里氏替换原则。
允许同时支持协变逆变
interface IVariant<out R, in A>
{
R GetSomething();
void SetSomething(A sampleArg);
R GetSetSomethings(A sampleArg);
}
协变、逆变只能针对泛型接口或者委托,而不能针对泛型类。
因为它们都只能定义方法成员(接口不能定义字段),而方法成员在创建对象的时候是不涉及到对象内存分配的,所以它们是类型(内存)安全的。
为什么不针对泛型?因为泛型类是模板类,而类成员是包含字段的,不同类型的字段是影响对象内存分配的,没有派生关系的类型它们是不兼容的,也是内存不安全的。
也就是说用接口因为接口不涉及具体的数据结构,如果类支持泛型的话,会设计具体的字段分配。比如List<Animal>和List<Dog>,字段肯定是不一样的,本质上他们属于不同的类型,所以无法执行这样的转换