Visual C# 2.0匿名方法揭密

80酷酷网    80kuku.com

  visual  匿名方法基础

  匿名方法是C#2.0的一个新的语言特性。本文的主要内容是提供给读者关于匿名方法的内部实现和工作方式的一个更好的理解。本文无意于成为匿名方法的完全语言特性参考。

  匿名方法允许我们定义委托对象可以接受的代码块。这个功能省去我们创建委托时想要传递给一个委托的小型代码块的一个额外的步骤。它也消除了类代码中小型方法的混乱。让我们看看:比方说,我们有一个字符串集合命名为MyCollection。这个类有一个方法:获得集合中满足用户提供的过滤准则的所有项,调用者决定在集合中的一个特殊项是否符合条件而被检索到,作为从此方法返回数组的一部分。

public class MyCollection
{
 public delegate bool SelectItem(string sItem);
 public string[] GetFilteredItemArray(SelectItem itemFilter)
 {
  List<string> sList = new List<string>();
  foreach(string sItem in m_sList)
  {
   if (itemFilter(sItem) == true) sList.Add(sItem);
  }
  return sList.ToArray();
 }

 public List<string> ItemList
 {
  get
  {
   return m_sList;
  }
 }
 private List<string> m_sList = new List<string>();
}
  我们可以用上面定义的类写如下所示的代码:

public class Program
{
 public static void Main(string[] args)
 {
  MyCollection objMyCol = new MyCollection();
  objMyCol.ItemList.Add("Aditya");
  objMyCol.ItemList.Add("Tanu");
  objMyCol.ItemList.Add("Manoj");
  objMyCol.ItemList.Add("Ahan");
  objMyCol.ItemList.Add("Hasi");

  //获得集合中以字母’A‘开头的字符项数组
  string[] AStrings = objMyCol.GetFilteredItemArray(FilterStringWithA);
  Console.WriteLine("----- Strings starting with letter ''A'' -----");
  foreach(string s in AStrings)
  {
   Console.WriteLine(s);
  }
  //获得集合中以字母’T‘开头的字符项数组
  string[] TStrings = objMyCol.GetFilteredItemArray(FilterStringWithT);
  Console.WriteLine("----- Strings starting with letter ''T'' -----");
  foreach(string s in TStrings)
  {
   Console.WriteLine(s);
  }
 }

 public static bool FilterStringWithA(string sItem)
 {
  if (sItem[0] == ''A'')
   return true;
  else
   return false;
 }
 public static bool FilterStringWithT(string sItem)
 {
  if (sItem[0] == ''T'')
   return true;
  else
   return false;
 }
}
  可以看出对于每个我们想要提供的简单过滤准则,我们应该定义一个方法(静态或实例的)。这很快就搞乱了类的代码。而用匿名方法,代码变得相当自然。下面是这个Program类用匿名方法重写后的:

