65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 2.0 实现 Perl 风格的列表操作

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.26/5 (8投票s)

2004年7月23日

6分钟阅读

viewsIcon

71995

downloadIcon

2

介绍如何使用 C# 2.0 实现 Perl 的 map 和 grep 操作。

引言

许多人讨厌 Perl,因为它可怕的语法。虽然许多 Perl 脚本确实难以阅读,但 Perl 的一些操作却非常方便。其中两个是 grepmap 列表操作符。我将首先简要介绍它们,以便不熟悉 Perl 的人能够理解,然后深入探讨一些关于如何在 C# 中轻松获得类似操作的想法。

这里的代码基于 .NET Framework/C# 2.0。新的框架版本(在本文撰写时为 beta 版)为类似功能提供了良好的支持。对于 .NET 的 1.x 系列,实现此类操作是可能的,但难以通用化;您将需要大量接口,并且很难与现有类型(如 int 等)集成。此外,由于缺乏匿名方法支持,代码将非常混乱。尽管下面发布的代码是使用 .NET Framework 2.0 Beta 1 编写的,但预计在最终发布时可以按原样工作。

背景

Perl 中的列表

首先,我们必须了解 Perl 中的列表。它大致相当于大多数其他语言中的一维数组。Perl 相对宽松的类型意味着列表元素可以是任何东西;整数、字符串、结构体的引用(“指针”)等等。在这些示例中,我们只看基础知识。

Perl 列表用变量名前的 @ 符号表示。局部变量使用关键字 my 声明,因此 my @list = (1, 2, 3, 4); 大致相当于 C# 语句 int[] list = {1, 2, 3, 4};。“大致”是因为 Perl 列表的大小是灵活的;您可以添加更多元素或删除旧元素,并且可以添加任何您想要的类型,即使它最初仅用 int 值初始化。然而,这些细节对于我们这里处理的内容并没有太大影响,所以我们暂且搁置。

map 操作符

Perl 的 map 操作符实际上是某种 foreach 语句的语法糖。它所做的是对列表中的每个元素求值一个特定的表达式,并将结果推送到新列表中。表达式可以是任何有效的 Perl 代码;正在迭代的数组元素位于一个名为 $_ 的特殊变量中。

例如,您可以这样做:

my @numbers = (1, 2, 3, 4);
my @doubles = map { $_*2 } @numbers;
print @doubles;     # prints 2468

在 Perl 中使用 foreach 的等效构造将是这样的:

my @numbers = (1, 2, 3, 4);
my @doubles;
foreach (@numbers) {
  push @doubles, $_*2;
}
print @doubles;

在 C# 中,foreach 方法需要更多的代码,因为您必须处理数组本质上不是灵活大小的事实;您需要将结果推送到 ArrayListList<T> 中。我们稍后会回到这一点。

grep 操作符

Perl 中的 grep 操作符与其在 Unix 命令行上的对应项几乎相同:它会选择符合特定标准的列表元素。语法与 map 非常相似;指定了一个表达式。区别在于:如果表达式的结果为真(“非零”——在这种意义上,Perl 将布尔值视为 C/C++),则该元素将被包含在结果数组中。

my @names = ("John", "Mike", "Jane", "Adam");
my @oNames = grep { substr($_, 0, 1) eq 'J' } @names;
print @oNames;

稍加思考,我认为您可以弄清楚上面的代码是做什么的。是的,它会打印出名字以 'J' 开头的名字(“长度为 1 的子字符串,从索引位置 0 开始”)——即 John 和 Jane。

C# 方法

我一直渴望这两种 Perl 操作已经有一段时间了。当我第一次阅读 C# 2.0 规范时,我意识到泛型和匿名方法在这方面都有潜力。然而,后来我注意到 Framework 2.0 已经以一些新方法的形式为这些操作提供了相当好的支持。虽然 Perl 的简洁语法会让人怀念,但在 C# 2.0 中使用类似 mapgrep 的操作几乎是微不足道的。让我们更仔细地看看。

map 的等价物:ForEach

类型化数组和新的 List<t> 类型容器在 2.0 版本中提供了一个新的静态方法:ForEach。它接受一个类型化数组(或列表;我将在下文中仅讨论数组,但大部分内容也适用于列表)和一个 Action 类型委托作为参数,然后为每个数组元素运行该委托。Action 类型委托接受列表元素类型的单个参数,并且不返回任何内容。因此,您可以使用 ForEach 如下:

  private static void PrintNumber(int num) {
    Console.WriteLine("The number is " + num);
  }

  public static void Main() {
    int[] list = {1, 2, 3, 4};
    Array.ForEach(list, new Action<int>(PrintNumber));
  }

C# 的一项重大新增强功能是定义匿名方法的能力。因此,我们也可以摆脱 PrintNumber 方法。下面的代码等同于上面的代码片段:

  public static void Main() {
    int[] list = {1, 2, 3, 4};
    Array.ForEach(
      list, 
      delegate(int num) { Console.WriteLine("The number is " + num); } 
    );
  }

这里缺失的部分是 Action 委托中缺少返回值;您不能轻易地使用它来构造新的列表或数组。所以,让我们构建一些我们自己的工具代码来帮助:

  public delegate T MapAction<T>(T item);

  public static T[] MapToArray<T>(T[] source, 
                                  MapAction<T> action) {

    T[] result = new T[source.Length];
    for (int i = 0; i < source.Length; ++i)
      result[i] = action(source[i]);
    return result;
  }

因此,我们现在有了一个 MapAction 委托,它非常类似于框架的 Action,但还返回一个类型为 T 的元素。然后我们有 MapToArray 方法,它总是返回一个与源参数类型和大小相同的数组——但包含的元素通过 MapAction 处理程序进行处理,按您希望的方式进行修改。结果,我们可以这样写:

  public static void Main() {
    int[] list = {1, 2, 3, 4};
    int[] doubled = MapToArray(list, 
                               delegate(int num) { return num*2; });
    
    foreach (int i in doubled) Console.WriteLine(i);
  }

……并让它打印出 2、4、6 和 8。就这样!只不过这不像 Perl 的 map 那样灵活;您仍然不能将元素映射到完全不同的类型。幸运的是,所需的更改非常简单。让我们重写 MapAction 委托和 MapToArray 方法以支持不同的类型:

  public delegate DstType MapAction<SrcType, DstType>(SrcType item);

  public static DstType[] MapToArray<SrcType, DstType>(
                            SrcType[] source, 
                            MapAction<SrcType, DstType> action) {

    DstType[] result = new DstType[source.Length];
    for (int i = 0; i < source.Length; ++i)
      result[i] = action(source[i]);
    return result;
  }

现在,我们在委托和 MapToArray 方法中都有了许多两个类型参数——我也为了清晰起见将它们命名为 SrcTypeDstType ——这允许我们创建从一种类型到另一种类型的映射,并具有任意复杂的运算:

  public static void Main() {
    string[] files = { "map.pl", "testi.cs" };
    long[] fileSizes = 
      MapToArray<string,long>(
        files, 
        delegate(string file) { return new FileInfo(file).Length; }
      );
    
    for (int i=0; i < files.Length; ++i) 
      Console.WriteLine(files[i] + ": " + fileSizes[i]);
  }

是的,这个示例将文件名数组(strings)映射到文件大小数组(longs)。请注意,此示例的复杂性超出了 C# 推断泛型类型的能力;您必须在 MapArray 调用中手动指定 SrcTypeDstType。不过,这并非完全是坏事——它确实为代码增加了一些清晰度。

grep 的等价物:FindAll

在经历了实现 map 操作的所有麻烦之后,实现 grep 的等价物就变得微不足道了。ArrayList<T> 都有一个 FindAll 方法,它非常类似于 ForEach,但接受一个 Predicate 类型委托,该委托本质上是一个接受项并返回 true(如果它符合标准)的方法。因此,要从 int 数组中筛选偶数,只需键入:

  int[] list = {1, 2, 3, 4, 5, 6};
  int[] even = Array.FindAll(list, 
                             delegate(int num) { return num%2 == 0; });
  foreach (int i in even) Console.WriteLine(i);

……是的,上面的代码打印 2、4 和 6。grep 也就到此为止了!

结束语

FindAllForEach 是 Framework 提供的列表/数组处理机制的显著改进。结合上面发布的 MapArray 方法之类的逻辑,您可以使复杂的数组操作相对简单。虽然 Perl 代码的简洁性(或丑陋性或优雅性——随您喜欢)无法通过编写新方法来简单地实现,但同样强大的功能大部分都可以实现。

上面描述的 map 操作远非完美。它们在输入和输出上都仅限于数组。可以相对容易地使它们能够处理 IEnumerable<T>,这样您就可以将任何内容作为输入。如果您希望 map 操作产生一个列表,您可以编写一个单独的 MapToList 方法。但是,在大多数情况下,上面描述的相对简单的操作就足够了。此外,FindAll 存在于 List<T>Array 上,因此您无需为此费力。

历史

2004-07-23:发布初始版本。

© . All rights reserved.