知识结构:
泛型广泛应用于容器(Collections)和对容器操作的方法中。
从 .NET Framework2.0 开始,微软提供了一个新的命名空间System.Collections.Generic
,其中包含了一些新的基于泛型的容器类。当然,我们也可以自己创建泛型类、泛型接口、泛型方法以及泛型委托,通过这种类型安全且高效的方式来满足不同应用场景的需求。
下面的示例代码以一个简单的泛型链表类作为示范。(多数情况下,推荐使用 .NET Framework 类库提供的LinkedList
,而不是创建自己的链表。)
单链表结构代码如下所示:
public class SNode<T> where T : IComparable<T>
{
public T Data {
get; set; }
public SNode<T> Next {
get; set; }
public SNode(T data, SNode<T> next = null)
{
Data = data;
Next = next;
}
}
public class SLinkList<T> : IEnumerable<T> where T : IComparable<T>
{
public SNode<T> PHead {
get; protected set; }
public int Length {
get; private set; }
public SLinkList()
{
Length = 0;
PHead = null;
}
private SNode<T> Locate(int index)
{
if (index < 0 || index > Length - 1)
throw new IndexOutOfRangeException();
SNode<T> temp = PHead;
for (int i = 0; i < index; i++)
{
temp = temp.Next;
}
return temp;
}
public void InsertAtFirst(T data)
{
PHead = new SNode<T>(data, PHead);
Length++;
}
public void InsertAtRear(T data)
{
if (PHead == null)
{
PHead = new SNode<T>(data);
}
else
{
Locate(Length - 1).Next = new SNode<T>(data);
}
Length++;
}
public void Insert(int index, T data)
{
if (index < 0 || index > Length)
throw new IndexOutOfRangeException();
if (index == 0)
{
InsertAtFirst(data);
}
else if (index == Length)
{
InsertAtRear(data);
}
else
{
Locate(index - 1).Next = new SNode<T>(data, Locate(index));
Length++;
}
}
public void Remove(int index)
{
if (index < 0 || index > Length - 1)
throw new IndexOutOfRangeException();
if (index == 0)
{
PHead = PHead.Next;
}
else
{
Locate(index - 1).Next = Locate(index).Next;
}
Length--;
}
public T this[int index]
{
get
{
if (index < 0 || index > Length - 1)
throw new IndexOutOfRangeException();
return Locate(index).Data;
}
set
{
if (index < 0 || index > Length - 1)
throw new IndexOutOfRangeException();
Locate(index).Data = value;
}
}
public bool IsEmpty()
{
return Length == 0;
}
public void Clear()
{
PHead = null;
Length = 0;
}
public IEnumerator<T> GetEnumerator()
{
SNode<T> current = PHead;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
客户端代码如下所示:
class Program
{
static void Main(string[] args)
{
SLinkList<int> lst = new SLinkList<int>();
for (int i = 0; i < 3; i++)
{
lst.InsertAtFirst(i);
}
foreach (int i in lst)
{
Console.WriteLine(i);
}
// 2
// 1
// 0
}
}
上面实例代码演示了客户端代码如何使用泛型类SLinkList
,来创建一个整数链表。通过简单地改变参数的类型,很容易改写上面的代码,以创建字符串或其它自定义类型的链表。
客户端代码如下所示:
class Program
{
static void Main(string[] args)
{
SLinkList<string> lst = new SLinkList<string>();
for (int i = 0; i < 3; i++)
{
lst.InsertAtFirst("第" + i + "个,进入链表.");
}
foreach (string s in lst)
{
Console.WriteLine(s);
}
// 第2个,进入链表.
// 第1个,进入链表.
// 第0个,进入链表.
}
}
可见,泛型是由编译器提供的语法糖,在调用的时候,编译器针对客户端提供的类型参数生成不同的副本,以方便开发人员编码,不必为每种类型编写那些逻辑相同的代码。
以前类型泛化(Generalization)是靠类型与全局基类System.Object
的相互转换来实现的(里氏代换原则)。如 .NET Framework 基础类库中的ArrayList
容器类。
ArrayList
是一个很方便的容器类,使用中无需更改就可以存储任何引用类型或值类型。
ArrayList list1 = new ArrayList();
list1.Add(3);
list1.Add(105);
//...
ArrayList list2 = new ArrayList();
list2.Add("Tom");
list2.Add("John");
//...
但是这种便利是有代价的,这需要把任何一个加入ArrayList
的引用类型或值类型都隐式地向上转换成System.Object
。如果这些元素是值类型,那么当加入到ArrayList
中时,它们必须被装箱,当重新取回它们时,要拆箱。类型转换和装箱、拆箱的操作都降低了性能,在迭代(Iterator)大容器的情况下,装箱和拆箱对性能的影响可能十分显著。
另一个局限是缺乏编译时的类型检查,引起类型安全问题,当一个ArrayList
把任意类型都转换为Object
,就无法在编译时预防客户代码类似这样的操作:
ArrayList list = new ArrayList();
list.Add(3);
list.Add("Tom");
int t = 0;
foreach (int x in list)
{
t += x;
}
虽然这样完全合法,但这个错误直到运行的时候才会被发现。ArrayList
和其它相似的类真正需要的是一种途径,能让客户端代码在实例化之前指定所需的特定数据类型。这样就不需要向上类型转换为Object
,而且编译器可以同时进行类型检查。换句话来说,ArrayList
需要一个类型参数。这正是泛型所提供的,泛型即参数化类型,在编码时通过指定类型参数就不会存在装箱,拆箱问题,避免性能损失,且保证类型安全。
在System.Collections.Generic
命名空间中的泛型List
容器里,同样把元素加入容器的操作,类似这样:
List<int> list = new List<int>();
list.Add(3);
//list.Add("Tom"); //编译错误
//错误 CS1503 参数 1: 无法从“string”转换为“int”
与ArrayList
相比,在客户端代码中唯一增加的List
语法是声明和实例化中的类型参数。代码略微复杂,但创建的容器类不仅比ArrayList
更安全,而且明显地更加快速,尤其当容器中的元素是值类型的时候。
在泛型类、泛型接口、泛型方法或泛型委托的定义中,类型参数是一个占位符(Place Holder),通常为一个大写字母,如T
。在客户端通过代码声明或实例化该类型的变量时,把T
替换为客户端所指定的数据类型。
如果要使用泛型类SLinkList
,客户端代码必须在尖括号内指定一个类型参数。同样,也可以使用不同的类型参数来创建不同的对象,如下所示:
SLinkList<int> list1 = new SLinkList<int>();
SLinkList<double> list2 = new SLinkList<double>();
SLinkList<string> list3 = new SLinkList<string>();
对于这些SLinkList
的实例,泛型类中出现的每个T
都将在编译时被类型参数指定的具体类型所取代。依靠这样的取代,我们仅用一份泛型类的代码,就创建了三个独立的,类型安全且高效的对象。
通常情况下一个泛型类写成如下形式:
public class MyClass<T>
{
//…
}
以上泛型类中的T
,被称为未绑定类型参数(Unbounded type parameters)。
未绑定类型参数必须遵守以下规则:
!=
和==
运算符,因为无法保证具体的类型参数是否能够支持这些运算符。System.Object
相互转换,也可显式地转换成任何类型。在设计泛型类或泛型方法时,如果对泛型成员执行任何赋值以外的操作或者调用System.Object
中所没有的方法(编译器默认类型参数是直接继承System.Object
),就需要在类型参数上使用约束。
利用where
子句,可以为类型参数指定一个或多个约束,从而增加了类型参数可调用的属性和方法的数量。这些属性和方法受约束类型及其派生层次中的类型的支持。如果客户端代码尝试使用某个约束不允许的类型来进行实例化,则会产生编译时错误。下表列出了六类约束:
(1)值约束和引用约束举例
public class MyClass<T, U> where T : class where U : struct
{
//对于多个类型参数,每个类型参数都使用一个where子句
}
注意:在应用where T : class
约束时,避免对类型参数使用==
和!=
运算符,因为这些运算符仅测试引用同一性而不测试值相等性。即使在用作参数的类型中重载这些运算符也是如此。下面的代码说明了这一点,即使string
类重载==
运算符,输出也为false
。
class Program
{
private static void OpTest<T>(T s, T t) where T : class
{
Console.WriteLine(s == t);
}
static void Main(string[] args)
{
string s1 = "target";
StringBuilder sb = new StringBuilder("target");
string s2 = sb.ToString();
OpTest(s1, s2); // False
Console.WriteLine(s1 == s2); // True
}
}
由于编译器在编译时仅知道T是引用类型,因此必须使用对所有引用类型都有效的默认运算符。如果必须测试值相等性,建议的方法是同时应用where T : IComparable
约束,并在将用于构造泛型类的任何类中实现该接口。
(2)接口约束举例
public class MyGenericClass<T> where T : IComparable<T>
{
//...
}
public interface IMyInterface
{
//...
}
public class Test
{
public bool MyMethod<T>(T t) where T : IMyInterface
{
//...
return false;
}
}
(3)构造约束举例
public class ItemFactory<T> where T : new()
{
public T CreateItem()
{
return new T();
}
}
public delegate T MyDelegate<T>() where T : new();
public class ItemFactory2<T> where T : IComparable, new()
{
//当与其它约束一起使用时,new()约束必须最后指定。
}
(4)类型参数约束举例
internal class MyList<T>
{
// T为未绑定类型参数
private void Add<TU>(MyList<TU> items) where TU : T
{
//此处为类型参数约束
}
}
类型参数也可以用在泛型类的定义中,但这种约束用途十分有限。只有希望强制两个类型参数具有继承关系的时候,才考虑使用。
public class MyClass<T, TU, TV> where T : TV
{
// ...
}
下面提供一个完整的,有关泛型约束的例子。
Employee代码如下:
public class Employee : IComparable<Employee>
{
public string Name {
get; set; }
public int Age {
get; set; }
public Employee(string name, int age)
{
Name = name;
Age = age;
}
public Employee()
{
;
}
public int CompareTo(Employee other)
{
if (other == null)
throw new ArgumentNullException();
return Age - other.Age;
}
public override string ToString()
{
return Name + ":" + Age;
}
}
public class MyList<T> : SLinkList<T>
where T : Employee, IComparable<T>, new()
{
public T FindFirstOccurence(string str)
{
T result = null;
SNode<T> current = PHead;
while (current != null)
{
if (current.Data.Name == str)
{
result = current.Data;
break;
}
current = current.Next;
}
return result;
}
}
客户端代码如下:
class Program
{
static void Main(string[] args)
{
// MyList lst = new MyList();
// 编译错误
// 错误 CS0310
// “string”必须是具有公共的无参数构造函数的非抽象类型,
// 才能用作泛型类型或方法“MyList < T >”中的参数“T”
MyList<Employee> list = new MyList<Employee>();
string[] names = {
"Tom", "John", "Patrick", "Maya", "Smith" };
int[] ages = {
45, 19, 28, 23, 35 };
for (int i = 0; i < names.Length; i++)
{
list.InsertAtFirst(new Employee(names[i], ages[i]));
}
foreach (Employee e in list)
{
Console.WriteLine(e.ToString());
}
// Smith:35
// Maya:23
// Patrick:28
// John:19
// Tom:45
Console.WriteLine(list[0] == list[1] ? "True" : "False");
// False
}
}
泛型类封装了不针对任何特定数据类型的操作。常用于容器类,如 .NET Framework 类库中的动态数组List
、双向链表LinkedList
、栈Stack
、队列Queue
等等。
List
。List
。在进行泛化的设计时,应该注意以下问题:
(1)泛型类可以继承自实体类、封闭构造类型的基类、开放构造类型的基类,如下所示。
public class BaseNode
{
// 实体类
}
public class BaseNodeGeneric<T>
{
// 普通泛型类
}
public class BaseNodeMultiple<T, TU>
{
// 普通泛型类
}
public class NodeConcrete<T> : BaseNode
{
// 泛型类继承实体类
}
public class NodeClosed<T> : BaseNodeGeneric<int>
{
//泛型类继承封闭构造类型的基类
}
public class NodeOpen<T> : BaseNodeGeneric<T>
{
//泛型类继承开放构造类型的基类
}
public class Node5<T, TU> : BaseNodeMultiple<T, TU>
{
//泛型类继承开放构造类型的基类
}
public class Node4<T> : BaseNodeMultiple<T, int>
{
//除了与子类共用的类型参数外,必须为所有的其它类型参数指定类型
}
//public class Node6 : BaseNodeMultiple
//{
// //Generates an Error
//}
(2)非泛型的具体类可以继承自封闭构造类型的基类,但不能继承自开放构造类型的基类。这是因为客户代码无法提供基类所需的类型参数,如下所示。
public class BaseNodeGeneric<T>
{
// 普通泛型类
}
public class Node1 : BaseNodeGeneric<int>
{
// No Error
}
//public class Node2 : BaseNodeGeneric
//{
// // Generates an Error
//}
//public class Node3 : T
//{
// // Generates an Error
//}
(3)从开放式构造类型继承的泛型类,如果基类指定约束,那么子类必须指定约束,这些约束是基类约束的超集或等于基类型约束,如下所示。
public class NodeItem<T> where T : IComparable<T>, new()
{
//...
}
public class SpecialNodeItem<T> : NodeItem<T>
where T : IComparable<T>, new()
{
//...
}
(4)开放结构类型和封闭结构类型可以用作方法的参数,如下所示:
class Program
{
private void Swap<T>(List<T> list1, List<T> list2)
{
//...
}
private void Swap(List<int> list1, List<int> list2)
{
//...
}
}
我们来看一个综合的例子,构造SortList
结构。
public class SortList<T> : SLinkList<T> where T : IComparable<T>
{
public void BubbleSort()
{
if (PHead == null || PHead.Next == null)
return;
bool swapped;
do
{
swapped = false;
SNode<T> previous = null;
SNode<T> current = PHead;
while (current.Next != null)
{
if (current.Data.CompareTo(current.Next.Data) > 0)
{
SNode<T> tmp = current.Next;
current.Next = tmp.Next;
tmp.Next = current;
if (previous == null)
{
PHead = tmp;
}
else
{
previous.Next = tmp;
}
previous = tmp;
swapped = true;
}
else
{
previous = current;
current = current.Next;
}
}
} while (swapped);
}
}
客户端代码如下:
class Program
{
static void Main(string[] args)
{
SortList<Employee> list = new SortList<Employee>();
string[] names = {
"Tom", "John", "Patrick", "Maya", "Smith" };
int[] ages = {
45, 19, 28, 23, 35 };
for (int i = 0; i < names.Length; i++)
{
list.InsertAtFirst(new Employee(names[i], ages[i]));
}
foreach (Employee e in list)
{
Console.WriteLine(e);
}
// Smith:35
// Maya:23
// Patrick:28
// John:19
// Tom:45
list.BubbleSort();
foreach (Employee e in list)
{
Console.WriteLine(e);
}
// John:19
// Maya:23
// Patrick:28
// Smith:35
// Tom:45
}
}
通常,我们设计泛型类是从一个已有的具体类开始的,每次把一个类型改为类型参数,直至达到一般性和可用性的最佳平衡。在设计泛型类时,需要重点考虑的事项有:
(1)考虑要把哪些类型泛化为类型参数。一般的规律是,用参数表示的类型越多,代码的灵活性和复用性也越大。但过多的泛化会导致代码难以被其它开发人员理解,需要权衡。
(2)考虑这些类型参数需要什么样的约束。一个良好的习惯是,尽可能使用最大的约束,同时保证可以处理所有需要处理的类型。例如,如果你知道你的泛型类只打算使用引用类型,那么就应用使用引用约束。这样可以防止无意中使用值类型,同时可以对T
使用as
运算符,并且检查空引用。
(3)考虑泛型类的继承关系,以及泛型行为应该放在基类中还是子类中。
(4)考虑泛型类是否要实现一个或多个泛型接口。例如,要设计一个泛型容器,就要考虑是否需要实现类似IEnumerable
,IComparable
这样的接口,以方便容器元素的遍历和查找。
后台回复「搜搜搜」,随机获取电子资源!
欢迎关注,请扫描二维码: