一、C# 中的泛型
泛型(generic)特性可以让多个类型共享一组代码。
泛型类型不是类型,而是类型的模板。
C# 提供了5种类型:类、结构、接口、委托和方法。
泛型类
泛型的主要优点:
-
性能
类型转换时,非泛型的类型进行装箱和拆箱时,会使得性能损失比较大。(需要转换运算符以及拷贝内存) -
类型安全
如果在泛型类中,定义了具体类型,编译器就不会编译代码。 -
二进制代码重用
泛型可以定义一次,就可以用于不同类型的实例化。 -
代码的扩展
1)泛型类的定义会放在程序集中,所以用特定类型实例化泛型类不会在 IL 代码中复制这些类。(即类型实例化的代码在使用泛型时,不会进行拷贝这些实例化的代码)。
2)在 JIT 编译器把泛型类编译为本地代码时,会给每个值类型创建一个新类,引用类型共享同一个本地类的所有相同的实现代码。(比如 List《int》会对其类型进行复制,而 List《MyClass》会对其类型进行引用。) -
命名约定
命名规则:
泛型类型的名称用字母 T 作为前缀。
泛型类型允许用任意类替代,且只使用了一个泛型类型。
public class List<T> { }
public class LinkedList<T> { }
- 如果泛型类型有特定的要求(例如,它必须实现一个接口或派生自基类),或者使用了两个或多个泛型类型,就应该泛型类型使用描述性的名称:
//泛型委托
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
public delegate TOutput Converter<TInput, TOutput>(TInput from);
//泛型类
public class SortedList<TKey, TValue> { }
二、声明泛型类
泛型声明定义的代码:
//T1,T2 为类型参数
class SomeClass<T1,T2>
{
public T1 SomeVar;
public T2 OtherVar;
}
class Program
{
static void Main(string[] args)
{
//构造的类型
var first = new SomeClass<short, int>();
var second = new SomeClass<int, long>();
Console.ReadKey();
}
}
执行测试代码的作用:
//SomeClass<short, int>()
class SomeClass<short,int>
{
public short SomeVar;
public int OtherVar;
}
//SomeClass<int, short>()
class SomeClass<int,short>
{
public int SomeVar;
public short OtherVar;
}
三、比较泛型和非泛型栈
实现泛型栈的代码例子:
class MyStack<T>
{
T[] StackArray;
int StackPointer = 0;
public void Push(T x)
{
if (!IsStackFull)
StackArray[StackPointer++] = x;
}
public T Pop()
{
return (!IsStackEmpty) ? StackArray[--StackPointer] : StackArray[0];
}
const int MaxStack = 10;
bool IsStackFull { get { return StackPointer >= MaxStack; } }
bool IsStackEmpty { get { return StackPointer <= 0; } }
public MyStack()
{
StackArray = new T[MaxStack];
}
public void Print()
{
for (int i = StackPointer - 1; i >= 0; i--)
Console.WriteLine($" Value: { StackArray[i] }");
}
}
class Program
{
static void Main(string[] args)
{
MyStack<int> StackInt = new MyStack<int>();
MyStack<string> StackString = new MyStack<string>();
StackInt.Push(3);
StackInt.Push(5);
StackInt.Push(7);
StackInt.Push(9);
StackInt.Print();
StackString.Push("This is fun");
StackString.Push("Hi there! ");
StackString.Print();
Console.ReadKey();
}
}
表-非泛型栈和泛型栈之间的区别
非泛型 | 泛型 | |
---|---|---|
源代码大小 | 更大:需要为每一种类型编写一个新的实现(有重复的代码逻辑) | 更小:不管构造类型的数量有多少,只需要一个实现 |
可执行文件大小 | 无论每一个版本的栈是否会被使用,都会在编译的版本中出现 | 可执行文件中只会出现有构造类型的类型 (疑问:设计泛型类型的代码逻辑不在可执行文件里吗?似乎有些明白了,泛型只会放在程序集中,而不会拷贝这些实例化的代码,所以编译器不会编译设计泛型的代码。) |
写的难易度 | 易于书写,因为它更具体 | 比较难写,因为它更抽象 (我觉得因为想要通用任何可支持的类型,需要考虑点多,越是抽象(类型特征越多),设计的代码结构就越复杂。) |
维护的难易度 | 更容易出问题,因为所有修改需要应用到每一个可用的类型上 (因为类型已固定了,如果更改类型或者代码结构,就会把所有应用到的地方都修改一遍,容易出错。) | 易于维护,因为只需要修改一个地方 |
四、类型参数的约束
1、类型参数的约束
由于泛型不知道它们保存的项的类型是什么,也就不会知道这些类型实现的成员,就不会对这些成员进行运算符的事情。
所以,如果泛型类需要调用泛型类型中的方法,就必须添加约束。
未绑定的类型参数: 符合约束的未绑定的类型参数。如果泛型里有 Object 成员,那么这个Object 成员是已知的类型,可以对它做一些如 ToSting、Equals 以及 GetType 方法的处理。而如果其他未知类型的成员不能像 Object 成员那样可以直接处理的,是未绑定的类型参数。
class Simple<T>
{
static public bool LessThan(T i1,T i2)
{
//错误,因为i1 和 i2 属于未绑定的类型参数,
//不能使用 < 运算符进行处理
retrun i1 < i2;
}
}
2、Where 子句
约束使用 where 子句列出。
- 每一个有约束的类型参数都有自己的 where 子句。
- 如果形参有多个约束,它们在 where 子句中使用逗号分隔。
where 子句的语法如下:
where TypeParam : constraint,constraint,...
//where:关键字
//TypeParam:类型参数
//constraint,constraint,...:约束列表
关于 where 子句的要点:
- 它们在类型参数列表的关闭尖括号之后列出。
- 它们不使用逗号或其他符号分隔。
- 它们可以以任何次序列出。
- where 是上下文关键字,所以可以在其他上下文中使用。
//T1 未绑定,T2 和 T3 具有约束
class MyClass < T1,T2,T3 >
where T2:Customer //T2的约束
where T3:IComparable //T3的约束
{
...
}
3、约束类型和次序
公有5种类型的约束:
约束类型 | 描述 |
---|---|
类名 | 只有这个类型或从它派生的类才能用作类型实参 |
class | 任何引用类型,包括类、数组、委托和接口都可以用作类型实参 |
struct | 任何值类型都可以用作类型实参(包括枚举? 好吧,所有值类型,那就是包括枚举) |
接口名 | 只有这个接口或实现这个接口的类型才能用作类型实参 |
new() | 任何带有无参公共构造函数的类型都可以用作类型实参。这叫作构造函数约束 |
where 子句可以以任何次序列出。但是,where 子句中的约束必须有特定的顺序。
- 最多只能有一个主约束,而且必须放在第一位。
- 可以有任意多的接口名称约束。
- 如果存在构造函数约束,则必须放在最后。
约束 | 约束类型 | 个数 |
---|---|---|
主约束 | ClassName class struct | 0或1个 |
次约束 | InterfaceName | 0或多个 |
构造函数约束 | new() | 0或1个 |
class SortedList<S>
where S:IComparable<S>{...}
calss LinkedList<M,N>
where M : IComparable<M>
where M : ICloneable{...}
class MyDictonary<KeyType,ValueType>
where KeyType:IEnumerable,
new() {...}