撸·泛型
因为下面这张图片,逼自己再看一遍泛型,并做下记录,加深印象。翻看.net core源码,发现泛型已经使用的到处都是,所以掌握泛型是成为最基础的知识了。
前奏
泛型
- 引入时间:.net framework 2.0
- 引入目的:为了解决装箱、拆箱带来的性能损失。
- 安全效果:
- 装箱拆箱有一个安全转换问题,所以又解决了类型安全问题。
- 在父子类或者接口继承的时候,泛型很好的起到了安全约束的作用,如第一张图片。
- 阳光普照效果: 节约代码量,提高重复利用(object也可以实现,但是有性能损失)。
泛型的协变和逆变
- 引入时间:.net framework4.0
- 引入目的:为了解决泛型父子类型的转换问题。
- 使用规则:
- 只能放在接口或者委托的泛型参数前面。
- out 协变covariant,用来修饰返回值,儿子可以赋值给老父亲。
- in:逆变contravariant,用来修饰传入参数,老父亲可以赋值给儿子。
正文
泛型方法
定义:泛型方法是通过类型参数声明的方法。在下面的代码中,Method方法名称后面必须紧跟类型占位符T,如果没有,编译器会将方法参数类型T默认识别为正常类型,而不是占位符,所以会报错的。正确样例如下:
public class Generic
{
/// <summary>
/// 泛型方法
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tParameter"></param>
public static void Method<T>(T tParameter)
{
Console.WriteLine("This is {0},parameter={1},type={2}",
typeof(Generic), tParameter.GetType().Name, tParameter.ToString());
}
}
泛型类
定义:泛型类封装不特定于特定数据类型的操作。 泛型类最常见用法是用于链接列表、哈希表、堆栈、队列和树等集合。 无论存储数据的类型如何,添加项和从集合删除项等操作的执行方式基本相同。
通常,创建泛型类是从现有具体类开始,然后每次逐个将类型更改为类型参数,直到泛化和可用性达到最佳平衡。样例如下:
public class Generic<T>
{
public T t;
}
泛型接口
跟泛型类定义一样,只要将class改为interface。样例如下:
public interface Generic<T>
{
T GetT(T t);
}
泛型委托
跟方法一样,同样是约束方法参数类型。样例如下:
public delegate void SayHi<T>(T t);
default 关键字
在泛型类和泛型方法中会出现的一个问题是,如何把缺省值赋给参数化类型,此时无法预先知道以下两点:
- T将是值类型还是引用类型
- 如果T是值类型,那么T将是数值还是结构
对于一个参数化类型T的变量t,仅当T是引用类型时,t = null语句才是合法的; t = 0只对数值的有效,而对结构则不行。这个问题的解决办法是用default关键字,它对引用类型返回空,对值类型的数值型返回零。而对于结构,它将返回结构每个成员,并根据成员是值类型还是引用类型,返回零或空。如下所示:
public class MyList<T>
{
//...
public T GetNext()
{
T temp = default(T);
if (current != null)
{
temp = current.Data;
current = current.Next;
}
return temp;
}
}
使用注意点(重点)
注意点1
泛型在声明的时候可以不指定具体的类型,但是在使用的时候必须指定具体类型;
如果子类也是泛型的,那么继承的时候可以不指定具体类型
namespace MyGeneric
{
/// <summary>
/// 使用泛型的时候必须指定具体类型,
/// 这里的具体类型是int
/// </summary>
public class CommonClass :GenericClass<int>
{
}
/// <summary>
/// 子类也是泛型的,继承的时候可以不指定具体类型
/// </summary>
/// <typeparam name="T"></typeparam>
public class CommonClassChild<T>:GenericClass<T>
{
}
}
注意点2
类实现泛型接口也是这种情况
namespace MyGeneric
{
/// <summary>
/// 必须指定具体类型
/// </summary>
public class Common : IGenericInterface<string>
{
public string GetT(string t)
{
throw new NotImplementedException();
}
}
/// <summary>
/// 可以不知道具体类型,但是子类也必须是泛型的
/// </summary>
/// <typeparam name="T"></typeparam>
public class CommonChild<T> : IGenericInterface<T>
{
public T GetT(T t)
{
throw new NotImplementedException();
}
}
}
泛型约束
为什么要泛型约束,其主要问题还是解决安全问题,规范开发人员写代码的规范性,避免一些在运行时期才能检查到的错误。比如下面的代码,在编译器是不会报错的,但是在运行期会出现转换异常。
public class 动物
{
}
public class 狗 :动物
{
}
public class 猫
{
}
public class Generic
{
public void method(object cat)
{
动物 animal= (动物)cat;
}
}
所谓的泛型约束,实际上就是约束的类型T。使T必须遵循一定的规则。比如T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单,只需要where关键字,加上约束的条件。
序号 | 约束 | 说明 |
---|---|---|
1 | T:struct | 类型参数必须是值类型 |
2 | T:class | 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。 |
3 | T:new() | 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。 |
4 | T:基类名 | 类型参数必须是指定的基类或派生自指定的基类。 |
5 | T:接口名称 | 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。 |
6 | T:基类名,接口名称,new() | 泛型约束也可以同时约束多个,但是new()必须放在最后 |
协变和逆变
在OO的世界里,可以安全地把子类的引用赋给父类引用。但是在T的世界里,就不一定了。有的能变,有的不能变,先了解以下几点:
- 以前的泛型系统(或者说没有in/out关键字时),是不能“变”的,无论是“逆”还是“顺(协)”。
- 当前仅支持接口和委托的逆变与协变 ,不支持类和方法。但数组也有协变性。
- 值类型不参与逆变与协变。
如果不能理解以上几句话,就先看下面的知识点
协变
所谓协变,就是为了解决子类泛型接口或委托能回到父类泛型接口或委托上来。(Foo = Foo )
//泛型委托:
public delegate T MyFuncA<T>();//不支持逆变与协变
public delegate T MyFuncB<out T>();//支持协变
MyFuncA<object> funcAObject = null;
MyFuncA<string> funcAString = null;
MyFuncB<object> funcBObject = null;
MyFuncB<string> funcBString = null;
MyFuncB<int> funcBInt = null;
funcAObject = funcAString;//编译失败,MyFuncA不支持逆变与协变
funcBObject = funcBString;//变了,协变
funcBObject = funcBInt;//编译失败,值类型不参与协变或逆变
//泛型接口
public interface IFlyA<T> { }//不支持逆变与协变
public interface IFlyB<out T> { }//支持协变
IFlyA<object> flyAObject = null;
IFlyA<string> flyAString = null;
IFlyB<object> flyBObject = null;
IFlyB<string> flyBString = null;
IFlyB<int> flyBInt = null;
flyAObject = flyAString;//编译失败,IFlyA不支持逆变与协变
flyBObject = flyBString;//变了,协变
flyBObject = flyBInt;//编译失败,值类型不参与协变或逆变
//数组:
string[] strings = new string[] { "string" };
object[] objects = strings;
逆变
所谓协变,就是为了解决父类泛型接口或委托能回到子类泛型接口或委托上来。(Foo = Foo)
public delegate void MyActionA<T>(T param);//不支持逆变与协变
public delegate void MyActionB<in T>(T param);//支持逆变
public interface IPlayA<T> { }//不支持逆变与协变
public interface IPlayB<in T> { }//支持逆变
MyActionA<object> actionAObject = null;
MyActionA<string> actionAString = null;
MyActionB<object> actionBObject = null;
MyActionB<string> actionBString = null;
actionAString = actionAObject;//MyActionA不支持逆变与协变,编译失败
actionBString = actionBObject;//变了,逆变
IPlayA<object> playAObject = null;
IPlayA<string> playAString = null;
IPlayB<object> playBObject = null;
IPlayB<string> playBString = null;
playAString = playAObject;//IPlayA不支持逆变与协变,编译失败
playBString = playBObject;//变了,逆变
注意
in/out是什么意思呢?为什么加了它们就有了“变”的能力,是不是我们定义泛型委托或者接口都应该添加它们呢?
原来,在泛型参数上添加了in关键字作为泛型修饰符的话,那么那个泛型参数就只能用作方法的输入参数,或者只写属性的参数,不能作为方法返回值等,总之就是只能是“入”,不能出。out关键字反之。
泛型的本质
为什么泛型可以解决的各种类型问题呢?
泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。 延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。
泛型究竟是如何工作的呢?
控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译生成的占位符替换成具体的数据类型。请看下面一个例子:
1 Console.WriteLine(typeof(List<>));
2 Console.WriteLine(typeof(Dictionary<,>));
从上面的截图中可以看出:泛型在编译之后会生成占位符。
其次,通过测试发现泛型方法的性能与普通方法相等,object方法的性能最低。
总结
泛型已经发展了很多年,现在流行DDD开发方式,使用泛型解决各种问题也越来越多,以上总结只是个人翻看别人博客,然后学习留下记录。如果要查看更多信息,可以浏览更多链接。如有帮助,点个赞。
撸完泛型,接下来还要重新回顾撸两个东西
引用