摘要:作为一名 C# 开发人员,您可能已经在编写一些函数式代码而没有意识到这一点。本文将介绍一些您已经在C#中使用的函数方法,以及 C# 7 中对函数式编程的一些改进。 尽管 .NET 框架的函数式编程语言是F#,同时,C# 是一个面向对象的语言,但它也有很多可以用于函数式编程技术的特性。你可能已经写了一些功能的代码而没有意识到它!

函数式编程范例

函数式编程是相对于目前比较流行和通用的面向对象编程的另一种编程模式。 有几个与其他编程范例不同的关键概念。我们首先为最常见的定义提供阐述,以便我们在整个文章中看清这些定义。 函数式编程的基本组成是纯函数。它们由以下两个属性定义:

  • 他们的结果完全取决于传递给它的参数。没有内部或外部的状态影响它。
  • 他们不会造成任何副作用。被调用的次数不会改变程序行为。

由于这些属性,函数调用可以被安全地替换其结果,例如函数每次执行的结果都缓存到一个键值对(被称为memoization的技术)。 纯函数很适合形成 组合函数,将两个或多个函数组合成一个新函数的过程,该函数返回相同的结果,就好像其所有的构成函数都按顺序调用一样。如果ComposedFn是Fn1和Fn2的函数组合,那么下面的断言将永远正确:

Assert.That(ComposedFn(x), Is.EqualTo(Fn2(Fn1(x))));

作为其他函数的参数可以进一步提高其可重用性。这样的高阶函数可以作为通用的 辅助者 (helper) ,它应用多次作为参数传递的另一个函数,例如一个数组的所有项目:

Array.Exists(persons, IsMinor);

在上面的代码中,IsMinor 是一个在别处定义的函数。使之有效,语言必须支持其为第一类对象,即允许函数像类型一样用作参数的语言结构。

数据总是用不可变的对象来表示的,也就是在初始创建后不能改变状态的对象。每当一个值发生变化,就必须创建一个新的对象,而不是修改现有的对象。因为所有对象都保证不会改变,所以它们本质上是线程安全的,也就是说,它们可以安全地用于多线程程序中,而不会受到竞争条件的威胁。 由于函数是纯粹的,对象是不可变的直接结果,在函数编程中没有共享状态。 函数只能根据参数进行操作,而参数不能改变,从而影响其他接收相同参数的函数。他们可以影响程序的其余部分的唯一方法是将返回的结果作为参数传递给其他函数。 这样可以防止函数之间的任何隐藏的交叉交互,使得它们可以安全地以任何顺序甚至并行运行,除非一个函数直接依赖于另一个函数的结果。 有了这些基本的模块,函数式编程最终会被比命令式更具声明,即用 描述 代替 如何计算 。 以下两个将字符串数组转换为小写的函数清楚地表明了两种方法之间的区别:

string[] Imperative(string[] words)
{
var lowerCaseWords = new string[words.Length];
for (int i = 0; i < words.Length; i++)
{
lowerCaseWords[i] = words[i].ToLower();
}
return lowerCaseWords;
} string[] Declarative(string[] words)
{
return words.Select(word => word.ToLower()).ToArray();
}

虽然你会听到很多其他的函数式编程概念,比如 monads, functors, currying, referential transparency等,但是这些模块应该足以让你了解什么是函数式编程,以及它与面向对象编程有什么不同。

在 C# 中编写函数式代码

由于语言主要是面向对象的,所以默认并不总是引导你使用这样的代码,但是有了意图和足够的自律,你的代码可以变得更加实用。

不可变类型

你很可能习惯于在C#中编写可变类型,但只需很少的改变,就可以使它们不可变:

public class Person
{
public string FirstName { get; private set; }
public string LastName { get; private set; } public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}

私有属性构造器使对象初始创建后不可能为它们分配不同的值。为了使对象真正不可变,所有的属性也必须是不可变的类型。否则,它们的值将通过改变属性来改变,而不是为它们分配一个新的值。 上面的 Person 类型是不可变的,因为 string 也是一个不可变的类型,也就是说它的值不能像其所有的实例方法一样被改变,所以返回一个新的字符串实例。但是这是规则的一个例外,大多数 .NET 框架中类型都是可变的。 如果你希望你的类型是不可变的,你不应该使用除了原始类型以外的其他内建类型,而应该使用字符串作为公共属性。 要更改对象的属性,例如更改人物的名字,需要创建一个新的对象:

public static Person Rename(Person person, string firstName)
{
return new Person(firstName, person.LastName);
}

当一个类型有很多属性时,编写这样的函数可能会变得非常繁琐。因此,对于不可变类型来说,为这样的场景实现 With helper 函数是一个好习惯:

public Person With(string firstName = null, string lastName = null)
{
return new Person(firstName ?? this.FirstName, lastName ?? this.LastName);
}

这个函数创建了修改了任意数量属性的对象的副本。我们的 Rename 函数现在可以简单地调用这个帮助器来创建修改后的 Person :

public static Person Rename(Person person, string firstName)
{
return person.With(firstName: firstName);
}

只有两个属性的好处可能不是很明显,但不管这个类型有多少个属性,这个语法允许我们只列出我们想要修改的属性作为命名参数。

纯函数

使函数变 "纯" 需要更多的训练,而不是使对象不可变。 没有语言功能可以帮助程序员确保一个特定的功能是纯粹的。不要使用任何内部或外部的状态,不要引起副作用,不要调用任何不纯的函数。 当然,也没有什么能阻止你使用函数参数和调用其他纯函数,从而使函数变得纯粹。上面的 Rename 函数是一个纯函数的例子:它不调用任何非纯函数,也不使用传递给它的参数以外的任何其他数据。

组合函数

通过定义一个新的函数,可以将多个函数合并成一个函数,该函数调用其所有组合函数(让我们忽略不需要连续多次调用Rename的事实):

public static Person MultiRename(Person person)
{
return Rename(Rename(person, "Jane"), "Jack");
}

重命名方法的签名迫使我们嵌套调用,随着函数调用次数的增加,这些调用会变得难以理解和理解。如果我们使用With方法,我们的意图变得更清晰:

public static Person MultiRename(Person person)
{
return person.With(firstName: "Jane").With(firstName: "Jack");
}

为了使代码更具可读性,我们可以将调用链分成多行,保持可管理性,无论我们将多少个函数组合成一个:

public static Person MultiRename(Person person)
{
return person
.With(firstName: "Jane")
.With(firstName: "Jack");
}

没有好的方法来分割与重命名类似的嵌套调用函数。当然,With 方法允许链接语法,因为它是一个实例方法。但是,在函数式编程规范中,函数应该和它们所作用的数据分开声明,比如 Rename 函数。 虽然在 函数式语言 F# 中有一个流水线操作符(|>)来允许组合这些函数,但我们可以利用 C# 中的扩展方法:

public static class PersonExtensions
{
public static Person Rename(this Person person, string firstName)
{
return person.With(firstName: firstName);
}
}

这允许我们组合非实例方法调用,就像实例方法调用一样:

public static Person MultiRename(Person person)
{
return person.Rename("Jane").Rename("Jack");
}

.NET Framework中的函数式 API 示例

为了体验C#中的函数式编程,你不需要自己编写所有的对象和函数。 在 .NET 框架中有一些可用的函数式 API 供您使用。

不变集合

我们已经提到,在.NET框架中,字符串和原始类型是不可变的类型。 但是,也有一些可选的 不可变集合类型 。从技术上讲,它们并不是.NET框架的一部分,因为它们是作为独立的 NuGet 包 System.Collections.Immutable 分发。 另一方面,它们是新的开源跨平台 .NET 运行时 .NET Core 的一个组成部分。 命名空间包括所有常用的集合类型:数组,列表,集合,字典,队列和堆栈。 顾名思义,它们都是不可改变的,即它们在创建之后不能被改变。相反,每个更改都会创建一个新实例。这使得不可变集合以与.NET框架基类库中包含的并发集合不同的方式完全线程安全。 使用并发集合,多个线程不能同时修改数据,但仍可以访问修改。对于不可变的集合,任何更改只对创建它们的线程可见,因为原始集合保持不变。 尽管为每个可变操作创建了一个新的实例,为了保持集合的高性能,它们的实现利用了结构共享。 这意味着在集合的新修改实例中,来自先前实例的未修改的部分尽可能被重用,因此需要较少的内存分配并且导致垃圾收集器的工作较少。 在函数式编程中这种常见的技术是可以实现的,即对象不能改变,因此可以安全地重用。

使用不可变集合和常规集合最大的区别在于它们的创建。

由于每次更改都创建一个新实例,因此您希望创建集合中已包含所有初始项目的集合。因此,不可变集合不具有公共构造函数,但提供了三种创建方法:

  • 工厂方法创建接受 0个 或 更多的项目来初始化集合:var list = ImmutableList.Create(1, 2, 3, 4);
  • Builder 是一个高效的可变集合,可以很容易地转换为不可变的集合:var builder = ImmutableList.CreateBuilder<int>(); builder.Add(1); builder.AddRange(new[] { 2, 3, 4 }); var list = builder.ToImmutable();</int>
  • 可以使用扩展方法从IEnumerable创建不可变集合:var list = new[] { 1, 2, 3, 4 }.ToImmutableList();

