您的位置:首页 > 编程语言 > C#

C# 函数式编程

2015-10-30 09:49 405 查看
编程语言范式
许多现存的编程语言都可基于其计算模型加以分类,归入某些语言族,或者属于某种编程范式。按照不同的规则,可以有多种分类的方法,而且不同的学者对某些语言的具体归属也有不同的意见。
给出一种系谱:
说明式(Declarative)
  函数式 Lisp, ML, Haskell
  数据流 ld, Val
  逻辑式 Prolog
  基于模板 XSLT
命令式(Imperative )
  冯诺依曼 C, Ada, Fortran
  脚本式 Perl, Python, PHP
  面向对象 Smalltalk, C++, Java, C#
有些编程范式并不能按以上的方法进行分类,并没有列出。比如:元编程,泛型编程。另外还有一点就是并不是一种语言就只从属于一种编程范式,事实上有些语言本身就是为支持多范式设计的,比如:Lisp就同时支持函数式编程、面向对象、元编程。

函数式编程
定义
In computer science, functional programmingis a programming paradigm that treats computation as the evaluation ofmathematical 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>

函数式崛起
AndersHejlsberg,C#编程语言的首席架构师,2010年关于《编程语言的发展趋势及未来方向》演讲
How 与 What 的区别
命令式是在告诉计算机下一步要怎么怎么做(How);声明式是在描述我想要拿到手什么(What)。
从一个数组中找出所有的偶数
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表达式。

函数是一等公民
对象是面向对象的第一型,那么函数式编程也是一样,函数是函数式编程的第一型。
我们在函数式编程中努力用函数来表达所有的概念,完成所有的操作。
在面向对象编程中,我们把对象传来传去,那在函数式编程中,我们要做的是把函数传来传去,而这个,说成术语,我们把他叫做高阶函数。
高阶函数定义:
1:函数自身接受一个或多个函数作为输入
2:函数自身能输出(返回)一个函数。
C#中定义函数类型
函数对象必须是某种委托类型. 在C#中,我们可以定义强类型的委托类型或泛型的委托类型,委托可以代表跟这个委托类型有相同参数的方法(静态方法,类方法)的引用.

在使用LINQ的时候我们可以经常看到高阶函数。举个例子,如果你想将一个已有的序列使用一些函数转换为一个新的序列,你将使用类似LINQ的select函数(函数作为输入):
var squares = numbers.Select( num =>num*num );

部分应用函数(Partial Function)
将多个参数的函数进行拆分,拆成多个只有一个参数的函数。为什么要拆分,λ 演算。
示例:
常规的写法:
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(intx,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 fffffrom ffff");

如果想先传入Sql语句再传入SqlConnection:
Func<String,
Func<SqlConnection,
DataSet>> ExecSqlT = x => y =>ExecSql(y)(x);

同样,也可以通过Currey方法将已经实现的标准两参方法来转化为部分应用。这个过程有个专业的术语叫做Currey(柯里化),人名。

看一个函数:
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));

闭包
闭包是可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包”一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域)。在 Scala、Scheme、Common Lisp、Smalltalk、Groovy、JavaScript、Ruby、 Python、Go、Lua、objective
c 以及Java(Java8及以上)等语言中都能找到对闭包不同程度的支持。<百度百科>

C#中实现闭包,实际就是通过类,封装匿名方法及方法涉及的外部变量,提升生命周期。

IL验证
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();
}

IL结果:

缓存技术
怎样使用闭包来实现缓存。如果我们创建了一个用于缓存的接收函数就可以实现缓存,并返回一个在一段时间内缓存结果的新函数。以下列表显示的例子:

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\tCachedtime");
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 FunctionExcuted");
return 7;
}

static void StrictEvaluation(bool flag,
int dsVal)
{
if (flag)
Console.WriteLine("dsVal resultvalue 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 resultvalue 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 squareresult value is {0}", lzDshting.Value * lzDshting.Value);
Console.WriteLine("Done!");
}

尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码

不可变\可持久化数据结构

变量的标识与状态
一个变量的值是多少 是一个标识,不同的时刻该标识可能会对应不同的状态(值),实现可持久化数据,就是每次修改都是返回新的值,而不修改原来的值,会对有些情况的并发比较有效,实现无锁。

并发程序状态处理:共享可变性、隔离可变性、纯粹不可变性

.NET不可变数据结构
String、委托 每一次修改都是新实例

可持久化集合设计
如果是集合,每次都完全拷贝创建新对象,内存消耗上~~<
PersistentData Structures>
.net 提供了一个不可变数据集合(无锁访问)<MSDN>

共享可变性优化
细粒度锁、无锁
<非阻塞同步算法与CAS(Compareand Swap)无锁算法>
<无锁编程以及CAS>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: