首页 > 编程开发 > Objective-C编程 >
-
C#匿名、迭代和局部类
热衷于 C# 语言的人会喜欢上 Visual C# 2005。Visual Studio 2005 为 Visual C# 2005 带来了大量令人兴奋的新功能,例如泛型、迭代程序、局部类和匿名方法等。虽然泛型是人们最常谈到的也是预期的功能,尤其是在熟悉模板的 C++ 开发人员中间,但是其他的新功能同样是对Microsoft .NET开发宝库的重要补充。与 C# 的第一个版本相比,增加这些功能和语言将会提高您的整体生产效率,从而使您能够以更快的速度写出更加简洁的代码。有关泛型的一些背景知识,您应该看一看提要栏“什么是泛型?”。
迭代程序
在 C# 1.1 中,您可以使用 foreach 循环来遍历诸如数组、集合这样的数据结构:
string[] cities = {"New York","Paris","London"};
foreach(string city in cities)
{
Console.WriteLine(city);
}
实际上,您可以在 foreach 循环中使用任何自定义数据集合,只要该集合类型实现了返回 IEnumerator 接口的 GetEnumerator 方法即可。通常,您需要通过实现 IEnumerable 接口来完成这些工作:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
object Current{get;}
bool MoveNext();
void Reset();
}
在通常情况下,用于通过实现 IEnumerable 来遍历集合的类是作为要遍历的集合类型的嵌套类提供的。此迭代程序类型维持了迭代的状态。将嵌套类作为枚举器往往较好,因为它可以访问其包含类的所有私有成员。当然,这是迭代程序设计模式,它对迭代客户端隐藏了底层数据结构的实际实现细节,使得能够在多种数据结构上使用相同的客户端迭代逻辑,如图 1 所示。
图 1 迭代程序设计模式
此外,由于每个迭代程序都保持单独的迭代状态,所以多个客户端可以执行单独的并发迭代。通过实现 IEnumerable,诸如数组和队列这样的数据结构可以支持这种超常规的迭代。在 foreach 循环中生成的代码调用类的 GetEnumerator 方法简单地获得一个 IEnumerator 对象,然后将其用于 while 循环,从而通过连续调用它的 MoveNext 方法和当前属性遍历集合。如果您需要显式地遍历集合,您可以直接使用 IEnumerator(不用求助于 foreach 语句)。
但是使用这种方法有一些问题。首先,如果集合包含值类型,则需要对它们进行装箱和拆箱才能获得项,因为 IEnumerator.Current 返回一个对象。这将导致潜在的性能退化和托管堆上的压力增大。即使集合包含引用类型,仍然会产生从对象向下强制类型转换的不利结果。虽然大多数开发人员不熟悉这一特性,但是在 C# 1.0 中,实际上不必实现 IEnumerator 或 IEnumerable 就可以为每个循环实现迭代程序模式。编译器将选择调用强类型化版本,以避免强制类型转换和装箱。结果是,即使在 1.0 版本中,也可能没有导致性能损失。
为了更好地阐明这个解决方案并使其易于实现,Microsoft .NET 框架 2.0 在 System.Collections.Generics 命名空间中定义了一般的类型安全的 IEnumerable <ItemType> 和 IEnumerator <ItemType> 接口:
public interface IEnumerable<ItemType>
{
IEnumerator<ItemType> GetEnumerator();
}
public interface IEnumerator<ItemType> : IDisposable
{
ItemType Current{get;}
bool MoveNext();
}
除了利用泛型之外,新的接口与其前身还略有差别。与 IEnumerator 不同,IEnumerator <ItemType> 是从 IDisposable 派生而来的,并且没有 Reset 方法。图 2 中的代码显示了实现 IEnumerable <string> 的简单 city 集合,而图 3 显示了编译器在跨越 foreach 循环的代码时如何使用该接口。图 2 中的实现使用了名为 MyEnumerator 的嵌套类,它将一个引用作为构造参数返回给要枚举的集合。MyEnumerator 清楚地知道 city 集合(本例中的一个数组)的实现细节。MyEnumerator 类使用 m_Current 成员变量维持当前的迭代状态,此成员变量用作数组的索引。
Figure 2Implementing IEnumerable<string>
public class CityCollection : IEnumerable<string>
{
string[] m_Cities = {"New York","Paris","London"};
public IEnumerator<string> GetEnumerator()
{
return new MyEnumerator(this);
}
//Nested class definition
class MyEnumerator : IEnumerator<string>
{
CityCollection m_Collection;
int m_Current;
public MyEnumerator(CityCollection collection)
{
m_Collection = collection;
m_Current = -1;
}
public bool MoveNext()
{
m_Current++;
if(m_Current < m_Collection.m_Cities.Length)
return true;
else
return false;
}
public string Current
{
get
{
if(m_Current == -1)
throw new InvalidOperationException();
return m_Collection.m_Cities[m_Current];
}
}
public void Dispose(){}
}
}
图 2
第二个问题也是更难以解决的问题,就是迭代程序的实现。虽然对于简单的例子(如图 3所示),实现是相当简单的,但是对于更高级的数据结构,实现将非常复杂,例如二叉树,它需要递归遍历,并需在递归时维持迭代状态。另外,如果需要各种迭代选项,例如需要在一个链接表中从头到尾和从尾到头选项,则此链接表的代码就会因不同的迭代程序实现而变得臃肿。这正是设计 C# 2.0 迭代程序所要解决的问题。通过使用迭代程序,您可以让 C# 编译器为您生成 IEnumerator 的实现。C# 编译器能够自动生成一个嵌套类来维持迭代状态。您可以在一般集合或特定于类型的集合中使用迭代程序。您需要做的只是告诉编译器在每个迭代中产生的是什么。如同手动提供迭代程序一样,您需要公开 GetEnumerator 方法,此方法通常是通过实现 IEnumerable 或 IEnumerable <ItemType> 来公开的。
Figure 3Simple Iterator
CityCollection cities = new CityCollection();
//For this foreach loop:
foreach(string city in cities)
{
Trace.WriteLine(city);
}
//The compiler generates this equivalent code:
IEnumerable<string> enumerable = cities;
IEnumerator<string> enumerator = enumerable.GetEnumerator();
using(enumerator)
{
while(enumerator.MoveNext())
{
Trace.WriteLine(enumerator.Current);
}
}
图 3
您可以使用新的 C# 的 yield return 语句告诉编译器产生什么。例如,下面的代码显示了如何在 city 集合中使用 C# 迭代程序来代替图 2 中的手动实现:
public class CityCollection : IEnumerable<string>
{
string[] m_Cities = {"New York","Paris","London"};
public IEnumerator<string> GetEnumerator()
{
for(int i = 0; i<m_Cities.Length; i++)
yield return m_Cities[i];
}
}
您还可以在非一般集合中使用 C# 迭代程序:
public class CityCollection : IEnumerable
{
string[] m_Cities = {"New York","Paris","London"};
public IEnumerator GetEnumerator()
{
for(int i = 0; i<m_Cities.Length; i++)
yield return m_Cities[i];
}
}
此外,您还可以在完全一般的集合中使用 C# 迭代程序,如图 4 所示。当使用一般集合和迭代程序时,编译器从声明集合(本例中的 string)所用的类中型知道 foreach 循环内 IEnumerable <ItemType> 所用的特定类型:
Figure 4Providing Iterators on a Generic Linked List
//K is the key, T is the data item
class Node<K,T>
{
public K Key;
public T Item;
public Node<K,T> NextNode;
}
public class LinkedList<K,T> : IEnumerable<T>
{
Node<K,T> m_Head;
public IEnumerator<T> GetEnumerator()
{
Node<K,T> current = m_Head;
while(current != null)
{
yield return current.Item;
current = current.NextNode;
}
}
/* More methods and members */
}
图 4
LinkedList<int,string> list = new LinkedList<int,string>();
/* Some initialization of list, then */
foreach(string item in list)
{
Trace.WriteLine(item);
}
这与任何其他从一般接口进行的派生相似。如果出于某些原因想中途停止迭代,请使用 yield break 语句。例如,下面的迭代程序将仅仅产生数值 1、2 和 3:
public IEnumerator<int> GetEnumerator()
{
for(int i = 1;i< 5;i++)
{
yield return i;
if(i > 2)
yield break;
}
}
您的集合可以很容易地公开多个迭代程序,每个迭代程序都用于以不同的方式遍历集合。例如,要以倒序遍历 CityCollection 类,提供了名为 Reverse 的 IEnumerable <string> 类型的属性:
public class CityCollection
{
string[] m_Cities = {"New York","Paris","London"};
public IEnumerable<string> Reverse
{
get
{
for(int i=m_Cities.Length-1; i>= 0; i--)
yield return m_Cities[i];
}
}
}
这样就可以在 foreach 循环中使用 Reverse 属性:
CityCollection collection = new CityCollection();
foreach(string city in collection.Reverse)
{
Trace.WriteLine(city);
}
对于在何处以及如何使用 yield return 语句是有一些限制的。包含 yield return 语句的方法或属性不能再包含其他 return 语句,因为这样会错误地中断迭代。不能在匿名方法中使用 yield return 语句,也不能将 yield return 语句放到带有 catch 块的 try 语句中(也不能放在 catch 块或 finally 块中)。
迭代程序实现
编译器生成的嵌套类维持迭代状态。当在 foreach 循环中(或在直接迭代代码中)首次调用迭代程序时,编译器为 GetEnumerator 生成的代码将创建一个带有 reset 状态的新的迭代程序对象(嵌套类的一个实例)。在 foreach 每次循环调用迭代程序的 MoveNext 方法时,它都从前一次 yield return 语句停止的地方开始执行。只要 foreach 循环执行,迭代程序就会维持它的状态。然而,迭代程序对象(以及它的状态)在多个 foreach 循环之间并不保持一致。因此,再次调用 foreach 是安全的,因为您将使新的迭代程序对象开始新的迭代。这就是为什么 IEnumerable <ItemType> 没有定义 Reset 方法的原因。
但是嵌套迭代程序类是如何实现的呢?并且如何管理它的状态呢?编译器将一个标准方法转换成一个可以被多次调用的方法,此方法使用一个简单的状态机在前一个 yield return 语句之后恢复执行。您需要做的只是使用 yield return 语句指示编译器产生什么以及何时产生。编译器具有足够的智能,它甚至能够将多个 yield return 语句按照它们出现的顺序连接起来:
public class CityCollection : IEnumerable<string>
{
public IEnumerator<string> GetEnumerator()
{
yield return "New York";
yield return "Paris";
yield return "London";
}
}
让我们看一看在下面几行代码中显示的该类的 GetEnumerator 方法:
public class MyCollection : IEnumerable<string>
{
public IEnumerator<string> GetEnumerator()
{
//Some iteration code that uses yield return
}
}
当编译器遇到这种带有 yield return 语句的类成员时,它会插入一个名为 GetEnumerator$<random unique number>__IEnumeratorImpl 的嵌套类的定义,如图 5 中 C# 伪代码所示。(记住,本文所讨论的所有特征 — 编译器生成的类和字段的名称 — 是会改变的,在某些情况下甚至会发生彻底的变化。您不应该试图使用反射来获得这些实现细节并期望得到一致的结果。)嵌套类实现了从类成员返回的相同 IEnumerable 接口。编译器使用一个实例化的嵌套类型来代替类成员中的代码,将一个指向集合的引用赋给嵌套类的 <this> 成员变量,类似于图 2 中所示的手动实现。实际上,该嵌套类是一个提供了 IEnumerator 的实现的类。
Figure 5The Compiler-generated Iterator
public class MyCollection : IEnumerable<string>
{
public virtual IEnumerator<string> GetEnumerator()
{
GetEnumerator$0003__IEnumeratorImpl impl;
impl = new GetEnumerator$0003__IEnumeratorImpl;
impl.<this> = this;
return impl;
}
private class GetEnumerator$0003__IEnumeratorImpl :
IEnumerator<string>
{
public MyCollection <this>; // Back reference to the collection
string $_current;
// state machine members go here
string IEnumerator<string>.Current
{
get
{
return $_current;
}
}
bool IEnumerator<string>.MoveNext()
{
//State machine management
}
IDisposable.Dispose()
{
//State machine cleanup if required
}
}
}
图 5
递归迭代
当在像二叉树或其他任何包含相互连接的节点的复杂图形这样的数据结构上进行递归迭代时,迭代程序才真正显示出了它的优势。通过递归迭代手动实现一个迭代程序是相当困难的,但是如果使用 C# 迭代程序,这将变得很容易。请考虑图 6 中的二叉树。这个二叉树的完整实现是本文所提供的源代码的一部分。这个二叉树在节点中存储了一些项。每个节点均拥有一个一般类型 T(名为Item)的值。每个节点均含有指向左边节点的引用和指向右边节点的引用。比 Item 小的值存储在左边的子树中,比 Item 大的值存储在右边的子树中。这个树还提供了 Add 方法,通过使用参数限定符添加一组开放式的 T 类型的值:
Figure 6Implementing a Recursive Iterator
class Node<T>
{
public Node<T> LeftNode;
public Node<T> RightNode;
public T Item;
}
public class BinaryTree<T>
{
Node<T> m_Root;
public void Add(params T[] items)
{
foreach(T item in items)
Add(item);
}
public void Add(T item)
{...}
public IEnumerable<T> InOrder
{
get
{
return ScanInOrder(m_Root);
}
}
IEnumerable<T> ScanInOrder(Node<T> root)
{
if(root.LeftNode != null)
{
foreach(T item in ScanInOrder(root.LeftNode))
{
yield return item;
}
}
yield return root.Item;
if(root.RightNode != null)
{
foreach(T item in ScanInOrder(root.RightNode))
{
yield return item;
}
}
}
}
图 6
public void Add(params T[] items);
这棵树提供了一个 IEnumerable <T> 类型的名为 InOrder 的公共属性。InOrder 调用递归的私有帮助器方法 ScanInOrder,把树的根节点传递给 ScanInOrder。ScanInOrder 定义如下:
IEnumerable<T> ScanInOrder(Node<T> root);
它返回 IEnumerable <T> 类型的迭代程序的实现,此实现按顺序遍历二叉树。对于 ScanInOrder 需要注意的一件事情是,它通过递归遍历这个二叉树的方式,即使用 foreach 循环来访问从递归调用返回的 IEnumerable <T>。在顺序 (in-order) 迭代中,每个节点都首先遍历它左边的子树,接着遍历该节点本身的值,然后遍历右边的子树。对于这种情况,需要三个 yield return 语句。为了遍历左边的子树,ScanInOrder 在递归调用(它以参数的形式传递左边的节点)返回的 IEnumerable <T>上使用 foreach 循环。一旦 foreach 循环返回,就已经遍历并产生了左边子树的所有节点。然后,ScanInOrder 产生作为迭代的根传递给其节点的值,并在 foreach 循环中执行另一个递归调用,这次是在右边的子树上。通过使用属性 InOrder,可以编写下面的 foreach 循环来遍历整个树:
BinaryTree<int> tree = new BinaryTree<int>();
tree.Add(4,6,2,7,5,3,1);
foreach(int num in tree.InOrder)
{
Trace.WriteLine(num);
}
// Traces 1,2,3,4,5,6,7
可以通过添加其他的属性用相似的方式实现前序 (pre-order) 和后序 (post-order) 迭代。虽然以递归方式使用迭代程序的能力显然是一个强大的功能,但是在使用时应该保持谨慎,因为可能会出现严重的性能问题。每次调用 ScanInOrder 都需要实例化编译器生成的迭代程序,因此,递归遍历一个很深的树可能会导致在幕后生成大量的对象。在对称二叉树中,大约有 n 个迭代程序实例,其中 n 为树中节点的数目。在任一特定的时刻,这些对象中大约有 log(n) 个是活的。在具有适当大小的树中,许多这样的对象会使树通过 0 代 (Generation 0) 垃圾回收。也就是说,通过使用栈或队列维护一列将要被检查的节点,迭代程序仍然能够方便地遍历递归数据结构(例如树)。
局部类型
C# 1.1 要求将类的全部代码放在一个文件中。而 C# 2.0 允许将类或结构的定义和实现分开放在多个文件中。通过使用 new partial 关键字来标注分割,可以将类的一部分放在一个文件中,而将另一个部分放在一个不同的文件中。例如,可以将下面的代码放到文件 MyClass1.cs 中:
public partial class MyClass
{
public void Method1()
{...}
}
在文件 MyClass2.cs 中,可以插入下面的代码:
public partial class MyClass
{
public void Method2()
{...}
public int Number;
}
实际上,可以将任一特定的类分割成任意多的部分。局部类型支持可以用于类、结构和接口,但是不能包含局部枚举定义。
图 1 迭代程序设计模式
此外,由于每个迭代程序都保持单独的迭代状态,所以多个客户端可以执行单独的并发迭代。通过实现 IEnumerable,诸如数组和队列这样的数据结构可以支持这种超常规的迭代。在 foreach 循环中生成的代码调用类的 GetEnumerator 方法简单地获得一个 IEnumerator 对象,然后将其用于 while 循环,从而通过连续调用它的 MoveNext 方法和当前属性遍历集合。如果您需要显式地遍历集合,您可以直接使用 IEnumerator(不用求助于 foreach 语句)。
但是使用这种方法有一些问题。首先,如果集合包含值类型,则需要对它们进行装箱和拆箱才能获得项,因为 IEnumerator.Current 返回一个对象。这将导致潜在的性能退化和托管堆上的压力增大。即使集合包含引用类型,仍然会产生从对象向下强制类型转换的不利结果。虽然大多数开发人员不熟悉这一特性,但是在 C# 1.0 中,实际上不必实现 IEnumerator 或 IEnumerable 就可以为每个循环实现迭代程序模式。编译器将选择调用强类型化版本,以避免强制类型转换和装箱。结果是,即使在 1.0 版本中,也可能没有导致性能损失。
为了更好地阐明这个解决方案并使其易于实现,Microsoft .NET 框架 2.0 在 System.Collections.Generics 命名空间中定义了一般的类型安全的 IEnumerable <ItemType> 和 IEnumerator <ItemType> 接口:
public interface IEnumerable<ItemType>
{
IEnumerator<ItemType> GetEnumerator();
}
public interface IEnumerator<ItemType> : IDisposable
{
ItemType Current{get;}
bool MoveNext();
}
除了利用泛型之外,新的接口与其前身还略有差别。与 IEnumerator 不同,IEnumerator <ItemType> 是从 IDisposable 派生而来的,并且没有 Reset 方法。图 2 中的代码显示了实现 IEnumerable <string> 的简单 city 集合,而图 3 显示了编译器在跨越 foreach 循环的代码时如何使用该接口。图 2 中的实现使用了名为 MyEnumerator 的嵌套类,它将一个引用作为构造参数返回给要枚举的集合。MyEnumerator 清楚地知道 city 集合(本例中的一个数组)的实现细节。MyEnumerator 类使用 m_Current 成员变量维持当前的迭代状态,此成员变量用作数组的索引。
制作者:剑锋冷月