不可变集合的可变操作与常规集合中的可变操作类似,但它们都返回集合的新实例,表示将操作应用于原始实例的结果。 如果您不想丢失更改,则必须在此之后使用此新实例:

var modifiedList = list.Add(5);

执行上述语句后,列表的值仍然是 {1,2,3,4} 。得到的 modifiedList 将具有 {1,2,3,4,5} 的值。 无论对于一个非功能性程序员来说,不可变的集合看起来是多么的不寻常,它们是编写.NET框架功能代码的一个非常重要的基石。创建你自己的不可变集合类型将是一个重大的努力。

LINQ - 语言集成查询

.NET框架中一个更好的函数式的API是LINQ。 虽然它从来没有被宣传为函数式,但它体现了许多以前引入的函数式性质。 如果我们在 LINQ 扩展方法仔细一看,很明显几乎所有的都表明其函数式:他们允许我们声明我们想要获得什么,而不是如何做。

var result = persons
.Where(p => p.FirstName == "John")
.Select(p => p.LastName)
.OrderBy(s => s.ToLower())
.ToList();

以上查询返回名为 John 的姓氏的有序列表。我们只提供了预期的结果,而不是提供详细的操作顺序。可用的扩展方法也很容易使用链式语法进行组合。 尽管LINQ函数并不是作用于不可变的类型,但它们仍然是纯函数,除非通过传递变异函数作为参数来滥用。 它们被实现为对只读接口 IEnumerable 集合进行操作,而不修改集合中的项目。 他们的结果只取决于输入参数,只要作为参数传递的函数也是纯的,它们不会产生任何全局副作用。在我们刚刚看到的例子中,人员集合以及其中的任何项目都不会被修改。 许多 LINQ 函数是 高阶函数:它们接受其他函数作为参数。在上面的示例代码中,lambda表达式作为函数参数传入,但是它们可以很容易地在其他地方定义并传入,而不是以内联的方式创建:

public bool FirstNameIsJohn(Person p)
{
return p.FirstName == "John";
} public string PersonLastName(Person p)
{
return p.LastName;
} public string StringToLower(string s)
{
return s.ToLower();
} var result = persons
.Where(FirstNameIsJohn)
.Select(PersonLastName)
.OrderBy(StringToLower)
.ToList();

当函数参数和我们的情况一样简单时,代码通常会更容易理解内联 lambda 表达式而不是单独的函数。然而,随着实现的逻辑变得更加复杂和可重用,把它们定义为独立的函数,开始变得更有意义。

结论:

函数式编程范式当然有一些优点,这也促成了它近来日益普及。 在没有共享状态的情况下,并行和多线程变得更容易,因为我们不必处理同步问题和竞争条件。纯函数和不变性可以使代码更容易理解。 由于函数只依赖于它们明确列出的参数,因此我们可以更容易地识别一个函数是否需要另一个函数的结果,以及何时这两个函数是独立的,因此可以并行运行。单个纯函数也更容易进行单元测试,因为所有的测试用例都可以通过传递不同的输入参数和验证返回值来覆盖。没有其他的外部依赖模拟和检查。

如果所有这些都让你想为自己尝试函数式编程,那么首先在 C# 中执行它可能比在同一时间学习一种新语言更容易。您可以通过更多地利用现有的函数式 API 来缓慢起步,并以更具说明性的方式继续编写代码。 如果你看到了足够的好处,那么你可以学习 F#,稍后再熟悉这些概念。

