转载C#函数式程序设计初探——基础理论篇
转载网址:http://www.cnblogs.com/Hlia/archive/2013/04/20/3029701.html
个人认为,C#语言的某些设计并不非常适合函数式开发,比如它的类型推断并不是很近乎人意,我们知道C#还是主打面向对象的,不过这并不妨碍我们用C#来讨论函数式,至少可以借鉴函数式的一些思路来优化我们的代码。
我希望通过这篇文章让读者通过简单的例子,在短时间内掌握基本函数式编程方法,了解Action与Func类型的使用。同时我希望读者对C#泛型集合、Linq、lambda表达式和yield关键字有所了解。
主要内容
Action与Func类型介绍,在函数内部定义函数与返回函数,闭包与函数柯里化,高阶函数与Linq应用。
第一部分 Action与Func类型介绍
近来有一些人问我Action和Func类型是什么意思,为了整篇文章知识体系的完整性,先来给大家做一番介绍(如果你熟悉这两个类型,请跳过这部分)。
首先来看这样一个JavaScript函数:
function sum(n1, n2) {
return n1 + n2;
}
我们知道,在JavaScript当中,函数是可以赋值为一个变量的,即:
var sum = function(n1, n2) {
return n1 + n2;
}
定义这个“变量”之后,我们可以通过sum(1,2)的方式调用这个函数。那么,如果javaScript是一种强类型语言的话,这个var是什么类型呢?
来看一下这个函数的C#代码:
static int Sum(int n1, int n2)
{
return n1 + n2;
}
注意到这个函数接收了两个int型参数,返回了一个int值。那么,它的类型就是Func<int,int,int>,即它的等效代码为:
Func<int,int,int> Sum = (int n1, int n2) => {
return n1 + n2;
};
我们可以F12一下,看到Func类的定义如下:
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
这个类型实质上是一个委托,返回值是个泛型的TResult,从定义的参数表可以看出,前两个类型T1和T2是传入参数的类型,第三个类型是返回值类型。
根据这个道理,假设有一个Func<int,string,bool>型的变量,它表示一个委托,这个委托内包含了这样一个函数:该函数的两个参数是int和string类型,返回值为bool。当Func<TResult>只有一个类型参数时,TResult表示返回值类型,即Func<bool>表示一个委托,它的参数表为空,返回值为bool类型。为了方便说明,下文将委托与函数两个概念通通使用“函数”来表示。
猜猜看Func<object, Func<string,bool>>表示什么呢?它表示一个函数,接受一个object类型的参数,返回一个Func<string,bool>。这里可以看出,函数也是可以作为函数的返回值的。
接下来看Action,我们F12一下看Action<T>的定义:
public delegate void Action<in T>(T obj);
注意到委托的返回值为void,那么实际上Action就是一个没有返回值,只有参数表的委托,即Action<T1,T2>等价于Func<T1,T2,void>。
最后来说一下Predicate<T>,当我们写Linq方法的Where()时,可以看到它要求传入了一个Predicate类型的参数,它实际上就是一个bool型委托,等价于Func<T,bool>。
这里只是对这几个委托关键字做一个铺垫性的介绍,大家可以去网上搜这两个关键字的用法相关的帖子,如果没搞懂请不要往下看。
第二部分 在函数内部定义函数与返回函数
那么有人该问了,好好的一个函数,干嘛非写成Func这样蹩脚的形式呢?下面来看一个例子:
static void DoSth()
{
//前置逻辑
if (Validate())
{
//后续逻辑
}
} static bool Validate()
{
//校验逻辑
return true;
}
也许这个例子不够恰当,但是足以说明问题,我想略有经验的程序员都明白将校验方法(或者说,比较方法,嗯)重构到一个新函数里,这样能让程序脉络清晰,《重构》当中也提到了这一手段,但是有没有意识到这种做法有一个诟病:这两个方法处于一个类环境当中,通常来说DoSth方法是publish的,那么为了重构,我们不得不在这个类环境当中搞出一个private方法来支撑这个public方法,显然这个private方法没有什么可复用性可言,而且它污染了整个类空间,再说从面向对象的角度来看,校验成了我这个类要承担的职责,这岂不是很诡异?
那么我要做的,就是在提取这个Validate方法的前提下,保证这个方法别污染类空间。那么一个切实可行的办法,就是把这个校验函数定义在DoSth的内部,代码如下:
static void DoSth()
{
Func<bool> Validate = () => {
//校验逻辑
return true;
}; //前置逻辑
if (Validate())
{
//后续逻辑
}
}
这段代码把校验函数定义为了DoSth内部的一个变量,它的生存期就在DoSth内部,这样一来就丝毫不会影响类的结构了。这就是Func的应用之一——在函数内部定义局部函数。
但是这样还是让人觉得很啰嗦,这个Validate完全可以在其他地方定义,然后作为参数传进来,比如这样:
static void DoSth(Func<bool> Validate)
{
//前置逻辑
if (Validate())
{
//后续逻辑
}
}
如此一来,这个校验方法就可以定义在其他地方了,这就给我们做一些面向对象方面的方便(比如通过依赖注入搞到这个函数),当然也可以在调用的时候直接在参数里写lambda表达式:
static void Main(string[] args)
{
DoSth(() => {
//校验逻辑
return true;
});
}
可能这个“校验”的例子举得不是很恰当,但是这已经足够说明Func作为参数的用法。
如果你怀疑这种手段的实际价值,想想JavaScript里的SetTimeout的第二个参数吧!所谓的回调函数,就是一种由框架调用由客户端实现的函数,用这种写法可以大大增加客户端代码的直观性与灵活性!
既然Func类型可以作为函数的参数,那么它可不可以作为函数返回值呢?答案必然是肯定的,我们还是来看一个加法例子:
static Func<int, Func<int, int>> Sum = n1 => {
return n2 => n1 + n2;
};
观察返回值类型Func<int, Func<int,int>>,它表示这个函数接受一个int型参数,返回一个Func<int,int>,也就是返回一个接受int类型参数,返回int类型值的函数。即,Sum是一个返回函数的函数。
那么这个函数如何使用呢?观察下列主函数:
static void Main(string[] args)
{
var Sum5 = Sum();
int result = Sum5(); Console.WriteLine(result);
Console.ReadKey();
}
首先,我们通过Sum(5)的方式,返回了一个Sum5变量,这个变量的类型是Func<int,int>,也就是说,我们通过Sum函数返回了Sum5函数。接下来调用这个新函数Sum5(10),得到了答案15。当然,接下来我还可以调用Sum5(20)得到25。
自然地,这个调用可以写成Sum(5)(10),与原本的Sum(5,10)相比,新的写法将两个参数拆解到了多个括号之中分部调用。聪明的你一定能发现这么做的好处,就是把这个参数解耦,让各个算法(函数)之间有更高的灵活性和可复用性。但是要注意的是,要得到最终的结果,参数的数量依旧是一个都不能少的。
另外,你有没有从这里嗅出一些“重载”的味道?
第三部分 闭包与函数柯里化
不要被这个标题吓倒,嗯!我们来改写一下刚才的代码:
static void Main(string[] args)
{
var Sum5 = Sum();
int result = Sum5(); Console.WriteLine(result);
Console.ReadKey();
} static Func<Func<int, int>> Sum = () => {
int n1 = ;
return n2 => n1 + n2;
};
这次我们让Sum不再接收第一个参数了,而把n1定义在Sum方法的内部,调用就变成了Sum()(10),大家可以试一下,结果依旧输出15,一切看似很自然,不过请你反复读一读Sum的定义,是不敢觉得似乎少了点什么?希望你停下来多读几遍再往下看!
问题就出在n1的定义,请回答一个问题,变量n1的生存范围是多大?Sum函数返回的时候,n1既然是Sum的内部的局部变量,应该就被释放掉了,那么我调用Sum5(10)的时候,被释放掉的5是从哪里来的呢?
在解释这个问题之前,我想你应该可以理解“Func<Func<int,int>> Sum = xxx”这种写法,等价于“Func<int,int> Sum() { xxx }”,如果不理解,请停下来,把上面的部分再看一遍。
我们打开反编译器对这个Sum的定义,可以看到:
[CompilerGenerated]
private static Func<int, int> <.cctor>b__0()
{
<>c__DisplayClass3 CS$<>8__locals4;
return new Func<int, int>(CS$<>8__locals4, (IntPtr) this.<.cctor>b__1);
}
奇怪的是,在这个函数的第一句话,定义了一个“<>c__DisplayClass3”匿名类的对象,也就是说,Sum5这个函数的内部携带着这个对象,想必5这个数字就保存在这个类里,来看这个类的定义:
[CompilerGenerated]
private sealed class <>c__DisplayClass3
{
public int n1; public int <.cctor>b__1(int n2)
{
return (this.n1 + n2);
}
}
看到这里我想我不用再解释什么了吧。
观察我们的函数n2=>n1+n2,它能够拿到外部函数Sum中的n1,而Sum却不能拿到它内部的n2,这一类的函数,起个名字——闭包。于是现在你稍微理解JavaScript中那个叫作用域链的东西了吗?
嗯,这部分的标题上提到了函数的柯里化,那什么是柯里化呢?其实刚才已经看过了,把Sum(5,10,15,20)写成Sum(5)(10)(15)(20)就叫柯里化,或者说把Func<int,int,int>搞成Func<int, Func<int,int>>就叫柯里化,也是起个名字唬人的,就像“面向切面编程”这个名字一样!
第四部分 高阶函数与Linq应用
现在进入理论篇的最后一部分,神马叫高阶函数?还就是起个名字而已,以其他函数做参数、或者返回一个函数的函数,就叫高阶函数,刚才的Sum就是高阶函数。至此大家已经了解了如何在函数中调用一个作为参数的函数,为了给后面的应用篇做铺垫,这里介绍几个经典的高阶函数,希望大家都能理解。
(1)Map函数:接受一个转换函数和一个集合,对这个集合中的每个元素,延迟返回它执行转换函数后的值。
static IEnumerable<TR> Map<T, TR>(Converter<T, TR> select, IEnumerable<T> list)
{
foreach (T val in list)
{
yield return select(val);
}
}
其中Converter是一个委托,它接受一种类型的参数,返回另一种类型的参数,也就是说如果有一个Converter类型的函数,其作用就是将一种类型转换为另一种类型,当然,在使用的时候,我们可以传递一个很复杂的类,返回其中的某个字段。
public delegate TOutput Converter<in TInput, out TOutput>(TInput input);
(2)Filter函数:接受一个布尔函数作为判断条件,作用在一个集合上,延迟返回这个集合当中满足条件的元素。
static IEnumerable<T> Filter<T>(Predicate<T> selector, IEnumerable<T> list)
{
foreach (T val in list)
{
if (selector(val))
{
yield return val;
}
}
}
(3)Fold函数:接受一个返回TR类型的算法函数,一个TR类型的起始值,及一个集合,对这个集合中的所有值应用这一算法,并”折叠“到返回值上返回。
static TR Fold<T, TR>(Func<TR, T, TR> accumulator, TR startVal, IEnumerable<T> list)
{
TR result = startVal;
foreach (T val in list)
{
result = accumulator(result, val);
}
return result;
}
大家有没有看出这三个函数有什么猫腻?它们都有一个IEnumerable<T>的参数,那么下面我们就把他们改造为扩展方法,并且改个名:
public static partial class Enumerable
{
public static IEnumerable<TR> Select<T, TR>(this IEnumerable<T> list, Converter<T, TR> selectField)
{
foreach (T val in list)
{
yield return selectField(val);
}
} public static IEnumerable<T> Where<T>(this IEnumerable<T> list, Predicate<T> selector)
{
foreach (T val in list)
{
if (selector(val))
{
yield return val;
}
}
} public static TR Sum<T, TR>(this IEnumerable<T> list, Func<TR, T, TR> accumulator, TR startVal)
{
TR result = startVal;
foreach (T val in list)
{
result = accumulator(result, val);
}
return result;
}
}
我们可以这样使用:
static void Main(string[] args)
{
IEnumerable<int> list = new List<int>() { , , , , , , , }; list.Where(num => num % == )
.Select(num => num)
.ToList().ForEach(num => { //这里就直接调用Linq了
Console.WriteLine(num);
}); int sum = list.Where(num => num % == )
.Sum((x, y) => x + y, );
Console.WriteLine("sum=" + sum); Console.ReadKey();
}
这基本和Linq没有什么差别了,嗯,其实Linq里就是这么搞的,只是它更加丰富和严谨,依旧不用多解释了。
后记
相信大家读完这篇文章之后已经对函数式编程有了一个初步的认识,函数式还有很多精彩的应用,请关注下回分解!
转载C#函数式程序设计初探——基础理论篇的更多相关文章
- C#函数式程序设计之局部套用与部分应用
函数式设计的核心与函数的应用以及函数如何作为算法的基本模块有关.利用局部套用技术可以把所有函数看成是函数类的成员,这些函数只有一个形参,有了局部套用,才有部分应用.部分应用是使函数模块化成为可能的两个 ...
- C#函数式程序设计之代码即数据
自3.5版本以来,.NET以及微软的.NET语言开始支持表达式树.它们为这些语言的某个特定子集提供了eval形式的求值功能.考虑下面这个简单的Lambda表达式: Func<int, int, ...
- C#函数式程序设计之用闭包封装数据
如果一个程序设计语言能够用高阶函数解决问题,则意味着数据作用域问题已十分突出.当函数可以当成参数和返回值在函数之间进行传递时,编译器利用闭包扩展变量的作用域,以保证随时能得到所需要的数据. C#函数式 ...
- C#函数式程序设计之泛型(下)
C#函数式程序设计之泛型(下) 每当使用泛型类型时,可以通过where字句对泛型添加约束: + 这个例子直观地声明了一个约束:类型T必须与ListItem<string>相匹配.泛型类 ...
- C#函数式程序设计之泛型
Intellij修改archetype Plugin配置 2014-03-16 09:26 by 破狼, 204 阅读, 0 评论,收藏, 编辑 Maven archetype plugin为我们提供 ...
- C#函数式程序设计之函数、委托和Lambda表达式
C#函数式程序设计之函数.委托和Lambda表达式 C#函数式程序设计之函数.委托和Lambda表达式 相信很多人都听说过函数式编程,提到函数式程序设计,脑海里涌现出来更多的是Lisp.Haske ...
- Haskell学习-函数式编程初探
原文地址:Haskell学习-函数式编程初探 为什么要学习函数式编程?为什么要学习Haskell? .net到前端,C#和JavaScript对我来说如果谈不上精通,最起码也算是到了非常熟悉的 ...
- [java初探总结篇]__java初探总结
前言 终于,java初探系列的学习,要告一阶段了,java初探系列在我的计划中是从头学java中的第一个阶段,知识主要涉及java的基础知识,所以在笔记上实在花了不少的功夫.虽然是在第一阶段上面花费了 ...
- C#函数式程序设计之惰性列表工具——迭代器
有效地处理数据时当今程序设计语言和框架的一个任务..NET拥有一个精心构建的集合类系统,它利用迭代器的功能实现对数据的顺序访问. 惰性枚举是一个迭代方法,其核心思想是只在需要的时候才去读取数据.这个思 ...
随机推荐
- POJ3687——Labeling Balls(反向建图+拓扑排序)
Labeling Balls DescriptionWindy has N balls of distinct weights from 1 unit to N units. Now he tries ...
- 【.Net免费公开课】--授技.Net中的高帅富技术-"工作流"
课程简介 免费公开课主题: .Net中的高帅富技术-“工作流” 公开课开课时间: 10月17日 19:30--21:30 公开课YY频道: 85155393 (重要:公开课QQ ...
- GB2312 简体中文编码表
GB 2312中对所收汉字进行了“分区”处理,每区含有94个汉字/符号.这种表示方式也称为区位码. 01-09区为特殊符号. 16-55区为一级汉字,按拼音排序. 56-87区为二级汉字,按部首/笔画 ...
- poj1151Atlantis(离散化+扫描线)
http://poj.org/problem?id=1151 http://www.cnblogs.com/kane0526/archive/2013/02/26/2934214.html这篇博客写的 ...
- poj1180
斜率优化dp 据说这题朴素的O(n2)dp也可以A 没试过 朴素的dp不难想:f[i]=min(f[j]+sumtime[i]*sumcost[j+1,i]+c*sumcost[j+1,n]) 稍微解 ...
- LBS云端数据删除和上传
这里采用C#模拟表单提交,实现LBS云端删除和csv格式文件的上传. 删除: /// <summary> /// 从LBS云端删除数据 /// </summary> /// & ...
- Cacti 'graph_xport.php' SQL注入漏洞
漏洞版本: Cacti < 0.8.8b 漏洞描述: Bugtraq ID:66555 Cacti是一套基于PHP,MySQL,SNMP及RRDTool开发的网络流量监测图形分析工具. Cact ...
- CQOIX2007余数之和
朴素能得个差不多吧…… 这题改进算法真恶心 pascal一直过不了,难道非得转c++? 代码:(pascal) var n,k,i,l,r,m:longint; ans:qword; function ...
- HNOI2004打鼹鼠(LIS)
大水题…… 不过通过这题我们应该养成一个好习惯:好好看清题…… 竟然没有看到时限 10sec…… var i,j,n,m,ans:longint; f,time,x,y:..] of longint; ...
- POJ 3207 Ikki's Story IV - Panda's Trick (2-SAT,基础)
题意: 有一个环,环上n个点,现在在m个点对之间连一条线,线可以往圆外面绕,也可以往里面绕,问是否必定会相交? 思路: 根据所给的m条边可知,假设给的是a-b,那么a-b要么得绕环外,要么只能在环内, ...