C#高级编程第11版

导航

第十三章 Functional Programming with C#

13.1 概述

C#从来就不是一门纯粹的面向对象语言。早先,C#是面向组件设计的。什么叫面向组件?C#提供了继承和多态的功能,这点倒是与面向对象一样,但是,C#还提供了对属性、事件,特性的原生支持。最新版本的LINQ和表达式都包括了声明式编程(declarative programming),使用声明式的LINQ表达式,编译器保存了一颗表达式树,为之后的SQL语句提供动态生成。

C#不单单基于某一种语言范式(paradigm),那些对现代C#应用程序有用的特性会不断地添加进C#的语法里。最近这些年,越来越多跟函数式编程(functional programming)相关的特性都逐步地加到了C#里。

那么什么是函数式编程呢?最重要的概念主要体现在两个方面:一个是避免状态突变(avoid state mutation),一个是函数作为一等公民(function as a first-class citizens)。接下来的俩小节会着重介绍这两者的细节。

注意:本书仅仅只能介绍函数式编程的部分内容,你需要专门的书籍进行学习。如果你想要完全使用函数式编程范式,你需要考虑使用F#语言而不是用C#。本章仅仅介绍最实用的部分——比如C#能怎么做。一些函数式编程的特性对所有类型的应用程序都有用,这也是C#提供了函数式编程特性的原因。随着时间的发展,越来越多的函数式编程特性会被添加进C#的语法里,并且更符合C#的编程风格。

13.1.1 避免状态可变

如果你用过F#,一门函数优先的语言,创建一个自定义类型,然后用这个类型构造一个对象实例,默认这个实例是不可变(immutable)的。实例只能在构造函数里初始化,并且后续不能有任何改变。如果需要动态改变,那么类型就必须显式声明成可变(mutable)的。这点和C#不一样。

在C#里,一些预定义的类型是不可变的,如string。那些修改string的方法实际上返回的是一个新的string对象。集合怎么样?LINQ的方法从来不会改变任何一个集合,像where和orderby等方法实际上返回的是过滤好的新集合。

另一方面,List<T>集合提供了一系列的排序方法,但它是一种可变的类型,因此排序的时候,原始集合会发生变化。.NET提供完全不可变的集合类包含在System.Collections.Immutable命名空间下。这些集合类提供的方法不会改变集合本身,而是返回一个新的集合。

那么不可变类型有什么优点呢?因为它保证了没有人能修改它的实例,因此多线程在访问它的时候,就不需要考虑同步的问题。使用不可变的类型,创建单元测试也会更简单。

C# 6.0开始添加了一些创建自定义类型的特性。从C# 6.0开始,你可以创建一个自动实现的只读属性,它仅仅带有一个get访问器:

public string FirstName { get; }

因为需要相关类库的支持,因此并非在任何地方都可以随心所欲地使用不可变类型(immutable type)。最近这些年,这些各种各样的限制开始被移除。举个例子,NuGet包Newtonsoft.Json允许在JSON的序列化和反序列化的时候使用非可变类型。这个类库利用构造函数,使得匹配的参数需要创建一个新的实例。EF以前其实也有类似的限制。然而,从Entity Framework Core 1.1开始,数据列可以和fields直接映射,而非每次都需要读/写Properties。

注意:

  • 关于JSON的序列化在Bonus章节里会介绍,而EF Core则将在26章进行详细介绍。第21章任务和并行编程将介绍线程核同步。
  • 本章并没有再次展开C#创建不可变类型的特性,因为这部分内容已经在第三章中介绍过了。C#允许你创建一个通过get访问器自动实现的Property,编译器会自动帮你创建一个内置的readonly字段并且get访问器会返回这个字段值。后续的C#版本计划提供更多的特性来创建不可变类型,例如records。

13.1.2 函数作为一等类

在函数式编程中,函数就是第一级的类(functions as first class)。这意味着,函数可以作为函数的参数,函数可以作为函数的返回值,并且函数可以赋值给变量。

C#里也有类似的实现:委托可以存储函数地址,委托也可以当做方法的参数,委托也可以被当做返回值。然而,你需要意识到,使用一个委托和使用一个普通函数还是有区别,委托有额外的开销。使用委托的时候,其实你是创建了一个Delegate类的实例,并且这个实例拥有很多方法引用。当你调用一个委托,这个方法集合里保存的方法会挨个被调用。

高阶函数

函数式编程中,那些将其他函数作为参数或者返回值的"函数",称之为高阶函数。在C#里,委托就可以作为参数或者返回值。

在前面章节里介绍的LINQ,其实就是一种高阶函数。举个例子,where方法接收一个Func<TSource, bool>谓词(predicate):