public class Program
{
 public delegate void MyDelegate();
 public static void Main(string[] args)
 {
  MyCollection objMyCol = new MyCollection();
  objMyCol.ItemList.Add("Aditya");
  objMyCol.ItemList.Add("Tanu");
  objMyCol.ItemList.Add("Manoj");
  objMyCol.ItemList.Add("Ahan");
  objMyCol.ItemList.Add("Hasi");
  //获得集合中以字母’A‘开头的字符项数组
  string[] AStrings = objMyCol.GetFilteredItemArray(delegate(string sItem)
  {
   if (sItem[0] == ''A'')
    return true;
   else
    return false;
  });
  Console.WriteLine("----- Strings starting with letter ''A'' -----");
  foreach (string s in AStrings)
  {
   Console.WriteLine(s);
  } //获得集合中以字母’ T ‘开头的字符项数组
  string[] TStrings = objMyCol.GetFilteredItemArray(delegate(string sItem)
  {
   if (sItem[0] == ''T'')
    return true;
   else
    return false;
  });
  Console.WriteLine("----- Strings starting with letter ''T'' -----");
  foreach (string s in TStrings)
  {
   Console.WriteLine(s);
  }
 }
}

  正如上面示例中的所示,我们已能用内联代码块定义的过滤准则替代定义一个新的方法来代表每个过滤准则。老实说,用这种内联代码可能看起来自然并且避免了定义新方法,但是如果这个技术被用于更大的内联代码块,这时代码很快变得难于管理并可能导致代码重复。因此,使用方法与内联匿名方法都是委托/事件处理器的可选方案。

  好了,这些就是匿名方法的基础。本文的余下部分将讨论在不同的场景下匿名方法内部如何工作的。理解匿名方法如何被实现和内部如何工作对于正确地使用它们是重要的。否则,你使用匿名方法的代码的结果看起来将不可预知。

  匿名方法的静态数据成员的用法

  匿名方法总是以一个delegate关键字开始,后面跟着用在方法和方法体(the method body)本身中的参数。正如从上面示例中所见,用户不需要确定匿名方法的返回类型。它(译注:指返回类型)由方法体中的return语句推断而来。.NET CLR不能执行像匿名方法一样的自由流(free flowing)代码块。CLR要求:它执行的每个方法是一个类型的一部分,并且应该是一个静态(static)方法或实例(instance)方法(译注:若一个方法声明中含有 static 修饰符,则称该方法为静态方法。若其中没有 static 修饰符时,则称该方法为实例方法。静态方法不对特定实例进行操作,在静态方法中引用 this 是编译时错误。实例方法对类的某个给定的实例进行操作,而且可以用 this来访问该实例)。因此当你在一个类的代码中写匿名方法并编译这个代码时,C#编译器默默地在你定义匿名方法的相同的类中创建了一个静态或实例方法。所以匿名方法只是一个在类中定义你自己方法以传递到委托(委托处理器/事件处理器)的方便的语法。

  当你编译上面的示例时,C#编译器在类''Program''内部即我们定义匿名方法的地方创建了两个private static方法。它此时用这些static方法的地址取代了匿名方法。编译器决定如何创建静态方法或实例方法取决于匿名方法被定义的类中的静态或实例数据成员的用法。在我们的示例中,我们没有用到任何类''Program''的数据成员,因为调用一个静态方法而不是一个实例方法将是高效的,因此C#编译器创建一个static方法来封装我们的匿名方法的代码。下面是这个示例程序集''Program'' 类的ILDASM视图。高亮部分显示了由C#编译器默默添加到''Program''类的新的静态方法。


  如果我们已经使用了用匿名方法的''Program'' 类的任何静态数据,C#编译器将仍然在''Program'' 类里创建一个静态方法来包装匿名方法。

  匿名方法的实例数据成员用法

  让我们在我们的示例中的''Program''类中定义一个新的实例方法,并使用示例类(译注:即''Program''类)一个实例数据成员。下面的代码显示了修改后的示例:

public class Program
{
 public delegate void MyDelegate();
 public static void Main(string[] args)
 {
  //实例数据成员测试
  Program p = new Program();
  for(int i=1;i<=5;i++)
   p.TestInstanceDataMembers();
 }
 public void TestInstanceDataMembers()
 {
  MyDelegate d = delegate
  {
   Console.WriteLine("Count: {0}",++m_iCount);
  };
  d();
 }
 public int m_iCount = 0;
}
  我们定义了一个新的实例方法:TestInstanceDataMembers,在''Program''类中这个方法定义了一个匿名方法,匿名方法使用了实例数据成员:隶属''Program''类的m_iCount。当这个示例编译时,C#编译器将创建一个private实例方法来包装这个在TestInstanceDataMembers中定义的匿名方法。C#编译器必须创建一个实例方法因为该方法需要访问''Program''类的实例数据成员。下面是这个示例程序集''Program''类的ILDASM视图。在图的下部选中部分显示了由C#编译器默默添加到''Program''类的新的private实例方法。

  匿名方法的局部变量用法

  到现在为止,我们对匿名方法如何工作以及内部如何实现有了一点基本的理解。从根本上说,C#创建了private方法来包装匿名方法。同时这些方法的签名与它们被分配到的委托相匹配。现在,让我们看看下面的代码:

public class Program
{
 public delegate void MyDelegate();
 public static void Main(string[] args)
 {
  int iTemp = 100;
  MyDelegate dlg = delegate
  {
   Console.WriteLine(iTemp);
  };
  dlg();
 }
}
  对于我们到现在为止对匿名方法已了解的内容来说,这段代码不应该编译。因为我们没有使用如何实例数据成员,C#编译器应该在''Program''类中创建一个private静态方法来包装这个匿名方法。但是新的方法如何访问局部变量呢?这让我们相信该代码将不能被编译。但是令人惊讶的是,C#编译器成功编译了这个代码而没有任何错误或报警。而且,当你执行这个示例时,在控制台屏幕上输出打印出iTemp变量的正确的值。现在让我们进入匿名方法的高级话题。一个匿名方法有封装在其方法体中使用了的环境变量的值的能力。这个封装应用于匿名方法被定义的方法中的所有局部变量。当C#编译器在一个匿名方法的方法体中识别出用到一个局部变量,它就会做如下事情:

  1. 创建一个新的private类作为匿名方法被定义的类的一个内部类。

  2. 在新类(译注:即内部类)中创建一个公共数据成员,使用与用在匿名方法体中的局部变量相同的类型和名称。

  3. 在包装匿名方法的新类中创建一个public实例方法。

  4. 用新类中的声明替代局部变量的声明。创建该新类的一个实例代替局部变量的声明。

  5. 用新类实例的数据成员替代在匿名方法体内部和外部使用的局部变量。

  6. 用在新类中定义的实例方法的地址取代匿名方法的定义。

  因此在编译时,上面的代码将被C#编译器翻译为如下代码:

public class Program
{
 private class InnerClass
 {
  private void InstanceMethod()
  {
   Console.WriteLine(iTemp);
  }
  public int iTemp;
 }
 public delegate void MyDelegate();
 public static void Main(string[] args)
 {
  InnerClass localObject = new InnerClass();
  localObject.iTemp = 100;
  MyDelegate dlg = new MyDelegate(localObject.InstanceMethod);
  dlg();
 }
}
  正如上面的伪代码所示,C#编译器为''Program''类生成了一个private内部类。在匿名方法中使用的局部变量作为新的已创建的内部类的一个实例数据成员而捕获。并且匿名方法本身被包装在内部类的实例方法中。最后,该实例方法在Main方法中作为一个委托处理器而使用。这样,当委托被调用时,对于在被封装入匿名方法中的局部变量将会有一个正确的值。下面图中选定的部分显示了由C#编译器默默添加到''Program'' 类的新的private内部类。


  被用在匿名方法中的局部变量有着超出用到它们的外部常规方法的生命周期。这个技术,在其它语言中,就是大家都知道的closures。除去匿名方法提供的简单语法,closures是匿名方法提供给开发者的一个功能强大的技术。该技术允许委托处理器代码(匿名方法)访问在常规方法内部被定义的局部变量。这就允许out-of-band数据,除了委托参数之外还有数据将被传递到委托,以供在其方法执行时使用。没有这个技术,每个委托和其相应的处理器方法就不得不声明表示局部上下文数据的参数,随着时间的过去这(译注:指不断声明表示局部上下文数据的参数)将变得难于管理。 匿名方法的作用域和局部变量用法

  我们讨论了在方法的主作用域(the main scope)中的匿名方法的实现。当一个匿名方法在一个嵌套作用域中被定义时,并且匿名方法中用到独立作用域级的局部变量,C#为每个作用域创建一个private内部类。比如,假设scope 1有局部变量iTemp,而scope 2,是scope 1的嵌套作用域,有一个局部变量jTemp。让在使用来自scope 1 和 scope 2局部变量iTemp 和 jTemp的 scope 2中,我们定义一个匿名方法。下面的代码显示了上面描述的示例:

public class Program
{
 public delegate void MyDelegate();
 public static void Main(string[] args)
 {
  MyDelegate dlg = null;
  int iTemp = 100;
  if (iTemp > 50)
  {
   int jTemp = 200;
   dlg = delegate
   {
    Console.WriteLine("iTemp: {0}, jTemp: {1}",iTemp,jTemp);
   };
  }
  dlg();
 }
}
  当上面的代码被编译时,C#编译器在''Program''类中创建两个内部类。一个内部类包装局部变量iTemp作为一个public数据成员。第二个内部类包装在嵌套作用域中的局部变量,jTemp,作为一个public数据成员,同时在相同的嵌套作用域中包装匿名方法作为public实例方法。C#编译器为上面的代码生成下面的伪代码:

public class Program
{
 //包装来自外部作用域的局部变量''iTemp''的类
 private class InnerClassScope1
 {
  public int iTemp;
 }
 //包装来自内部作用域和匿名方法的局部变量的类
 private class InnerClassScope2
 {
  public void InstanceMethod()
  {
   Console.WriteLine("iTemp: {0}, jTemp: {1}", localObjectScope1.iTemp, jTemp);
  }
  public InnerClassScope1 localObjectScope1;
  public int jTemp;
 }
 public delegate void MyDelegate();
 public static void Main(string[] args)
 {
  MyDelegate dlg = null;
  InnerClassScope1 localObject1 = new InnerClassScope1();
  localObject1.iTemp = 100;
  if (localObject1.iTemp > 50)
  {
   InnerClassScope2 localObject2 = new InnerClassScope2();
   localObject2.localObjectScope1 = localObject1;
   localObject2.jTemp = 200;
   dlg = new MyDelegate(localObject2.InstanceMethod);
  }
  dlg();
 }
}
  正如上面的代码所示,包装匿名方法的内部类将拥有所有代表外部作用域局部变量的对象,这些变量被用在匿名方法中,像public数据成员。下图显示了C#默默创建的内部类的ILDASM视图:

  在循环控制结构内使用匿名方法的局部变量的用法

  当处理循环控制结构时将局部变量封装入类的数据成员有着有趣但危险的一面,让我们看看下面代码:

public class Program
{
 public delegate void MyDelegate();
 public static void Main(string[] args)
 {
  MyDelegate d = null;
  for (int i = 1; i <= 5; i++)
  {
   MyDelegate tempD = delegate
   {
    Console.WriteLine(i);
   };
   d += tempD;
  }
  d();
 }
}
  上面的代码运行时将会有什么输出呢?我们的意图是捕获在我们的匿名方法中的循环计数变量''i''并显示之。我们预期的输出应该如下所示:

  1
  2
  3
  4
  5

  但是如果你运行上面的代码,输出将是如下所示:
 
  6
  6
  6
  6
  6

  如果我们仔细回忆我们关于匿名方法的内部工作机制的知识,我提到:在匿名方法中被捕获的任何局部变量将会被该作用域的一个新的已创建内部类的实例数据成员替代。对于循环控制变量,作用域是包含了for循环的作用域,这就是上面的简单代码所示的main方法体。因此当该代码编译时,C#编译器生成创建了内部类的实例的代码,包装了匿名方法和循环计数变量,在for循环的外部。并且该内部类的实例的数据成员,代表了循环计数变量,将被用来替代用于for循环而且也在匿名方法中使用的原始循环计数变量。因此来自内部类的相同实例的数据成员被用于for循环并且也用在包装匿名方法的实例方法中。作为循环完成时的结果,实例数据成员会增加六次。这里有一个需要注意的重要地方:尽管这个循环在五次迭代后结束,在它跳出循环控制结构时循环计数变量被增加了六次。既然该循环控制变量是一个实例数据成员,第六次增加触发了已由循环计数变量提供的循环结束条件。既然相同实例的一个方法被用做匿名方法的委托处理器,在委托结束时被调用,所有委托的实例将被指向相同实例,同时将为数据成员显示相同值,就是6。这就是我在本节开始已提到过的有危险影响的一面。

  为了克服这个问题并获得预期的结果,匿名方法应该在for循环的作用域中捕获一个局部变量,它将有与循环计数变量的相同的值。这可以通过如下修改示例代码获得:

public class Program
{
 public delegate void MyDelegate();
 public static void Main(string[] args)
 {
  MyDelegate d = null;
  for (int i = 1; i <= 5; i++)
  {
   int k = i;
   MyDelegate tempD = delegate
   {
    Console.WriteLine(k);
   };
   d += tempD;
  }
  d();
 }
}
  在你运行上面的代码示例时,将会获得预期的输出,也就是:

  1
  2
  3
  4
  5

  原因就是,C#编译器将为for循环的每次迭代而包装局部变量''k''的内部类创建 实例。同时包装了每个循环迭代的实例上的匿名方法的这个方法被用做一个委托处理器。

  总结

  匿名方法是C#2.0语言增加的一个非常有用和强大的功能。除了介绍的一些对委托声明和用法上的语法改进,Microsoft已在使匿名方法代码自然融入所包含的方法体方面获得很大进展,包括访问在包含(匿名方法)的方法定义的作用域中的局部变量。最后,我希望本文提供给C#开发人员正确而聪明地利用匿名方法的必备知识。

分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: