相信很多人都听说过函数式编程,提到函数式程序设计,脑海里涌现出来更多的是Lisp、Haskell等语言,而C#,似乎我们并不把它当成函数式语言,其实,函数式程序设计并不是只针对某个特定的程序设计语言,而C#,也正一步步使用函数式丰富自己的语言结构,帮助人们更好的实现期望的结果。
函数式程序设计
函数式程序设计把重点放在函数的应用上,函数式程序设计人员以函数为基本模块来建立新函数,这并不是说没有其他语言的成分,而是说函数是程序体系创建的主要构造。
引用透明(Referential transparency)是函数式程序设计领域中的一个重要思想。一个引用透明的函数的返回值只取决于传递给它的参数的值。这正好与指令程序设计的基本思想相反。在指令程序设计中,程序的状态通常会影响函数的返回值。引用透明的函数的数学意义仅存在于函数式程序设计中,这样的函数称为纯函数,没有副作用。
函数式程序设计属于一种定向思维。如果我们愿意按某种方式去思考,则它可以给我们提供有趣的解决方案或者至少思考的源头,它们都与当前程序设计的许多实际问题有关。
C#无法做到像Lisp、Haskell或同属于.NET平台的F#那样很容易实现函数式程序设计,这点我们必须承认,但从各方面来讲,用C#实现函数式程序设计确实是有意义的。
C#函数式程序设计基础之函数与方法
由于C#的函数只能出现在类中,因此它们通常被称为方法。方法可以接受若干个参数,并且可以有一个返回值。
与许多面向对象语言一样,C#类中的方法可以是实例方法,也可以是类方法。而在纯函数式程序设计中,没有类,也没有类的实例——当然,有很多方法保存数据,但通常不是用类来保存数据,它们总是在许多方面表现出不同。
在面向对象环境中,所有其他元素只能出现在类和对象的内部(对象是类实例的另一个说法);而在函数式程序设计中,所有其他元素都出现在函数内部。有些数据保存在函数的局部变量中,就像C#那样定义在方法内部的变量,但这并不是保存数据最理想的方法。
F#把类级别的成员当成全局成员,同时由于得到特殊语法的支持,程序员不需要考虑实际发生的“转换”过程,遗憾的是,在C#中无法实现这一点,但是解决方法是一样的。
为了调用全局级的函数(或者任何其他作用域的函数),必须在类内创建类级别的成员。这些成员要用static关键字。由于它们都封装在类中,因此类中的成员有不同的可见度。大多数函数式设计环境都有不同的封装级别——如模块级或命名空间级——因此除了C#中一些比较复杂的语法外,实际上两者没有多大的区别。
有些函数式语言使用顶级函数或者允许导入模块或命名空间,这样就不需要函数调用的修饰符:
DoSomething "string paramers"
在C#中,这样的调用总是需要一个修饰符,即类名,除非这个函数出现在同一个类的内部:
SomeClass.DoSomething("string paramers");
C#函数式程序设计基础之重用函数
在计算机程序设计中,重用是一个非常重要的综合问题。函数并不是可重用性的唯一方法,特别在面向对象程序设计中,很快出现了其他方法。作为C#的一个内置功能,它只支持函数的重载作为函数级模块化的直接办法,C#4.0支持命名参数和可选参数,因此重载函数的解析过程变得相当复杂,特别当它与其他相关方法(如在方法调用时进行泛型类型推断)一起使用时。
下面举一个重载方法的简单例子:
1 int Add(int x, int y) 2 { 3 return x + y; 4 } 5 6 int Add(int x, int y,int z) 7 { 8 return Add(x, y) + z; 9 } 10 11 double Add(double x, double y) 12 { 13 return x + y; 14 } 15 16 double Add(double x, double y, double z) 17 { 18 return Add(x, y) + z; 19 }
在这个例子中,我们很清楚地看出为什么重载与重用有关:它允许程序员创建与原函数类似的新函数,同时尽可能利用原函数已有的功能。
C#函数式程序设计基础之匿名函数与Lambda表达式
并非所有的函数都重要到需要一个名称,一般而言,这些函数并不是类级别的函数,它们没有名称,这些函数的引用地址保存在变量中,因此只要有这些函数的引用地址就可以调用它们。
从技术上讲,匿名函数肯定要受到某些限制。很遗憾的是,其中之一就是它们不可以是泛型,它们也不可以用来实现迭代器。除此之外,匿名函数几乎可以包括所有做任何“正常”方法可以做的事情。
1 static void AnonymousMethods() 2 { 3 BubbleSorter.IsAGeaterThanBDelegate compareInt = 4 delegate(object a, object b) 5 { 6 return ((int)a) > ((int)b); 7 }; 8 }
以上是C#2.0的代码,可以看出,关键字delegate委托代替了方法名。参数列表和方法体还是与前面一样。这个匿名方法也可以改写成如下形式,这里用了C#3.0的Lambda表达式语法:
1 BubbleSorter.IsAGeaterThanBDelegate compareInt2 = 2 (object a, object b) => { return ((int)a) > ((int)b); };
这段代码较短,因为少了delegate关键字,方法体已经写成一行格式。Lambda表达式中的主体=>运算符右侧的部分。可以采取若干方法进一步简化代码。首先, 可以省略参数类型,因为编译器可以根据委托类型的声明语句推断出参数的类型:
1 BubbleSorter.IsAGeaterThanBDelegate compareInt2 = 2 (a, b) => { return ((int)a) > ((int)b); };
其次,由于函数除了返回一个值外不执行任何操作,因此可以把函数体转换为表达式体,并且可以利用隐式返回:
BubbleSorter.IsAGeaterThanBDelegate compareInt2 = (a, b) =>(int)a) > ((int)b);
表达式体很有用。因为有了它,在函数式程序中本来需要用函数实现的某个操作现在可以简化为一个表达式。与函数一样,表达式体也要接受参数并返回一个值。表达式体不可以包含任何与返回值求值无关的代码(即只要有一个返回值就行,遗憾的是,经常在表达式体中使用没有返回值的表达式)。
前面的例子如果使用其中一个泛型委托类型,就可以变成如下的形式:
1 Func<object,object,bool> compareInt3= 2 (a, b) => ((int)a) > ((int)b);
这个委托需要接受两个object类型的参数,返回一个bool值。使用泛型委托类型的另一个好处是,它们的参数类型更容易看明白,因为它们在委托类型中采用显式声明,而且编译器可以为Lambda表达式推断出它们的类型。
使用Lambda表达式时,有一个细节需要牢记:只有当所有类型都确定后,编译器才会根据几个比较复杂的准则进行类型推断。编译器并不是总能正确地推断出类型,因此,如果所有的类型都确定了,编译器的要求就满足了:
1 Func<int, int, int> add = 2 (a, b) => a + b;
在这个Lambda表达式中不可以使用var关键字,C#中,编译器必须能够在声明的位置推断出参数的类型,对于下面的语句则无法推断出参数的类型:
1 var add = 2 (a, b) => a + b;
函数式程序设计语言要求,在所有与类型推断有关的情形中都需要像这样的显式说明。这在某些C#程序员看来是遗憾的。
C#函数式程序设计基础之扩展方法
扩展方法是静态类中用特殊方法表示的静态方法:
1 namespace CompanyWideTools 2 { 3 public static class StringHelper 4 { 5 public static string ConCat(this string[] strings, string separator) 6 { 7 bool first = true; 8 var builder = new StringBuilder(); 9 foreach (var s in strings) 10 { 11 if (!first) 12 builder.Append(separator); 13 else 14 first = false; 15 builder.Append(s); 16 } 17 return builder.ToString(); 18 } 19 } 20 }
表示Concat是一个扩展方法的标志是在该方法的参数列表中使用this关键字。这个关键字是C#专有的,用于命令编译器给这个方法中增加ExtensionMethodAttribute属性。可以像调用静态方法那样调用扩展方法:
1 string[] strings = new[] 2 { 3 "to","be","or","not","to","be" 4 }; 5 6 Console.WriteLine(StringHelper.ConCat(strings," "));
然而,由于它是扩展方法,因此也可以像下面这样调用:
1 Console.WriteLine(strings.ConCat(""));
当我们需要充分利用扩展方法的优点时,这种调用方法比较简单。
每个扩展方法都有一个可扩展的特定类型:第一个参数的类型,即用this标志的那个参数。这个标志只可以用于第一个参数,不可以用于其他参数。扩展方法的第一个参数可以是一个基类类型或者一个接口,甚至可以是System.Object中的对象。扩展方法也可以是泛型的,他们可以扩展泛型类型。
C#函数式程序设计基础之引用透明
在指令式程序设计中,这些模块的基本作用是防止代码重复,把代码分解成更容易管理的函数级模块。指令式程序设计的最大问题之一是随着时间的推移,模块会变得越来越大。由于指令式程序设计把重点放在执行序列上,因此函数和方法的引用总是不透明的。
引用透明:表达式可以用表达式的值取代而不会影响程序,也就是不会影响使用此替换操作的算法的最终结果。