找回密码
 注册
搜索
热搜: 回贴

在Visual C# 2.0中创建优雅代码

2009-12-13 13:54| 发布者: admin| 查看: 98| 评论: 0|原作者: 云忆

▲热衷于C#语言的人会喜欢上VisualC......


  热衷于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返回一个Object类的对象。这将导致潜在的性能降低 和托管堆上的压力增大。即使集合包含引用类型,仍然会产生从对象向下强制类型转换的不利结果。虽然大多数开发人员不熟悉这一特性,事实上在C# 1.0中,不必实现IEnumerator或IEnumerable接口就可以为每个循环实现迭代器模式。编译器将选择调用强类型化版本,以避免强制类型转换和装箱。结果是,即使在1.0版本中,也可能没有导致性能损失。
  为了更好地阐明这个解决方案并使其易于实现,Microsoft .NET框架2.0在System.Collections.Generics命名空间中定义了类型安全的泛型IEnumerable和IEnumerator
public interface IEnumerable
{
IEnumerator GetEnumerator();
}

public interface IEnumerator : IDisposable
{
ItemType Current{get;}
bool MoveNext();
}
  除了利用泛型之外,新的接口与其前身还略有差别。与IEnumerable不同,IEnumerator是从IDisposable派生而来的,并且没有Reset方法。图2中的代码显示了实现IEnumerable的简单city集合,而图3显示了编译器展开foreach循环的代码中如何使用该接口。图2中的实现使用了名为MyEnumerator的嵌套类,它将一个引用作为构造参数返回给要枚举的集合。MyEnumerator清楚地知道city集合(本例中的一个数组)的实现细节。此外,MyEnumerator类使用m_Current成员变量维持当前的迭代状态,此成员变量用作数组的索引。
public class CityCollection : IEnumerable
{
string[] m_Cities = {"New York","Paris","London"};

public IEnumerator GetEnumerator()
{
return new MyEnumerator(this);
}

//Nested class definition
class MyEnumerator : IEnumerator
{
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 实现IEnumerable
CityCollection cities = new CityCollection();

//For this foreach loop:
foreach(string city in cities)
{
Trace.WriteLine(city);
}

//The compiler generates this equivalent code:
IEnumerable enumerable = cities;
IEnumerator enumerator = enumerable.GetEnumerator();

using(enumerator)
{
while(enumerator.MoveNext())
{
Trace.WriteLine(enumerator.Current);
}
}
图3 简单的迭代程序
  第二个问题迭代器的实现也是难以解决的问题。虽然对于简单的应用实例中(如图3所示),实现是相当简单的,但是对于高级的数据结构,实现将非常复杂,例如二叉树,它需要递归遍历,并需在递归时维持迭代状态。另外,如果需要各种迭代选项,例如需要在一个链表中从头到尾和从尾到头选项,则此链表的代码就会因为使用多种迭代器实现而变得臃。这正是设计C# 2.0迭代器所要解决的问题。通过使用迭代器,可以让C#编译器生成IEnumerator的实现。C#编译器能够自动生成一个嵌套类来维持迭代状态。可以在泛型集合或特定于类型的集合中使用迭代器。开发人员需要做的只是告诉编译器在每个迭代中产生的是什么。如同手动提供迭代器一样,需要公开GetEnumerator方法,此方法是在实现IEnumerable接口或IEnumerable公开的。
  可以使用新的C#的yield return语句告诉编译器产生什么。例如,下面的代码显示了如何在city集合中使用C#迭代器来代替图2中的人工实现部分:
public class CityCollection : IEnumerable
{
string[] m_Cities = {"New York","Paris","London"};

public IEnumerator GetEnumerator()
{
for(int i = 0; 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 yield return m_Cities[i];
}
}
  此外,还可以在如图4所示的在完全泛型(Fully Generic)集合中使用C#迭代器。当使用泛型集合和迭代器时,从声明的集合(本例中的string)中,编译器就可以检索到foreach循环内IEnumerable所用的特定类型:
LinkedList list = new LinkedList();

/* Some initialization of list, then */
foreach(string item in list)
{
Trace.WriteLine(item);
}
图4在普通链表中使用迭代程序
//K is the key, T is the data item
class Node
{
public K Key;
public T Item;
public Node NextNode;
}

public class LinkedList : IEnumerable
{
Node m_Head;

public IEnumerator GetEnumerator()
{
Node current = m_Head;

while(current != null)
{
yield return current.Item;
current = current.NextNode;
}
}

/* More methods and members */
}
  这与其他任何从泛型接口派生的相似。如果想中止迭代,请使用yield break语句。例如,下面的迭代器将仅仅产生数值1、2和3:
public IEnumerator GetEnumerator()
{
for(int i = 1;i< 5;i++)
{
yield return i;

if(i > 2)
yield break;
}
}
  这样,集合可以很容易地公开多个迭代器,每个迭代器都用于以不同的方式遍历集合。例如,要以倒序遍历CityCollection类,在这个类中提供了IEnumerable类型的Reverse属性,它是
public class CityCollection
{
string[] m_Cities = {"New York","Paris","London"};

public IEnumerable 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函数产生的编译生成(Compiler-Generated)代码将创建一个带有reset状态的新的迭代器对象(即嵌套类的一个实例)。在foreach每次循环调用迭代器的MoveNext方法时,它都从前一次yield return语句停止的地方开始执行。只要foreach循环执行,迭代器就会维持它的状态。然而,迭代器对象(以及它的状态)在多个foreach循环之间并不保持一致。因此,再次调用foreach是安全的,因为将生成新的迭代器对象并开始新的迭代。这就是为什么IEnumerable没有定义Reset方法的原因。
  但是嵌套迭代器类是如何实现的呢?并且如何管理它的状态呢?编译器将一个标准方法转换成一个可以被多次调用的方法,此方法使用一个简单的状态机在前一个yield return语句之后恢复执行。开发人员需要做的只是使用yield return语句指示编译器产生什么以及何时产生。编译器具有足够的智能,它甚至能够将多个yield return语句按照它们出现的顺序连接起来:
public class CityCollection : IEnumerable
{
public IEnumerator GetEnumerator()
{
yield return "New York";
yield return "Paris";
yield return "London";
}
}
  让我们看一看在下面几行代码中显示的该类的GetEnumerator方法:
public class MyCollection : IEnumerable
{
public IEnumerator GetEnumerator()
{
//Some iteration code that uses yield return
}
}
  当编译器遇到这种带有yield return语句的类成员时,它会插入一个名为GetEnumerator$__IEnumeratorImpl的嵌套类的定义,如图5中C#伪代码所示。(记住,本文所讨论的所有特征,包括编译器生成的类和字段的名称是会改变的,在某些情况下甚至会发生彻底的变化。您不应该试图使用反射来获得这些实现细节并期望得到一致的结果。)
public class MyCollection : IEnumerable
{
public virtual IEnumerator GetEnumerator()
{
GetEnumerator$0003__IEnumeratorImpl impl;
impl = new GetEnumerator$0003__IEnumeratorImpl;
impl. = this;
return impl;
}

private class GetEnumerator$0003__IEnumeratorImpl :
IEnumerator
{
public MyCollection ; // Back reference to the collection
string $_current;

// state machine members go here
string IEnumerator.Current
{
get
{
return $_current;
}
}

bool IEnumerator.MoveNext()
{
//State machine management
}

IDisposable.Dispose()
{
//State machine cleanup if required
}
}
}
图5编译器生成的迭代程序
  嵌套类实现了从类成员返回的相同IEnumerable接口。编译器使用一个实例化的嵌套类型来代替类成员中的代码,将一个指向集合的引用赋给嵌套类的this成员变量,类似于图2中所示的手动实现。实际上,该嵌套类是一个实现IEnumerator接口的类。
 递归迭代
  当在像二叉树或包含相互连通节点的图这样的数据结构上进行递归迭代时,迭代器才真正显示出了它的优势。手工实现一个递归迭代的迭代器是相当困难的,但是如果使用C#迭代器,就很容易。请考虑图6中的二叉树。本文所提供的源代码包含了此二叉树的完整实现。
class Node
{
public Node LeftNode;
public Node RightNode;
public T Item;
}

public class BinaryTree
{
Node m_Root;

public void Add(params T[] items)
{
foreach(T item in items)
Add(item);
}

public void Add(T item)
{...}

public IEnumerable InOrder
{
get
{
return ScanInOrder(m_Root);
}
}

IEnumerable ScanInOrder(Node 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实现递归迭代
  这个二叉树在节点中存储了一些项。每个节点均拥有一个类型T(名为Item)的值。每个节点均含有指向左边节点的引用和指向右边节点的引用。比Item小的值存储在左边的子树中,比Item大的值存储在右边的子树中。这个树还提供了Add方法,通过使用参数限定符添加一组的T类型的值:
public void Add(params T[] items);
  这棵树提供了一个IEnumerable类型的名为InOrder的公共属性。InOrder调用私有的辅助递归函数ScanInOrder并把树的根节点传递给ScanInOrder。ScanInOrder定义如下:
IEnumerable ScanInOrder(Node root);
  它返回IEnumerable类型的迭代器的实现,此实现按顺序遍历二叉树。对于ScanInOrder需要注意的一件事情是,它通过递归遍历这个二叉树的方式,即使用foreach循环来访问从递归调用返回的IEnumerable实现。在顺序(in-order)迭代中,每个节点都首先遍历它左边的子树,接着遍历该节点本身的值,然后遍历右边的子树。对于这种情况,需要三个yield return语句。为了遍历左边的子树,ScanInOrder在递归调用(它以参数的形式传递左边的节点)返回的IEnumerable上使用foreach循环。一旦foreach循环返回,就已经遍历左边子树的所有节点。然后,ScanInOrder产生作为迭代的根传递给其节点的值,并在foreach循环中执行另一个递归调用,这次是在右边的子树上。
  通过使用属性InOrder,可以编写下面的foreach循环来遍历整个树:
BinaryTree tree = new BinaryTree();
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允许将类或结构的定义和实现分开放在多个文件中。通过使用新的partial关键字来标注分割,可以将类的一部分放在一个文件中,而将另一个部分放在一个不同的文件中。例如,可以将下面的代码放到文件MyClass1.cs中:
public partial class MyClass
{
public void Method1()
{...}
}
  在文件MyClass2.cs中,可以插入下面的代码:
public partial class MyClass
{
public void Method2()
{...}

public int Number;
}
  实际上,可以将任一特定的类分割成任意多的部分。局部类型支持可以用于类、结构和接口,但是不能包含局部枚举定义。局部类型是一个非常有用的功能。有时,需要修改机器生成的文件,例如Web服务客户端包装类。然而,当重新生成此包装类时,对该文件的修改将会被丢弃。通过使用局部类,可以将这些改变分开放在单独的文件中。在ASP.NET中可以将局部类用于code-beside编辑(从code-behind演变而来),单独存储页面中机器生成的部分,而在Windows窗体中使用局部类来存储InitializeComponent方法的可视化设计器输出以及成员控件。通过使用局部类型,两个或者更多的开发人员可以工作在同一个类型上,同时都可以从源代码控制中签出其文件而不互相影响。
  但是,如果多个不同的部分对同一个类做出了相互矛盾的定义会出现什么样的后果?答案很简单。一个类(或一个结构)可能具有两个不同的方面或性质:累积性的(accumulative)和非累积性的(non-accumulative)。累积性的方面是指类可以选择添加它的各个部分,比如接口派生、属性、索引器、方法和成员变量。例如,下面的代码显示了一个部分是如何添加接口派生和实现的:
public partial class MyClass
{}

public partial class MyClass : IMyInterface
{
public void Method1()
{...}

public void Method2()
{...}
}
  非累积性的方面是指一个类型的所有部分都必须一致。无论这个类型是一个类还是一个结构,类型可见性(公共或内部)和基类都是非累积性的方面。例如,下面的代码不能编译,因为并非MyClass的所有部分都出现在基类中:
public class MyBase
{}

public class SomeOtherClass
{}

public partial class MyClass : MyBase
{}

public partial class MyClass : MyBase
{}

//Does not compile
public partial class MyClass : SomeOtherClass
{}
  除了所有的部分都必须定义相同的非累积性部分以外,只有一个部分能够重写虚方法或抽象方法,并且只有一个部分能够实现接口成员。
  C# 2.0是这样来支持局部类型的:当编译器构建程序集时,它将来自多个文件的同一类型的各个部分组合起来,并用中间语言(Microsoft intermediate language, MSIL)将这些部分编译成单一类型。生成的中间语言中不含有哪一部分来自哪个文件的记录。正如在C# 1.1中一样。另外值得注意的是,局部类型不能跨越程序集,并且通过忽略其定义中的partial限定符,一个类型可以拒绝包含其他部分。

  因为编译器所做的只是将各个部分累积,所以一个单独的文件可以包含多个部分,甚至是包含同一类型的多个部分,尽管这样做的意义值得怀疑。
  在C#中,开发人员通常根据文件所包含的类来为文件命名,这样可以避免将多个类放在同一个文件中。在使用局部类型时,建议在文件名中指示此文件包含哪个类型的哪些部分(例如MyClassP1.cs、MyClassP2.cs),或者采用其他一致的方式从类的名称上指示源文件的内容。例如,Windows窗体设计人员将用于该窗体的局部类的一部分存放在Form1.cs中,并将此文件命名为Form1.Designer.cs。

  局部类的另一个不利之处是,当开始接触一个不熟悉的代码时,所维护的类的各个部分可能遍布在整个项目的文件中。在这种情况下,可以使用Visual Studio Class View,因为它可以将一个类型的所有部分积累起来展示给您,并允许通过单击它的成员来导航各个不同的部分。导航栏也提供了这个功能。
 匿名方法
  C#支持用于调用一个或多个方法的委托(delegate)。委托提供运算符和方法来添加或删除目标方法,它也可以在整个.NET框架中广泛地用于事件、回调、异步调用、多线程等。然而,仅仅为了使用一个委托,有时不得不创建一个类或方法。在这种情况下,不需要多个目标,并且调用的代码通常相对较短而且简单。在C# 2.0中,匿名方法是一个新功能,它允许定义一个由委托调用的匿名(也就是没有名称的)方法。例如,下面是一个常规SomeMethod方法的定义和委托调用:
class SomeClass
{
delegate void SomeDelegate();

public void InvokeMethod()
{
SomeDelegate del = new SomeDelegate(SomeMethod);
del();
}

void SomeMethod()
{
MessageBox.Show("Hello");
}
}
可以用一个匿名方法来定义和实现这个方法:
class SomeClass
{
delegate void SomeDelegate();

public void InvokeMethod()
{
SomeDelegate del = delegate()
{
MessageBox.Show("Hello");
};
del();
}
}
  匿名方法被定义为内联方法,而不是作为任何类的成员方法。此外,无法将方法属性应用到一个匿名方法,并且匿名方法也不能定义泛型类型或添加泛型约束。
  关于匿名方法有两个地方值得注意:delegate保留关键字的重载使用和委托指派。稍后,将看到编译器如何实现一个匿名方法,而通过查看代码,就可以了解编译器如何推理所使用的委托的类型,实例化推理类型的新委托对象,将新的委托包装到匿名方法中,并将其指派给匿名方法定义中使用的委托(前面的示例中的del)。
  匿名方法可以用在任何需要使用委托类型的地方。可以将匿名方法传递给任何方法,只要该方法接受适当的委托类型作为参数即可:
class SomeClass
{
delegate void SomeDelegate();

public void SomeMethod()
{
InvokeDelegate(delegate(){MessageBox.Show("Hello");});
}

void InvokeDelegate(SomeDelegate del)
{
del();
}
}
  如果需要将一个匿名方法传递给一个接受抽象Delegate参数的方法,例如:
void InvokeDelegate(Delegate del);
  则首先需要将匿名方法强制转换为特定的委托类型。
  下面是一个将匿名方法作为参数传递的具体的实用例子,它在没有显式定义ThreadStart委托或线程方法的情况下启动一个新的线程:
public class MyClass
{
public void LauchThread()
{
Thread workerThread = new Thread(delegate()
{
MessageBox.Show("Hello");
});

workerThread.Start();
}
}
在前面的示例中,匿名方法被当作线程方法来使用,这会导致消息框从新线程中显示出来。将参数传递到匿名方法。
当定义带有参数的匿名方法时,应该在delegate关键字后面定义参数类型和名称,就好像它是一个常规方法一样。方法调用必须与它指派的委托的定义相匹配。当调用委托时,可以传递参数的值,与正常的委托调用完全一样:
class SomeClass
{
delegate void SomeDelegate(string str);

public void InvokeMethod()
{
SomeDelegate del = delegate(string str)
{
MessageBox.Show(str);
};
del("Hello");
}
}
如果匿名方法没有参数,则可以在delegate关键字后面使用一对空括号:
class SomeClass
{
delegate void SomeDelegate();

public void InvokeMethod()
{
SomeDelegate del = delegate()
{
MessageBox.Show("Hello");
};
del();
}
}
然而,如果将delegate关键字与后面的空括号一起忽略,则定义一种特殊的匿名方法,它可以指派给具有任何调用的任何委托:
class SomeClass
{
delegate void SomeDelegate(string str);

public void InvokeMethod()
{
SomeDelegate del = delegate
{
MessageBox.Show("Hello");
};
del("Parameter is ignored");
}
}
很明显,如果匿名方法并不依赖于任何参数,而且想要使用这种与委托调用无关的方法代码,则只能使用这样的语法。
注意,当调用委托时,仍然需要提供参数,因为编译器为从委托签名中推理的匿名方法生成无名参数,就好像编写了下面的代码(C#伪码)一样:
SomeDelegate del = delegate(string)
{
MessageBox.Show("Hello");
};
此外,不带参数的匿名方法不能与带参数的委托一起使用。
class SomeClass
{
string m_Space = " ";
delegate void SomeDelegate(string str);

public void InvokeMethod()
{
string msg = "Hello";
SomeDelegate del = delegate(string name)
{
MessageBox.Show(msg + m_Space + name);
};
del("Juval");
}
}
图 7 匿名方法代码中的局部变量
匿名方法可以使用任何类成员变量,并且它还可以使用定义在其包含方法范围之内的任何局部变量。图7对此进行了展示。一旦知道如何为一个匿名方法传递参数,也就可以很容易地定义匿名事件处理,如图8所示。
public class MyForm : Form
{
Button m_MyButton;

public MyForm()
{
InitializeComponent();

m_MyButton.Click += delegate(object sender,EventArgs args)
{
MessageBox.Show("Clicked");
};
}

void InitializeComponent()
{...}
}
图 8 匿名方法作为事件处理程序
因为+=运算符仅仅将一个委托的内部调用列表与另一个委托的内部调用列表连接起来,所以可以使用+=来添加一个匿名方法。注意,在匿名事件处理的情况下,不能使用-=运算符来删除事件处理方法,除非将匿名方法作为处理程序加入,要这样做,可以首先将匿名方法存储为一个委托,然后通过事件注册该委托。这样,可以将-=运算符作用于存储的委托来取消注册。
 匿名方法的实现
编译器为匿名方法生成的代码很大程度上依赖于匿名方法使用的参数或变量的类型。例如,匿名方法使用其包含方法的局部变量(也叫做外层变量)还是使用类成员变量和方法参数。无论是哪一种情况,编译器都会生成不同类型的中间代码。如果匿名方法不使用外层变量(也就是说,它只使用自己的参数或者类成员),则编译器会将一个私有方法添加到该类中,以便赋予方法一个唯一的名称。该方法的名称具有以下格式:
__AnonymousMethod$()
和其他编译器生成的成员一样,这都是会改变的,并且最有可能在最终版本发布之前改变。方法调用将成为它指派的委托的调用。
编译器只是简单地将匿名方法定义和赋值转换成所推理的委托类型的标准实例,以包装机器生成的私有方法:
SomeDelegate del = new SomeDelegate(__AnonymousMethod$00000000);
非常有趣的是,机器产生的私有方法并不显示在智能感知(IntelliSense)中,也不能显式地调用它,因为其名称中的美元符号对于C#方法来说是一个非法标记(但它是一个有效的中间代码标记)。当匿名方法使用外部变量时,情况会更加困难。如果这样,编译器将用下面的格式添加具有唯一名称的私有嵌套类:
__LocalsDisplayClass$
嵌套类有一个指向包含类的this引用,它是一个有效的中间代码成员变量名。嵌套类包含与匿名方法使用的每个外层变量对应的公共成员变量。编译器向嵌套类定义中添加一个具有唯一名称的公共方法,格式如下:
__AnonymousMethod$()
方法调用将成为被指派的委托的调用。编译器用新的代码来替代匿名方法定义,此代码创建一个嵌套类的实例,并进行必要的从外层变量到该实例的成员变量的赋值。最后,编译器创建一个新的委托对象,以便包装嵌套类实例的公共方法,然后调用该委托来调用此方法。图9用C#伪代码展示了编译器为图7中定义的匿名方法生成的代码。
class SomeClass
{
string m_Space = " ";
delegate void SomeDelegate(string str);

private sealed class __LocalsDisplayClass$00000001
{
public SomeClass ; //Back pointer, name is valid in MSIL
public string msg; //Outer variable

public void __AnonymousMethod$00000000(string name)
{
MessageBox.Show(msg + .m_Space + name);
}
}

public void InvokeMethod()
{
string msg = "Hello";
__LocalsDisplayClass$00000001 locals;
locals = new __LocalsDisplayClass$00000001();
locals. = this;
locals.msg = msg;
SomeDelegate del = new
SomeDelegate(locals.__AnonymousMethod$00000000);
del("Juval");
}
}
图 9 使用外部变量的匿名方法代码
 泛型匿名方法
一个匿名方法可以使用泛型参数类型,就像其他方法一样。它可以使用在类范围内定义的泛型类型,例如:
class SomeClass
{
delegate void SomeDelegate(T t);

public void InvokeMethod(T t)
{
SomeDelegate del = delegate(T item){...}
del(t);
}
}
因为委托可以定义泛型参数,所以匿名方法可以使用在委托层定义的泛型类型。可以指定用于方法调用的类型,在这种情况下,方法调用必须与其所指派的委托的特定类型相匹配:
class SomeClass
{
delegate void SomeDelegate(T t);

public void InvokeMethod()
{
SomeDelegate del = delegate(int number)
{
MessageBox.Show(number.ToString());
};
del(3);
}
}
在匿名方法例子中,匿名方法虽然看起来有点像另类的编程技术,但是它是相当有用的,因为在只要一个委托就足够的情况下,使用它就可以不必再创建一个简单方法。图10展示了一个有用的匿名方法的实际例子—SafeLabel Windows窗体控件。
Windows窗体依赖于基本的Win32消息。因此,它继承了典型的Windows编程要求:只有创建窗口的线程可以处理它的消息。在.NET框架2.0中,调用错误的线程总会触发一个Windows窗体方面的异常。因此,当在另一个线程中调用窗体或控件时,必须将该调用发送到正确的所属线程中。Windows窗体有内置的支持,可以解决这个问题,其方法是用Control基类实现ISynchronizeInvoke接口,其定义如下:
public interface ISynchronizeInvoke
{
bool InvokeRequired {get;}
IAsyncResult BeginInvoke(Delegate method,object[] args);
object EndInvoke(IAsyncResult result);
object Invoke(Delegate method,object[] args);
}
Invoke方法接受针对所属线程中的方法的委托,并且将调用从正在调用的线程发送到该线程。因为方法可能并不总是知道自己是否真的在正确的线程中执行,所以通过使用InvokeRequired属性,可以进行查询,从而弄清楚是否需要调用Invoke方法。但是问题是使用ISynchronizeInvoke接口将会大大增加编程模型的复杂性,因此较好的方法往往是将带有ISynchronizeInvoke接口的交互封装在控件或窗体中,它们会自动地按照需要使用ISynchronizeInvoke。
public class SafeLabel : Label
{
delegate void SetString(string text);
delegate string GetString();

override public string Text
{
set
{
if(InvokeRequired)
{
SetString setTextDel = delegate(string text)
{base.Text = text;};
Invoke(setTextDel,new object[]{value});
}
else
base.Text = value;
}
get
{
if(InvokeRequired)
{
GetString getTextDel = delegate(){return base.Text;};
return (string)Invoke(getTextDel,null);
}
else
return base.Text;
}
}
}
图 10 SafeLabel 控件
例如,为了替代公开Text属性的Label控件,可以定义从Label派生的SafeLabel控件,如图10所示。SafeLabel重写了其基类的Text属性。在其get和set中,检查Invoke是否是必须的。如果是这样,则它需要使用一个委托来访问此属性。该实现实现了在正确的线程上调用了基类属性的实现。因为SafeLabel只定义这些方法,所以它们可以通过委托进行调用。这些方法都是匿名方法很好的候选者。SafeLabel传递这样的委托,以便将匿名方法作为其Text属性的安全实现包装到Invoke方法中。
 委托推理
C#编译器从匿名方法指派推理哪个委托类型将要实例化的能力是一个非常重要的功能。实际上,它还提供了另一个叫做委托推理的C# 2.0功能。委托推理允许直接给委托变量指派方法名,而不需要先使用委托对象包装它。例如下面的C# 1.1代码:
class SomeClass
{
delegate void SomeDelegate();

public void InvokeMethod()
{
SomeDelegate del = new SomeDelegate(SomeMethod);
del();
}

void SomeMethod()
{...}
}
现在,可以编写下面的代码来代替前面的代码片断:
class SomeClass
{
delegate void SomeDelegate();

public void InvokeMethod()
{
SomeDelegate del = SomeMethod;
del();
}

void SomeMethod()
{...}
}
当将一个方法名指派给委托时,编译器首先推理该委托的类型。然后,编译器根据此名称检验是否存在一个方法,并且它的调用是否与推理的委托类型相匹配。最后,编译器创建一个推理委托类型的新对象,以便包装此方法,并将其指派给该委托。如果该类型是一个具体的委托类型(即除了抽象类型Delegate之外的其他类型),则编译器只能推理委托类型。委托推理的确是一个非常有用的功能,它可以使代码变得简练而优雅。
作为C# 2.0中的惯例,开发人员将会使用委托推理,而不是以前的委托实例化方法。例如,下面的代码说明了如何在不显式地创建一个ThreadStart委托的情况下启动一个新的线程:
public class MyClass
{
void ThreadMethod()
{...}

public void LauchThread()
{
Thread workerThread = new Thread(ThreadMethod);
workerThread.Start();
}
}
当启动一个异步调用并提供一个完整的回调方法时,可以使用一对委托推理,如图11所示。首先,指定异步调用的方法名来异步调用一个匹配的委托。然后调用BeginInvoke,提供完整的回调方法名而不是AsyncCallback类型的委托。
class SomeClass
{
delegate void SomeDelegate(string str);

public void InvokeMethodAsync()
{
SomeDelegate del = SomeMethod;
del.BeginInvoke("Hello",OnAsyncCallBack,null);
}

void SomeMethod(string str)
{
MessageBox.Show(str);
}

void OnAsyncCallBack(IAsyncResult asyncResult)
{...}
}
图 11 使用委托推理
 属性和索引可见性
C# 2.0允许为属性或索引器的get和set访问器指定不同的可见性。例如,在通常情况下,可能想将get访问器公开为public,而把set访问器公开为protected。为此,可以为set关键字添加protected可见性限定符。类似地,可以将索引器的set方法定义为protected(请参见图12)。
public class MyClass
{
public string this[int index]
{
get
{
return m_Names[index];
}
protected set
{
m_Names[index] = value;
}
}

string[] m_Names;
//Rest of the class
}
图 12 Public Get 与 Protected Set
当使用属性可见性时有几项规定。首先,应用在set或get上的可见性限定词只能是此属性本身可见性的严格子集。换句话说,如果此属性是public,那么就可以指定internal、protected、protected internal、private。如果此属性可见性是protected,就不能将get或set公开为public。此外,只能分别为get或set指定可见性,而不能同时为它们指定可见性。静态类有些类只有静态方法或静态成员(静态类),这是非常常见的。在这种情况下,实例化这些类的对象没有意义。例如,Monitor类或类工厂(例如.NET框架1.1中的Activator类)都是静态类。在C# 1.1中,如果想要阻止开发人员实例化类的对象,可以只提供一个私有的默认构造函数。如果没有任何公共的构造函数,就不可以实例化类的对象:
public class MyClassFactory
{
private MyClassFactory()
{}

static public object CreateObject()
{...}
}
然而,因为C#编译器仍然允许添加实例成员(尽管可能从来都不使用它们),所以是否在类中只定义静态成员完全由您决定。C# 2.0通过允许将类限定为static来支持静态类:
public static class MyClassFactory
{
static public T CreateObject()
{...}
}
C# 2.0编译器不允许将一个非静态成员添加到一个静态类中,也不允许创建此静态类的实例,就好像它是一个抽象类一样。此外,不能从一个静态类派生子类。这就如同编译器在静态类定义中加入了abstract和sealed一样。注意,可以定义静态类而不能定义静态结构,并且可以添加静态构造函数。
 全局命名空间限定符
很可能有这样一个嵌套的命名空间,它的名称与一些其他的全局命名空间相匹配。在这种情况下,C# 1.1编译器在解析命名空间引用时会出现问题。请考虑下例:
namespace MyApp
{
namespace System
{
class MyClass
{
public void MyMethod()
{
System.Diagnostics.Trace.WriteLine("It Works!");
}
}
}
}
在C# 1.1中,调用Trace类会产生编译错误(没有全局命名空间限定符::)。出现这种错误的原因在于,当编译器尝试解析对System命名空间的引用时,它使用直接包含范围,此范围包含System命名空间但不包含Diagnostics命名空间。C# 2.0允许您使用全局命名空间限定符::来表示编译器应该在全局范围内进行搜索。可以将::限定符应用于命名空间和类型,如图13所示。
namespace MyApp
{
class MyClass
{
public void MyMethod()
{
::MyClass obj = new ::MyClass();
obj.MyMethod(); // Traces "Hello" instead of recursion
}
}
}

public class MyClass
{
public void MyMethod()
{
Trace.WriteLine("Hello");
}
}
图 13 使用全局命名空间限定符
 内联警告
C# 1.1允许使用项目设置或者通过向编译器发布命令行参数来禁止特殊的编译器警告。但是这是一个全局性的取消,这样做会取消一些仍然需要的警告。C# 2.0允许使用#pragma警告指令显式地取消和恢复编译器警告:
// Disable 'field never used' warning
#pragma warning disable 169
public class MyClass
{
int m_Number;
}
#pragma warning restore 169
在产品代码中通常并不鼓励禁止警告。禁止警告只是为了进行某些分析,比如,当您尝试隔离一个问题时,或者当您设计代码并且想要得到代码合适的初始结构而不必先行对其加以完善时。而在所有其他的情况下,都要避免取消编译器警告。注意,不能通过编程的方式来重写项目设置,这意味着不能使用pragma警告指令来恢复全局取消的警告。
 小结
本文所提到的C# 2.0中的一些新功能是专门的解决方案,旨在处理特定的问题,同时可以简化整体编程模型。如果关注工作效率和质量,就需要让编译器生成尽可能多的实现,减少重复性的编程任务,使最后得到的代码简洁易读。新的功能带来的正是这些特色,它们象征着C#时代的到来,将会使C#成为服务于.NET专业开发人员的优秀工具。

最新评论

QQ|小黑屋|最新主题|手机版|微赢网络技术论坛 ( 苏ICP备08020429号 )

GMT+8, 2024-9-29 15:26 , Processed in 0.084118 second(s), 12 queries , Gzip On, MemCache On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

返回顶部