泛型,顾名思义,首先它是一个“类”型,其次修饰它的是“泛”,有广泛、宽泛之意。
简单而言,带<T>就是泛型。
初识泛型,是在四五年前刚学习C#时,看当时公司大牛的一段代码(向数据库插入一条数据,类似的还有删改查):
public bool Insert<T>(T entity)
{
try
{
var type = typeof(T);
var className = type.Name;
//...code to realize
return true;
}
catch{
return false
}
}
就感觉这样的代码好高大上啊,一个方法可以根据传入的参数的不同类型来进行统一的实现,针方便呢!(泛型方法、泛型参数)
于是便有了这篇博客(ps:至于为什么是几年前就知道,现在才想起来写博客,最大的原因就是懒吧,另一原因也是水平的限制)!
1.泛型
先上微软文档中对泛型概念的描述(不是系统地学习,但要学会系统地查资料):泛型概念
*概述
●使用泛型类型可以最大限度地重用代码、保护类型安全性以及提高性能。
●泛型最常见的用途是创建集合类。
●.NET 类库在System.Collections.Generic命名空间中包含几个新的泛型集合类。 应尽可能使用这些类来代替某些类,如System.Collections命名空间中的ArrayList。
●可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
●可以对泛型类进行约束以访问特定数据类型的方法。
●在泛型数据类型中所用类型的信息可在运行时通过使用反射来获取。
对于概述的第一条,以下是自己的理解。
可重用性,可以根据需要传入各种类型的参数,比如要对类型的所有属性名称、值进行获取(获取从文件中进行读取并对其赋值),可以通过反射获取该实例或该类型的type对象,然后对其属性进行遍历即可。如下代码:
private void InputPropertiesAndValue<T>(T obj)
{
for (int i = 0; i < obj.GetType().GetProperties().Length; i++)
{
PropertyInfo _property = obj.GetType().GetProperties()[i];
Type _property_type = _property.PropertyType;
var _property_value = _property.GetValue(obj, null);
Console.WriteLine(_property.Name + "=" + _property_value?.ToString());
}
}
类型安全性,在泛型方法使用时,需要将T指定为特定的类型,而在需要传入的对象并非指定类型时,编译器就会报错,导致无法编译通过,则保证了类型的安全性。如下代码:
List<int> aList = new List<int>();
aList.Add(3);
aList.Add(4);
//aList.Add(5.0);//若取消注释,则编译器在此处报错——System.InvalidCastException:指定转换无效
int totalList = 0;
foreach(int val in aList)
{
totalList = totalList + val;
}
Console.WriteLine("Total is {0}", totalList);
效率的话个人认为一方面是针对运行时中的泛型,即编译好的程序在运行时刻对内存的使用。是对与非泛型的对比而言的,见下文泛型和非泛型集合对比。
2.泛型类、泛型接口
* 泛型类封装不特定于特定数据类型的操作。(下文将对概念不再进行赘述)
对于泛型类的继承,其中很重要的一句话:非泛型类(即具体类)可继承自封闭式构造基类,但不可继承自开放式构造类或类型参数,因为运行时客户端代码无法提供实例化基类所需的类型参数。
* 泛型接口,主要是为泛型集合类或表示集合中的项的泛型类定义接口。参考:泛型接口
(协变、逆变,重点在于理解)
* 说起泛型类,就不得不说到泛型集合(常见的有List、Dictionary、Queue等),当然还有非泛型集合(ArrayList),如何正确地使用这两类集合?
可以记住这么一句话,不用考虑使用非泛型集合。就性能而言,非泛型可能包含对object类型的装箱拆箱操作(消耗CPU);就易出错程度而言,非泛型集合的装箱拆箱导致的类型转换在运行时可能出错。举例如下:
ArrayList arr=new ArrayList();
arr.add(100);
arr.add("100");
foreach(int item in arr)//编译可以通过,但运行时会出错,但是另外的解决方法可以改为下方注释内容
//foreach(var item in arr)
{
Console.WriteLine(item);
}
参考:不应使用非泛型集合
3.泛型类型参数、类型参数约束、泛型方法
* 泛型类型参数
将泛型类型作为方法、类、接口等的参数。
* 类型参数约束
where的使用,是对类型参数进行约束,影响在于不符合约束的泛型类在实现时,编译器会报错。
* 泛型方法
就是带有泛型类型参数的方法。
4.运行时中的泛型
在上一小节“泛型类型参数”参考文档中提到了运行时中的泛型,其中主要内容可以理解为:
泛型类型或方法在编译为微软中间语言MSIL时,它包含将其标识为具有类型参数的元数据。
泛型类型参数为值类型:
运行时会为值类型的泛型生成不同的专用版的Stack<T>“模板”类,如根据需要创建了Stack<int>和Stack<double>类,所有泛型类型参数为int型的对象使用的是相同的Stack<int>类的实例,doule亦然,而两者使用的是不同“模板”(不同的地址)。
泛型类型参数为引用类型:
如值类型,但是不同的是对所有引用类型,只会生成一个专用版的Stack<T>“模板”类,根据T为不同引用类型,使用的是同一个“模板”的不同实例(相同的地址)。
5.泛型委托、泛型和反射、泛型与特性
* 泛型委托,除了下面代码中使用委托定义事件,还包括Func、Action等形式的委托
/// <summary>
/// 根据典型设计模式定义事件时,泛型委托特别有用,因为发件人参数可以为强类型,无需在它和 Object 之间强制转换
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="sender"></param>
/// <param name="eventArgs"></param>
delegate void StackEventHandler<T, U>(T sender, U eventArgs);
class Stacks<T>
{
public class StackEventArgs : System.EventArgs { }
public event StackEventHandler<Stacks<T>, StackEventArgs> stackEvent;
public virtual void OnStackChanged(StackEventArgs a)
{
stackEvent(this, a);
}
}
class SampleClass
{
public void HandleStackChange<T>(Stacks<T> stack, Stacks<T>.StackEventArgs args)
{
// do work
}
}
public static void Test()
{
Stacks<double> s = new Stacks<double>();
SampleClass o = new SampleClass();
s.stackEvent += o.HandleStackChange;
//触发事件
s.OnStackChanged(new Stacks<double>.StackEventArgs());
}
* 泛型和反射
参考“可重用性”代码,通过反射获取T的属性信息。
* 泛型和特性
使用特性,可以有效地将元数据或声明性信息与代码(程序集、类型、方法、属性等)相关联。常见的特性使用场景有:DllImportAttribute调用非托管代码、SerializableAttribute将类或类成员标记为可序列化、调用 Attribute.GetCustomAttribute方法获取用户自定义特性(如对属性的描述)。
下面列出了代码中特性的一些常见用途:
- 在 Web 服务中使用
WebMethod
特性标记方法,以指明方法应可通过 SOAP 协议进行调用。 有关详细信息,请参阅 WebMethodAttribute。 - 描述在与本机代码互操作时如何封送方法参数。 有关详细信息,请参阅 MarshalAsAttribute。
- 描述类、方法和接口的 COM 属性。
- 使用 DllImportAttribute 类调用非托管代码。
- 从标题、版本、说明或商标方面描述程序集。
- 描述要序列化并暂留类的哪些成员。
- 描述如何为了执行 XML 序列化在类成员和 XML 节点之间进行映射。
- 描述的方法的安全要求。
- 指定用于强制实施安全规范的特征。
- 通过实时 (JIT) 编译器控制优化,这样代码就一直都易于调试。
- 获取方法调用方的相关信息。
6.协变和逆变
协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。实际上无论是逆变还是协变,都是对引用类型(数组、委托、泛型类型参数)的隐式转换。而我们自定义的父类子类实际上并不存在转换,只能作为支持协变、逆变的数组中、委托中、泛型类型参数的类型时。参考:协变和逆变
如果泛型接口或委托的泛型参数被声明为协变或逆变,该泛型接口或委托则被称为“变体”。参考:具有协变、逆变类型参数的泛型接口、泛型接口中的变体 、委托中的变体
可以参考另一篇不错的博客,对协变和逆变讲的比较清楚:协变、逆变
看完这些知识内容,再看下面的代码,是不是豁然开朗,记住一句,协变和逆变,都只是进行了隐式引用转换。
public static void Main()
{
IEnumerable<Base2> base2s = new List<Derived>();//IEnumerable是协变接口
//IEnumerable<Derived> deriveds = base2s;//无法将类型
//“System.Collections.Generic.IEnumerable<Generic.Base2>”隐式转换为
//“System.Collections.Generic.IEnumerable<Generic.Derived>”。
//存在一个显式转换(是否缺少强制转换?)
//如要实现上述转换,需要自定义的逆变接口(即变体),并将Base2转换为Derived
//(new一个Derived并根据Base2属性将其赋值),个人觉得实际上小工程中不存在这样的使用场景
//(需要时直接返回派生对象即可)
Action<Base2> action = new Action<Base2>((o) => Console.WriteLine(o.ToString()));
action(new Base2());
action(new Derived());
Action<Derived> action1 = action;
//action1(new Base2());//无法从“Generic.Base2”转换为“Generic.Derived”
action1(new Derived());
}
public class Base2{}
public class Derived:Base2{}
7.Git参考
具体代码及微软文档中简单demo可参考本人Github库:C#基础演练,参考其中的“Generic”工程,如果对您有帮助,还麻烦点个小小的Star,谢谢!