泛型

   

泛    型

 

 

 

 

C#语言和CLR的一个最大变化是引入了泛型。在.NET 1.0中,要创建一个灵活的类或方法,但该类或方法在编译期间不知道使用什么类,就必须以Object类为基础。而Object类在编译期间没有类型安全性,因此必须进行强制类型转换。另外,给值类型使用Object类会有性能损失。

.NET 2.0提供了泛型。有了泛型,就不再需要Object类了。泛型类使用泛型类型,并可以根据需要用特定的类型替换泛型类型。这就保证了类型安全性:如果某个类型不支持泛型类,编译器就会生成错误。

泛型是一个很强大的特性,对于集合类而言尤其如此。.NET 1.0中的大多数集合类都基于Object类型。.NET 2.0提供了实现为泛型的新集合类。

泛型不仅限于类,本章还将介绍用于委托、接口和方法的泛型。

本章的主要内容如下:

●       泛型概述

●       创建泛型类

●       泛型类的特性

●       泛型接口

●       泛型方法

●       泛型委托

●       Framework的其他泛型类型

9.1 概述

泛型并不是一个全新的结构,其他语言中有类似的概念。例如,C++模板就与泛型相当。但是,C++模板和.NET泛型之间有一个很大的区别。对于C++模板,在用特定的类型实例化模板时,需要模板的源代码。相反,泛型不仅是C#语言的一种结构,而且是CLR定义的。所以,即使泛型类是在C#中定义的,也可以在Visual Basic中用一个特定的类型实例化该泛型。

下面介绍泛型的优点和缺点,尤其是:

●       性能

●       类型安全性

●       二进制代码重用

●       代码的扩展

●       命名约定

9.1.1 性能

泛型的一个主要优点是性能。第10章介绍了System.Collections和System.Collections. Generic命名空间的泛型和非泛型集合类。对值类型使用非泛型集合类,在把值类型转换为引用类型,和把引用类型转换为值类型时,需要进行装箱和拆箱操作。

注意:

装箱和拆箱详见第6章,这里仅简要复习一下这些术语。

值类型存储在堆栈上,引用类型存储在堆上。C#类是引用类型,结构是值类型。.NET很容易把值类型转换为引用类型,所以可以在需要对象(对象是引用类型)的任意地方使用值类型。例如,int可以赋予一个对象。从值类型转换为引用类型称为装箱。如果方法需要把一个对象作为参数,而且传送了一个值类型,装箱操作就会自动进行。另一方面,装箱的值类型可以使用拆箱操作转换为值类型。在拆箱时,需要使用类型转换运算符。

下面的例子显示了System.Collections命名空间中的ArrayList类。ArrayList存储对象, Add()方法定义为需要把一个对象作为参数,所以要装箱一个整数类型。在读取ArrayList中的值时,要进行拆箱,把对象转换为整数类型。可以使用类型转换运算符把ArrayList集合的第一个元素赋予变量i1,在访问int类型的变量i2的foreach语句中,也要使用类型转换运算符:

ArrayList 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

}

装箱和拆箱操作很容易使用,但性能损失比较大,迭代许多项时尤其如此。

System.Collections.Generic命名空间中的List<T>类不使用对象,而是在使用时定义类型。在下面的例子中,List<T>类的泛型类型定义为int,所以int类型在JIT编译器动态生成的类中使用,不再进行装箱和拆箱操作:

List<int> 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);

}

9.1.2 类型安全

泛型的另一个特性是类型安全。与ArrayList类一样,如果使用对象,可以在这个集合中添加任意类型。下面的例子在ArrayList类型的集合中添加一个整数、一个字符串和一个MyClass类型的对象:

ArrayList list = new ArrayList();

list.Add(44);

list.Add("mystring");

list.Add(new MyClass());

如果这个集合使用下面的foreach语句迭代,而该foreach语句使用整数元素来迭代,编译器就会编译这段代码。但并不是集合中的所有元素都可以转换为int,所以会出现一个运行异常:

foreach (int i in list)

{

   Console.WriteLine(i);

}

错误应尽早发现。在泛型类List<T>中,泛型类型T定义了允许使用的类型。有了List<int>的定义,就只能把整数类型添加到集合中。编译器不会编译这段代码,因为Add()方法的参数无效:

