目录
- 创建泛型类
- 泛型类的特性
- 泛型接口
- 泛型方法
- 泛型委托
泛型并不是一个全新的结构,其他语言中也有类似的概念,比如C++中的模板,但C++中的模板和.NET中的泛型还是有很的的区别的,下面就对泛型做些研究。
- 创建泛型类
学习数据结构时,最为常见的就是链表,我们就以链表为例来描述泛型类创建的细节。首先,我们知道,链表是由不定数目的结点连接而成的,结点中有数据字段和指向下一个或上一个结点的对象应用,下面是双向链表结点类型代码:
public class LinkedListNode { public LinkedListNode(Object value) { this.value = value; } //结点数据值 private object value; public object Value { get { return this.value; } } //双向结点指向引用 private LinkedListNode next; private LinkedListNode pre; public LinkedListNode Next { get { return this.next; } internal set { this.next = value; } } public LinkedListNode Pre { get { return this.pre; } internal set { this.pre = value; } } }
其中,结点的数据存储在Object类型的Value字段中。下面我们来创建链表类,该链表类有一个头结点、尾结点和在链表的末尾添加/删除结点的方法:
//双向链表类 public class LinkedList { //头结点 private LinkedListNode head; public LinkedListNode Head { get { return this.head; } } //尾结点 private LinkedListNode last; public LinkedListNode Last { get { return this.last; } } //在链表尾插入结点 public LinkedListNode AddNode(Object value) { LinkedListNode node = new LinkedListNode(value); if (head == null) { head = node; last = head; } else { last.Next = node; node.Pre = last; last = node; } return last; } //在链表尾删除结点 public LinkedListNode SubNode() { if (last == null) { return null; } else { LinkedListNode node = last.Pre; node.Next = null; last = node; } return last; } }
因为是链表,所以我们希望能使用foreach遍历链表,那么,将链表类LinkedList继承IEnumerable接口并实现GetEnumerator()方法:
public class LinkedList : IEnumerable { ... public IEnumerator GetEnumerator() { LinkedListNode curr = head; while (curr != null) { yield return curr.Value; curr = curr.Next; } } }
全部代码:
这样我们就可以使用链表了,但该链表中的存储数据只能使Object类型的,这就并不能保证链表中的数据是类型统一的,因此在使用foreach遍历链表时可能出现致命的错误。比如该链表中有整形数据和字符串数据。而且,该链表并不能够为我们提供一个标准的模板。
下面,我们改进LinkedListNode结点类,使其使用泛型——存储数据的类型使我们任意指定的类型:
public class LinkedListNode<T> { public LinkedListNode(T value) { this.value = value; } //结点数据值 private T value; public T Value { get { return this.value; } } //双向结点指向引用 private LinkedListNode<T> next; private LinkedListNode<T> pre; public LinkedListNode<T> Next { get { return this.next; } internal set { this.next = value; } } public LinkedListNode<T> Pre { get { return this.pre; } internal set { this.pre = value; } } }
同样,也需要将链表类改为泛型类:
public class LinkedList<T> : IEnumerable<T> { //头结点 private LinkedListNode<T> head; public LinkedListNode<T> Head { get { return this.head; } } //尾结点 private LinkedListNode<T> last; public LinkedListNode<T> Last { get { return this.last; } } //在链表尾插入结点 public LinkedListNode<T> AddNode(T value) { LinkedListNode<T> node = new LinkedListNode<T>(value); if (head == null) { head = node; last = head; } else { last.Next = node; node.Pre = last; last = node; } return last; } //在链表尾删除结点 public LinkedListNode<T> SubNode() { if (last == null) { return null; } else { LinkedListNode<T> node = last.Pre; node.Next = null; last = node; } return last; } #region IEnumerable 成员 public IEnumerator<T> GetEnumerator() { LinkedListNode<T> curr = head; while (curr != null) { yield return curr.Value; curr = curr.Next; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion }
这样我们就保证了链表中数据的统一性,因此可以安全的使用foreach遍历链表了。
其实,我个人认为,泛型也就是我们在编写代码时并不知道要使用的数据类型是什么,而是在使用代码时规定数据类型的,这样,我们就很容易的创建复用的模板代码,即减少了代码编写的工作,也节省了空间资源。
- 泛型类特性
到此为止,相信大家已经对泛型有了较为具体的概念了,接下来,继续我们的研究。
(1)默认值
我们知道,泛型类中,可以使用任意数据类型,比如引用类型和值类型。那么泛型有没有默认只呢?很显然,我们不能单纯的只将null赋值给泛型——非可空值类型的默认值并不是null。
这里我们可以用——default——关键字来获取泛型的默认值(null或0)来初始化泛型,格式如下:
public class LinkedListNode<T> { public LinkedListNode(T value) { this.value = default(T); ... } //结点数据值 private T value; ... }
(2)约束
我们的链表可以使用foreach进行遍历了,但作为集合类,可能还需要排序等功能,那么就需要结点与结点之间也能进行比较,这样我们就需要将节点类实现IComparable<T>泛型接口了,具体代码如下:
public class LinkedListNode<T> : IComparable<LinkedListNode<T>> { ... #region IComparable<LinkedListNode<T>> 成员 public int CompareTo(LinkedListNode<T> other) { return other.value.CompareTo(other.value); } #endregion }
这样我们就可以在值类型结点与结点之间直接比较大小了。但是,我们又会发现另一个问题——如果泛型T数据类型不是值类型的或是较为复杂的引用类型时(换句话说就是该数据类型对象之间不能直接比较),那么结点之间的直接比较就会出错了,怎么办?当然,我们只需要给T类型的数据添加一个约束就行了:
public class LinkedListNode<T> : IComparable<LinkedListNode<T>> where T:IComparable<T> { ... }
同样,泛型链表类也需要添加约束:
public class LinkedList<T> : IEnumerable<T> where T:IComparable<T> { ... }
添加约束后,这样结点与结点之间直接比较就有了安全保障了。上述代码中,我们给泛型数据类型T添加约束后,就规定在使用链表时必须传入已经实现了IComparable<T>泛型接口的数据类型。
(3)继承
泛型类型可以实现泛型接口,也可以派生于一个类。
泛型类型可以派生与泛型基类,但必须要求重复泛型类型或必须指定基类的泛型类型:
public class Base<T> { ... } public class MyClass<T> : Base<T> { ... } 或者 public class MyClass<T> : Base<int> { ... }
另外,如果指定了泛型基类的数据类型,就可以被泛型类和非泛型类继承,其实道理很简单,当泛型类指定了泛型数据类型时就和普通的类没什么区别了,当然就可以被其他类继承了。
注意:泛型类也可以是抽象类,然后派生其他类。
(4)静态成员
我们知道,非泛型类中的静态数据成员是类所专有的,从这个类创建的所有对象都可以使用它。那么泛型类的静态数据成员就比非泛型类有趣多了。
首先,我们来定义一个泛型类:
public class Base<T> { public static int x; }
然后我们这样赋值:
Base<String> .x = 4;
Base<int> .x = 5;
那么,"x" 的值到底是4还是5呢?对于上面Base<T>的两句赋值语句,"x"的值分别是Base<String >的4和Base<int>的5.,而并不是Base<T>类独有的。也就是说,Base<String >和Base<int>各自都有自己独有的X静态成员,两者之间并不混淆。
- 泛型接口
上面的链表中,我们使用好几个泛型接口了,在使用泛型接口时个人认为和泛型类用户几乎形同,这里就不再罗嗦了。
- 泛型方法
将泛型应用与方法,可以得到一些很了不起的效果,下面还以以一个列子说明,就那最简单的两个数据交换函数吧,我们可以这样的定义该泛型方法:
public void Swap<T>(ref T x, ref T y) { T tmp = x; x = y; y = tmp; }
这样,所有的数据类型都可以使用这个函数来交换数据了,是不是很有趣。这就大大减少了C++中函数重载的代码重写过程。在调用泛型时,也很简单:
int i = 1, j = 0;
Swap<T>(ref i, ref j);
其实,我们也可以像非泛型方法那样调用泛型方法——因为,C#编译器会通过调用泛型方法来获取参数类型:
Swap(ref i, ref j);
- 泛型委托
我们知道,委托是类型安全的方法的引用,通过泛型委托,委托的参数可以以后定义,不如.NET库中定义的事件泛型委托:
public sealed delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;
它的第二个参数是TEventArgs泛型类型,并且将该泛型类型添加了约束——就规定了TEventArgs泛型类型必须派生自EventArgs类。
接下来,我们看看一个例子,说明泛型委托的好处:
首先创建一个个人账户信息类(只包含姓名和工薪字段):
public class Account { private String name; public String Name { get { return name; } } private decimal balance; public decimal Balance { get { return balance; } } public Account(String name, Decimal balance) { this.name = name; this.balance = balance; } }
然后再创建一个统计所有薪水的类:
public class Algorithm { public decimal AccumulateSimple(IEnumerable e) { decimal sum = 0; foreach(Account a in e) { sum += a.Balance; } return sum; } }
主函数中这样的调用:
class Program { static void Main() { List<Account> accounts = new List<Account>(); accounts.Add(new Account("Name1", 2500)); accounts.Add(new Account("Name2", 4500)); accounts.Add(new Account("Name3", 2000)); Algorithm alg = new Algorithm(); alg.AccumulateSimple(accounts); } }
但是,这样我们会发现,这只能统计Account类型的数据了,接下来使用泛型委托灵活的改变统计的范围了。
public delegate TSummary Action<TInput, TSummary>(TInput t, TSummary s);//定义一个泛型委托
然后再在Algorithm 类中定义一个泛型方法:
public TSummary Accumulate<TInput, TSummary>(IEnumerable<TInput> coll, Action<TInput, TSummary> action) { TSummary sum = default(TSummary); foreach (TInput input in coll) { sum = action(input, sum); } return sum; }
最后,我们就可以在Main函数中这样调用了:
alg.Accumulate<Account, decimal>(accounts, delegate(Account a, decimal d) { return a.Balance + d; });
上述代码中使用了匿名委托,大家没有忘记吧。
本例代码:
最后:
本文中,有大量了代码,本人个人认为,代码的表现力远远超出了语言所能表达的含义,因为我喜欢代码给我的直观感觉。
好了,下篇该整理集合了,主要是描述列表,队列,栈,链表,有序表,字典,LookUp,HashSet,位数组等数据类型,下回见。