作者:浪子花梦,一个有趣的程序员 ~
此文章将讲述泛型中一些核心的知识,不会像前面那些文章非常详细,所以就提炼出核心的内容来写吧 . . . C# 语言方面知识相关的文章,以后经常讲述代码,文字尽量少一点,不然我自己都不想看了,哈哈哈 ^ _ ^ . . .
为了我们能理解泛型的性能优势,我们通过下面这个程序来比较 List算法 和 FCL(Framework Class Library) 算法的性能,我们分别测试值类型与引用类型作为对象的性能区别 . . .
首先,我们创建一个类,能来测试算法的时间,与垃圾回收的次数,代码如下所示:
internal sealed class OperationTimer : IDisposable
{
private Stopwatch m_stopwatch; // 用于测量时间
private String m_text;
private Int32 m_collectionCount;
public OperationTimer(String text)
{
PrepareForOperation();
m_text = text;
m_collectionCount = GC.CollectionCount(0); // 获取垃圾回收的次数
m_stopwatch = Stopwatch.StartNew(); // 测量运行时间
}
private static void PrepareForOperation()
{
GC.Collect(); // 垃圾回收
GC.WaitForPendingFinalizers(); // 挂起线程
GC.Collect();
}
public void Dispose()
{
// 输入相关的信息:运行时间、垃圾回收次数、类型名称
Console.WriteLine("{0} (GCs = {1,3}){2}", (m_stopwatch.Elapsed),
GC.CollectionCount(0) - m_collectionCount, m_text);
}
}
我们使用 using 开辟上面的对象,将测试代码放在 using的方法体中进行测试 . . .
测试值类型的方法如下所示:
private static void ValueTypePerfTest()
{
const Int32 count = 100000000;
using (new OperationTimer("List" ))
{
List<Int32> l = new List<int>();
for (Int32 n = 0; n < count; ++n)
{
l.Add(n); // 不装箱
Int32 x = l[n]; // 不拆箱
}
l = null;
}
using (new OperationTimer("ArrayList of Int32"))
{
ArrayList a = new ArrayList();
for (Int32 n = 0; n < count; ++n)
{
a.Add(n); // 装箱
Int32 x = (Int32)a[n]; // 拆箱
}
a = null;
}
}
测试引用类型的方法如下所示:
private static void ReferenceTypePerfText()
{
const Int32 count = 100000000;
using (new OperationTimer("List" ))
{
List<String> l = new List<String>();
for (Int32 n = 0; n < count; ++n)
{
l.Add("X"); // 复制引用
String x = l[n]; // 复制引用
}
l = null; // 确保进行垃圾回收
}
using (new OperationTimer("ArrayList of String"))
{
ArrayList a = new ArrayList();
for (Int32 n = 0; n < count; ++n)
{
a.Add("X"); // 复制引用
String x = (String)a[n]; // 检查强转
}
a = null;
}
}
我们在 Main中执行上面的两个方法:
public static void Main()
{
ValueTypePerfTest();
ReferenceTypePerfText();
}
测试所得的数据如下所示(可能会花费几十秒的时间):
对应的分别是测试的时间、垃圾回收的次数、类型
我们发现当我们对引用类型进行测试时,区别并不是太大,但是,泛型的优势是不容忽视的 . . .
.
使用泛型实现链表如下所示:
internal sealed class Node<T>
{
public T m_data;
public Node<T> m_next; // 看作是 C++ 中的指针(指向一个东西)
public Node(T data) : this(data, null) { }
public Node(T data, Node<T> next)
{
m_data = data;
m_next = next;
}
public override string ToString()
{
// 重写 ToString 递归
return this.m_data + (this.m_next != null ? this.m_next.ToString() : String.Empty);
}
}
测试代码如下所示:
public static void Main()
{
Node<Char> head = new Node<Char>('C');
head = new Node<char>('B', head);
head = new Node<char>('A', head);
Console.WriteLine(head.ToString());
}
我们已经实现了一个链表,但是这种链表一但类型确定下来之后,就无法改变了,如下所示:
所以,我们有更好的方法来写出一个链表来,我们可以定义一个基类(非泛型),然后再定义一个派生类(泛型),把派生类中的数据用基类保存下来,这样我们就不需要一种具体的数据类型了,而且这种方法类型安全性高,并防止了类型装箱。代码实现如下所示:
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 + (this.m_next != null ? this.m_next.ToString() : String.Empty);
}
}
测试如下所示:
public static void Main()
{
Node head = new TypedNode<Int32>(20);
head = new TypedNode<Char>('A', head);
head = new TypedNode<String>(" langzi ", head);
head = new TypedNode<Boolean>(true, head);
Console.WriteLine(head.ToString());
}
结果如下所示:
这样,我们就可以指定各种各样的类型了,这样的技巧你们学会了吗?
.
1)逆变量:泛型类型参数可以从一个类更改为它的某个派生类,用 in 关键字标记\
2)协变量:泛型类型参数可以从一个类更改为它的某个基类, 用 out 关键字标记
例如,假定存在以下委托类型定义:
public delegate TResult Func<T, TResult>(T arg);
现在我们没有用 in out 来修饰它的泛型参数,测试如下所示:
现在我们来用下面的方面来定义这个委托类型:
public delegate TResult Func<in T, out TResult>(T arg);
只有这样,上面的代码才能编译通过 . . .
上面我们测试的是委托类型,那么接口是怎么样的呢?其中接口也是一样处理的,如下所示:
interface ITest<in T> { }
像上面一样,直接可以这样赋值:
ITest<Object> test1 = null;
ITest<String> test2 = test1;
.
我们有的时候会定义一些方法,但这些方法的类型不适用于所有的类型,所以我们需要对外界传过来的类型进行一个约束,如下所示,我们定义一个学生类,其中有学生的年龄,我们定义一个泛型方法,用来比较学生的年龄,通过 where关键字对 学生类进行约束,使得其它的类型无法使用这个方法,如下所示:
输出的结果为:20 . . .
.
.
.