List<int> list = new List<int>();

list.Add(44);

list.Add("mystring");   // compile time error

list.Add(new MyClass());   // compile time error

9.1.3 二进制代码的重用

泛型允许更好地重用二进制代码。泛型类可以定义一次,用许多不同的类型实例化。不需要像C++模板那样访问源代码。

例如,System.Collections.Generic命名空间中的List<T>类用一个int、一个字符串和一个MyClass类型实例化:

List<int> list = new List<int>();

list.Add(44);

 

List<string> stringList = new List<string>();

stringList.Add("mystring");

 

List<MyClass> myclassList = new List<MyClass>();

myClassList.Add(new MyClass());

泛型类型可以在一种语言中定义,在另一种.NET语言中使用。

9.1.4  代码的扩展

在用不同的类型实例化泛型时,会创建多少代码?

因为泛型类的定义会放在程序集中,所以用某个类型实例化泛型类不会在IL代码中复制这些类。但是,在JIT编译器把泛型类编译为内部码时,会给每个值类型创建一个新类。引用类型共享同一个内部类的所有实现代码。这是因为引用类型在实例化的泛型类中只需要4字节的内存单元(32位系统),就可以引用一个引用类型。值类型包含在实例化的泛型类的内存中。而每个值类型对内存的要求都不同,所以要为每个值类型实例化一个新类。

9.1.5 命名约定

如果在程序中使用泛型,区分泛型类型和非泛型类型会有一定的帮助。下面是泛型类型的命名规则:

●       泛型类型的名称用字母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> { }

9.2 创建泛型类

首先介绍一个一般的、非泛型的简化链表类,它可以包含任意类型的对象,以后再把这个类转化为泛型类。

在链表中,一个元素引用其后的下一个元素。所以必须创建一个类,将对象封装在链表中,引用下一个对象。类LinkedListNode包含一个对象value,它用构造函数初始化,还可以用Value属性读取。另外,LinkedListNode类包含对链表中下一个元素和上一个元素的引用,这些元素都可以从属性中访问。

public class LinkedListNode

{

private object value;

public LinkedListNode(object value)

{

this.value = value;

}

public object Value

{

get { return value; }

}

 

private LinkedListNode next;

public LinkedListNode Next

{

get { return next; }

internal set { next = value; }

}

 

private LinkedListNode prev;

public LinkedListNode Prev

{

get { return prev; }

internal set { prev = value; }

}

}

LinkedList类包含LinkedListNode类型的first和last字段,它们分别标记了链表的头尾。AddLast()方法在链表尾添加一个新元素。首先创建一个LinkedListNode类型的对象。如果链表是空的,则first和last字段就设置为该新元素;否则,就把新元素添加为链表中的最后一个元素。执行GetEnumerator()方法时,可以用foreach语句迭代链表。GetEnumerator()方法使用yield语句创建一个枚举器类型。

提示:

yield语句参见第5章。

public class LinkedList : IEnumerable

{

private LinkedListNode first;

public LinkedListNode First

{

get { return first; }

}

 

private LinkedListNode last;

public LinkedListNode Last

{

get { return last; }

}

 

public LinkedListNode AddLast(object node)

{

LinkedListNode 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;

}

}

}

现在可以给任意类型使用LinkedList类了。在下面的代码中,实例化了一个新LinkedList对象,添加了两个整数类型和一个字符串类型。整数类型要转换为一个对象,所以执行装箱操作,如前面所述。在foreach语句中执行拆箱操作。在foreach语句中,链表中的元素被强制转换为整数,所以对于链表中的第三个元素,会发生一个运行异常,因为它转换为int时会失败。

LinkedList list1 = new LinkedList();

list1.AddLast(2);

list1.AddLast(4);

list1.AddLast("6");

foreach (int i in list1)

{

Console.WriteLine(i);

}

下面创建链表的泛型版本。泛型类的定义与一般类类似,只是要使用泛型类型声明。之后,泛型类型就可以在类中用作一个字段成员,或者方法的参数类型。LinkedListNode类用一个泛型类型T声明。字段value的类型是T,而不是object。构造函数和Value属性也变为接受和返回T类型的对象。也可以返回和设置泛型类型,所以属性Next和Prev的类型是LinkedListNode<T>。

public class LinkedListNode<T>

{

private T value;

public LinkedListNode(T value)

{

this.value = value;

}

 

public T Value

{

get { return value; }

}

 

private LinkedListNode<T> next;

public LinkedListNode<T> Next

{

get { return next; }

internal set { next = value; }

}

 

private LinkedListNode<T> prev;

public LinkedListNode<T> Prev

{

get { return prev; }

internal set { prev = value; }

}

}

LinkedList类也改为泛型类。LinkedList<T>包含LinkedListNode<T>元素。LinkedList中的类型T定义了类型T的包含字段first和last。AddLast()方法现在接受类型T的参数,实例化LinkedListNode<T>类型的对象。

在.NET 2.0推出后,IEnumerable接口也有一个泛型版本IEnumerable<T>。IEnumerable<T>派生于IEnumerable,添加了返回IEnumerator<T>的GetEnumerator()方法,LinkedList<T>执行泛型接口IEnumerable<T>。

提示:

枚举、接口IEnumerable和IEnumerator详见第5章。

public class LinkedList<T> : IEnumerable<T>

{

private LinkedListNode<T> first;

public LinkedListNode<T> First

{

get { return first; }

}

 

private LinkedListNode<T> last;

public LinkedListNode<T> Last

{

get { return last; }

}

 

public LinkedListNode<T> AddLast(T node)

{

LinkedListNode<T> 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<T>,可以用int类型实例化它,且无需装箱操作。如果不使用AddLast()方法传送int,就会出现一个编译错误。使用泛型IEnumerable<T>,foreach语句也是类型安全的,如果foreach语句中的变量不是int,也会出现一个编译错误。

LinkedList<int> list2 = new LinkedList<int>();

list2.AddLast(1);

list2.AddLast(3);

list2.AddLast(5);

foreach (int i in list2)

{

Console.WriteLine(i);

}

同样,可以给泛型LinkedList<T>使用string类型,将字符串传送给AddLast()方法。

LinkedList<string> list3 = new LinkedList<string>();

list3.AddLast("2");

list3.AddLast("four");

list3.AddLast("foo");

foreach (string s in list3)

{

Console.WriteLine(s);

}

提示:

每个处理对象类型的类都可以有泛型实现方式。另外,如果类使用了继承,泛型非常有助于去除类型转换操作。

9.3 泛型类的特性

在创建泛型类时,需要一些其他C#关键字。例如,不能把null赋予泛型类型。此时,可以使用default关键字。如果泛型类型不需要Object类的功能,但需要调用泛型类上的某些特定方法,就可以定义约束。

本节讨论如下主题:

●       默认值

●       约束

●       继承

●       静态成员

下面开始一个使用泛型文档管理器的示例。文档管理器用于从队列中读写文档。先创建一个新的控制台项目DocumentManager,添加类DocumentManager<T>。AddDocument()方法将一个文档添加到队列中。如果队列不为空,IsDocumentAvailable只读属性就返回true。

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; }

}

}

}

9.3.1 默认值

现在给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;

}

注意:

default关键字根据上下文可以有多种含义。switch语句使用default定义默认情况。在泛型中,根据泛型类型是引用类型还是值类型,default关键字用于将泛型类型初始化为null或0。

9.3.2 约束

如果泛型类需要调用泛型类型上的方法,就必须添加约束。对于DocumentManager<T>,文档的标题应在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)

{

this.title = title;

this.content = content;

}

 

private string title;

public string Title

{

get { return title; }

set { title = value; }

}

 

private string content;

public string Content

{

get { return content; }

set { content = value; }

}

}

要使用DocumentManager<T>类显示文档,可以将类型T强制转换为IDocument接口,以显示标题:

public void DisplayAllDocuments()

{

foreach (T doc in documentQueue)

{

Console.WriteLine((IDocument)doc).Title);

}

}

问题是,如果类型T没有执行IDocument接口,这个类型转换就会生成一个运行异常。最好给DocumentManager<TDocument>类定义一个约束:TDocument类型必须执行IDocument接口。为了在泛型类型的名称中指定该要求,将T改为TDocument。where子句指定了执行IDocument接口的要求。

 

