泛型的目的及优点
我们在写一些通用库的时候,经常需要写一个算法,比如交换,搜索,比较,排序,转换等算法,但是需要支持int,string等多种类型。通常我们可能会把代码复制多遍分别处理不同类型的数据。有没有一种办法,让我们只写一遍算法的实现,就可以支持所有类型的数据?泛型(generic)是C#提供的一种机制,它可以提供这种形式的代码重用,即“算法重用”。简单来说,开发人员在定义算法的时候并不设定算法操作的数据类型,而是在使用这个算法的时候再指定具体的数据类型。大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,以及泛型接口和泛型委托。所以CLR允许在类或接口中定义泛型方法。来看一个简单例子,Framework类库定义了一个泛型列表算法,它知道如何管理对象集合。泛型算法没有设定数据的类型。要在使用这个泛型列表算法时指定具体的数据类型。封装了泛型列表算法的FCL类称为List<T>。这个类是System.Collections.Generic命名空间中定义的。下面展示了类的定义:
// List<T> 泛型类
// IList<T> 泛型接口
// T:类型参数,在定义泛型类的时候不设定,在使用泛型类的时候指定具体类型,如int,string等
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection
{
public List();
public int Count { get; }
public T this[int index] { get; set; }
public void Add(T item);
public void Clear();
//Predicate<T>泛型委托,类似:Func<T,bool>
public int RemoveAll(Predicate<T> match);
public bool Contains(T item);
public bool Exists(Predicate<T> match);
public T Find(Predicate<T> match);
public List<T> FindAll(Predicate<T> match);
public int FindIndex(Predicate<T> match);
public void Sort();
//Comparison<T>泛型委托,类似:Func<T,T,int>
//当第一参数比第二个参数小,返回-1.
//当第一参数和第二个参数相等,返回0.
//当第一参数比第二个参数大,返回1.
public void Sort(Comparison<T> comparison);
//IComparer<T>泛型接口: int Compare(T x, T y);
//当第一参数比第二个参数小,返回-1.
//当第一参数和第二个参数相等,返回0.
//当第一参数比第二个参数大,返回1.
public void Sort(IComparer<T> comparer);
public T[] ToArray();
//泛型方法,方法声明里面包含了额外的类型:TOutput
//Converter<T, TOutput>泛型委托,类似:Func<T, TOutput>
public List<TOutput> ConvertAll<TOutput>(Converter<T, TOutput> converter);
}
通过上面的代码,总结一下:
1. 定义泛型类: class List<T>。
2. 定义泛型接口: interface IList<T>。
3. T是一个未指定的数据类型,称为类型参数。
4. T是变量名,源代码能使用数据类型的任何地方都可以使用T。
5. 泛型参数变量要么是T,要么至少以T开头,如TKey和TValue。大写T代表类型(Type)。
6. 很多方法接受Predicate<T>类型的参数,它是一个委托,类似:Func<T,bool>。
7. Sort方法可以传入自定义的委托或接口,实现自定义排序。
8. ConvertAll<TOutput>方法是一个泛型方法,可以传入自定义的委托,实现自定义转型。
定义好泛型之后,其他开发人员可以使用这个泛型算法,使用的时候需要指定具体数据类型,这个具体数据类型称为类型实参。代码示例
List<DateTime> dtlist = new List<DateTime>();
dtlist.Add(DateTime.Now);//不进行装箱
dtlist.Add(DateTime.MinValue);
dtlist.Add("2015-1-1");//编译时错误
DateTime dt = dtlist[0];//不需要转型
泛型为开发人员提供了一下优势:
1. 类型安全,在编译时可以检测错误。
2. 代码更加清晰,不需要手动转型。
3. 更好的性能,不进行装箱。
FCL中的泛型
泛型最明显的应用就是集合类。FCL在System.Collections.Generic和System.Collections.ObjectModel命名空间中提供了很多泛型集合类。System.Collections.Concurrent命名空间则提供了线程安全的泛型集合类。微软建议尽量使用泛型集合类。因为泛型集合类更加安全,代码更加清晰,性能更加出色。泛型集合类具有更好的对象模型,提供了更多的新功能。
开放类型和封闭类型
具有泛型类型参数的类型仍然是类型,CLR同样会为它创建内部的类型对象。具有泛型类型参数的类型称为开发类型,CLR禁止构造开放类型的任何实例,这一点类似于CLR禁止构造接口类型的实例。代码引用泛型类型时可指定一组泛型类型实参,为所有类型参数都传递了实际的数据类型,该类型就成为了封闭类型。CLR允许构造封闭类型的实例。CLR会在类型对象内部分配类型的静态字段。每一个封闭类型都有自己的静态字段。另外假如泛型类型定义了静态构造器,那么针对每一个封闭类型,都会执行一次。泛型类型定义静态构造器的目的是保证传递的类型实参满足特定的条件。例如,我们可以这样定义只能处理枚举类型的泛型类型:
class GenericTypeThatRequireAnEnum<T>
{
static GenericTypeThatRequireAnEnum()
{
if(!typeof(T).IsEnum)
{
throw new ArgumentException("T must be an enumerated typeof.");
}
}
}
泛型与继承
泛型类型仍然是类型,所以能从其他任何类型派生。使用泛型类型并指定类型实参时,实际是在CLR中定义一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生。例如,由于List<T>从Object派生,所以List<string>和List<int>也从Object派生。指定类型实参不影响继承层次结构。理解这一点之后,有助于你判断哪些强制类型的转换是允许的。现在定义一个链表节点类:
public class Node<T>
{
public T Data;
public Node<T> Next;
public Node(T data) : this (data, null){}
public Node(T data, Node<T> next)
{
Data = data;
Next = next;
}
}
private Node<char> CreateNodes()
{
Node<char> head = new Node<char>('c');
head = new Node<char>('b', head);
head = new Node<char>('a', head);
head = new Node<int>(1, head);//编译错误,Data字段必须包含相同的数据类型。
return head;
}
在这个Node类中,对于Next字段引用的另一个节点来说,它的字段必须包含相同的数据类型。这意味着所有的数据项都必须具有相同的类型(或派生类型)。例如,不能使用Node类来创建一个包含char值,另一个包含DateTime值。当然,如果使用Node<object>,是可以做到的,但是会丧失编译时类型安全性,而且值类型会被装箱。所以,有没有更好的办法?我们利用泛型继承的特点是可以做到。示例代码:
public class Node
{
protected Node Next;
public Node(Node next){ Next = next; }
}
public sealed class TypedNode<T> : Node
{
public T Data;
public TypedNode(T data, Node next) : base(next)
{
Data = data;
}
public TypedNode(T data) : this(data, null){}
}
private Node<char> CreateNodes()
{
Node<char> head = new TypedNode<char>('a');
head = new TypedNode<DateTime>(DateTime.Now, head);
head = new Node<int>(1, head);
return head;
}
上面的代码实现了链表,其中每个节点都是不同的数据类型,同时获得了编译的类型安全性,并防止了值类型装箱。
泛型接口
泛型的主要作用是定义泛型的引用类型和值类型。泛型接口的支持对CLR来说也很重要。若没有泛型接口,每次用非泛型接口(如 IComparable)来操作值类型都会发生装箱,而且会失去编译时的类型安全性。这将严重制约泛型类型的应用范围。因此,CLR提供了对泛型接口的支持。引用类型和值类型可指定类型实参实现泛型接口,也可以保持类型实参的未指定状态来实现泛型接口。一下是FCL的一部分代码:
public interface IEnumerator<T> : IDisposable, IEnumerator
{
T Current { get; }
}
下面的Triangle实现了上面的泛型接口,而且指定了泛型实参。
class Triangle : IEnumerator<Point>
{
private Point[] _points;
public Point Current { get { ... } }
...
}
下面实现了相同的泛型接口,但是保持类型实参的未指定状态:
class ArrayEnumerator<T> : IEnumerator<T>
{
private T[] array;
public T Current { get { ... } }
}
泛型方法
定义泛型类、结构或接口时,类型中定义的任何方法都可引用类型指定的类型参数。类型参数可以作为方法参数,返回值或者方法内部定义的局部变量的类型使用。不仅如此,CLR还运行方法指定自己的类型参数。这些类型参数也可作为参数、返回值或局部变量的类型使用。看个例子:
class GenericType<T>
{
private T Value;
public GenericType(T value) { Value = value; }
public TOutput Converter<TOutput>()
{
TOutput result = ...;
return result;
}
}
这个例子中,GenericType类型定义了类型参数T,Converter方法也定义了自己的类型参数TOutput。Converter方法能将Value字段引用的对象转换成任意类型--具体取决于调用时传递的类型实参是什么。泛型方法的存在,为开发人员提供了极大的灵活性。
泛型的验证和约束
前面我们提到,使用静态构造器来约束泛型,我们可以这样定义只能处理枚举类型的泛型类型:
class GenericTypeThatRequireAnEnum<T>
{
static GenericTypeThatRequireAnEnum()
{
if(!typeof(T).IsEnum)
{
throw new ArgumentException("T must be an enumerated typeof.");
}
}
}
C#还提供更多对泛型验证和约束的能力,编译泛型代码时,C#编译器会进行分析,确保代码使用于当前已有或将来可能定义的任何类型。先看一段代码:
private static bool MethodTakingAnyType<T>(T o)
{
T t = o;
string str1 = o.ToString();
bool isSame = t.Equals(o);
return b;
}
这个方法适用于任何类型,无论T是引用类型,值类型,枚举类型,接口还是委托类型,它都能工作。这个方法适用于当前存在的所有类型,也适用于将来可能定义的新类型,因为所有类型都继承与object类型,可以调用object类型定义的方法(比如ToString和Equals)。再看一个方法:
private static T Min<T>(T o1, T o2)
{
if(o1.CompareTo(o2) < 0) return o1;
return o2;
}
Min方法试图使用o1变量来调用CompareTo方法,但是许多类型都没有提供CompareTo方法,所以C#编译器不能编译上述代码,因为这个方法不适用于所有类型。强行编译会报错。所以,我们需要一种机制,让泛型变得真正有用。幸好,编译器和CLR支持称为约束的机制。约束的作用是限制能指定成泛型实参的类型范围。通过限制类型的范围,比如指定T的类型实参必须是实现了IComparer<T>接口,代码如下:
private static T Min<T>(T o1, T o2) where T : IComparer<T>
{
if(o1.CompareTo(o2) < 0) return o1;
return o2;
}
C#的where关键字告诉编译器,为T指定的任何类型都必须实现了IComparer<T>接口。有了这个约束,就可以在方法中调用CompareTo,因为IComparer<T>接口定义了CompareTo方法。定义好泛型约束之后,当其他代码引用这个泛型类型或方法时,编译器要负责确保类型实参符合指定的约束。假如编译一下代码,编译器会报错:
private static void CallMin()
{
object o1 = "str1", o2="str2";
object oMin = Min<object>(o1, o2); //error
}
编译器报错是因为object没有实现IComparer<object>接口。我们现在对约束及其工作方式有了基本的认识。约束可应用于泛型类型的类型参数,也可以应用于泛型方法的类型参数。当重写虚泛型方法时,会自动继承基类方法上的约束,并且不能修改。泛型约束有以下几种:
1. 主要约束
2. 次要约束
3. 构造器约束
主要约束
为类型参数指定一个引用类型约束。相当于向编译器承诺:一个指定的类型实参要么是约束类型相同的类型,要么是从约束类型派生的类型。如下示例:
class PrimaryConstraintOfStream<T> where T : Stream
{
public void M(T stream) { stream.Close() }
}
这个代码设置了主要约束Stream,在使用PrimaryConstraintOfStream代码指定类型参数时,必须指定Stream或者从Stream派生的类型。有两个特殊的主要约束: class和struct。 class约束是类型实参时引用类型。任何类类型、接口类型、委托类型和数组类型都是满足这个约束的。示例:
class PrimaryConstraintOfStream<T> where T : class
{
public void M() { T temp = null; }//允许,因为T肯定是引用类型
}
struct约束向编译器承诺类型实参是值类型。包括枚举在内的任何值类型都满足这个约束,但是Nullable<T>值类型除外,编译器和CLR认为它是特殊类型。一下示例:
class PrimaryConstraintOfStream<T> where T : struct
{
public void M()
{
//允许,因为所以的值类型都有一个公共无参数构造器
T temp = new T();
}
}
次要约束
为类型参数指定多个接口类型。相当于向编译器承诺:一个指定的类型实参实现了指定的所有接口。示例:
class Dictionary<TKey,TVal>
where TKey: IComparable, IEnumerable
where TVal: IMy
{
...
}
构造器约束
为类型参数指定一个构造器约束。相当于向编译器承诺:一个指定的类型实参实现了公共无参数构造器。示例:
class PrimaryConstraintOfStream<T> where T : new()
{
public void M()
{
//允许,都有一个公共无参数构造器
T temp = new T();
}
}
泛型类型变量的转型
使用C# as操作符:
T obj = arg1;
string s = obj as string;
为泛型类型变量设置默认值
T temp = default(T);
不要将类型参数约束成具体的值类型,因为值类型是密封类型,不可能存在从值类型派生的类型。如果允许将类型参数约束成具体的值类型,那么泛型方法会被约束为只支持该具体的类型,这还不如不要泛型呢!