> 泛型概述 - 泛型不仅是C#编程语言的一部分,而且与程序集中的IL代码紧密地集成。有了泛型,就可以创建独立于被包含类型的类和方法。 **泛型的优点** - 性能 - 类型安全性 - 二进制代码重用 - 代码的扩展 - 命名约定 **性能** - 对值类型使用非泛型集合类,在把值类型转换为引用类型,和把引用类型转换为值类型时,需要进行装箱和拆箱操作。 - 值类型存储在栈上,引用类型存储在堆上。C#类时引用类型,结构是值类型。.NET很容易把值类型转换为引用类型,所以可以在需要对象(对象是引用类型)的任意地方使用值类型。 - 从值类型转换为引用类型称为装箱。 **命名约定** - 泛型类型的命名规则 - 泛型类型的名称用字母T作为前缀 - 如果没有特殊要求,泛型类型允许用任意类替代,且只使用一个泛型类型,就可以用字符T作为泛型类型的名称。 ``` public class List{} public class LinkedList{} ``` - 如果泛型类型有特定的要求,或者使用了两个或多个泛型类型,就应给泛型类型使用描述性的名称: ``` public delegate void EventHandle(object sender,TEventArgs e); public delegate TOutput Converter(TInput from); public class SortedList{} ``` > 创建泛型类 - 在链表中,一个元素引用下一个元素。所以必须创建一个类,它将对象封装在链表中,并引用下一个对象。 - 类LinkedListNode包含一个属性value,该属性用构造函数初始化。另外,LinkedListNode类包含对链表中下一个元素和上一个元素的引用,这些元素都可以从属性中访问。 ``` public class LinkedListNode { public LinkedListNode(object value) { Value=value; } public object Value{get;private set;} public LinkedListNode Next{get;internal set;} public LinkedListNode Prev{get;internal set;} } ``` - LinkedList类包含LinkedListNode类型的First和Last属性,它们分别标记了链表的头尾。AddLast()方法在链表尾添加一个新元素。首页创建一个LinkedListNode类型的对象。如果链表是空的,First和Last属性就设置为该新元素;否则,就把新元素添加为链表中的最后一个元素。通过实现GetEnumerator()方法,可以用foreach语句遍历链表。GetEnumerator()方法使用yield语句创建一个枚举器类型。 ``` 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 { LinkedListNode previous=Last; Last.Next=newNode; Last=newNode; Last.Prev=previous; } return newNode; } public IEnumerator GrtEnumerator() { LinkedListNode current =First; while(current!=null) { yield return current.Value; current=current.Next; } } } ``` > 创建链表的泛型版本 - LinkedListNode 类用一个泛型类型T声明。属性Value的类型是T,而不是object。构造函数也变为可以接受T类型的对象。也可以返回和设置泛型类型,所以属性Next和Prev的类型是LinkedLisTNode ``` public class LinkedListNode { public LinkedListNode(T value) { Value=value; } public T Value{get;private set;} public LinkedListNode Next{get;internal set;} public LinkedListNode Prev{get;internal set;} } public class LinkedList:IEnumerable { public LinkedListNodeFrist{get;private set;} public LinkedListNodeLast{get;private set;} public LinkedListNOdeAddLast(T node) { var newNode=new LinkedListNode(node); if(First==null) { First=newNode; Last=First; } else { LinkedListNodeprevious=Last; LAst.Next=newNode; Lsat=newNode; Last.Prev=previous; } return newNode; } public IEnumerator GetEnumerator() { LinkedListNodecurrent=First; while(current!=null) { yield return current.Value; current=current.Next; } } IEnumerator IEnumerable.GetEnumerator()=>GetEnumerator(); } ``` - 使用泛型LinkedList,可以用int类型实例化它,且无需装箱操作。如果不使用AddLast()去传递int,就会出现一个编译器错误。使用泛型IEnumerable,foreach语句也是类型安全的,如果foreach语句中的变量不是int,就会出现一个编译器错误 ``` var list2=new LinkedList(); list2.AddLast(1); list2.AddLAst(3); list2.AddLAst(5); foreach(int i in list2) { WriteLine(i); } 对于字符串类型使用泛型LinkList,将字符串传递给AddLast()方法。 var list3=new LinkedList(); list3.AddLast("2"); list3.AddLast("four"); list3.AddLast("foo"); foreach(string s in list3) { WriteLine(s); } ``` - 每个处理对象类型的类都可以有泛型实现方式,另外,如果类使用了层次结构,泛型就非常有助于消除类型强制转换操作 > 泛型类的功能 - 默认值 - 约束 - 继承 - 静态成员 **默认值** - 给DocumentManager类添加一个GetDocument()方法。在这个方法中,应把类型T指定为null。但是,不能把null赋予泛型类型。原因是泛型类型也可以实例化值类型,而null只能用于引用类型。为了解决这个问题,可以使用default关键字。通过default关键字,将null赋予引用类型,将0赋予值类型。 ``` public T GetDocument() { T doc =default(T); lock(this) { doc=documentQueue.Dequeue(); } return doc; } ``` **约束** - 如果泛型类需要调用泛型类型中的方法,就必须添加约束 - 对于DocumentManager,文档的所有标题应在DisplayAllDocuments()方法中显示。Document类实现带有Title和Content属性的IDocument接口 ``` public interface IDocument { string Title{get;set;} string Content{get;set;} } public class Document:IDocument { public Document() { } public Document(string title,string content) { Title=title; Content=content; } public string Title{get;ste;} public string Content{get;set;} } - 要使用DocumentManager类显示文档,可以将类型T强制转换为IDocument接口,以显示标题 public void DisplayAllDocuments() { foreach(T doc in documentQueue) { WriteLine(((IDocument)doc).Title); } } ``` - 如果类型T没有实现IDocument接口,这个类型强制转换就会导致一个运行时异常。最好给DocumentManager类定义一个约束:TDocument类型必须实现IDocument接口。 - 为了在泛型类型的名称中制定该要求,将T改为TDocument。where子句指定了实现IDocument接口的需求 ``` public class DocumentManager where TDocument:IDocument { public void DisplayAllDocuments() { foreach (TDocument doc in documentQueue) { WriteLine(doc.Title); } } } ``` - 在main()方法中,用Document类型实例化DocumentManager类,而Document类型实现了需要的IDocument接口。接着添加和显示新文档,检索其中一个文档 ``` public static void Main() { var dm=new DocumentManager(); dm.AddDocument(new Document("Title A","Sample A")); dm.AddDocument(new Document("Title B","Sample B")); dm.DisplayAllDocuments(); if(dm.IsDocumentAvailable) { Document d=dm.GetDocument(); WriteLine(d.Content); } } DocumentMAnager现在可以处理任何实现了IDocument接口的类。 ``` - where T:struct :对于结构约束,类型T必须是值类型 - where T:class :类约束指定类型T必须是引用类型 - where T:IFoo :指定类型T必须实现接口IFoo - where T:Foo :指定类型T必须派生自基类Foo - where T:new() :这是一个构造函数约束,指定类型T必须有一个默认构造函数 - where T1:T2 :这个约束也可以指定,类型T1派生自泛型类型T2 **继承** 前面创建的LinkedList类实现了IEnumerable接口 ``` public class LinkedList:IEnumerable { //泛型类型可以实现泛型接口,也可以派生自一个类。泛型类可以派生自泛型基类 } public class Base { } public class Derived:Base { } 其要求是必须重复接口的泛型类型,或者必须指定基类的类型 public class Base { } public class Derived:Base {} ``` - 派生类可以是泛型类或非泛型类。例如,可以定义一个抽象的泛型基类,它在派生类中用一个具体的类实现。这允许对特定类型执行特殊的操作 ``` public abstract class Calc { public abstract T Add(T x,T y); public abstract T Sub(T x,T y); } public class IntCalc:Calc { public override int Add(int x,int y)=>x+y; public override int Sub(int x,int y)=>x-y; } ``` 还可以创建一个部分的特殊操作,如从Query中派生StringQuery类,只定义一个泛型参数,如字符串TResult。要实例化StringQuery,只需要提供TRequest ``` public class Query {} public StringQuery:Query {} ``` **静态成员** - 泛型类的静态成员需要特别关注。泛型类的静态成员只能在类的一个实例中共享。 ``` public class StaticDemo { public static int x; } 由于同时对一个string类型和一个int类型使用StaticDemole类,因此存在两组静态字段: StaticDemo.x=4; StaticDemo.x=5; WriteLine(StaticDemo.x); ``` > 泛型接口的协变 - 如果泛型类型用out关键字标注,泛型接口就是协变得。这也意味着返回类型只能是T。接口IIndex与类型T是协变得,并从一个只读索引器中返回这个类型 ``` public interface IIndex { T this[int index]{get;} int Count{get;} } IIndex接口用RectangleCollection类来实现。RectangleCollection类为泛型类型T定义了Rectangle ``` >泛型接口的抗变 - 如果泛型类型用in关键字标注,泛型接口就是抗变。这样,接口只能把泛型类型T用作其方法的输入 ``` public interface IDisplay { void Show(T item); } ``` - ShapeDisplay 类实现IDisplay,并使用Shape对象作为输入参数 ``` public class ShapeDisplay:IDisplay { public void Show(Shape s)=> WriteLine($"{s.GetType().Name}Width:{s.Width},Height:{s.Height}"); } ``` > 泛型方法 - 在泛型方法中,泛型类型用方法声明来定义。泛型方法可以在非泛型类中定义. - Swap()方法把T定义为泛型类型,该泛型类型用于两个参数和一个变量temp: ``` void Swap(ref T x,ref T y) { T temp; temp=x; x=y; y=temp; } 把泛型类型赋予方法调用,就可以调用泛型方法: int i=4; int j=5; Swap(ref i,ref j); ``` 但是,因为C#编译器会通过调用Swap()方法来获取参数的类型,所以不需要把泛型类型赋予方法调用。泛型方法可以像非泛型方法那样调用: ``` int i=4; int j=5; Swap(ref i,ref j) ``` > 泛型方法示例 - 使用包含Name和Balance属性的Account类 ``` public class Account { public string Name{get;} public decimal Balance{get;private set;} public Account(string name,Decimal balance) { Name=name; Balance=balance; } } - 其中应累加余额的所有账户操作都添加到List类型的账户列表中 var accounts=new List() { new Account("Christian",1500), new Account("Stephanie",2200), new Account("Angela",1800), new Account("Matthias",2400) }; ``` - 累加所有Account对象的传统方式是用foreach语句遍历所有的Account对象。 - 如下所示。foreach语句使用IEnumerable接口迭代集合的元素,所以AccumulateSimple()方法的参数是IEnumerable类型。foreach语句处理实现IEnumerable接口的每个对象。这样,AccumulateSimple()方法就可以用于所有实现IEnumerable接口的集合类。在这个方法的实现代码中,直接访问Account对象的Balance属性 ``` public static class Algorithms { public static decimal AccumulateSimple(IEnumrablesource) { decimal sum=0; foreach(Account a in source) { sum+=a.Balance; } return sum; } } decimal amount =Algorithms.AccumulateSimple(accounts); ```