什么是泛型?(C#概念)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是泛型?(C#概念)相关的知识,希望对你有一定的参考价值。

不要MSDN,CSDN上的概念。要比较容易懂的解释或者比方。

在给你比较好懂的:
通过泛型可以定义类型安全类,而不会损害类型安全、性能或工作效率。您只须一次性地将服务器实现为一般服务器,同时可以用任何类型来声明和使用它。为此,需要使用 < 和 > 括号,以便将一般类型参数括起来。例如,可以按如下方式定义和使用一般堆栈:
public class Stack T[] m_Items; public void Push(T item) ... public T Pop() ...Stack stack = new Stack();stack.Push(1);stack.Push(2);int number = stack.Pop();
代码块 2 显示一般堆栈的完整实现。将代码块 1 与代码块 2 进行比较,您会看到,好像 代码块 1 中每个使用 Object 的地方在代码块 2 中都被替换成了 T,除了使用一般类型参数 T 定义 Stack 以外:
public class Stack...
在使用一般堆栈时,必须通知编译器使用哪个类型来代替一般类型参数 T(无论是在声明变量时,还是在实例化变量时):
Stack stack = new Stack();
编译器和运行库负责完成其余工作。所有接受或返回 T 的方法(或属性)都将改为使用指定的类型(在上述示例中为整型)。
代码块 2. 一般堆栈
public class Stack readonly int m_Size; int m_StackPointer = 0; T[] m_Items; public Stack():this(100) public Stack(int size) m_Size = size; m_Items = new T[m_Size]; public void Push(T item) if(m_StackPointer >= m_Size) throw new StackOverflowException(); m_Items[m_StackPointer] = item; m_StackPointer++; public T Pop() m_StackPointer--; if(m_StackPointer >= 0) return m_Items[m_StackPointer]; else m_StackPointer = 0; throw new InvalidOperationException("Cannot pop an empty stack");

注 T 是一般类型参数(或类型参数),而一般类型为 Stack。Stack 中的 int 为类型实参。
该编程模型的优点在于,内部算法和数据操作保持不变,而实际数据类型可以基于客户端使用服务器代码的方式进行更改。
泛型实现
表面上,C# 泛型的语法看起来与 C++ 模板类似,但是编译器实现和支持它们的方式存在重要差异。正如您将在后文中看到的那样,这对于泛型的使用方式具有重大意义。
注 在本文中,当提到 C++ 时,指的是传统 C++,而不是带有托管扩展的 Microsoft C++。
与 C++ 模板相比,C# 泛型可以提供增强的安全性,但是在功能方面也受到某种程度的限制。
在一些 C++ 编译器中,在您通过特定类型使用模板类之前,编译器甚至不会编译模板代码。当您确实指定了类型时,编译器会以内联方式插入代码,并且将每个出现一般类型参数的地方替换为指定的类型。此外,每当您使用特定类型时,编译器都会插入特定于该类型的代码,而不管您是否已经在应用程序中的其他某个位置为模板类指定了该类型。C++ 链接器负责解决该问题,并且并不总是有效。这可能会导致代码膨胀,从而增加加载时间和内存足迹。
在 .NET 2.0 中,泛型在 IL(中间语言)和 CLR 本身中具有本机支持。在编译一般 C# 服务器端代码时,编译器会将其编译为 IL,就像其他任何类型一样。但是,IL 只包含实际特定类型的参数或占位符。此外,一般服务器的元数据包含一般信息。
客户端编译器使用该一般元数据来支持类型安全。当客户端提供特定类型而不是一般类型参数时,客户端的编译器将用指定的类型实参来替换服务器元数据中的一般类型参数。这会向客户端的编译器提供类型特定的服务器定义,就好像从未涉及到泛型一样。这样,客户端编译器就可以确保方法参数的正确性,实施类型安全检查,甚至执行类型特定的 IntelliSense。
有趣的问题是,.NET 如何将服务器的一般 IL 编译为机器码。原来,所产生的实际机器码取决于指定的类型是值类型还是引用类型。如果客户端指定值类型,则 JIT 编译器将 IL 中的一般类型参数替换为特定的值类型,并且将其编译为本机代码。但是,JIT 编译器会跟踪它已经生成的类型特定的服务器代码。如果请求 JIT 编译器用它已经编译为机器码的值类型编译一般服务器,则它只是返回对该服务器代码的引用。因为 JIT 编译器在以后的所有场合中都将使用相同的值类型特定的服务器代码,所以不存在代码膨胀问题。
如果客户端指定引用类型,则 JIT 编译器将服务器 IL 中的一般参数替换为 Object,并将其编译为本机代码。在以后的任何针对引用类型而不是一般类型参数的请求中,都将使用该代码。请注意,采用这种方式,JIT 编译器只会重新使用实际代码。实例仍然按照它们离开托管堆的大小分配空间,并且没有强制类型转换。
泛型的好处
.NET 中的泛型使您可以重用代码以及在实现它时付出的努力。类型和内部数据可以在不导致代码膨胀的情况下更改,而不管您使用的是值类型还是引用类型。您可以一次性地开发、测试和部署代码,通过任何类型(包括将来的类型)来重用它,并且全部具有编译器支持和类型安全。因为一般代码不会强行对值类型进行装箱和取消装箱,或者对引用类型进行向下强制类型转换,所以性能得到显著提高。对于值类型,性能通常会提高 200%;对于引用类型,在访问该类型时,可以预期性能最多提高 100%(当然,整个应用程序的性能可能会提高,也可能不会提高)。本文随附的源代码包含一个微型基准应用程序,它在紧密循环中执行堆栈。该应用程序使您可以在基于 Object 的堆栈和一般堆栈上试验值类型和引用类型,以及更改循环迭代的次数以查看泛型对性能产生的影响。
参考技术A 2.0 版 C# 语言和公共语言运行库 (CLR) 中增加了泛型。泛型将类型参数的概念引入 .NET Framework,类型参数使得设计如下类和方法成为可能:这些类和方法将一个或多个类型的指定推迟到客户端代码声明并实例化该类或方法的时候。例如,通过使用泛型类型参数 T,您可以编写其他客户端代码能够使用的单个类,而不致引入运行时强制转换或装箱操作的成本或风险,如下所示:

C# 复制代码
// Declare the generic class.
public class GenericList<T>

void Add(T input)

class TestGenericList

private class ExampleClass
static void Main()

// Declare a list of type int.
GenericList<int> list1 = new GenericList<int>();

// Declare a list of type string.
GenericList<string> list2 = new GenericList<string>();

// Declare a list of type ExampleClass.
GenericList<ExampleClass> list3 = new GenericList<ExampleClass>();



泛型概述
使用泛型类型可以最大限度地重用代码、保护类型的安全以及提高性能。

泛型最常见的用途是创建集合类。

.NET Framework 类库在 System.Collections.Generic 命名空间中包含几个新的泛型集合类。应尽可能地使用这些类来代替普通的类,如 System.Collections 命名空间中的 ArrayList。

您可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。

可以对泛型类进行约束以访问特定数据类型的方法。

关于泛型数据类型中使用的类型的信息可在运行时通过使用反射获取。
参考技术B 下面的代码示例演示客户端代码如何使用泛型 GenericList<T> 类来创建整数列表。只需更改类型参数,即可方便地修改下面的代码示例,创建字符串或任何其他自定义类型的列表:

C# 复制代码
class TestGenericList

static void Main()

// int is the type argument
GenericList<int> list = new GenericList<int>();

for (int x = 0; x < 10; x++)

list.AddHead(x);


foreach (int i in list)

System.Console.Write(i + " ");

System.Console.WriteLine("\nDone");

参考技术C 泛型编程让你编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。泛型编程的代表作品STL是一种高效、泛型、可交互操作的软件组件。所谓泛型(Genericity),是指具有在多种数据类型上皆可操作的含意,与模板有些相似。STL巨大,而且可以扩充,它包含很多计算机基本算法和数据结构,而且将算法与数据结构完全分离,其中算法是泛型的,不与任何特定数据结构或对象类型系在一起。STL以迭代器(Iterators)和容器(Containers)为基础,是一种泛型算法(Generic Algorithms)库,容器的存在使这些算法有东西可以操作。STL包含各种泛型算法(algorithms)、泛型指针(iterators)、泛型容器(containers)以及函数对象(function objects)。STL并非只是一些有用组件的集合,它是描述软件组件抽象需求条件的一个正规而有条理的架构。

C#泛型详解

这篇文章主要讲解C#中的泛型,泛型在C#中有很重要的地位,尤其是在搭建项目框架的时候。

一、什么是泛型

泛型是C#2.0推出的新语法,不是语法糖,而是2.0由框架升级提供的功能。

我们在编程程序时,经常会遇到功能非常相似的模块,只是它们处理的数据不一样。但我们没有办法,只能分别写多个方法来处理不同的数据类型。这个时候,那么问题来了,有没有一种办法,用同一个方法来处理传入不同种类型参数的办法呢?泛型的出现就是专门来解决这个问题的。

二、为什么使用泛型

先来看下面一个例子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

public class CommonMethod

/// <summary>
/// 打印个int值
///
/// 因为方法声明的时候,写死了参数类型
/// 已婚的男人 Eleven San
/// </summary>
/// <param name="iParameter"></param>
public static void ShowInt(int iParameter)

Console.WriteLine("This is 0,parameter=1,type=2",
typeof(CommonMethod).Name, iParameter.GetType().Name, iParameter);

/// <summary>
/// 打印个string值
/// </summary>
/// <param name="sParameter"></param>
public static void ShowString(string sParameter)

Console.WriteLine("This is 0,parameter=1,type=2",
typeof(CommonMethod).Name, sParameter.GetType().Name, sParameter);

/// <summary>
/// 打印个DateTime值
/// </summary>
/// <param name="oParameter"></param>
public static void ShowDateTime(DateTime dtParameter)

Console.WriteLine("This is 0,parameter=1,type=2",
typeof(CommonMethod).Name, dtParameter.GetType().Name, dtParameter);


结果:

技术图片

从上面的结果中我们可以看出这三个方法,除了传入的参数不同外,其里面实现的功能都是一样的。在1.0版的时候,还没有泛型这个概念,那么怎么办呢。相信很多人会想到了OOP三大特性之一的继承,我们知道,C#语言中,object是所有类型的基类,将上面的代码进行以下优化:

public static void ShowObject(object oParameter)

      Console.WriteLine("This is 0,parameter=1,type=2",
         typeof(CommonMethod), oParameter.GetType().Name, oParameter);

 结果:

技术图片

从上面的结果中我们可以看出,使用Object类型达到了我们的要求,解决了代码的可复用。可能有人会问定义的是object类型的,为什么可以传入int、string等类型呢?原因有二:

1、object类型是一切类型的父类。

2、通过继承,子类拥有父类的一切属性和行为,任何父类出现的地方,都可以用子类来代替。

但是上面object类型的方法又会带来另外一个问题:装箱和拆箱,会损耗程序的性能。

微软在C#2.0的时候推出了泛型,可以很好的解决上面的问题。

三、泛型类型参数

在泛型类型或方法定义中,类型参数是在其实例化泛型类型的一个变量时,客户端指定的特定类型的占位符。 泛型类( GenericList<T>)无法按原样使用,因为它不是真正的类型;它更像是类型的蓝图。 若要使用 GenericList<T>,客户端代码必须通过指定尖括号内的类型参数来声明并实例化构造类型。 此特定类的类型参数可以是编译器可识别的任何类型。 可创建任意数量的构造类型实例,其中每个使用不同的类型参数。

上面例子中的代码可以修改如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

public class GenericMethod

/// <summary>
/// 泛型方法
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tParameter"></param>
public static void Show<T>(T tParameter)

Console.WriteLine("This is 0,parameter=1,type=2",
typeof(GenericMethod), tParameter.GetType().Name, tParameter.ToString());


调用:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

class Program

static void Main(string[] args)

int iValue = 123;
string sValue = "456";
DateTime dtValue = DateTime.Now;

Console.WriteLine("***********CommonMethod***************");
CommonMethod.ShowInt(iValue);
CommonMethod.ShowString(sValue);
CommonMethod.ShowDateTime(dtValue);
Console.WriteLine("***********Object***************");
CommonMethod.ShowObject(iValue);
CommonMethod.ShowObject(sValue);
CommonMethod.ShowObject(dtValue);
Console.WriteLine("***********Generic***************");
GenericMethod.Show<int>(iValue);
GenericMethod.Show<string>(sValue);
GenericMethod.Show<DateTime>(dtValue);
Console.ReadKey();


显示结果:

技术图片

为什么泛型可以解决上面的问题呢?

泛型是延迟声明的:即定义的时候没有指定具体的参数类型,把参数类型的声明推迟到了调用的时候才指定参数类型。 延迟思想在程序架构设计的时候很受欢迎。例如:分布式缓存队列、EF的延迟加载等等。

泛型究竟是如何工作的呢?

控制台程序最终会编译成一个exe程序,exe被点击的时候,会经过JIT(即时编译器)的编译,最终生成二进制代码,才能被计算机执行。泛型加入到语法以后,VS自带的编译器又做了升级,升级之后编译时遇到泛型,会做特殊的处理:生成占位符。再次经过JIT编译的时候,会把上面编译生成的占位符替换成具体的数据类型。请看下面一个例子:

1 Console.WriteLine(typeof(List<>));
2 Console.WriteLine(typeof(Dictionary<,>));

 结果:

技术图片

从上面的截图中可以看出:泛型在编译之后会生成占位符。

注意:占位符需要在英文输入法状态下才能输入,只需要按一次波浪线(数字1左边的键位)的键位即可,不需要按Shift键。

1、泛型性能问题

请看一下的一个例子,比较普通方法、Object参数类型的方法、泛型方法的性能。

添加一个Monitor类,让三种方法执行同样的操作,比较用时长短:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

public class Monitor

public static void Show()

Console.WriteLine("****************Monitor******************");

int iValue = 12345;
long commonSecond = 0;
long objectSecond = 0;
long genericSecond = 0;


Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 100000000; i++)