public class DocumentManager<TDocument>

where TDocument : IDocument

{

这样,就可以编写foreach语句,让类型T包含属性Title了。Visual Studio IntelliSense和编译器都会提供这个支持。

public void DisplayAllDocuments()

{

foreach (TDocument doc in documentQueue)

{

Console.WriteLine(doc.Title);

}

}

在Main()方法中,DocumentManager<T>类用Document类型实例化,而Document类型执行了需要的IDocument接口。接着添加和显示新文档,检索其中一个文档:

static void Main()

{

DocumentManager<Document> dm = new DocumentManager<Document>();

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();

Console.WriteLine(d.Content);

}

}

DocumentManager现在可以处理任何执行了IDocument接口的类。

在示例应用程序中,介绍了接口约束。还有几种约束类型,如表9-1所示。

表 9-1

约    束

说    明

where T : struct

使用结构约束,类型T必须是值类型

where T : class

类约束指定,类型T必须是引用类型

where T : IFoo

指定类型T必须执行接口IFoo

where T : Foo

指定类型T必须派生于基类Foo

where T : new()

这是一个构造函数约束,指定类型T必须有一个默认构造函数

where T : U

这个约束也可以指定,类型T派生于泛型类型V。该约束也称为裸类型约束

注意:

在CLR 2.0中,只能为默认构造函数定义约束,不能为其他构造函数定义约束。

使用泛型类型还可以合并多个约束。where T : IFoo,new()约束和MyClass<T>声明指定,类型T必须执行IFoo接口,且必须有一个默认构造函数。

public class MyClass<T>

where T : IFoo, new()

