到目前为止,我所介绍的都是 Microsoft C# 团队所期望的函数式编程。您会在 Microsoft 网站上找到这些功能以及示例。但是在本章中,我想开始对 C# 进行一些更具创造性的尝试。
我不知道您怎么想,但我喜欢偷懒,或者至少我不喜欢在乏味的样板代码上浪费时间。与命令式代码相比,函数式编程的众多优点之一是它非常简洁。
在本章中,我将向您展示如何将函数式编程的范围推得比开箱即用的 C# 允许的范围更远,以及如何在旧版本中实现一些较新版本的 C#,并希望让您更快地完成日常工作。
本章将探讨三个类别:
- 枚举中的函数
Func
委托似乎并没有得到太多使用,但它们是 C# 中非常强大的功能。我将展示几种使用它们的方法,这些方法有助于扩展 C# 的功能。在本例中,通过将它们添加到 Enumerables 并使用 Linq 表达式对它们进行操作。
- 函数作为过滤器
您还可以将 Funcs 用作过滤器 - 它位于您和您尝试达到的实际值之间。您可以使用这些原则编写一些简洁的代码。
- 自定义 Enumerables
我之前讨论过 IEnumerables 以及它们有多酷,但您知道您可以分解它们并实现自己的自定义行为吗?我将向您展示如何操作。
让我们开始 Func-y
我已经在第十章中讨论过 Func
委托类型,但只是回顾一下 - 它们是作为变量存储的函数。您可以定义它们采用的参数、它们返回的内容,并像任何其他函数一样调用它们。这是一个简单的例子:
private readonly Func<string, string, string> SayHello =
(string firstName, string lastName) => $"hello {
firstName} {
lastName}";
public string SayHelloSimon() =>
SayHello("Simon", "Painter");
两个 V 形之间的列表中的最后一个泛型类型是返回值,所有先前的类型都是参数。我上面的示例采用 2 个字符串参数并返回一个字符串。
从现在开始,我们将看到大量的 Func 委托,因此在继续阅读之前,请确保您熟悉它们。
可枚举中的函数
我已经看到很多将 Func 作为函数参数的示例,但我不确定是否有人意识到您可以将它们放在可枚举中,并创建一些有趣的行为。
首先,很明显 - 将它们放在数组中以多次对同一数据进行操作:
private IEnumerable<Func<Employee, string>> descriptors = new []
{
x => $"First Name = {
x.firstName}",
x => $"Last Name = {
x.lastName}",
x => $"Sex = {
(x.Sex == Sex.Male ? "Male" : "Female")}"
}
public string DescribeEmployee(Employee emp) =>
string.Join(Environment.NewLine, descriptors.Select(x => x(emp)));
使用这种方法,我们可以拥有一个原始数据源(这里是一个 Employee 对象),并从中生成多个相同类型的记录,在我的例子中,我使用内置的 .NET 方法 string.Join
进行聚合,以向最终用户呈现一个统一的字符串。
与简单的 StringBuilder 相比,此方法有几个优点。
首先,可以动态组装数组。每个属性及其呈现方式可能有多个规则,可以根据某些自定义逻辑从一组局部变量中选择这些规则。
其次,这是一个 Enumerable,因此通过以这种方式定义它,我们利用了 Enumerable 的一项称为 延迟加载 的功能。Enumerable 不是数组,甚至不是数据。它们只是指向某个会告诉您如何提取数据的指针。Enumerable 背后的源可能是一个简单的数组,事实上通常也是如此,但不一定如此。每次通过 ForEach
循环访问下一个项时,Enumerable 都需要执行一个函数。Enumerable 的开发方式是,它只在最后一刻转换为实际数据 - 通常是在开始 ForEach
循环迭代时。大多数情况下,如果内存中有一个数组为 Enumerable 提供数据,这并不重要,但如果有一个昂贵的函数或查找外部系统为其提供支持,那么延迟加载对于避免不必要的工作非常有用。
最后,从维护的角度来看,这更容易接受。在最终结果中添加新行就像在数组中添加新元素一样简单。它还可以作为对未来程序员的约束,使他们更难尝试将过多的复杂逻辑放在不属于它的地方。
超级简单的验证器
让我们想象一个快速验证函数。它们通常看起来像这样:
public bool IsPasswordValid(string password)
{
if(password.Length <= 6)
{
return false;
}
if(password.Length > 20)
{
return false;
}
if(!password.Any(x => Char.IsLower(x)))
{
return false;
}
if(!password.Any(x => Char.IsUpper(x)))
{
return false;
}
if(!password.Any(x => Char.IsSymbol(x)))
{
return false;
}
if(password.Contains("Justin", StringComparison.OrdinalIgnoreCase)
&& password.Contains("Bieber", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
首先,对于实际上相当简单的事情来说,这可是很多代码。命令式代码迫使我们在这里编写一大堆重复的样板代码。除此之外,如果我们想添加另一条规则,那么可能需要添加大约 4 行新代码,而实际上只有 1 行对我们来说特别有趣。
如果有办法将其压缩成几行简单的代码就好了……
好吧……既然你这么好心地问了,那就给你:
public bool IsPasswordValid(string password) =>
new Func<string, bool>[]
{
x => x.Length > 6,
x => x.Length <= 20,
x => x.Any(y => Char.IsLower(y)),
x => x.Any(y => Char.IsUpper(y)),
x => x.Any(y => Char.IsSymbol(y)),
x => !x.Contains("Justin", StringComparison.OrdinalIgnoreCase)
&& !x.Contains("Bieber", StringComparison.OrdinalIgnoreCase)
}.All(f => f(password));
现在不是那么久了,是吗?我在这里做了什么?我把所有的规则都放到了一个 Funcs 数组中,这些 Funcs 将字符串转换为布尔值 - 即检查单个验证规则。我使用了 Linq 语句 - .All()
。此函数的目的是根据它所附加到的数组的所有元素评估我给它的任何 lambda 表达式。如果其中一个返回 false,则该过程将提前终止,并且 All
将返回 false。如果每个项目都返回 true,则 All 也会返回 true。
我们有效地重新创建了第一个代码示例,但我们被迫编写的样板代码 - If 语句和早期返回 - 现在已隐含在结构中。
这也具有再次非常易于维护的优势,作为代码结构。如果您愿意,您甚至可以将其概括为扩展方法。我经常这样做。像这样:
public static bool IsValid<T>(this T @this, params Func<T,bool>[] rules) =>
rules.All(x => x(@this));
这进一步减少了密码验证器的大小,并为您提供了一个可在其他地方使用的方便的通用结构:
public bool IsPasswordValid(string password) =>
password.IsValid(
x => x.Length > 6,
x => x.Length <= 20,
x => x.Any(y => Char.IsLower(y)),
x => x.Any(y => Char.IsUpper(y)),
x => x.Any(y => Char.IsSymbol(y