ShowInt(iValue);

watch.Stop();
commonSecond = watch.ElapsedMilliseconds;


Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 100000000; i++)

ShowObject(iValue);

watch.Stop();
objectSecond = watch.ElapsedMilliseconds;


Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 100000000; i++)

Show<int>(iValue);

watch.Stop();
genericSecond = watch.ElapsedMilliseconds;

Console.WriteLine("commonSecond=0,objectSecond=1,genericSecond=2"
, commonSecond, objectSecond, genericSecond);

#region PrivateMethod
private static void ShowInt(int iParameter)

//do nothing

private static void ShowObject(object oParameter)

//do nothing

private static void Show<T>(T tParameter)

//do nothing

#endregion


Main()方法调用:

1 Monitor.Show();

 结果:

技术图片

从结果中可以看出:泛型方法的性能最高,其次是普通方法,object方法的性能最低。

四、泛型类

除了方法可以是泛型以外,类也可以是泛型的,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

/// <summary>
/// 泛型类
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericClass<T>

public T _T;

Main()方法中调用:

// T是int类型
GenericClass<int> genericInt = new GenericClass<int>();
genericInt._T = 123;
// T是string类型
GenericClass<string> genericString = new GenericClass<string>();
genericString._T = "123";

除了可以有泛型类,也可以有泛型接口,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

