你的 C# 已经可以运行了,但前提是你得让它
几天前,我在推特上发布了一段 C# 代码片段,展示了使用C# 8.0 的一些新功能实现的FizzBuzz。正如孩子们所说,这条推文“火了”,一些人赞赏它的简洁和功能性,而另一些人则问我为什么不一开始就用 F# 来写呢?
四年来第一次编写 C# :P pic.twitter.com/m7kM9j9skN
— 伊加尔·塔巴赫尼克 ( @hmemcpy ) 2020 年 3 月 4 日
距离我上次编写 C# 代码已经过去四年多了,接触函数式编程显然影响了我今天编写代码的方式。我写的代码片段看起来非常简洁自然,然而,有些人担心它看起来不像C # 代码——“它看起来太函数式了” 。
你一直在用那个词...
“函数式”这个词对不同的人有不同的含义,这取决于你问的对象。但与其争论语义,我更想解释一下为什么这段小小的 FizzBuzz 代码片段感觉像函数式的。
但首先,让我们解开代码片段(对上面的图片进行一些调整):
public static void Main(string[] args)
{
string FizzBuzz(int x) => // Local function, defined as an expression-bodied method
(x % 3 == 0, x % 5 == 0) switch { // Tuple definition
(true, true) => "FizzBuzz", // Pattern-matching on the tuple values
(true, _) => "Fizz", // Discard (_) is used to omit
(_, true) => "Buzz", // the values we don't care about
_ => x.ToString()
};
Enumerable.Range(1, 100) // Make a range of numbers from 1 to 100
.Select(FizzBuzz).ToList() // Map each number to a corresponding FizzBuzz value
.ForEach(Console.WriteLine); // Print the result to the console
}
这里的创新之处在于使用元组(对)来捕获两个等式的结果(x % 3 == 0
和x % 5 == 0
)。这样就可以使用模式匹配来解构元组并同时检查两个值。如果所有情况都不匹配,则默认情况(_
)将始终匹配,并返回数字的字符串值。
然而,代码片段中使用的许多“函数式”特性(包括 LINQ 风格的foreach
循环)都不是使这种方法本身具有功能性的原因。它之所以具有功能性,是因为除了最后打印到控制台之外,该程序中使用的所有方法都是表达式。
面向表达式的编程
简而言之,表达式是一个总有答案的问题。用编程术语来说,表达式是常量、变量、运算符和函数的组合,由运行时求值以计算(“返回”)一个值。为了说明与语句的区别,让我们为 FizzBuzz 编写一个更常见的 C# 解决方案:
public static void Main(string[] args)
{
foreach (int x in Enumerable.Range(1, 100)) {
FizzBuzz(x);
}
}
public static void FizzBuzz(int x)
{
if (x % 3 == 0 && x % 5 == 0)
Console.WriteLine("FizzBuzz");
else if (x % 3 == 0)
Console.WriteLine("Fizz");
else if (x % 5 == 0)
Console.WriteLine("Buzz");
else
Console.WriteLine(x);
}
显然,这段代码可以修改一下,去除重复,但我怀疑有人会说这看起来不像 C#。然而,仔细检查一下这个FizzBuzz
方法,就会发现它存在一些设计问题(即使是像 FizzBuzz 这样简单的程序)。
首先,这个程序违反了单一职责原则。它把根据数字计算输出值的“业务逻辑”和将该值打印到控制台的行为混在一起。因此,它与控制台输出紧密耦合,违反了依赖倒置原则。最后,如果不引入一些间接调用,我们就无法单独重用和测试这个程序。对于像 FizzBuzz 这样简单的程序,我们永远不会这样做,但如果极端情况下,很容易最终变成企业版FizzBuzz。
上述所有问题都可以通过分离生成 FizzBuzz 值和将其打印到控制台的操作来解决。即使不使用花哨的语言特性,将值返回给调用者这个最简单的操作也能让我们摆脱使用该值执行某些操作的责任:
public static string FizzBuzz(int x)
{
if (x % 3 == 0 && x % 5 == 0)
return "FizzBuzz";
else if (x % 3 == 0)
return "Fizz";
else if (x % 5 == 0)
return "Buzz";
else
return x.ToString();
}
这看起来可能不像是一个巨大的变化(它可以以各种方式实现,这里使用最简单的方式进行说明),但这里发生了一些事情:
- 该
FizzBuzz
方法现在是一个表达式,给定一些数字输入产生一个字符串输出 - 它没有其他职责或副作用,因此它是一个纯函数
- 它可以单独测试和重复使用,无需任何额外的依赖或设置
- 此函数的调用者可以自由地对结果进行任何操作——这不是我们的责任
这就是函数式编程的精髓——所有函数式代码都由表达式组成,它们产生一些值并将其返回给调用者。这些表达式通常是独立的,完全由输入参数指定。在最顶层,也就是入口点(有时也称为“世界尽头”),这些值被收集起来并与外部世界进行交互。用面向对象的术语来说,这有时被称为“洋葱架构”(或“端口和适配器”)——一个由业务逻辑组成的纯粹核心,以及负责与外部世界交互的命令式外壳。
C#(以及 Java、Python 等)可以实现函数式
如今,几乎所有编程语言的核心都是在短小的篇幅中使用表达式而非语句。C# 随着时间的推移不断发展,引入了一些特性,使其更易于操作表达式:LINQ、表达式体方法、模式匹配等等。这些特性通常被称为“函数式”——因为它们——在 F# 等语言中,这些特性被广泛使用,允许数据从内向外流动。而其他函数式语言,例如 Haskell,几乎不可能使用除表达式之外的其他任何语言。
事实上,这种编程风格现在受到了 C# 团队的大力推崇。在最近于 NDC 伦敦举行的一次演讲中,Bill Wagner 敦促大家改变(命令式的)习惯,拥抱现代技术:
C#(以及其他命令式语言,例如 Java)可以函数式地使用,但这需要高度的勤奋。这些语言将函数式编程视为例外,而非常态。我强烈建议你探索其他默认采用面向表达式编程的语言,使其成为一等公民。
这最初发布在我的博客上。
文章来源:https://dev.to/hmemcpy/your-c-is-already-function-but-only-if-you-let-it-3afn