C#函数式编程
编程语言范式
常见的编程范式有命令式编程(Imperative programming),函数式编程,逻辑式编程;
许多现存的编程语言都可基于其计算模型加以分类,归入某些语言族,或者属于某种编程范式。按照不同的规则,可以有多种分类的方法,而且不同的学者对某些语言的具体归属也有不同的意见。
给出一种系谱:
说明式(Declarative ) 命令式( Imperative )
函数式 Lisp, ML, Haskell 冯诺依曼 C, Ada, Fortran
数据流 ld, Val 脚本式 Perl, Python, PHP
逻辑式 Prolog 面向对象 Smalltalk, C++, Java, C#
基于模板 XSLT
有些编程范式并不能按以上的方法进行分类,比如:元编程,泛型编程。
一种语言并不是只从属于一种编程范式,有些语言本身就是为支持多范式设计的;
比如:Lisp就同时支持函数式编程、面向对象、元编程。
命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令)表达式(内存引用和算术运算)和控制语句(跳转指令);
函数式编程
定义
In computer science, functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids state and mutable data.
函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念。<Wiki>
函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式。
函数式编程最重要的基础是 λ 演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值)。和指令式编程相比,函数式编程的函数的计算比指令的执行更重要。和过程化编程相比,函数式编程的函数的计算可随时调用。纯函数式编程不需要变量;
语言族
函数式编程中最古老的例子可能要数1958年被创造出来的LISP,当年的Lisp由于各种设计缺陷(内存损耗、闭包问题、生成程序执行效率等)没能发展好。较现代的例子包括Haskell、Clean、Erlang 和Miranda等。
现代编程语言,如C#、Python、Ruby、Scala等等,它们都受到了函数式编程语言的影响,比如C#中的lamada表达式、Linq。
基于JVM实现的Lisp方言如Scala, Clojure也是越来越受关注,这里所谓的Lisp方言,主要是因为语法上沿用了Lisp中的S表达式。
基于.net平台的有F#,微软的首个函数式编程语言。<MSDN>
不同语言的抽象层次
高 计算
C# -----> 对象
Python -----> 函数式
C语言 -----> 函数 (面向过程)
汇编语言
低 计算机硬件 -----> 指令 计算机
函数式复兴
Anders Hejlsberg,C#编程语言的首席架构师,2010年关于《编程语言的发展趋势及未来方向》演讲
从一个数组中找出所有的偶数
List<int> list = new List<int> { 1,2,3,4,5,6,7};
常规的命令式写法:
List<int> ret = new List<int>();
foreach (var item in list)
{ if (item % 2 == 0)
ret.Add(item); }
声明式的写法: var ret = list.Where((x) => x % 2 == 0);
多核与并行
使用命令式编程语言写程序时,我们经常会编写如x = x + 1这样的语句,此时我们大量依赖的是可变状态,或者说是“变量”,它们的值可以随程序运行而改变。可变状态非常强大,但随之而来的便是被称为“副作用”的问题,例如一个无需参数的void方法,它会根据调用次数或是在哪个线程上进行调用对程序产生影响,它会改变程序内部的状态,从而影响之后的运行效果。而在函数式编程中则不会出现这个情况,因为所有的状态都是不可变的。事实上对函数式编程的讨论更像是数学、公式,而不是程序语句,如x = x + 1对于数学家来说,似乎只是个永不为真的表达式而已。
函数式编程十分容易并行,因为它在运行时不会修改任何状态,因此无论多少线程在运行时都可以观察到正确的结果。假如两个函数完全无关,那么它们是并行还是顺序地执行便没有什么区别。
函数式编程特性 与技术
函数是一等公民 闭包
高阶函数 惰性求值
递归 缓存技术
不可变状态 尾调用消除
柯里化 内存回收
C#函数式支持
Linq涉及的C#语言特性:隐式类型、匿名类型、初始化器、迭代器、委托、泛型、泛型委托、匿名方法、Lamada表达式。
函数对象必须是某种委托类型. 在C#中,我们可以定义强类型的委托类型或泛型的委托类型,委托可以代表跟这个委托类型有相同参数的方法(静态方法,类方法)的引用.
在使用LINQ的时候我们可以经常看到高阶函数。举个例子,如果你想将一个已有的序列使用一些函数转换为一个新的序列,你将使用类似LINQ的select函数(函数作为输入):
var squares = numbers.Select( num => num*num );
函数是一等公民
对象是面向对象的第一型,那么函数式编程也是一样,函数是函数式编程的第一型。
我们在函数式编程中努力用函数来表达所有的概念,完成所有的操作。
在面向对象编程中,我们把对象传来传去,那在函数式编程中,我们要做的是把函数传来传去。
函数这个术语不是指计算机中的函数,而是指数学中的函数,即自变量的映射。
函数可以在任何地方定义,在函数内或函数外,可以作为函数的参数和返回值,可以对函数进行组合。
高阶函数:能接收函数做参数的函数。
1:函数自身接受一个或多个函数作为输入参数;
2:函数自身能输出(返回)一个函数;
不可变状态
纯函数式编程语言中的变量也不是命令式编程语言中的变量,即存储状态的单元,而是代数中的变量,即一个值的名称。变量的值是不可变的;
函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,使得单元测试和调试都更容易。
递归
由于变量不可变,纯函数编程语言无法实现循环,这是因为For循环使用可变的状态作为计数器,而While循环或DoWhile循环需要可变的状态作为跳出循环的条件。因此在函数式语言里就只能使用递归来解决迭代问题,这使得函数式编程严重依赖递归。
递归定义的计算的Scala代码如下:
def fact(n: Int):Int= {
if(n == 0) return 1
n * fact(n-1)
}
C#代码
Public int Fact(int n)
{
int acc = 1;
for(int k = 1; k <= n; k++){
acc = acc * k;}
}
尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码(将尾递归转化为迭代);
柯里化(Currying)和部分(偏)函数(Partial Function)
是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
主要功能是提供了强大的动态函数创建方法,通过调用另一个函数并为它传入要柯里化(currying)的函数和必要的参数而得到。通俗点说就是利用已有的函数,再创建一个动态的函数,该动态函数内部还是通过已有的函数来发生作用。
柯里化就是一个函数在参数没给全时返回另一个函数,返回的函数的参数正好是余下的参数。比如:你制定了x和y, 如2的3次方,就返回8, 如果你只制定x为2,y没指定, 那么就返回一个函数:2的y次方, 这个函数只有一个参数:y。
curry就是对高阶函数(就是一种对过程的抽象 参考map它就是一个抽象的过程)的降阶处理。
2大特性:
- 匿名函数
- 每个函数只有1个参数
将多个参数的函数进行拆分,拆成多个只有一个参数的函数。为什么要拆分,λ 演算。
示例:
常规的写法:Func<int, int, int> Add = (x, y) => x + y;
拆分: Func<int, Func<int, int>> Add = x => y => x + y;
输入一个参数,返回一个具有一个参数的函数,接着再调用返回的函数,就完成整个调用。
调用:
var add2 = Add(3);
var ret = add2(4);
写成一行:
var ret = Add(3)(4);
或者不重写,只要为原来的方法加一个扩展方法:
public static Func<T1,Func<T2,T3>> Currey<T1,T2,T3>(this Func<T1,T2,T3> func)
{
return x => y => func(x,y);
}
这样就可以对C#标准的GenralAdd(int x,int y)方法执行Currey转换为部分(偏)函数了:
Func<int, int, int> Add = GenralAdd;
var CurreyedAdd = Add.Currey()(3)(4);
示例:比如我们经常需要执行SQL语句,当然需要使用SqlConnection,然后附加上对应的SQL语句,为此我们可以开发一个简单的函数,用来简化这一过程:
Func<SqlConnection, Func<String, DataSet>> ExecSql = x => y =>
{
using (x)
{
x.Open();
var com = x.CreateCommand();
DataSet ds = new DataSet();
com.CommandText = y;
SqlDataAdapter adapter = new SqlDataAdapter(com);
adapter.Fill(ds);
return ds;
}
};
调用:
var esql = ExecSql(new SqlConnection("xxx"));
var rds = esql("select xxxx from xxx");
rds = esql("select ffff from ffff");
如果想先传入Sql语句再传入SqlConnection:
Func<String, Func<SqlConnection, DataSet>> ExecSqlT = x => y => ExecSql(y)(x);
看一个函数:
static Func<int, int> GetAFunc()
{
var myVar = 1;
Func<int, int> inc = delegate(int var1)
{
myVar = myVar + 1;
return var1 + myVar;
};
return inc;
}
如下调用输入什么结果:
var inc = GetAFunc();
Console.WriteLine(inc(5));
Console.WriteLine(inc(6));
闭包
闭包是可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。
“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。<百度百科>
C#中实现闭包,实际就是通过类,封装变量和方法,提升生命周期。
static void Closure1()
{
List<Action> actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}
foreach (Action action in actions)
action();
}
static void Closure2()
{
int copy;
List<Action> actions = new List<Action>();
for (int i = 0; i < 10; i++)
{
copy = i;
actions.Add(() => Console.WriteLine(copy));
}
foreach (Action action in actions)
action();
}
缓存技术
怎样使用闭包来实现缓存。如果我们创建了一个用于缓存的接收函数就可以实现缓存,并返回一个在一段时间内缓存结果的新函数。以下列表显示的例子:
public static Func<T> Cache<T>(this Func<T> func, int cacheInterval)
{
var cachedValue = func();
var timeCached = DateTime.Now;
Func<T> cachedFunc = () => {
if ((DateTime.Now - timeCached).Seconds >= cacheInterval)
{
timeCached = DateTime.Now;
cachedValue = func();
}
return cachedValue;
};
return cachedFunc;
}
变量 cacheInterval, cachedValue 和 timeCached 绑定到缓存的函数并作为函数的一部分。这个可以让我们记住最后的值并确认被缓存多长时间。
下面的例子中我们可以看到如何使用这个扩展来缓存函数值和返回当前时间:
Func<DateTime> now = () => DateTime.Now;
Func<DateTime> nowCached = now.Cache(4);
Console.WriteLine("\tCurrent time\tCached time");
for (int i = 0; i < 20; i++)
{
Console.WriteLine("{0}.\t{1:T}\t{2:T}", i + 1, now(), nowCached());
Thread.Sleep(1000);
}
惰性求值
C#语言小部分采用了非严格求值策略,大部分还是严格求值策略。
非严格求值的例子:逻辑或
static void NonStrictEvaluation()
{
bool ret = true || DoSomeThing() > 0;
Console.WriteLine("Done!");
}
严格求值策略:首先定义一个返回Int的方法
static int DoSomeThing()
{
Console.WriteLine("DoSomeThing Function Excuted");
return 7;
}
static void StrictEvaluation(bool flag, int dsVal)
{
if (flag)
Console.WriteLine("dsVal result value is {0}", dsVal);
Console.WriteLine("Done!");
}
调用:StrictEvaluation(false, DoSomeThing());
输出:
DoSomeThing Function Excuted
Done!
虽然flag为false,但是DoSomeThing还是被执行了,如何改变?
将第二个参数改成方法:
static void LazyEvaluation(bool flag,Func<int> dsthing)
{
if (flag)
Console.WriteLine("dsthing result value is {0}", dsthing());
Console.WriteLine("Done!");
}
调用:StrictEvaluation(false, DoSomeThing);
如果flag为true,并且其中调用两次,那么DoSomeThing就会被执行两次。再次修改
static void LazyEvaluationEx(bool flag, Func<int> dsthing)
{
Lazy<int> lzDshting = new Lazy<int>(dsthing);
if (flag)
Console.WriteLine("dsthing square result value is {0}", lzDshting.Value * lzDshting.Value);
Console.WriteLine("Done!");
}
参考 http://www.cnblogs.com/yaozhenfa/category/652982.html