/// <summary>
/// 泛型接口
/// </summary>
public interface IGenericInterface<T>

//泛型类型的返回值
T GetT(T t);

也可以有泛型委托:

1 public delegate void SayHi<T>(T t);//泛型委托

 注意:

1、泛型在声明的时候可以不指定具体的类型,但是在使用的时候必须指定具体类型,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

/// <summary>
/// 使用泛型的时候必须指定具体类型,
/// 这里的具体类型是int
/// </summary>
public class CommonClass :GenericClass<int>


如果子类也是泛型的,那么继承的时候可以不指定具体类型,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

/// <summary>
/// 使用泛型的时候必须指定具体类型,
/// 这里的具体类型是int
/// </summary>
public class CommonClass :GenericClass<int>

/// <summary>
/// 子类也是泛型的,继承的时候可以不指定具体类型
/// </summary>
/// <typeparam name="T"></typeparam>
public class CommonClassChild<T>:GenericClass<T>


2、类实现泛型接口也是这种情况,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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();


五、泛型约束

先来看看下面的一个例子:

定义一个People类,里面有属性和方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

public interface ISports

void Pingpang();

public interface IWork

void Work();


public class People

public int Id get; set;
public string Name get; set;

public void Hi()

Console.WriteLine("Hi");

public class Chinese : People, ISports, IWork

public void Tradition()

Console.WriteLine("仁义礼智信,温良恭俭让");

public void SayHi()

Console.WriteLine("吃了么?");

public void Pingpang()

Console.WriteLine("打乒乓球...");

public void Work()

throw new NotImplementedException();

public class Hubei : Chinese

public Hubei(int version)

public string Changjiang get; set;
public void Majiang()

Console.WriteLine("打麻将啦。。");


public class Japanese : ISports

public int Id get; set;
public string Name get; set;
public void Hi()

Console.WriteLine("Hi");

public void Pingpang()

Console.WriteLine("打乒乓球...");


在Main()方法里面实例化:

People people = new People()

Id = 123,
Name = "走自己的路"
;
Chinese chinese = new Chinese()

Id = 234,
Name = "晴天"
;
Hubei hubei = new Hubei(123)

Id = 345,
Name = "流年"
;
Japanese japanese = new Japanese()

Id = 7654,
Name = "werwer"
;

这时有一个需求:需要打印出Id和Name属性的值,将ShowObject()方法修改如下:

技术图片

但是这样修改报错了:object类里面没有Id和Name属性,可能会有人说,强制类型转换一下就行了啊:

public static void ShowObject(object oParameter)

Console.WriteLine("This is 0,parameter=1,type=2",
typeof(CommonMethod), oParameter.GetType().Name, oParameter);

