c#函数式编程 Functional Programming in C# [3]

1.3 用函数式思考

  在本节中,我将澄清我对函数的理解。我将从这个词的数学用法开始,然后转向C#提供的代表函数的各种语言结构。

1.3.1 作为映射的函数

  在数学中,一个函数是两个集合之间的映射,分别称为域和子域。也就是说,给定其域中的一个元素,函数会产生其子域中的一个元素。这就是全部–不管这个映射是基于某个公式还是完全任意的,都不重要。
  在这个意义上,函数是一个完全抽象的数学对象,函数产生的值完全由其输入决定。 你会发现,编程中的函数并不总是这样的。
  例如,设想一个将小写字母映射到大写字母的函数,如图1.3所示。 在这种情况下,域是{a,b,c,…}的集合,而code domain是{A,B,C,…}的集合。(当然,有些函数的域和code domain是同一个集合,你能想出一个例子吗?
  这与编程功能有什么关系?在像C#这样的静态类型语言中,集合(域和码域)是用类型表示的。 例如,如果你编写了上面的函数,你可以用char来表示域和码域。那么你的函数的类型可以写成

char -> char

  也就是说,该函数将字符映射为字符,或者说,给定一个字符,它产生一个字符。
  域和子域的类型构成了一个函数的接口,也被称为它的类型,或签名。你可以把它看作是一个契约:一个函数的签名声明,给定域中的一个元素,它将产生一个来自子域的元素。这听起来很明显,但你会在第三章看到,在现实中,违反签名契约的情况比比皆是。
  接下来,让我们看看对函数本身进行编码的方法。

1.3.2 在 C# 中表示函数

  C# 中有多种语言结构可用于表示函数:

  • Methods
  • Delegates
  • Lamba expressions
  • Dictionaries

  如果您精通这些,请跳到下一部分;否则,这里有一个快速复习。

Methods
  方法是 C# 中函数最常见和惯用的表示形式。例如,System.Math 类包括表示许多常见数学函数的方法。方法可以表示函数,但它们也适合面向对象的范式——它们可以用于实现接口,它们可以被重载,等等。
  真正使您能够以函数式风格进行编程的构造是委托和 lambda 表达式。

Delegates
  委托是类型安全的函数指针。这里的类型安全意味着委托是强类型的:函数的输入和输出值的类型在编译时是已知的,并且一致性由编译器强制执行。
  创建一个委托是一个两步的过程:你首先声明委托的类型,然后提供一个实现。(这类似于编写一个接口,然后实例化一个实现该接口的类)。)
  第一步是通过使用delegate关键字和提供delegate的符号来完成。 例如,.NET包括以下Comparison< T >委托的定义。

清单 1.5 声明一个委托

namespace System
{
	public delegate int Comparison<in T>(T x, T y);
}

  如您所见,Comparison< T > 委托可以被赋予两个 T,并会产生一个更大的指示。
  一旦你有了一个委托类型,你就可以通过提供一个实现来实例化它,就像这样。

清单 1.6 实例化和使用委托

var list = Enumerable.Range(1, 10).Select(i => i * 3).ToList();
list // => [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]
// 提供 Comparison 的实现
Comparison < int > alphabetically = (l, r)  
	=> l.ToString().CompareTo(r.ToString());
// 使用比较委托作为 Sort 的参数
list.Sort(alphabetically); 
list // => [12, 15, 18, 21, 24, 27, 3, 30, 6, 9]

  如您所见,委托只是一个表示操作的对象(在技术意义上)——在本例中,是一个比较。就像任何其他对象一样,您可以使用委托作为另一个方法的参数,如清单 1.6 所示,因此委托是使函数成为 C# 中第一公民的语言特性。

Func 和 Action 委托
  .NET框架包括了几个委托 “家族”,可以代表几乎所有的函数类型:

  • Func< R > 表示一个不带参数并返回 R 类型结果的函数。
  • Func< T1,R > 表示一个函数,它接受一个类型为 T1 的参数并返回一个类型为 R 的结果。
  • Func<T1,T2,R> 表示接受 T1 和 T2 并返回 R 的函数。

  以此类推。 有一些代表可以代表各种 "arity "的函数(见 "函数arity "侧边栏)。

  自从引入Func后,使用自定义委托就变得很罕见。 例如,不需要像这样声明一个自定义的委托。

delegate Greeting Greeter(Person p);

你可以直接使用该类型:

Func<Person, Greeting>

  前面示例中的 Greeter 类型等效于或“兼容”Func<Person,Greeting>。在这两种情况下,它都是一个接受 Person 并返回 Greeting 的函数。
  有一个类似的委托系列来表示Actions ——没有返回值的函数,例如 void 方法:

  • Action 表示没有输入参数的动作。
  • Action< T1 > 表示具有 T1 类型输入参数的操作。
  • Action<T1,T2> 等表示具有多个输入参数的操作。

  .NET的发展已经远离了自定义委托,而倾向于使用更多的普通的Func和Action委托。例如,以一个谓词的表述为例:

  • 在 .NET 2 中,引入了 Predicate< T > 委托,例如,在用于过滤 List< T > 的 FindAll 方法中使用该委托。
  • 在 .NET 3 中,Where 方法也用于过滤,但定义在更通用的 IEnumerable< T > 上,它不采用 Predicate< T >,而是采用 Func<T,bool>。

  这两种函数类型都是等价的。我们推荐使用Func,以避免代表相同函数签名的委托类型的泛滥,但仍有一些东西可以说是支持自定义委托的表达能力的。 在我看来,Predicate比Func<T,bool>更清楚地表达了意图,而且更接近口语。

Function arity Arity 是一个有趣的词,指的是函数接受的参数数量:

  • 空(nullary)函数不带参数。
  • 一元(unary)函数接受一个参数。
  • 二元(binary)函数有两个参数。
  • 三元(ternary)函数接受三个参数。

以此类推。在现实中,所有的函数都可以被看作是单项的,因为传递n个参数就相当于传递一个n个元组作为唯一的参数。例如,加法(像任何其他二进制算术运算)是一个函数,其域是所有数字对的集合。

LAMBDA 表达式

  Lambda 表达式,简称为 lambda,用于声明一个内联函数。例如,可以使用像这样的 lambda 来按字母顺序对数字列表进行排序。

清单 1.7 使用 lambda 声明内联函数

var list = Enumerable.Range(1, 10).Select(i => i * 3).ToList();
list // => [3, 6, 9, 12, 15, 18, 21, 24, 27, 30]
list.Sort((l, r) => l.ToString().CompareTo(r.ToString()));
list // => [12, 15, 18, 21, 24, 27, 3, 30, 6, 9]

  如果你的函数很短,而且你不需要在其他地方重复使用它,lambdas提供了最有吸引力的符号。还请注意,在前面的例子中,编译器不仅将x和y的类型推断为int,而且还将lambda转换为Sort方法所期望的委托类型Comparison< int >,因为所提供的lambda与这种类型兼容。
  就像方法一样,委托和lambdas可以访问它们被声明的范围内的变量。 这在利用lambda表达式中的闭包时特别有用。

清单 1.8 Lambdas 可以访问封闭范围内的变量

var days = Enum.GetValues(typeof(DayOfWeek)).Cast<DayOfWeek>();
// => [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday]
//pattern变量是在lambda中被引用的,因此被捕获在一个闭包中。
IEnumerable<DayOfWeek> daysStartingWith(string pattern) => days.Where(d => d.ToString().StartsWith(pattern)); 
daysStartingWith("S") // => [Sunday, Saturday]

  在这个例子中,Where期望的是一个接收DayOfWeek并返回bool的函数。实际上,lambda表达式所表达的函数也使用pattern的值,该值被捕获在一个闭包中,来计算其结果。
  这很有趣。如果你用更数学的眼光看 lambda 表达的函数,你可能会说它实际上是一个二元函数,它以 DayOfWeek 和一个字符串(模式)作为输入,并产生一个布尔值。然而,作为程序员,我们通常最关心的是函数签名,因此您可能更可能将其视为从 DayOfWeek 到 bool 的一元函数。这两种观点都是有效的:函数必须符合它的一元签名,但它依赖于两个值来完成它的工作。

Dictionaries
  字典也被恰当地称为映射(或哈希表);它们是提供非常直接的函数表示的数据结构。它们字面上包含键(域中的元素)与值(域中的相应元素)的关联。
  我们通常把字典看作是数据,因此,暂时改变一下观点,把它们看作是函数,是很有意义的。字典适合用来表达完全任意的函数,其中的映射不能被计算,但必须被详尽地存储。 例如,为了将布尔值映射到法语中的名字,你可以写如下。

清单1.9 一个函数可以用一个字典详尽地表示出来

//C# 6 字典初始值设定项语法
var frenchFor = new Dictionary<bool, string>{
	[true] = "Vrai",
	[false] = "Faux",
};
//函数的应用是通过查找来进行的
frenchFor[true] 
// => "Vrai"

  函数可以用字典来表示,这也使得通过将其计算结果存储在字典中而不是每次重新计算来优化计算量大的函数成为可能。
  为方便起见,在本书的其余部分,我将使用术语函数来表示函数的一种 C# 表示,因此请记住,这与术语的数学定义不太匹配。您将在第 2 章中了解有关数学函数和编程函数之间差异的更多信息。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值