函数式编程是相对于目前比较流行和通用的面向对象编程的另一种编程模式。有几个与其他编程范例不同的关键概念。我们首先为最常见的定义提供阐述,以便我们在整个文章中看清这些定义。函数式编程的基本组成是纯函数。
它们由以下两个属性定义:
1.他们的结果完全取决于传递给它的参数。没有内部或外部的状态影响它。Assert.That(ComposedFn(x), Is.EqualTo(Fn2(Fn1(x))));作为其他函数的参数可以进一步提高其可重用性。这样的高阶函数可以作为通用的 辅助者 (helper) ,它应用多次作为参数传递的另一个函数,例如一个数组的所有项目:
Array.Exists(persons, IsMinor);在上面的代码中,IsMinor 是一个在别处定义的函数。使之有效,语言必须支持其为第一类对象,即允许函数像类型一样用作参数的语言结构。数据总是用不可变的对象来表示的,也就是在初始创建后不能改变状态的对象。每当一个值发生变化,就必须创建一个新的对象,而不是修改现有的对象。因为所有对象都保证不会改变,所以它们本质上是线程安全的,也就是说,它们可以安全地用于多线程程序中,而不会受到竞争条件的威胁。
/// 堆代码 www.duidaima.com 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等,但是这些模块应该足以让你了解什么是函数式编程,以及它与面向对象编程有什么不同。
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); }只有两个属性的好处可能不是很明显,但不管这个类型有多少个属性,这个语法允许我们只列出我们想要修改的属性作为命名参数。
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框架中,字符串和原始类型是不可变的类型。但是,也有一些可选的 不可变集合类型 。从技术上讲,它们并不是.NET框架的一部分,因为它们是作为独立的 NuGet 包 System.Collections.Immutable 分发。另一方面,它们是新的开源跨平台 .NET 运行时 .NET Core 的一个组成部分。
命名空间包括所有常用的集合类型:数组,列表,集合,字典,队列和堆栈。顾名思义,它们都是不可改变的,即它们在创建之后不能被改变。相反,每个更改都会创建一个新实例。这使得不可变集合以与.NET框架基类库中包含的并发集合不同的方式完全线程安全。
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} 的值。
// 堆代码 www.duidaima.com var result = persons .Where(p => p.FirstName == "John") .Select(p => p.LastName) .OrderBy(s => s.ToLower()) .ToList();以上查询返回名为 John 的姓氏的有序列表。我们只提供了预期的结果,而不是提供详细的操作顺序。可用的扩展方法也很容易使用链式语法进行组合。尽管LINQ函数并不是作用于不可变的类型,但它们仍然是纯函数,除非通过传递变异函数作为参数来滥用。它们被实现为对只读接口 IEnumerable 集合进行操作,而不修改集合中的项目。
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 表达式而不是单独的函数。然而,随着实现的逻辑变得更加复杂和可重用,把它们定义为独立的函数,开始变得更有意义。