Console.WriteLine($"((People)oParameter).Id_((People)oParameter).Name");

这样修改以后,代码不会报错了,这时我们在Main()方法里面调用:

1 CommonMethod.ShowObject(people);
2 CommonMethod.ShowObject(chinese);
3 CommonMethod.ShowObject(hubei);
4 CommonMethod.ShowObject(japanese);

 结果:

技术图片

可以看出程序报错了,因为Japanese没有继承自People,这里类型转换的时候失败了。这样会造成类型不安全的问题。那么怎么解决类型不安全的问题呢?那就是使用泛型约束。

所谓的泛型约束,实际上就是约束的类型T。使T必须遵循一定的规则。比如T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单,只需要where关键字,加上约束的条件。

泛型约束总共有五种。

约束 s说明
T:结构 类型参数必须是值类型
T:类 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。
T:new() 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。
T:<基类名> 类型参数必须是指定的基类或派生自指定的基类。
T:<接口名称> 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。

1、基类约束

上面打印的方法约束T类型必须是People类型。

/// <summary>
/// 基类约束:约束T必须是People类型或者是People的子类
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tParameter"></param>
public static void Show<T>(T tParameter) where T : People

Console.WriteLine($"tParameter.Id_tParameter.Name");
tParameter.Hi();

注意:

基类约束时,基类不能是密封类,即不能是sealed类。sealed类表示该类不能被继承,在这里用作约束就无任何意义,因为sealed类没有子类。

2、接口约束

/// <summary>
/// 接口约束
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
/// <returns></returns>
public static T Get<T>(T t) where T : ISports

t.Pingpang();
return t;

3、引用类型约束 class

引用类型约束保证T一定是引用类型的。

/// <summary>
/// 引用类型约束
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
/// <returns></returns>
public static T Get<T>(T t) where T : class

return t;

4、值类型约束  struct

值类型约束保证T一定是值类型的。

/// <summary>
/// 值类型类型约束
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
/// <returns></returns>
public static T Get<T>(T t) where T : struct

return t;

5、无参数构造函数约束  new()

/// <summary>
/// new()约束
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="t"></param>
/// <returns></returns>
public static T Get<T>(T t) where T : new()

return t;

泛型约束也可以同时约束多个,例如:

public static void Show<T>(T tParameter)
where T : People, ISports, IWork, new()

Console.WriteLine($"tParameter.Id_tParameter.Name");
tParameter.Hi();
tParameter.Pingpang();
tParameter.Work();

注意:有多个泛型约束时,new()约束一定是在最后。

六、泛型的协变和逆变

协变和逆变是在.NET 4.0的时候出现的,只能放在接口或者委托的泛型参数前面,out 协变covariant,用来修饰返回值;in:逆变contravariant,用来修饰传入参数。

先看下面的一个例子:

定义一个Animal类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

public class Animal

public int Id get; set;

然后在定义一个Cat类继承自Animal类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

public class Cat :Animal

public string Name get; set;

在Main()方法可以这样调用:

// 直接声明Animal类
Animal animal = new Animal();
// 直接声明Cat类
Cat cat = new Cat();
// 声明子类对象指向父类
Animal animal2 = new Cat();
// 声明Animal类的集合
List<Animal> listAnimal = new List<Animal>();
// 声明Cat类的集合
List<Cat> listCat = new List<Cat>();

那么问题来了:下面的一句代码是不是正确的呢?

1 List<Animal> list = new List<Cat>();

 可能有人会认为是正确的:因为一只Cat属于Animal,那么一群Cat也应该属于Animal啊。但是实际上这样声明是错误的:因为List<Cat>和List<Animal>之间没有父子关系。

技术图片

