看完这篇,终于知道自己会不会 C# 泛型了!
作者 | 羽生结弦
责编 | 胡巍巍
在开发过程中,同一段代码处出现多次调用,并且会有不同的类型在使用,这种就叫做跨类型代码复用。一般情况下跨类型代码复用我们会用到如下两种方法:
1. 继承;
2. 泛型。
继承是通过父类来代表代码复用,而泛型是通过带有占位符的模板来代码复用,其中占位符指的是类型。
比如:int、syting和实体等。本片主要讲解泛型的相关知识,下面就来详细讲解一下泛型。
零、泛型类型
泛型会声明类型参数,消费者需要提供类型参数来把占位符类型填充上。我们先来看一个例子:
publicclassGenericClass<T>
{
T[] tArray=newT[10];
intposition=0;
publicvoidPush(T t)=>tArray[position++]=t;
publicT Pop=>tArray[--position];
publicT[] GetTs => tArray;
}
classProgram
{
staticvoidMain(string[] args)
{
var genericStr=newGenericClass<string>;
generic.Push("张三");
generic.Push("李四");
generic.Push("王五");
generic.Pop;
var genericInt=newGenericClass<int>;
generic.Push(1);
generic.Push(2);
generic.Push(3);
generic.Pop;
}
}
在上面的代码中当我们将string传入泛型类中,将会隐式动态的创建类型,这种操作被称为合成,合成将发生在运行时,而非编译时。当我们在代码中传入非string类型的值时,在编译时将报错。同样在上面的代码中我们也看到了int传入泛型类中的情况,这就说明泛型类可以跨类型复用。
小知识1:
我们将GenericClass<T>称为开放类型(OpenType),而将GenericClass<string>称为封闭类型(CloseType),开放类型在编译后就变成了封闭类型,在运行时所有的泛型类型都是封闭类型,因为占位符已经被具体类型填充完毕。
小知识2:
对于每一种封闭类型,静态数据都是唯一的,例如:
classProgram
{
staticvoidMain(string[] args)
{
// 输出1
Console.WriteLine(++MyClass<int>.Count);
// 输出2
Console.WriteLine(++MyClass<int>.Count);
// 输出1
Console.WriteLine(++MyClass<string>.Count);
}
}
classMyClass<T>
{
publicstaticintCount;
}
上面代码中,MyClass中存在一个静态字段Count,我们看到前两次的调用输出的分别是1和2,但是第三次输出的确实1,那么这是为什么呢,原因就是前两次的类型参数和泛型类型种的静态字段的类型一致,而第三次的类型参数不一致导致的。
在泛型类型饿子类中可以继续让父类参数保持开放,也剋及关闭父类的类型参数,同样子类也可以引入新的类型,我们来看一下例子:
// 父类继续保持开放
classFather<T>{}
classChildren<T>:Father<T>{}
//关闭父类
classFather<T>{}
classChildren:Father<string>{}
//引入新参数类型
classFather<T>{}
classChildren<T,U>:Father<T>{}
小知识:
封闭参数类型的时候,该类型可以把自己作为具体的类型,例子如下:
classAClass<T>{}
classBClass:AClass<BClass> { }
泛型方法
泛型方法在方法签名内声明类型参数,例如下:
classProgram
{
staticvoidMain(string[] args)
{
GenericFun<int>(12, 4);
}
staticvoidGenericFun<T>(T x, T y)
{
T tmp = x;
x = y;
y = tmp;
Console.WriteLine("x:"+ x + " y:"+ y);
}
}
在上面的代码中我们看到 ```generic.GenericFun<int>(12,4);``` ,我们将int传入泛型方法中,现在我们将这行代码改写成如下形式 ```generic.GenericFun(12,4);```。
我们发现现在这段代码和前面那段代码缺少了 <int> ,那么这么写代码是否有错呢?
答案是要看情况,当编译器可以推断出参数类型的话,我们可以省略掉参数类型,但是当编译器无法推断出参数类型的话我们就必须写上参数类型了。
当然上面这段代码是可以正确运行的,因为编译器可以正确的推断出参数类型。
我们还有如下几点需要注意的:
1. 泛型类中的方法,如果方法引入了参数类型,那它就是泛型方法,反之就不是泛型方法;
2. 除了class、struct、interface、delegate 和方法可以引入类型参数外,属性、字段、索引器、事件和构造函数等都不能声明类型参数,但是可以使用所在泛型类的类型参数。
小知识:
泛型类型和泛型方法可以有多个参数类型,例如 ```class a<T,U>``` 调用方法和单个参数类型一样。结合这一点我们就可以推断出泛型类型和泛型方法可以出现重载,只要参数类型的数量不同就没问题。
在一些情况下我们需要获取参数类型的默认值,这时我们就可以使用default(T)来获得。
约束
我们虽然可以使用所有类型作为泛型类型参数,但是我们在实际开发时很少会这么使用,一般会将类型参数约束到指定的范围内。泛型可用约束如下:
1. base-class:某一个父类的子类;
2. interface:必须是实现了指定的接口;
3. class:必须是引用类型;
4. struct:必须是非空值类型;
5. new:必须包含无参构造函数;
6. U:T:U必须继承T
我们使用的时候是这样的:
classAclass{}
interface Binterface { }
classGeneric1<T> whereT:Aclass { }
classGeneric2<T,U>
whereT:Aclass,Binterface
where U : new
{ }
上面的代码表示 Generic1 类的参数类型T继承自Aclass,Generic2 类的T继承子Aclass,并且实现了 Binterface 接口,而且U包含了无参构造函数。
注意:约束可以用于泛型类型和泛型方法
类型参数与转换
C#种转换支持如下几种:
1. 数值转换;
2. 引用转换;
3. 装箱拆箱转换;
4. 自定义转换。
在发生编译的时候,会根据一直类型的操作数来决定采用那种转换,但是在泛型中我们不知道具体的类型是什么,编译器就会默认使用自定义转换,那么就有可能出现错误。
为了解决这个问题,我们引入了as ,例如我们将传进来的值转换成StringBuilder,这时我们可以这么做:
StringBuilder ToFloat<T>(T t)
{
StringBuilder f = t asStringBuilder;
returnf;
}
variance 转换
讲解variance 转换前,先来简单了解一下协变、逆变和不变。
1. 协变(Covariance):当T作为返回值输出的时候;
2. 逆变(Contravariance):当T作为输出值的时候;
3. 不变(Invariance):当T即是输入又是输出时。
注意:以上这三种就是variance,只能用在接口和委托中。
所谓variance 转换就是上面三种之间的相互转换,variance 转换是引用转换的一个方法,从a转换到b如果是本体转换或者隐式引用转换,那么就是正确的。例如:
IEnumerable<string> toIEnumerable<object>
IEnumerable<IDisposable> toIEnumerable<object>
作者简介:朱钢,笔名羽生结弦,CSDN博客专家,.NET高级开发工程师,7年一线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计,目前就职于北京恒创融慧科技发展有限公司,从事企业级安全监控系统的开发。