{

//...

提示:

在C# 2.0中,where子句的一个重要限制是,不能定义必须由泛型类型执行的运算符。运算符不能在接口中定义。在where子句中,只能定义基类、接口和默认构造函数。

9.3.3 继承

前面创建的LinkedList<T>类执行了IEnumerable<T>接口:

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 SimpleCalc : Calc<int>

{

public override int Add(int x, int y)

{

return x + y;

}

 

public override int Sub(int x, int y)

{

return x - y;

}

}

9.3.4 静态成员

泛型类的静态成员需要特别关注。泛型类的静态成员只能在类的一个实例中共享。下面看一个例子。StaticDemo<T>类包含静态字段x:

public class StaticDemo<T>

{

public static int x;

}

由于对一个string类型和一个int类型使用了StaticDemo<T>类,所以存在两组静态字段:

StaticDemo<string>.x = 4;

StaticDemo<int>.x = 5;

Console.WriteLine(StaticDemo<string>.x); // writes 4

9.4 泛型接口

使用泛型可以定义接口,接口中的方法可以带泛型参数。在链表示例中,就执行了IEnumerable<T>接口,它定义了GetEnumerator()方法,以返回IEnumerator<T>。对于.NET 1.0中的许多非泛型接口,.NET 2.0定义了新的泛型版本,例如IComparable<T>:

public interface IComparable<T>

{

int CompareTo(T other);

}

第5章中的非泛型接口IComparable需要一个对象,Person类的CompareTo()方法才能按姓氏给人员排序:

public class Person : IComparable

{

public int CompareTo(object obj)

{

Person other = obj as Person;

return this.lastname.CompareTo(other.lastname);

} 

//...

执行泛型版本时,不再需要将object的类型强制转换为Person:

public class Person : IComparable<Person>

{

public int CompareTo(Person other)

{ 

return this.lastname.CompareTo(other.lastname);

}

//...

9.5 泛型方法

除了定义泛型类之外,还可以定义泛型方法。在泛型方法中,泛型类型用方法声明来定义。

Swap<T>方法把T定义为泛型类型,用于两个参数和一个变量temp:

void Swap<T>(ref T x, ref T y)

{

   T temp;

   temp = x;

   x = y;

   y = temp;

}

把泛型类型赋予方法调用,就可以调用泛型方法:

int i = 4;

int j = 5;

Swap<int>(ref i, ref j);

但是,因为C#编译器会通过调用Swap方法来获取参数的类型,所以不需要把泛型类型赋予方法调用。泛型方法可以像非泛型方法那样调用:

int i = 4;

int j = 5;

Swap(ref i, ref j);

下面的例子使用泛型方法累加集合中的所有元素。为了说明泛型方法的功能,下面的Account类包含name和balance:

   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;

      }

   }

应累加结余的所有账目操作都添加到List<Account>类型的账目列表中:

         List<Account> accounts = new List<Account>();

         accounts.Add(new Account("Christian", 1500));

         accounts.Add(new Account("Sharon", 2200));

         accounts.Add(new Account("Katie", 1800));

累加所有Account对象的传统方式是用foreach语句迭代所有的Account对象,如下所示。foreach语句使用IEnumerable接口迭代集合的元素,所以AccumulateSimple()方法的参数是IEnumerable类型。这样,AccumulateSimple()方法就可以用于所有实现IEnumerable接口的集合类。在这个方法的实现代码中,直接访问Account对象的Balance属性:

   public static class Algorithm

   {

      public static decimal AccumulateSimple(IEnumerable e)

      {

         decimal sum = 0;

         foreach (Account a in e)

         {

            sum += a.Balance;

         }

         return sum;

      }

   }

Accumulate()方法的调用方式如下:

      decimal amount = Algorithm.AccumulateSimple(accounts);

第一个实现代码的问题是,它只能用于Account对象。使用泛型方法就可以避免这个问题。

Accumulate()方法的第二个版本接受实现了IAccount接口的任意类型。如前面的泛型类所述,泛型类型可以用where子句来限制。这个子句也可以用于泛型方法。Accumulate()方法的参数改为IEnumerable<T>。IEnumerable<T>是IEnumerable接口的泛型版本,由泛型集合类实现。

      public static decimal Accumulate<TAccount>(IEnumerable<TAccount> coll)

            where TAccount : IAccount

      {

         decimal sum = 0;

 

         foreach (TAccount a in coll)

         {

            sum += a.Balance;

         }

         return sum;

      }

将Account类型定义为泛型类型参数,就可以调用新的Accumulate()方法:

      decimal amount = Algorithm.Accumulate<Account>(accounts);

因为编译器会从方法的参数类型中自动推断出泛型类型参数,所以以如下方式调用Accumulate()方法是有效的:

      decimal amount = Algorithm.Accumulate(accounts);

泛型类型实现IAccount接口的要求过于严厉。这个要求可以使用泛型委托来改变。在下一节中,Accumulate()方法将改为独立于任何接口。

9.6 泛型委托

如第7章所述,委托是类型安全的方法引用。通过泛型委托,委托的参数可以在以后定义。

.NET 2.0定义了一个泛型委托EventHandler,它的第二个参数是TEventArgs类型,所以不再需要为每个新参数类型定义新委托了。

public sealed delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)

where TEventArgs : EventArgs

9.6.1 执行委托调用的方法

把Accumulate()方法改为有两个泛型类型。TInput是要累加的对象类型,TSummary是返回类型。Accumulate的第一个参数是IEnumerable<T>接口,这与以前相同。第二个参数需要Action委托引用一个方法,来累加所有的结余。

在实现代码中,现在给每个元素调用Action委托引用的方法,再返回计算的总和:

      public delegate TSummary Action<TInput, TSummary>(TInput t, TSummary u);

 

      public static TSummary Accumulate<TInput, TOutput>(IEnumerable<T> coll,

            Action<TInput, TSummary> action)

      {

         TSummary sum = default(TSummary);

 

         foreach (TInput input in coll)

         {

            sum = action(input, sum);

         }

         return sum;

      }

Accumulate方法可以通过匿名方法调用,该匿名方法指定,账目的结余应累加到第二个参数中:

      decimal amount = Accumulate<Account, decimal>(

            accounts, delegate(Account a, decimal d) { return a.Balance + d; });

如果Account结余的累加需要进行多次,就可以把该功能放在一个AccountAdder()方法中:

      static decimal AccountAdder(Account a, decimal d)

      {

         return a.Balance + d;

      }

联合使用AccountAdder方法和Accumulate方法:

      decimal amount = Accumulate<Account, decimal>(

            accounts, AccountAdder);

