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






4.26/5 (8投票s)
2004年7月23日
6分钟阅读

71995

2
介绍如何使用 C# 2.0 实现 Perl 的 map 和 grep 操作。
引言
许多人讨厌 Perl,因为它可怕的语法。虽然许多 Perl 脚本确实难以阅读,但 Perl 的一些操作却非常方便。其中两个是 grep
和 map
列表操作符。我将首先简要介绍它们,以便不熟悉 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
方法需要更多的代码,因为您必须处理数组本质上不是灵活大小的事实;您需要将结果推送到 ArrayList
或 List<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 中使用类似 map
和 grep
的操作几乎是微不足道的。让我们更仔细地看看。
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
方法中都有了许多两个类型参数——我也为了清晰起见将它们命名为 SrcType
和 DstType
——这允许我们创建从一种类型到另一种类型的映射,并具有任意复杂的运算:
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]);
}
是的,这个示例将文件名数组(string
s)映射到文件大小数组(long
s)。请注意,此示例的复杂性超出了 C# 推断泛型类型的能力;您必须在 MapArray
调用中手动指定 SrcType
和 DstType
。不过,这并非完全是坏事——它确实为代码增加了一些清晰度。
grep 的等价物:FindAll
在经历了实现 map 操作的所有麻烦之后,实现 grep
的等价物就变得微不足道了。Array
和 List<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 也就到此为止了!
结束语
FindAll
和 ForEach
是 Framework 提供的列表/数组处理机制的显著改进。结合上面发布的 MapArray
方法之类的逻辑,您可以使复杂的数组操作相对简单。虽然 Perl 代码的简洁性(或丑陋性或优雅性——随您喜欢)无法通过编写新方法来简单地实现,但同样强大的功能大部分都可以实现。
上面描述的 map 操作远非完美。它们在输入和输出上都仅限于数组。可以相对容易地使它们能够处理 IEnumerable<T>
,这样您就可以将任何内容作为输入。如果您希望 map 操作产生一个列表,您可以编写一个单独的 MapToList
方法。但是,在大多数情况下,上面描述的相对简单的操作就足够了。此外,FindAll
存在于 List<T>
和 Array
上,因此您无需为此费力。
历史
2004-07-23:发布初始版本。