C# 开发人员的函数式编程的更多相关文章

  1. [搬运] 写给 C# 开发人员的函数式编程

    原文地址:http://www.dotnetcurry.com/csharp/1384/functional-programming-fsharp-for-csharp-developers 摘要:作 ...

  2. 李洪强iOS开发之函数式 编程初窥

    函数式 编程初窥   最近在学习Erlang和Python.Erlang是完全的函数式编程语言,Python语言是面向对象的语言,但是它的语法引入了大量的函数式编程思想.越研究越觉得函数式的编程思路可 ...

  3. 9、scala函数式编程-集合操作

    一.集合操作1 1.Scala的集合体系结构 // Scala中的集合体系主要包括:Iterable.Seq.Set.Map.其中Iterable是所有集合trait的根trai.这个结构与Java的 ...

  4. scala 函数式编程之集合操作

    Scala的集合体系结构 // Scala中的集合体系主要包括:Iterable.Seq.Set.Map.其中Iterable是所有集合trait的根trai.这个结构与Java的集合体系非常相似. ...

  5. 【JS】394- 简明 JavaScript 函数式编程-入门篇

    转载自公众号"程序员成长指北" 写在开头 本文较长,总共分为三大部分:(对于函数式编程以及其优点有一定理解的童鞋,可以直接从 第二部分 开始阅读) 第一部分:首先会通过实际代码介绍 ...

  6. 用函数式编程,从0开发3D引擎和编辑器(一)

    介绍 大家好,欢迎你踏上3D编程之旅- 本系列的素材来自我们的产品:Wonder-WebGL 3D引擎和编辑器 的整个开发过程,探讨了在从0开始构建3D引擎和编辑器的过程中,每一个重要的功能点.设计方 ...

  7. 用函数式编程,从0开发3D引擎和编辑器(三):初步需求分析

    大家好,本文介绍了Wonder的高层需求和本系列对应的具体功能点. 确定Wonder高层需求 业务目标 Wonder是web端3D开发的解决方案,包括引擎.编辑器,致力于打造开放.分享.互助的生态. ...

  8. 用函数式编程,从0开发3D引擎和编辑器(二):函数式编程准备

    大家好,本文介绍了本系列涉及到的函数式编程的主要知识点,为正式开发做好了准备. 函数式编程的优点 1.粒度小 相比面向对象编程以类为单位,函数式编程以函数为单位,粒度更小. 正所谓: 我只想要一个香蕉 ...

  9. 从0开发3D引擎(五):函数式编程及其在引擎中的应用

    目录 上一篇博文 函数式编程的优点与缺点 优点 缺点 为什么使用Reason语言 函数式编程学习资料 引擎中相关的函数式编程知识点 数据 不可变数据 可变数据 函数 纯函数 高阶函数 柯西化 参考资料 ...

随机推荐

  1. Oracle入门第五天(上)——数据库对象之视图

    一.概述 1.什么是视图(VIEW) 视图 从表中抽出的逻辑上相关的数据集合(是一张虚表). 2.为什么使用视图 1.控制访问 2.简化查询 3.视图的分类 二.视图管理 1.创建视图 CREATE ...

  2. 20155206 2016-2017-2 《JAVA程序设计》 第二周学习总结

    20155206 2016-2017-2<JAVA程序设计>第二周学习总结 教材学习内容总结 类型 整数 字节 浮点数 字符 布尔 变量 变量在命名时,不可以使用数字或一些特殊字符:*.& ...

  3. 20155207王雪纯 2006-2007-2 《Java程序设计》第1 周学习总结

    20155207王雪纯 2006-2007-2 <Java程序设计>第1 周学习总结 教材学习内容总结 第一周重点学习第一章和第二章. 第一章总的来讲,我认为可以称之为本书的绪论,介绍了包 ...

  4. 20155227 2016-2017-2 《Java程序设计》实验一 Java开发环境的熟悉(Windws + IDEA)实验报告

    20155227 2016-2017-2 <Java程序设计>实验一 Java开发环境的熟悉(Windws + IDEA)实验报告 实验内容 1.使用JDK编译.运行简单的Java程序: ...

  5. 20155302 实验三 敏捷开发与XP实践

    20155302 实验三 敏捷开发与XP实践 实验内容 XP基础 XP核心实践 相关工具 实验内容及步骤 (一)编码标准 在IDEA中使用工具(Code->Reformate Code)把代码重 ...

  6. [arc063F]Snuke's Coloring 2-[线段树+观察]

    Description 传送门 Solution 我们先不考虑周长,只考虑长和宽. 依题意得答案下限为max(w+1,h+1),并且最后所得一定是个矩形(矩形内部无点). 好的,所以!!!答案一定会经 ...

  7. spark on yarn 资源调度(cdh为例)

    一.CPU配置: ApplicationMaster 虚拟 CPU内核 yarn.app.mapreduce.am.resource.cpu-vcores ApplicationMaster占用的cp ...

  8. Jmeter性能测试使用记录

    使用背景 由于最近公司要求对一批接口做性能测试,所以重拾了一些对于Jmeter的使用,现将部分过程做记录,以便以后回溯. 接口参数化 数据参数文件使用了excel保存出的csv文件,dat格式的文件也 ...

  9. Django——多网页网站及网页互联

    在helloapp文件夹下添加名为templates的文件夹(此文件夹名称是固定的),并在其下添加html文件,文件内容根据自己网页想呈现的内容而定 在views文件内添加新的函数 在urls文件内添 ...

  10. unittest,requests——接口测试脚本及报告

    用unittest管理两个利用requests模块,做百度搜索的简单接口测试用例,之后自动输出报告 # encoding=utf-8import requests,unittest,HTMLTestR ...