Action委托引用的方法可以实现任何逻辑。例如,可以进行乘法操作,而不是加法操作。

Accumulate()方法和AccumulateIf()方法一起使用,会更灵活。在AccumulateIf()中,使用了另一个Predicate<T>类型的参数。Predicate<T>委托引用的方法会检查某个账目是否应累加进去。在foreach语句中,只有谓词match返回true,才会调用action方法:

      public static TSummary AccumulateIf<TInput, TSummary>(

            IEnumerable<TInput> coll,

            Action<TInput, TSummary> action,

            Predicate<TInput> match)

      {

         TSummary sum = default(TSummary);

 

         foreach (TInput a in coll)

         {

            if (match(a))

            {

               sum = action(a, sum);

            }

         }

 

         return sum;

      }

调用AccumulateIf()方法可以实现累加和执行谓词。这里只累加结余大于2000的账目:

         decimal amount = Algorithm.AccumulateIf<Account, decimal>(

            accounts,

            delegate(Account a, decimal d) { return a.Balance + d; },

            delegate(Account a) {return a.Balance > 2000 ? });

9.6.2 对Array类使用泛型委托

第5章使用IComparable和IComparer接口,演示了Array类的几个排序技术。从.NET 2.0开始,Array类的一些方法把泛型委托类型用作参数。表9-2列出了这些方法、泛型类型和功能。

表 9-2

方    法

泛型参数类型

说    明

Sort()

int Comparison<T>(T x, T y)

Sort()方法定义了几个重载版本。其中一个重载版本需要一个Comparison<T>类型的参数。Sort()使用委托引用的方法对集合中的所有元素排序

ForEach()

void Action<T>(T obj)

ForEach()方法对集合中的每一项调用由Action<T>委托引用的方法

FindAll()

Find()

FindLast()

FindIndex()

FindLastIndex()

bool Predicate<T>(T match)

FindXXX()方法将Predicate<T>委托作为其参数接受。由委托引用的方法会调用多次,并一个接一个地传送集合中的元素。Find()方法在谓词第一次返回true时停止搜索,并返回这个元素。FindIndex()返回查找到的第一个元素的索引。FindLast()和FindLastIndex()以逆序方式对集合中的元素调用谓词,因此返回最后一项或最后一个索引。FindAll()返回一个新列表,其中包含谓词为true的所有项

ConvertAll()

TOutput Converter<TInput,

TOutput>(TInput input)

ConvertAll()方法给集合中的每个元素调用Converter<TInput, TOutput>委托,返回一列转换好的元素

TrueForAll()

bool Predicate<T>(T match)

TrueForAll()方法给每个元素调用谓词委托。如果谓词给每个元素都返回true,则TrueForAll()也返回true。如果谓词为一个元素返回了false,TrueForAll()就返回false

下面看看如何使用这些方法。

Sort()方法把这个委托作为参数接受:

public delegate int Comparison<T>(T x, T y);

这样,就可以使用匿名委托传送两个Person对象,给数组排序。对于Person对象数组,参数T是Person类型:

Person[] persons = {

new Person("Emerson", "Fittipaldi"),

new Person("Niki", "Lauda"),

new Person("Ayrton", "Senna"),

new Person("Michael", "Schumacher")

};

Array.Sort(persons,

delegate(Person p1, Person p2)

{

return p1.Firstname.CompareTo(p2.Firstname);

});

Array.ForEach()方法将Action<T>委托作为参数,给数组的每个元素执行操作:

public delegate void Action<T>(T obj);

于是,就可以传送Console.WriteLine方法的地址,将每个人写入控制台。WriteLine()方法的一个重载版本将Object类作为参数类型。由于Person派生于Object,所以它适合于Person数组:

Array.ForEach(persons, Console.WriteLine);

ForEach()语句的结果将persons变量引用的集合中的每个人都写入控制台:

Emerson Fittipaldi

Niki Lauda

Ayrton Senna

Michael Schumacher

如果需要更多的控制,则可以传送一个匿名方法,其参数应匹配委托定义的参数:

Array.ForEach(persons,

delegate (Person p)

{

Console.WriteLine("{0}", p.Lastname);

}); 

