11.1.1 重用常见的代码块

一个最好的编程实践,就是避免在多个地方重复相同的代码。如果有两个类似程序,就值得考虑把它们合并成一个;新的程序需要有新的参数,描述代码按照不同于原来的路径。

在函数式编程中,我们有一个强大的武器:函数值作为参数值使用的能力,这使得函数或者方法的参数化更容易。为了演示,假设我们有一个关于城市的信息数据库,我们要用数据生成几份报表。

我们先写一个加载数据的函数。为了使示例简单,我们不考虑使用数据库;当然,你可以自己去做,只要使用标准的.NET 数据库API,可顺利地使用F#。这里,我们就使用下面的函数,简单地返回我们手工创建的列表:

let loadPlaces() =

[ ("Seattle",594210); ("Prague", 1188126)

("NewYork", 7180000); ("Grantchester", 552)

("Cambridge",117900) ]

这个数据结构虽然很简单,但已经接近于实际使用的应用程序。我们没有使用元组来保存名字和人口,而是可能使用记录或对象类型。清单11.1 是两个函数,从数据生成报表:一个输出超过一百万居民的城市名单,另一个输出按字母顺序所有城市。在实际应用程序中,可能会生成HTML 报表,但为了尽量简单,我们只以纯文本方式输出到控制台。

