【.NET Core】泛型(Generics)详解
文章目录
- [【.NET Core】泛型(Generics)详解](#【.NET Core】泛型(Generics)详解)
-
- 一、概述
- 二、泛型类型参数
- 三、泛型中类型参数的约束
-
- [3.1 where T:struct](#3.1 where T:struct)
- [3.2 where T:class](#3.2 where T:class)
- [3.3 where T:class?](#3.3 where T:class?)
- [3.4 where T:notnull](#3.4 where T:notnull)
- [3.5 where T:default](#3.5 where T:default)
- [3.6 where T:unmanaged](#3.6 where T:unmanaged)
- [3.7 where T:new()](#3.7 where T:new())
- [3.8 where T:<基类名>](#3.8 where T:<基类名>)
- [3.9 where T:<基类名>?](#3.9 where T:<基类名>?)
- [3.10 where T:<接口名称>](#3.10 where T:<接口名称>)
- [3.11 where T:<接口名称>?](#3.11 where T:<接口名称>?)
- [3.12 where T:U](#3.12 where T:U)
- [3.13 对参数应用多个约束](#3.13 对参数应用多个约束)
- [3.14 约束多个参数](#3.14 约束多个参数)
- 四、泛型类
- 五、泛型接口
- 六、泛型方法
-
- [6.1 泛型方法约束](#6.1 泛型方法约束)
- [6.2 泛型方法重载](#6.2 泛型方法重载)
- 七、泛型委托
- 八、运行时中的泛型
-
- [8.1 值类型](#8.1 值类型)
- [8.2 引用类型](#8.2 引用类型)
一、概述
泛型是为所存储或使用的一个或多个类型具有占位符(类型形参)的类、结构、接口和方法。泛型集合类可以将类型形参用作其存储的对象的占位符;类型形参程序为字段的类型或其方法的参数类型。泛型方法可将其类型形参用作其返回值的类型或用作其形参之一的类型。
为了方便理解,我们用ArrayList为例,在.NET Framework1.0中,ArrayList元素属于Object类型。添加到集合的任何元素都会以静默方式转换为Object。自此过程中会发生装箱
和拆箱
的过程,在装箱和拆箱的类型转换过程中,会影响性能。这个是因为在编译的时候无法确认数据的类型,数据的类型只能在运行阶段确定,这个过程就导致性能消耗。为了解决这个问题微软在NET Framework 2.0中首次引入了这个泛型,它本质上是一个"代码模板",让开发人定义类型安全的数据结构,这样就避免了在装箱和拆箱过程性能损失,或在运行中的异常。
下面我们演示一下非泛型和泛型性能的差异。
C#
List<int> ListGeneric = new List<int> { 5, 9, 1, 4 };
ArrayList ListNonGeneric = new ArrayList { 5, 9, 1, 4 };
Stopwatch s = Stopwatch.StartNew();
ListGeneric.Sort();
s.Stop();
Console.WriteLine($"Generic Sort: {ListGeneric} \n Time taken: {s.Elapsed.TotalMilliseconds}ms");
Stopwatch s2 = Stopwatch.StartNew();
ListNonGeneric.Sort();
s2.Stop();
Console.WriteLine($"Non-Generic Sort: {ListNonGeneric} \n Time taken: {s2.Elapsed.TotalMilliseconds}ms");
Console.ReadLine();
运行结果
shell
Generic Sort: System.Collections.Generic.List`1[System.Int32]
Time taken: 0.0119ms
Non-Generic Sort: System.Collections.ArrayList
Time taken: 0.2944ms
从运行结果我们可以看出装箱和拆箱的过程中性能损失挺大。
二、泛型类型参数
类型参数是在其创建泛型类型的一个实例时,客户端指定的特定类型的占位符。泛型类在定义以后,无法直接使用,必须指定真正的类型后,才能使用。每个类型必须通过指定尖括号内的类型参数来声明并实例化构造类型。此特定的类型一定是编译器可识别的任何类型。
实例如下:
c#
GenericList<float> list1 = new GenericList<float>();
GenericList<ExampleClass> list2 = new GenericList<ExampleClass>();
GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();
在GenericList的每个实例中,类中出现的每个T在运行时均会被替换为类型参数。通过这种替换,通过使用单个类定义创建了三个单独的类型安全的有效对象
三、泛型中类型参数的约束
约束告知编译器类型参数必须具备的功能。在没有任何约束的情况下,类型参数可以是任何类型。编译器只能预设为System.Object的成员,System.Object类型是任何类型的基类。如果在使用泛型时,不能满足约束的类型,编译器将会发生错误。通过使用where关键字指定约束。
下面列出了各种类型的约束:
3.1 where T:struct
类型参数必须是不可为null的值类型,由于所有值类型都具有可访问的无参数构造函数,因此struct
约束表示 new()
约束,并且不能与new()
约束一起使用。struct
约束也不能与 unmanaged
约束结合使用。
c#
public class GenericsStructCLS<T> where T : struct
{
}
3.2 where T:class
类型参数必须是引用类型,此约束还应用于任何类、接口、委托或数组类型。在可为null的上下文中,T必须是不可为null的引用类型。
c#
public class GenericsClassCLS<T> where T : class
{
}
3.3 where T:class?
类型参数必须是为null或不可为null的引用类型。此约束应该用于任何类,接口、委托或数组类型。
c#
public class GenericsClassNullCLS<T> where T : class?
{
}
3.4 where T:notnull
类型参数必须是不可为null的类型。参数可以是不可为null的引用类型,也可以是不可为null的值类型。
c#
public class GenericsClassNotNullCLS<T> where T : notnull
{
}
3.5 where T:default
重写方法或提供显示接口实现时,如果需要指定不受约束的类型参数,此约束可解决歧义。default
约束表示基方法,但不包含class
或struct
约束。
c#
public class GenericsClassDefault<T> where T:default
{
}
default 约束表示基方法,但不包含 class 或 struct 约束
3.6 where T:unmanaged
类型参数必须是不可为null的非托管类型。unmanaged约束表示structe约束,且不能与struct约束或new()约束结合使用。
c#
public class GenericsClassUnmanagedCLS<T> where T : unmanaged
{
}
3.7 where T:new()
类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new()
约束必须最后指定。 new()
约束不能与 struct
和 unmanaged
约束结合使用。
c#
public class GenericsClassNewCLS<T> where T : new()
{
//类方法
}
3.8 where T:<基类名>
类型参数必须是指定的基类或派生自指定的基类。在可为null的上下文中,T
必须是从指定基类派生的不可为null的引用类型。
c#
public class Base{}
public class GenericsClassBaseCLS<T> where T : Base
{
//类方法
}
3.9 where T:<基类名>?
类型参数必须是指定的基类或派生自指定的基类。 在可为 null 的上下文中,T
可以是从指定基类派生的可为 null 或不可为 null 的类型。
c#
public class Base{}
public class GenericsClassBaseCLS<T> where T : Base?
{
//类方法
}
3.10 where T:<接口名称>
类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。 在的可为 null 的上下文中,T
必须是实现指定接口的不可为 null 的类型。
C#
public interface IBase { }
public class GenericsClass<T> where T : IBase
{
//类方法
}
3.11 where T:<接口名称>?
类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。 在可为 null 的上下文中,T
可以是可为 null 的引用类型、不可为 null 的引用类型或值类型。 T
不能是可为 null 的值类型。
c#
public interface IBase { }
public class GenericsClass<T> where T : IBase?
{
//类方法
}
3.12 where T:U
为 T
提供的类型参数必须是为 U
提供的参数或派生自为 U
提供的参数。 在可为 null 的上下文中,如果 U
是不可为 null 的引用类型,T
必须是不可为 null 的引用类型。 如果 U
是可为 null 的引用类型,则 T
可以是可为 null 的引用类型,也可以是不可为 null 的引用类型。
c#
public class BaseClass<T> { }
public class UTClass<T> where T: BaseClass<T>
{
//类方法
}
3.13 对参数应用多个约束
c#
public class UTClass<T> where T: BaseClass<T> ,IBase,new()
{
}
3.14 约束多个参数
不但可以对参数应用多个约束,也可以对多个参数应用多个约束。
c#
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{
//类方法
}
四、泛型类
泛型类封装不特定于特定类型的操作。泛型类最常见用法是用于链接列表,哈希表、堆栈、队列和树等集合。无论存储数据的类型如何,添加项和从集合删除项等操作的执行方式基本相同。
创建自己的泛型类时,需要考虑以下重要注意事项:
- 要将哪些类型泛化为类型参数
- 如何给泛型类添加参数约束
- 是否将泛型行为分解基类和子类
- 实现一个泛型接口还是多个泛型接口
c#
class BaseNode { }
class BaseNodeGeneric<T> { }
class NodeConcrete<T> : BaseNode { }
class NodeClosed<T> : BaseNodeGeneric<int> { }
class NodeOpen<T> : BaseNodeGeneric<T> { }
泛型类的特性
- 非泛型类可继承自封闭式构造基类,但不可继承自开放式构造类或类型参数;
- 泛型类继承自开放构造类型的泛型类必须保持参数相同;
- 泛型类型可使用多个类型参数和约束;
- 开放式构造和封闭式构造类型可用作方法参数;
五、泛型接口
为避免对值类型执行装箱和拆箱操作,最好对泛型类使用泛型接口。.NET类库定义多个泛型接口,以便用于System.Collections.Generic命名空间中的集合类。
泛型接口提供与非泛型接口对应的类型安全接口,用于实现排序比较,相等比较以及泛型集合类型所共享的功能。
c#
public class GenericList<T>:System.Collections.Generic.IEnumerable<T>
{
//方法体
}
泛型接口可将多个接口指定微单个类型上的约束。
c#
class Stack<T> where T:System.IComparable<T>,IEnumerable<T>
{
//方法体
}
一个接口可定义多个类型参数:
C#
interface IDictionary<K,V>
{
//方法体
}
泛型类既可实现泛型接口或封闭式构造接口。
C#
interface IBaseInterface1<T> { }
interface IBaseInterface2<T,U> { }
class SampleClass1<T> : IBaseInterface1<T> { }
class SampleClass2<T> : IBaseInterface2<T, string> { }
从C# 11开始,接口可以声明static abstract
或static virtual
成员。声明任一static abstract
或static virtual
成员的接口几乎始终是泛型接口。编译器必须在编译时解析对 static virtual
和 static abstract
方法的调用。 接口中声明的 static virtual
和 static abstract
方法没有类似于类中声明的 virtual
或 abstract
方法的运行时调度机制。 相反,编译器使用编译时可用的类型信息。 这些成员通常是在泛型接口中声明的。
六、泛型方法
泛型方法是通过类型参数声明的方法。如下所示:
c#
public void Swap<T>(ref T ins, ref T ot)
{
T temp;
temp=ins;
ins = ot;
ot=temp;
}
如果定义一个具有与包含类相同的类型参数的泛型方法,编译器会生成警告CS0693(警CS0693类型参数"T"与外部类型"GenericsMethodClass<T>"中的类型参数同名
)。如果需要使用类型参数调用泛型类方法所具备的灵活性,可以考虑为此方法的类型参数提供另一标识符。
c#
class GenericList<T>
{
//CS0693.
void SampleMethod<T>(){}
}
class GenericList2<T>
{
//No warning.
void SampleMethod<U>(){}
}
6.1 泛型方法约束
c#
void SwapIfGreater<T>(ref T lhs, ref T rhs) where T : System.IComparable<T>
{
T temp;
if (lhs.CompareTo(rhs) > 0)
{
temp = lhs;
lhs = rhs;
rhs = temp;
}
}
6.2 泛型方法重载
c#
void DoWork(){}
void DoWork<T>(){}
void DoWork<T,U>(){}
七、泛型委托
委托可以定义它自己的类型参数。引用泛型委托的代码可以指定类型参数以创建封闭式构造类型。
示例如下:
C#
public delegate T DelMetho<T>(T item);
public static int Notify(int a) { return a; }
public static string Notify1(string b) { return b; }
DelMetho<int> metho = new DelMetho<int>(Notify);
DelMetho<string> method = new DelMetho<string>(Notify1);
C#2.0版具有一种称为方法组转换的新功能,使用于具体委托类型和泛型委托类型,能简化语法编写:
c#
public delegate T DelMetho<T>(T item);
public static int Notify(int a) { return a; }
public static string Notify1(string b) { return b; }
DelMetho<int> metho =Notify;
八、运行时中的泛型
泛型类型或方法编译为MSIL时,它包含将其标识为具有类型参数的元数据。MSIL根据所提供的类型参数是值类型还是引用类型而有不同。
8.1 值类型
使用值类型作为参数首次构造泛型类型时,运行时创建专用的泛型类型,MSIL内的适当位置替换提供的一个或多个参数。为每个用参数的唯一值类型一次创建专用化泛型类型。
8.2 引用类型
引用类型,泛型的作用方式略有不同。首先使用任意引用类型构造泛型类型时,运行时创建一个专用化泛型类型,用对象引用替换 MSIL 中的参数。 之后,每次使用引用类型作为参数实例化已构造的类型时,无论何种类型,运行时皆重新使用先前创建的专用版泛型类型。 原因可能在于所有引用大小相同。