泛型部分
泛型 :
- 定义 :泛型是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即算法重用。
- 实际形式 :定义算法的开发人员并不设定该算法要操作什么数据类型,该算法可广泛地应用于不同类型的对象。
-
适用对象 :
1. 引用类型
2. 值类型 (但是并不支持枚举类型。)
3. 接口
4. 委托
5. 方法
代码演示:
private static void SomeMethod()
{
//构造一个List来操作DateTime对象
List<DateTime> dtList = new List<DateTime>();
//向列表添加一个DateTime对象
dtList.Add(DateTime.Now); //不进行装箱
//向列表添加一个DateTime对象
dtList.Add(DateTime.MinValue); //不进行装箱
//尝试向列表中添加一个String对象
dtList.Add("1/1/2004"); //编译时错误
//从列表提取一个DataTime对象
DateTime dt = dtList[0]; //不需要转型
}
形如:List<DateTime>,DateTime这种可以被指定为任何变量的都称为类型实参。
泛型的优势所在:
- 源代码保护 :因为使用者不需要访问算法的源代码(使用 C++ 模板的泛型技术时,算法的源代码必须提供给使用者)
- 类型安全 :编译器和CLR保证只有与指定类型数据兼容的对象才能用于算法。试图使用不兼容的对象会报错(编译时错误或者运行时抛出异常)
- 更清晰的代码
- 更佳的性能 :由于能创建一个泛型算法来操作一个具体的值类型,所以值类型的实例能以传值方式传递,CLR不再需要执行任何装箱操作。(装箱操作会在托管堆上进行内存分配,造成更频繁的垃圾回收,从而损害应用程序的性能。)在操作值类型时候,泛型比非泛型算法速度要快得多
ref 和 out 修饰的值参数,类似于传引用,但不用装箱,只是传递值类型的地址。
FCL中的泛型:
Microsoft建议使用泛型集合类,不建议使用非泛型集合类。
集合类中实现了许多接口,放入集合中的对象可实现接口来执行排序和搜索等操作。常用接口在 System.Collections.Generic 命名空间中提供
System.Array类(所有数组类型的基类)提供了大量静态泛型方法:AsReadOnly,BinarySearch,ConvertAll,Exists,Find,FindAll,FindIndex,FindLast,FindLastIndex,ForEach,IndexOf,LastIndexOf,Resize,Sort和TrueForAll等,部分代码展示如下:
public abstract class Array : ICloneable,IList,ICollection,IEnumerable,ISructuralComparable,IStructuralEquatable
{
public static void Sort<T>(T[] array);
public static void Sort<T>(T[] array,IComparer<T> comparer);
public static Int32 BinarySearch<T>(T[] array,T value);
public static Int32 BinarySearch<T>(T[] array,T value,IComparer<T> comparer);
public static void Main()
{
//创建并初始化字节数组
Byte[] byteArray = new Byte[] {5,1,4,2,3};
//调用Byte[]排序算法
Array.Sort<Byte>(byteArray);
//调用Byte[]二分搜索算法
Int32 i = Array.BinarySearch<Byte>(byteArray,1);
Console.WriteLine(i); //显示“0”
}
}
具有泛型类型参数的类型称为开放类型。CLR禁止构造开放类型的任何实例。
只有为所有的类型参数都传递了实际的数据类型,类型才成为封闭类型。CLR允许构造封闭类型的实例。
演示代码如下:
//一个部分指定的开放类型
internal sealed class DictionaryStringKey<TValue> : Dictionary<String,TValue>
{
}
public static class Program
{
public static void Main(string[] args)
{
Object o = null;
//Dictionary<,> 是开放类型,有2个类型参数
Type t = typeof(Dictionary<,>);
//尝试创建该类型的实例(失败)
o = CreateInstance(t);
Console.WriteLine();
//DictionaryStringKey<>是开放类型,有一个类型参数
t = typeof(DictionaryStringKey<>);
//尝试创建该类型的实例(失败)
o = CreateInstance(t);
Console.WriteLine();
//DictionaryStringKey<Guid>是封闭类型
t = typeof(DictionaryStringKey<Guid>);
//尝试创建该类型的一个实例(成功)
o = CreateInstance(t);
//证明它确实能够工作
Console.WriteLine("对象类型 = "+o.GetType());
}
private static Object CreateInstance(Type t)
{
Object o = null;
try
{
o = Activator.CreateInstance(t);
Console.Write("已创建 {0} 的实例。", t.ToString());
}
catch(ArgumentException e)
{
Console.WriteLine(e.Message);
}
return o;
}
}
(使用泛型类型并指定类型实参时,实际是在CLR中定义一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生(如果有的话),指定类型实参,不影响继承层次结构。)
模拟链表:
- 代码如下 :
internal sealed class Node<T>
{
public T m_data;
public Node<T> m_next;
public Node(T data) : this(data,null)
{}
public Node(T data,Node<T> next)
{
m_data = data;
m_next = next;
}
public override String ToString()
{
return m_data.ToString + ((m_next != null) ? m_next.ToString() : String.Empty);
}
public static void SameDataLinkedList()
{
Node<Char> head = new Node<Char>('C');
head = new Node<Char>('B',head);
head = new Node<Char>('A',head);
Console.WriteLine(head.ToString()); //显示“ABC”
}
}
以上代码具有一定的限制,限制就是该链表只能存储相同数据类型的链表结点。
- 优化代码 :
internal class Node
{
protected Node m_next;
public Node(Node next)
{
m_next = next;
}
}
internal sealed class TypedNode<T> : Node
{
public T m_data;
public TypedNode(T data) : this(data,null)
{
}
public TypedNode(T data,Node next) : base(next)
{
m_data = data;
}
public override String ToString()
{
return m_data.ToString() + ((m_next != null) ? m_next.ToString() : String.Empty);
}
//该静态方法创建了一个链表,其中每个节点都是不同的数据类型。
private static void DifferentDataLinkedList()
{
Node head = new TypedNode<Char>('.');
head = new TypedNode<DateTime>(DateTime.Now,head);
head = new TypedNode<String>("Today is ",head);
Console.WriteLine(head.ToString());
}
}
以上优化代码,定义了非泛型的Node基类,在定义了泛型的TypedNode类(用Node类作为基类)。这样就可以创建一个链表,其中每个节点都可以是一种不同的具体的数据类型( 不能是Object)
泛型类型同一性:
-
绝对不要单纯出于增强源码可读性的目的来定义一个新类。这样会丧失类型的同一性和相等性。如下代码所示:
Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));
该部分的正确增强可读性简化语法:
1.在源文件顶部使用传统的using指令:
using DateTimeList = (typeof(List<DateTime>) == typeof(DateTimeList));
2.使用C#的"隐式类型局部变量"功能:
var dtl = new List<DateTime>();
代码爆炸:
使用泛型类型参数的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码(这些代码为操作指定数据类型“量身定做”)。这样做有一个重大缺点:
CLR要为每种不同的方法/类型组合生成本机代码。(这样造成的现象就成为代码爆炸。)
(代码爆炸可能造成应用程序的工具集显著增大,从而损坏性能)
CLR内建了一些优化措施来缓解代码爆炸:
- 假定为特定的类型实参调用了一个方法,以后再用相同的类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次代码。
- CLR认为所有引用类型都完全相同,所以代码可以共享。例如List<String>的方法编译代码可直接用于List<Stream>的方法,因为String和Stream均为引用类型。
泛型接口:
引用类型或值类型可指定类型实参实现泛型接口,也可保持类型实参的未指定状态来实现泛型接口:
public interface IEnumerator<T> : IDisposable,IEnumerator
{
T Current{ get; }
}
internal sealed class Triangle : IEnumerator<Point>
{
private Point[] m_vertices;
//IEnumerator<Point>的Current属性是Point类型
public Point Current{get{...}}
...
}
//下例实现了相同的泛型接口,但保持类型实参的未指定状态:
internal sealed class ArrayEnumerator<T> : IEnumerator<T>
{
private T[] m_array;
//IEnumerator<T>的Current属性时T类型
public T Current{get{...}}
...
}
泛型委托:
CLR支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。此外泛型委托允许值类型实例在传给回调方法时不进行任何装箱。(如果定义的委托类型指定了类型参数,编译器会定义委托类的方法,用指定的类型参数替换方法的参数类型和返回值类型。)
建议尽量使用FCL预定义的泛型Action和Func委托。
委托的每个泛型类型参数都可标记为 协变量 或 逆变量 。
泛型类型参数可以是以下任何一种形式:(只有接口和委托类型的类型参数能设置为in或者out)
- 不变量 :意味着泛型类型参数不能更改。(无修饰符修饰)
- 逆变量 :意味着泛型类型参数可以从一个类更改为它的某个派生类。在C#中是用in关键字标记逆变量形式的泛型类型参数。逆变量泛型类型参数只出现在输入位置,比如作为方法的参数。
- 协变量 :意味着泛型类型参数可以从一个类更改为它的某个基类。在C#中是用out关键字标记协变量形式的泛型类型参数。协变量泛型类型参数只能出现在输出位置,比如作为方法的返回类型。
泛型委托参数形式演示代码如下:
public delegate TResult Func<in T,out TResult>(T arg);
Func<Object,ArgumentException> fn1 = null;
Func<String,Exception> fn2 = fn1; //不需要显式转型。
Exception e = fn2(" ");
上述代码的意义:fn1变量引用一个方法,获取一个Object,返回一个ArgumentException。而fn2变量引用另一个方法,获取一个String,返回一个Exception。由于可将一个String传给期待Object的方法(因为String从Object派生),而且由于可以获取返回ArgumentException的一个方法的结果,并将这个结果当成一个Exception(因为Exception是ArgumentException的基类),所以上述代码能正常编译,而且编译时能维持类型安全性。
使用要获取泛型参数和返回值的委托时,建议尽量为逆变性和协变性指定in和out关键字。这样做不会有不良反应,并使你的委托能在更多的情形中使用。
和委托类似,具有泛型类型参数的接口也可将类型参数标记为逆变量和协变量。
泛型接口参数形式演示代码如下:
public interface IEnumerator<in T> : IEnumerator
{
Boolean MoveNext();
T Current{get;}
}
//由于T是逆变量,所以以下代码可以顺利编译和运行:
//这个方法接受任意引用类型的一个IEnumerable
Int32 Count(IEnumerable<Object> collection) {...}
...
//以下调用向Count传递一个IEnumerable<String>
Int32 c = Count(new[] {"Grant"});
在声明泛型类型参数时,必须由使用者显式使用in或out来标记可变性。以后使用这个类型参数时,加入用法与声明时指定的不符,编译器就会报错,提醒使用者违反订立的协定。
泛型方法
定义泛型类、结构或接口时,类型中定义的任何方法都可引用类型指定的类型参数。类型参数可作为方法参数、方法返回值或方法内部定义的局部变量的类型使用。与此同时,CLR还允许方法指定它自己的类型参数。这些类型参数也可作为参数、返回值或局部变量的类型使用。
泛型方法参数形式演示代码如下:
internal sealed class GenericType<T>
{
private T m_value;
public GenericType(T value){m_value = value;}
public TOutput Converter<TOutput>()
{
TOutput result = (TOutput)Convert.ChangeType(m_value,typeof(TOutput));
return result; //返回类型转换之后的结果
}
}
可验证性和约束
如果一个泛型没有提供任何约束,那么编译器不能保证该泛型类中的方法适用于所有类型,所以很容易报错。
编译器和CLR支持称为约束的机制。
约束 :
- 作用 :限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型执行更多操作。
- 关键字 :where
- 约束可应用于泛型类型的类型参数,也可应用于泛型方法的类型参数。CLR不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数个数)对类型或方法进行重载。