清单11.1 输出城市信息(F#)

let printBigCities() =

letplaces = loadPlaces()

printfn"===== Big cities ====="   [1] <--输出报表标题

letselected = List.filter (fun (_, p) -> p > 1000000) places  [2] <-- 列出人口百万的城市

forname, population in selected do

printfn " - %s (%d)" name population

let printAllByName() =

letplaces = loadPlaces()

printfn"===== All by name ====="   [3]<-- 输出报表标题

letselected = List.sortBy fst places   [4] <-- 按城市名字排序

forname, population in selected do

printfn" - %s (%d)" name population

这两个函数结构非常相似,但也有一些差异。最重要的不同是选择输出城市列表的方式。printBigCities 函数使用List.filter 来筛选城市[2],而printAllNames 使用List.sortBy 对城市列表重新排序[4];另外,输出的报表标题不同。

两者有许多共同的方面。首先,两个函数调用loadPlaces 以获得城市的集合;然后,以某种方式处理集合;最后,输出结果到屏幕。

重构这段代码,我们需要写一个的函数,能够用于这两项任务,还要让代码更具扩展性。使用完全不同策略的打印函数,应该是可能的。如果我们创建一个填字游戏,可能会寻找指定长度、有特定字母开头的城市。这样,我们应该能够提供几乎任何策略作为参数值。函数编程提供了一个很好的方法,使用函数实现这种参数化。

清单11.2 是高阶函数printPlaces,我们很快就会看到,它可以用前面清单中的两个函数来代替。

清单11.2 可重用的打印函数(F# Interactive)

> let printPlaces title select =

letplaces = loadPlaces()

printfn"== %s ==" title   [1]

letsel = select(places)   [2]

forname, pop in sel do

printfn " - %s (%d)" name pop

;;

val printPlaces : string –>                     | [3]

((string* int) list -> #seq<string * int>) –> unit  |

新的函数有两个参数,指定原有两个函数彼此不同的地方。第一个是报表的标题[1],第二个是选择要打印城市的函数[2]。通过输出的类型签名[3],我们可以看到更多有关这个函数的类型信息。

这个函数的参数是元组的列表,元组由字符串和整数组成,这是表示城市的数据结构,与我们所期待的函数返回类型是相同的,因为函数以同样的数据格式返回城市的集合,但是,F# 推断出的类型是#seq<string * int>。差别只在于推断的类型是 #seq,而不是 list。

这种选择有两个重要的原因:

■seq<‘a> 是所有集合都实现的共同接口,是标准的.NET  IEnumerable<T>类型的别名。这样,函数可以返回列表,同样也可以返回数组,因为,我们唯一需要的是能够遍历集合中所有元素。在下一章,我们将更详细讨论序列,如果我们知道LINQ to Object,应熟悉这一领域:大多数常见的操作都使用(返回)IEnumerable <T>。

■# 号表明,返回的集合不必显式向上转换为seq<‘a> 类型,就是说,可以提供函数,它实际的类型返回list<‘a>。在严格意义上说,这是不同类型的函数,但是,# 号增加了一些重要的灵活性。大多数时候,不必非常担心,它只表示,编译器推断出,代码可能是泛型。

我们既然有了函数,需要展示它真正可以代替我们开头用的两个函数。清单11.3 表明。我们可以通过提供参数值,得到与原有函数相同的行为。

清单11.3 使用‘printPlaces’ 函数(F#)

// Writing lambda function explicitly

printPlaces "Big cities" (fun places–>     [1]

List.filter(fun (_, s) -> s > 1000000) places)

// Using partial function application

printPlaces "Big cities" (List.filter(fun (_, s) -> s > 1000000))   [2]

printPlaces "Sorted by name" (List.sortByfst)    [3]

第一例子中唯一重要的方面是[1],第二个参数使用了lambda 函数。它把数据集作为参数值,使用List.filter 筛选出只超过100 万居民的城市;第二个例子表明[2],使用散函数应用,可以写出更简洁的调用;在最后一个例子中[3],使用List.sortBy 对集合进行排序。

在清单11.3 中可以看到,使用在重构过程中创建的函数是很容易的,只需要第二个参数指定不同的函数,就可以用来打印出不同的列表。

我们在这一节进行的重构,依靠使用函数作为参数的能力。C# 有相同的能力,因此,在这里,使用委托,可以有效地应用同样的重构。进行数据转换的参数,我们既可以指定为lambda 表达式,也可以从有合适签名的方法创建委托。

重构代码时的另一个重要的函数原则,是使用不可变数据。这里的影响,相比能够使用函数表达行为差异,要稍许有点微妙,但可能更重要。

时间: 2025-01-13 06:03:44

11.1.1 重用常见的代码块的相关文章

常见的代码块标识格式

目前主流代码块标识格式 使用{ } 如:java.C等编程语法 开始标识符 + 结束标识符 如:Lua.shell.SQL :表示开始 + 代码缩进 如:python 这其中主要是语言设计者个人喜好决定的,最终解释器(或编译器)来完成不同格式代码的解析,每个编程语言都有自己的解释器(或编译器) 原文地址:https://www.cnblogs.com/linkenpark/p/11684153.html

子父类 构造代码块 静态代码块 初始化过程

1 class Fu{ 2 Fu(){//调用show方法 被子类重写 num=0; 3 System.out.println("Fu constructor....."); 4 show();//实际是子类方法//zi..show 0 5 } 6 static{ 7 8 System.out.println("fu静态代码块"); 9 } 10 { 11 System.out.println("FU构造代码块"); 12 } 13 void

java中的代码块是什么意思,怎么用

代码块是一种常见的代码形式.他用大括号"{}"将多行代码封装在一起,形成一个独立的代码区,这就构成了代码块.代码块的格式如下: 方法/步骤 普通代码块:是最常见的代码块,在方法里用一对"{}"括起来的数据,就是普通的代码块,   构造代码块:是在类中直接定义的,用"{}"括起来的代码.每次调用构造方法前执行,都会先执行构造代码块.   静态代码块:他在类中的成员位置,用"{}"括起来的代码.只不过他用了static修饰了,,且

Java线程安全与同步代码块

因为在电商网站工作的原因,对于秒杀.闪购等促销形式相当熟悉.无外乎商家拿出一定量的库存去做所谓的"亏本买卖",其目的是用有限的库存去吸引无限的流量.然而,我却碰到过因为系统问题,导致秒杀品超卖的情况.可怜的商户其实只提供了10双9.9元的童鞋做秒杀,却在瞬间内卖出了1000双! 类似这样的问题,在非线程安全的程序设计中十分常见,我们下面运行一个秒杀的程序: public class SalesThread implements Runnable { private int stock

java代码块的理解

最近在复习java基础,在看到java代码块的时候,忽然发现自己貌似对于java代码块一无所知,于是赶紧对着一些资料实战演练了一把. 对于java代码块,不难根据名称看出其实就是一些java语句的集合,以{}的形式出现,共有4中形式: 1.类的方法体 这是我们最常见的一种java代码块,形式如下: 1 public class Boke { 2 public void say(){ 3 System.out.println("我就是代码块的内容啦"); 4 } 5 } say的方法体{

修饰符-包-内部类-代码块执行顺序

1.访问权限修饰符     从大到小的顺序为:public--protected--default--private     private--只能在同一类中使用;     default--不用写出来,默认不加.可以被同一包中的类使用     protected--可以被不同包的子类使用     public--可以被不同包的其它类使用 2.各种修饰符的修饰对象(可修饰哪些:类/接口/方法/属性)(多个修饰符连用是可以没有顺序的!)     1)访问权限修饰符:public/default--

iOS学习之代码块(Block)

代码块(Block) (1)主要作用:将一段代码保存起来,在需要的地方调用即可. (2)全局变量在代码块中的使用: 全局变量可以在代码块中使用,同时也可以被改变,代码片段如下: 1 int local = 1;//注意:全局变量 2 void (^block0)(void) = ^(void){ 3 local ++; 4 NSLog(@"local = %d",local); 5 }; 6 block0(); 7 NSLog(@"外部 local = %d",lo

静态代码块、构造代码块、构造函数的执行

1 public class Str { 2 3 static { 4 System.out.println("static{1}"); 5 } 6 static { 7 System.out.println("static{2}"); 8 } 9 { 10 System.out.println("{}"); 11 } 12 13 Str() { 14 System.out.println("Str()"); 15 } 16

java普通代码块、静态代码块、默认构造方法的执行顺序

1 package test; 2 3 class Parent{ 4 { 5 System.out.println("父类普通代码块"); 6 } 7 static{ 8 System.out.println("父类静态代码块"); 9 } 10 public Parent(){ 11 System.out.println("父类默认构造代码方法"); 12 } 13 } 14 class Child extends Parent{ 15 {