下面是写入控制台的姓氏:

Fittipaldi

Lauda

Senna

Schumacher

Array.FindAll()方法需要Predicate<T>委托:

public delegate bool Predicate<T>(T match);

Array.FindAll()方法为数组中的每个元素调用谓词,并返回一个谓词是true的数组。在这个例子中,对于Lastname以字符串"S"开头的所有Person对象,都返回true。

Person[] sPersons = Array.FindAll(persons,

delegate (Person p)

{

return p.Lastname.StartsWith("S");

});

迭代返回的集合sPersons,并写入控制台,结果如下:

Ayrton Senna

Michael Schumacher

Array.ConvertAll()方法使用泛型委托Converter和两个泛型类型。第一个泛型类型TInput是输入参数,第二个泛型类型TOutput是返回类型。

public delegate TOutput Converter<TInput, TOutput>(TInput input);

如果一种类型的数组应转换为另一种类型的数组,就可以使用ConvertAll()方法。下面是一个与Person类无关的Racer类。Person类有Firstname和Lastname属性,而Racer类为赛手的姓名定义了一个属性Name:

public class Racer

{

public Racer(string name)

{

this.name = name;

} 

 

private string name;

public string Name

{

get { return name; }

set { name = value; }

}

 

private string team;

public string Team

{

get { return team; }

set { team = value; }

}

}

使用Array.ConvertAll(),很容易将persons数组转换为Racer数组。给每个Person元素调用委托。在每个Person元素的匿名方法的执行代码中,创建了一个新的Racer对象,将firstname和lastname连接起来传送给带一个字符串参数的构造函数。结果是一个Racer对象数组:

Racer[] racers =

Array.ConvertAll<Person, Racer>(

persons,

delegate(Person person)

{

return new Racer(person.Firstname + " " + person.Lastname);

});

9.7 Framework的其他泛型类型

除了System.Collections.Generic命名空间之外,.NET Framework还有其他泛型类型。这里讨论的结构和委托都位于System命名空间中,用于不同的目的。

本节讨论如下内容:

●       结构Nullable<T>

●       委托EventHandler<TEventArgs>

●       结构ArraySegment<T>

9.7.1 结构Nullable<T>

数据库中的数字和编程语言中的数字有显著不同的特征,因为数据库中的数字可以为空,C#中的数字不能为空。Int32是一个结构,而结构实现为值类型,所以它不能为空。

只有在数据库中,而且把XML数据映射为.NET类型,才不存在这个问题。

这种区别常常令人很头痛,映射数据也要多做许多工作。一种解决方案是把数据库和XML文件中的数字映射为引用类型,因为引用类型可以为空值。但这也会在运行期间带来额外的系统开销。

使用Nullable<T>结构很容易解决这个问题。在下面的例子中,Nullable<T>用Nullable<int>实例化。变量x现在可以像int那样使用了,进行赋值或使用运算符执行一些计算。这是因为我们转换了Nullable<T>类型的运算符。x还可以是空。可以检查Nullable<T>的HasValue和Value属性,如果该属性有一个值,就可以访问该值:

Nullable<int> x;

x = 4;

x += 3;

if (x.HasValue)

{

   int y = x.Value;

}

x = null;

因为可空类型使用得非常频繁,所以C#有一种特殊的语法,用于定义这种类型的变量。定义这类变量时,不使用一般结构的语法,而使用?运算符。在下面的例子中,x1和x2都是可空int类型的实例:

Nullable<int> x1;

int? x2;

可空类型可以与null和数字比较,如上所示。这里,x的值与null比较,如果x不是null,就与小于0的值比较:

int? x = GetNullableType(); Please use brackets here. Good coding standards. [CN]

can do it here

if (x == null)

{

Console.WriteLine("x is null");

}

else if (x < 0)

{

Console.WriteLine("x is smaller than 0");

}

可空类型还可以使用算术运算符。变量x3是变量x1和x2的和。如果这两个可空变量中有一个的值是null,它们的和就是null。

int? x1 = GetNullableType();

int? x2 = GetNullableType();

int? x3 = x1 + x2;