public static IEnumerable<TSource> Where(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

纯函数

函数式编程里有个术语叫纯函数(pure function)。如果可能的话尽量使用纯函数,它必须满足两个条件:

  • 同样的输入,同样的输出。
  • 纯函数没有任何副作用,譬如改变某些状态,或者依托于外部资源。

当然不是所有的函数都需要当纯函数实现,纯函数也不是万能的。如果某个方法需要访问外部资源,你需要考虑它是否可以分成两部分,一部分包含复杂逻辑的纯函数和一部分仅仅用来访问资源的部分。

现在你已经对函数式编程的重要概念有一个大概的了解了,接下来让我们看看C#的语法是怎么实现这些概念的。

13.2 表达式体的成员

C# 6.0允许只带有get访问器的Property或者Method使用表达式来作为自己的方法体。现在,C# 7.0的时代,允许你在任何地方使用表达式来作为方法体,只要这个方法体的实现只需要一个语句。在函数式编程中,很多方法都仅仅只有一行,因此这个特性会被经常使用。代码可以精简不少行,因为你再也不需要使用{}了。

注意:在第三章的时候已经介绍过用表达式来书写Property和Method,而在第八章的时候介绍了如何在Event中使用表达式,所以本章可能不会再重复提及这些内容。

让我们先来看看以下的例子:

public class Person
{
	public Person(string name) =>name.Split(' ').ToStrings(out _firstName, out _lastName);
    
	private string _firstName;
	public string FirstName
	{
		get => _firstName;
		set => _firstName = value;
	}
    
	private string _lastName;
	public string LastName
	{
		get => _lastName;
		set => _lastName = value;
	}
    	
	public override string ToString() => $"{FirstName}{LastName}";
}

我们在Property里使用了表达式,get和set访问器都可以使用。我们在构造函数里使用了表达式,通过split方法,将接收到的参数name按照空格分隔成一个string数组,然后对这个数组调用ToStrings()方法,通过out参数的方式,赋值给两个field,_firstName和_lastName。

这个ToStrings()方法是我们自己定义的一个扩展方法:

public static class StringArrayExtensions
{
	public static void ToStrings(this string[] values, out string value1,out string value2)
	{
		if (values == null) throw new ArgumentNullException(nameof(values));
		if (values.Length != 2) throw new IndexOutOfRangeException("only arrays with 2 values allowed");
		value1 = values[0];
		value2 = values[1];
	}
}

根据上面的这些定义,一个Person实例可以通过一个string类型的名称进行声明,并提供了FirstName和LastName的属性,以便你可以读取相应的名称:

Person p = new Person("Katharina Nagel");
Console.WriteLine($"{p.FirstName} {p.LastName}");

13.3 扩展方法

我们在第12章的时候其实已经提到了扩展方法,前面小节里也演示了一个自定义扩展函数的例子。事实上,扩展函数这个特性能为函数式编程提供很大的帮助,接下来我打算再演示一个例子,请先看一下这段代码:

public static class FunctionalExtensions
{
	public static void Use<T>(this T item, Action<T> action) where T : IDisposable
	{
		using (item)
		{
			action(item);
		}
	}
}

通过函数式编程,很多方法都非常的短小精悍并且往往只有一行语句,就像前面那些例子中你看到的一样,这让我们的代码简化了不少。例如我们经常使用的using语句,它也可以用一个函数代替。在上面的代码里,我们为所有实现了IDisposable的类型,扩展了一个叫做Use的方法。在这个方法里我们会调用using语句,Action<T>是一个委托,用来传递实际调用的方法,只要执行完action的方法体,实例item的资源就会被释放。

Resource就是一个实现了IDisposable接口的类,我们假定它提供了一个Foo的方法,以便我们演示IDisposable功能:

class Resource : IDisposable
{
	public void Foo() => Console.WriteLine("Foo");
	private bool disposedValue = false;
	protected virtual void Dispose(bool disposing)
	{
		if (!disposedValue)
		{
			if (disposing)
			{
				Console.WriteLine("release resource");
			}
			disposedValue = true;
		}
	}

	public void Dispose() => Dispose(true);
}

以往我们通过using语句在调用完成之后释放资源,通常会这么写:

using (var r = new Resource())
{
	r.Foo();
}

而通过我们扩展的Use方法,这三行代码就可以通过一行来完成:

new Resouce().Use(r=>r.Foo());

13.4 using static

13.5 本地函数 278

13.5.1 本地函数与yield 语句 279

13.5.2 递归本地函数 281

13.6 元组 282

13.6.1 元组的声明和初始化 282

13.6.2 元组解构 283

13.6.3 元组的返回 283

13.6.4 幕后的原理 284

13.6.5 ValueTuple 与元组的兼容性 285

13.6.6 推断出元组名称 285

13.6.7 元组与链表 286

13.6.8 元组和LINQ 286

13.6.9 解构 287

13.6.10 解构与扩展方法 288

13.7 模式匹配 288

13.7.1 模式匹配与is 运算符 288

13.7.2 模式匹配与switch 语句 290

13.7.3 模式匹配与泛型 291

13.8 小结 291

原文地址:https://www.cnblogs.com/zenronphy/p/ProfessionalCSharp7Chapter13.html