这时就可以用到协变和逆变了。

1 // 协变
2 IEnumerable<Animal> List1 = new List<Animal>();
3 IEnumerable<Animal> List2 = new List<Cat>();

 F12查看定义:

技术图片

可以看到,在泛型接口的T前面有一个out关键字修饰,而且T只能是返回值类型,不能作为参数类型,这就是协变。使用了协变以后,左边声明的是基类,右边可以声明基类或者基类的子类。

协变除了可以用在接口上面,也可以用在委托上面:

1 Func<Animal> func = new Func<Cat>(() => null);

 除了使用.NET框架定义好的以为,我们还可以自定义协变,例如:

/// <summary>
/// out 协变 只能是返回结果
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ICustomerListOut<out T>

T Get();

public class CustomerListOut<T> : ICustomerListOut<T>

public T Get()

return default(T);

使用自定义的协变:

1 // 使用自定义协变
2 ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>();
3 ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>();

 在来看看逆变。

在泛型接口的T前面有一个In关键字修饰,而且T只能方法参数,不能作为返回值类型,这就是逆变。请看下面的自定义逆变:

/// <summary>
/// 逆变 只能是方法参数
/// </summary>
/// <typeparam name="T"></typeparam>
public interface ICustomerListIn<in T>

void Show(T t);

public class CustomerListIn<T> : ICustomerListIn<T>

public void Show(T t)


使用自定义逆变:

1 // 使用自定义逆变
2 ICustomerListIn<Cat> customerListCat1 = new CustomerListIn<Cat>();
3 ICustomerListIn<Cat> customerListCat2 = new CustomerListIn<Animal>();

协变和逆变也可以同时使用,看看下面的例子:

/// <summary>
/// inT 逆变
/// outT 协变
/// </summary>
/// <typeparam name="inT"></typeparam>
/// <typeparam name="outT"></typeparam>
public interface IMyList<in inT, out outT>

void Show(inT t);
outT Get();
outT Do(inT t);

public class MyList<T1, T2> : IMyList<T1, T2>

public void Show(T1 t)

Console.WriteLine(t.GetType().Name);

public T2 Get()

Console.WriteLine(typeof(T2).Name);
return default(T2);

public T2 Do(T1 t)

Console.WriteLine(t.GetType().Name);
Console.WriteLine(typeof(T2).Name);
return default(T2);

使用:

1 IMyList<Cat, Animal> myList1 = new MyList<Cat, Animal>();
2 IMyList<Cat, Animal> myList2 = new MyList<Cat, Cat>();//协变
3 IMyList<Cat, Animal> myList3 = new MyList<Animal, Animal>();//逆变
4 IMyList<Cat, Animal> myList4 = new MyList<Animal, Cat>();//逆变+协变

 七、泛型缓存

在前面我们学习过,类中的静态类型无论实例化多少次,在内存中只会有一个。静态构造函数只会执行一次。在泛型类中,T类型不同,每个不同的T类型,都会产生一个不同的副本,所以会产生不同的静态属性、不同的静态构造函数,请看下面的例子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric

public class GenericCache<T>

static GenericCache()

Console.WriteLine("This is GenericCache 静态构造函数");
_TypeTime = string.Format("0_1", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff"));

private static string _TypeTime = "";

public static string GetCache()

return _TypeTime;


然后新建一个测试类,用来测试GenericCache类的执行顺序:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace MyGeneric

public class GenericCacheTest

public static void Show()

for (int i = 0; i < 5; i++)

Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCache<long>.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCache<DateTime>.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCache<string>.GetCache());
Thread.Sleep(10);
Console.WriteLine(GenericCache<GenericCacheTest>.GetCache());
Thread.Sleep(10);



Main()方法里面调用:

1 GenericCacheTest.Show();

结果:

技术图片

从上面的截图中可以看出,泛型会为不同的类型都创建一个副本,所以静态构造函数会执行5次。 而且每次静态属性的值都是一样的。利用泛型的这一特性,可以实现缓存。

注意:只能为不同的类型缓存一次。泛型缓存比字典缓存效率高。泛型缓存不能主动释放

以上是关于什么是泛型?(C#概念)的主要内容,如果未能解决你的问题,请参考以下文章

java中什么是泛型

C#泛型学习

什么是泛型?泛型的基本原理与使用优势。

什么是泛型?泛型的基本原理与使用优势。

C#泛型基础知识点总结

C#泛型