泛型的好处
泛型是C#中的一个非常重要的语法,泛型的好处可以归结为一下几点:性能;类型安全;二进制重用;防止代码臃肿;命名规范
性能:性能是泛型最大的好处之一,当在非泛型的类中使用值类型的时候要涉及到装箱和拆箱。值类型是放在栈上的,引用类型是放在堆上的。C#类是引用类型,结构是值类型。.net提供了值类型到引用类型的转换,这个转换的过程叫做装箱,当将一个值类型传递给一个需要引用类型作为参数的方法的时候装箱会自动进行,相反,拆箱就是将引用类型转换为值类型,当使用拆箱的时候需要强制转换。如下面的代码:
var list = new ArrayList();
list.Add(44); // boxing — convert a value type to a reference type
int i1 = (int)list[0]; // unboxing — convert a reference type to
// a value type
foreach (int i2 in list)
{
Console.WriteLine(i2); // unboxing
}
装箱和拆箱虽然容易使用,但是非常耗费性能。泛型允许在使用的时候才定义类型,这样就不用拆箱和装箱了。如下面的代码
var list = new List < int > ();
list.Add(44); // no boxing — value types are stored in the List < int >
int i1 = list[0]; // no unboxing, no cast needed
foreach (int i2 in list)
{
Console.WriteLine(i2);
}
类型安全:使用泛型的另外一个好处是可以保证类型安全。先看下面的代码:
var list = new ArrayList();
list.Add(44);
list.Add("mystring");
list.Add(new MyClass());
这段代码的编译是没有问题的,因为int和string都可以装箱为object对象,但是在使用这个数组的时候,却会出现运行时错误,编译的时候却不会出错:
foreach (int i in list) { Console.WriteLine(i); }
但是使用泛型就不会出现这样的情况,因为在向里面插入值的时候IDE就会提示错误:
var list = new List<int>();
list.Add(44);
list.Add("mystring"); // compile time error
list.Add(new MyClass()); // compile time error
二进制代码重用:泛型可以更好的重用代码,因为一个泛型类可以被定义一次,然后被多种不同类型的对象使用,如下面的代码:
var list = new List<int>();
list.Add(44);
var stringList = new List<string>();
stringList.Add("mystring");
var myClassList = new List<MyClass>();
myClassList.Add(new MyClass());
同时,泛型类型可以在定义后被.NET平台的其他语言使用
防止代码臃肿:当使用类型声明了不同类型的实例的时候,是否会生成大量重复的代码呢?因为泛型是定义在程序集内部,用特定类型实例泛型的时候并不会重复这些类的IL代码。但是在将IL代码通过JIT转化为本地代码的时候,还是会为不同的类型分别创建代码。引用类型在本地代码中还是会分享相同的代码,因为引用类型值需要四个字节的内存空间地址来在泛型初始化的时候引用实例化的类型。但是因为值类型不同的类型对内存的要求不一样,因此值类型的每一次泛型实例都或重新创建一个完整的类型。
命名规范:使用泛型的时候,好的命名规范可以帮助区别泛型和非泛型,通常遵循下面的原则,泛型类型的名字用T作为前缀;如果对泛型类型没有任何特殊要求,那么直接使用T来代表泛型类型,如public class List<T>{} public class LinkedList<T>{};如果对泛型类型有一些要求,比如需要继承自某个类,或者涉及到两个或者多个类型,那么需要使用描述性的命名例如:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e); public delegate TOutput Converter<TInput, TOutput>(TInput from); public class SortedList<TKey, TValue> { }
创建泛型类
首先创建一个简单的LinkedListNode和LinkedList普通类,代码如下:
public class LinkedListNode { public LinkedListNode(object value) { this.Value = value; } public object Value { get; private set; } public LinkedListNode Next { get; internal set; } public LinkedListNode Prev { get; internal set; } } public class LinkedList: IEnumerable { public LinkedListNode First { get; private set; } public LinkedListNode Last { get; private set; } public LinkedListNode AddLast(object node) { var newNode = new LinkedListNode(node); if (First == null) { First = newNode; Last = First; } else { Last.Next = newNode; Last = newNode; } return newNode; } public IEnumerator GetEnumerator() { LinkedListNode current = First; while (current != null) { yield return current.Value; current = current.Next; } } }
现在使用这个通用类,书写如下代码,不仅仅会发生装箱和拆箱动作,而且还会在运行的时候报错:
var list1 = new LinkedList(); list1.AddLast(2); list1.AddLast(4); list1.AddLast("6"); foreach (int i in list1) { Console.WriteLine(i); }
下面我们来创建LinkedListNode和LinkedList的泛型类,使用T来代替object类型,代码如下:
public class LinkedListNode <T> { public LinkedListNode(T value) { this.Value = value; } public T Value { get; private set; } public LinkedListNode <T> Next { get; internal set; } public LinkedListNode <T> Prev { get; internal set; } } public class LinkedList <T> : IEnumerable <T> { public LinkedListNode <T> First { get; private set; } public LinkedListNode <T> Last { get; private set; } public LinkedListNode <T> AddLast(T node) { var newNode = new LinkedListNode <T> (node); if (First == null) { First = newNode; Last = First; } else { Last.Next = newNode; Last = newNode; } return newNode; } public IEnumerator <T> GetEnumerator() { LinkedListNode <T> current = First; while (current != null) { yield return current.Value; current = current.Next; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
现在你就可以声明不同类型的LinkedList对象了,如下:
var list2 = new LinkedList < int > (); list2.AddLast(1); list2.AddLast(3); list2.AddLast(5); foreach (int i in list2) { Console.WriteLine(i); } var list3 = new LinkedList < string > (); list3.AddLast("2"); list3.AddLast("four"); list3.AddLast("foo"); foreach (string s in list3) { Console.WriteLine(s); }
泛型特性
泛型包含了一些特性用来对泛型进行一些约束,这些约束包括默认值;约束;继承和静态变量。首先创建一个文档管理的泛型,用作下面的示例的基础:
using System; using System.Collections.Generic; namespace Wrox.ProCSharp.Generics { public class DocumentManager <T> { private readonly Queue <T> documentQueue = new Queue <T> (); public void AddDocument(T doc) { lock (this) { documentQueue.Enqueue(doc); } } public bool IsDocumentAvailable { get { return documentQueue.Count > 0; } } } }
默认值:在DocumentManager<T>类中的GetDocument方法中的类型T有可能为null,但是实际上不能对泛型赋予null值,因为null是针对引用类型的,而泛型有可能分配一个值类型。因此,为了解决这个问题,可以使用default关键字,通过default关键字,可以针对引用类型分配一个null,对值类型分配一个0,如:
public T GetDocument() { T doc = default(T); lock (this) { doc = documentQueue.Dequeue(); } return doc; }
现在我创建一个document类型,集成了IDocument接口,这个接口集成了title和content属性,那么我在需要使用这两个属性的时候依然需要进行类型转换,看如下代码:
public interface IDocument { string Title { get; set; } string Content { get; set; } } public class Document: IDocument { public Document() { } public Document(string title, string content) { this.Title = title; this.Content = content; } public string Title { get; set; } public string Content { get; set; } }
public void DisplayAllDocuments() { foreach (T doc in documentQueue) { Console.WriteLine(((IDocument)doc).Title); } }
类型转换是一方面,而且如果存入的类型并没有继承IDoucment继承,那么将会在运行时出现错误。一个好的解决方法是在定义泛型的时候规定类型必须继承自接口IDocument,这样就不用进行类型转直接可以使用,如下面的代码:
public class DocumentManager<TDocument> where TDocument: IDocument {} public void DisplayAllDocuments() { foreach (TDocument doc in documentQueue) { Console.WriteLine(doc.Title); } }
类似的约束还有以下这些:where T:struct规定类型T必须是一个值类型;where T:class规定了类型T必须是引用类型;where T:IFoo规定了类型T必须继承自IFoo接口;whereT:Foo规定了类型T必须继承自Foo基类;where T:new规定了类型T必须有默认的构造器;where T1:T2规定了泛型T1必须继承自泛型T2。还可以对泛型进行多重限制,例如:public class MyClass<T> where T: IFoo, new(){}
继承:泛型类可以继承自泛型接口,泛型类也可以继承自泛型基类。要求是接口的泛型类型必须一样或者基类的泛型类型被指定,如:
public class LinkedList<T>: IEnumerable<T>
public class Base<T>
{
}
public class Derived<T>: Base<T>
{
}
public class Base<T>
{
}
public class Derived<T>: Base<string>
{
}
public abstract class Calc<T>
{
public abstract T Add(T x, T y);
public abstract T Sub(T x, T y);
}
public class IntCalc: Calc<int>
{
public override int Add(int x, int y)
{
return x + y;
}
public override int Sub(int x, int y)
{
return x — y;
}
}
静态成员:泛型类的静态成员需要特别注意,泛型类的静态变量只被同一个类型的实例所共享:
public class StaticDemo<T> { public static int x; } StaticDemo<string>.x = 4; StaticDemo<int>.x = 5; Console.WriteLine(StaticDemo<string>.x); // writes 4
泛型接口
在老的.NET平台中还没有泛型接口的时候,很多类都需要进行类型转换你例如:
public class Person: IComparable
{
public int CompareTo(object obj)
{
Person other = obj as Person;
return this.lastname.CompareTo(other.LastName);
}
}
但是如果使用泛型接口,可以在使用的时候定义类型,这样就不用再进行类型转欢了,如下:
public interface IComparable<in T>
{
int CompareTo(T other);
}
public class Person: IComparable<Person>
{
public int CompareTo(Person other)
{
return this.LastName.CompareTo(other.LastName);
}
}
协变和逆变:我们都知道里氏替换原则,就是子类可以替换父类,这称之为协变,如果是父类替换子类,我们称之为逆变。在.NET 4.0之前的版本中,泛型接口是不支持协变和逆变的,但是.NET 4中扩展泛型接口的协变和逆变特性,为了下面的示例代码,首先定义一个基类和派生类:
public class Shape { public double Width { get; set; } public double Height { get; set; } public override string ToString() { return String.Format("Width: {0}, Height: {1}", Width, Height); } } public class Rectangle: Shape { }
泛型接口的协变:如果定义泛型的时候在T的前面加上了out关键字,那么这个泛型接口支持协变,也就是说类型T只允许作为返回类型,如下面的示例:
public interface IIndex < out T > { T this[int index] { get; } int Count { get; } } public class RectangleCollection: IIndex<Rectangle> { private Rectangle[] data = new Rectangle[3] { new Rectangle { Height=2, Width=5}, new Rectangle { Height=3, Width=7}, new Rectangle { Height=4.5, Width=2.9} }; public static RectangleCollection GetRectangles() { return new RectangleCollection(); } public Rectangle this[int index] { get { if (index < 0 || index > data.Length) throw new ArgumentOutOfRangeException("index"); return data[index]; } } public int Count { get { return data.Length; } } } static void Main() { IIndex<Rectangle> rectangles = RectangleCollection.GetRectangles(); IIndex<Shape> shapes = rectangles; for (int i = 0; i < shapes.Count; i++) { Console.WriteLine(shapes[i]); } }
上面的例子中,因为泛型T进允许作为返回类型,因此可以把子类接口赋值给父类接口,也就是
IIndex<Rectangle> rectangles = RectangleCollection.GetRectangles(); IIndex<Shape> shapes = rectangles;
泛型接口的逆变:如果泛型被in关键字定义,那么这个泛型接口就是逆变的,也就是接口进允许使用泛型T作为方法的输入,如下面的代码:
public interface IDisplay<in T>
{
void Show(T item);
}
public class ShapeDisplay: IDisplay<Shape>
{
public void Show(Shape s)
{
Console.WriteLine("{0} Width: {1}, Height: {2}", s.GetType().Name,s.Width, s.Height);
}
}
这里,因为泛型T仅仅允许作为方法的输入参数,因此就可以把父类型的接口赋值给子类型的接口,如下:
static void Main() { //... IDisplay<Shape> shapeDisplay = new ShapeDisplay(); IDisplay<Rectangle> rectangleDisplay = shapeDisplay; rectangleDisplay.Show(rectangles[0]); }
泛型结构
同类一样,结构也可以作为泛型类型,除了不能继承之外其余的同类都相似。这里我们使用泛型结构Nullable<T>为例,这个Nullable<T>是.NET平台提供的一个泛型结构。
在处理数据库或者XML向实体类转换的时候,数据库中的字段可以为空,但是实体中的int等值类型不能为空,这样会造成转换的不方便,一种解决方法是将数据库中的数字映射为引用类型,但是这样仍然会引起资源的不必要消耗。有了Nullable<T>就可以很方便的解决这个问题,下面是一个Nullable<T>精简版本
public struct Nullable<T> where T: struct { public Nullable(T value) { this.hasValue = true; this.value = value; } private bool hasValue; public bool HasValue { get { return hasValue; } } private T value; public T Value { get { if (!hasValue) { throw new InvalidOperationException("no value"); } return value; } } public static explicit operator T(Nullable<T> value) { return value.Value; } public static implicit operator Nullable<T>(T value) { return new Nullable<T>(value); } public override string ToString() { if (!HasValue) return String.Empty; return this.value.ToString(); } }
Nullable的使用方式如下:
Nullable<int> x; x = 4; x += 3; if (x.HasValue) { int y = x.Value; } x = null; Nullable < int > x1; int? x2; int? x = GetNullableType(); if (x == null) { Console.WriteLine("x is null"); } else if (x < 0) { Console.WriteLine("x is smaller than 0"); } int? x1 = GetNullableType(); int? x2 = GetNullableType(); int? x3 = x1 + x2; int y1 = 4; int? x1 = y1; int? x1 = GetNullableType(); int y1 = (int)x1; int? x1 = GetNullableType(); int y1 = x1 ?? 0;
泛型方法
除了定义泛型类之外,还可以定义泛型方法,在方法声明的时候定义泛型,在非泛型类中可以定义泛型方法。下面是一个泛型方法的示例,首先是会用到的Account类和一个一般的方法:
public class Account { public string Name { get; private set; } public decimal Balance { get; private set; } public Account(string name, Decimal balance) { this.Name = name; this.Balance = balance; } } var accounts = new List<Account>() { new Account("Christian", 1500), new Account("Stephanie", 2200), new Account("Angela", 1800) }; public static class Algorithm { public static decimal AccumulateSimple(IEnumerable<Account> source) { decimal sum = 0; foreach (Account a in source) { sum += a.Balance; } return sum; } } decimal amount = Algorithm.AccumulateSimple(accounts);
受约束的泛型方法:上面的方法只能针对Account对象,如果使用泛型方法可以避免这个缺点,如下:
public static decimal Accumulate<TAccount>(IEnumerable<TAccount> source) where TAccount: IAccount { decimal sum = 0; foreach (TAccount a in source) { sum += a.Balance; } return sum; } public interface IAccount { decimal Balance { get; } string Name { get; } } public class Account: IAccount { //... } decimal amount = Algorithm.Accumulate<Account>(accounts);
带委托的泛型方法:上面的方法要求泛型必须继承自IAccount,这样的方法限制太大,下面的例子使用的委托类改变Accumulate方法。Accumulate方法使用了两个泛型参数,T1和T2,T1用来作为IEnumerable<T1>类型的参数,T2使用了泛型委托Func<T1,T2,TResult>,第二个和第三个是相同的T2类型,需要传递这个方法作为参数,方法有两个参数,T1和T2,返回类型为T2,如下面的代码:
public static T2 Accumulate<T1, T2>(IEnumerable<T1> source,Func<T1, T2, T2> action) { T2 sum = default(T2); foreach (T1 item in source) { sum = action(item, sum); } return sum; } decimal amount = Algorithm.Accumulate<Account, decimal>(accounts, (item, sum) => sum += item.Balance);
使用说明的泛型方法:泛型方法可以通过定义特定类型的说明来进行重载,对于拥有泛型参数的方法也同样适用。下面的Foo方法有两个版本,第一个版本是用泛型作为参数,第二个版本是说明用int作为参数。在编译的时候,如果传递的是int类型,直接用第二个版本,如果参数是其他类型,则使用其他的版本:
public class MethodOverloads
{
public void Foo<T>(T obj)
{
Console.WriteLine("Foo<T>(T obj), obj type: {0}", obj.GetType().Name);
}
public void Foo(int x)
{
Console.WriteLine("Foo(int x)");
}
public void Bar<T>(T obj)
{
Foo(obj);
}
}
static void Main()
{
var test = new MethodOverloads();
test.Foo(33);
test.Foo("abc");
}
需要注意的一点是,方法的选择或者调用是在编译的时候就决定了,不是运行时,可以通过以下方法来验证一下,运行下面的代码:
static void Main() { var test = new MethodOverloads(); test.Bar(44); }
我们可以看到虽然我们传递的是int类型的参数,但是显示的是调用了Foo的泛型方法。原因就是编译器在编译的时候选择Bar的方法,Bar方法定义了一个泛型参数,而被调用的Foo方法又有满足泛型参数的方法,因此Foo的泛型方法被调用,在运行的时候这个选择是不会被改变的。