非可空类型可以转换为可空类型。从非可空类型转换为可空类型时,在不需要强制类型转换的地方可以进行隐式转换。这种转换总是成功的:

int y1 = 4;

int? x1 = y1;

但从可空类型转换为非可空类型可能会失败。如果可空类型的值是null,把null值赋予非可空类型,就会抛出InvalidOperationException类型的异常。这就是进行显式转换时需要类型转换运算符的原因:

int? x1 = GetNullableType();

int y1 = (int)x1;

如果不进行显式类型转换,还可以使用接合运算符(coalescing operator)从可空类型转换为非可空类型。接合运算符的语法是??,为转换定义了一个默认值,以防可空类型的值是null。这里,如果x1是null,y1的值就是0。

int? x1 = GetNullableType();

int y1 = x1 ?? 0;

9.7.2 EventHandler<TEventArgs>

在Windows Forms和Web应用程序中,为许多不同的事件处理程序定义了委托。其中一些事件处理程序如下:

public sealed delegate void EventHandler(object sender, EventArgs e);

public sealed delegate void PaintEventHandler(object sender, PaintEventArgs e);

public sealed delegate void MouseEventHandler(object sender, MouseEventArgs e);

这些委托的共同点是,第一个参数总是sender,它是事件的起源,第二个参数是包含事件特定信息的类型。

使用新的EventHandler<TEventArgs>,就不需要为每个事件处理程序定义新委托了。可以看出,第一个参数的定义方式与以前一样,但第二个参数是一个泛型类型TeventArgs。where子句指定TEventArgs的类型必须派生于基类EventArgs。

public sealed delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)

   where TEventArgs : EventArgs

9.7.3 ArraySegment<T>

结构ArraySegment<T>表示数组的一段。如果需要数组的一部分,就可以使用数组段。在ArraySegment<T>中,包含了数组段的信息(偏移量和元素个数)。

在下面的例子中,变量arr定义为有8个元素的int数组。ArraySegment<int>类型的变量segment用于表示该整数数组的一段。该段用构造函数初始化,在这个构造函数中,传送了该数组、偏移量和元素个数。其中偏移量设置为2,所以从第三个元素开始,元素个数设置为3,所以6是数组段的最后一个元素。

数组段可以用Array属性访问。ArraySegment<T>还有Offset和Count属性,表示定义数组段的初始化了的值。for循环用于迭代数组段。for循环的第一个表达式初始化为迭代开始的偏移量。第二个表达式指定数组段中的元素个数,以确定迭代是否停止。在for循环中,数组段包含的元素用Array属性来访问:

int[] arr = {1, 2, 3, 4, 5, 6, 7, 8};

ArraySegment<int> segment = new ArraySegment<int>(arr, 2, 3);

 

for (int i = segment.Offset; i < segment.Offset + segment.Count+1; i++)

{

   Console.WriteLine(segment.Array[i]);

}

在上面的例子中,ArraySegment<T>结构有什么用处?ArraySegment<T>可以作为参数传送给方法。这样,只要一个参数就可以定义数组、偏移量和元素个数,而不是3个参数。

WorkWithSegment()方法把ArraySegment<string>作为参数。在这个方法的实现代码中,Offset、Count和Array属性的用法与以前相同:

void WorkWithSegment(ArraySegment<string> segment)

{

   for (int i = segment.Offset; i < segment.Offset + segment.Count; i++)

   {

      Console.WriteLine(segment.Array[i]);

   }

}

注意:

数组段不复制原数组的元素,但原数组可以通过ArraySegment<T>访问。如果数组段中的元素改变了,这些变化也会反映到原数组中。

9.8 小结

本章介绍了.NET 2.0中一个非常重要的特性:泛型。通过泛型类可以创建独立于类型的类,泛型方法是独立于类型的方法。接口、结构和委托也可以用泛型的方式创建。泛型引入了一种新的编程方式。我们介绍了算法(尤其是操作和谓词)如何用于不同的类,而且它们都是类型安全的。泛型委托可以去除集合中的算法。

.NET Framework的其他类型包括Nullable<T>、EventHandler<TEventArgs>和ArraySegment<T>。

 

posted @ 2008-08-24 19:19  ike_li  阅读(656)  评论(0)    收藏  举报