购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2 一等函数

在当前 canSafelyEngageShip 的函数体中,主要的行为是为构成返回值的布尔条件组合进行编码。在这个简单的例子中,虽然想知道这个函数做了什么并不是太难,但我们还是想要一个更模块化的解决方案。

我们已经在 Position 中引入了辅助函数使几何运算的代码更清晰易懂。用同样的方式,我们现在要添加一个函数,以更加声明式的方式来判断一个区域内是否包含某个点。

原来的问题归根结底是要定义一个函数来判断一个点是否在范围内。这个函数的类型会像是下面这样的:

因为这个函数的类型非常重要,所以我们打算给它起一个独立的名字:

从现在开始, Region 类型将指代把 Position 转化为 Bool 的函数。严格来说这不是必需的,但是它可以让我们更容易理解在接下来即将看到的一些类型。

我们使用一个能判断给定点是否在区域内的 函数 来代表一个区域,而不是定义一个对象或结构体来表示它。如果你不习惯函数式编程,那么这可能看起来会很奇怪,但是要记住:在Swift中函数是一等值!我们有意识地选择了 Region 作为这个类型的名字,而非 CheckInRegion RegionBlock 这种字里行间暗示着它们代表一种函数类型的名字。 函数式编程 的核心理念就是函数是值,它和结构体、整型或是布尔型没有什么区别——对函数使用另外一套命名规则会违背这一理念。

我们现在要写几个函数来创建、控制和合并各个区域。

我们定义的第一个区域是以原点为圆心的圆( circle ):

当然,并不是所有圆的圆心都是原点。我们可以给 circle 函数添加更多的参数来解决这个问题。要得到一个圆心是任意定点的圆,只需要添加另一个代表圆心的参数,并确保在计算新区域时将这个参数考虑进去:

然而,如果我们想对更多的图形组件(例如,想象我们不仅有圆,还有矩形或其他形状)做出同样的改变,则可能需要重复这些代码。更加函数式的方式是写一个 区域变换函数 。这个函数按一定的偏移量移动一个区域:

调用 shift(region,offset:offset) 函数会将区域向右上方移动,偏移量分别是 offset.x offset.y 。我们需要的是一个传入 Position 并返回 Bool 的函数 Region 。为此,我们需要另写一个闭包,它接受我们要检验的点,这个点减去偏移量之后我们会得到一个新的点。最后,为了检验新点是否在 原来的 区域内,我们将它作为参数传递给 region 函数。

这是函数式编程的核心概念之一:为了避免创建像 circle2 这样越来越复杂的函数,我们编写了一个 shift(_:offset:) 函数来改变另一个函数。例如,一个圆心为 (5,5) 半径为 10 的圆,可以用下面的方式表示:

还有很多其他的方法可以变换已经存在的区域。例如,也许我们想要通过反转一个区域以定义另一个区域。这个新产生的区域由原区域以外的所有点组成:

我们也可以写一个函数将既存区域合并为更大、更复杂的区域。比如,下面两个函数分别可以计算参数中两个区域的交集和并集:

当然,我们可以利用这些函数来定义更丰富的区域。 difference 函数接受两个区域作为参数——原来的区域和要减去的区域——然后为所有在第一个区域中且不在第二个区域中的点构建一个新的区域:

这个例子告诉我们,在Swift中计算和传递函数的方式与整型或布尔型没有任何不同。这让我们能够写出一些基础的图形组件(比如圆),进而能以这些组件为基础,来构建一系列函数。每个函数都能修改或是合并区域,并以此创建新的区域。比起写复杂的函数来解决某个具体的问题,现在我们完全可以通过将一些小型函数装配起来,广泛地解决各种各样的问题。

现在让我们把注意力转回原来的例子。关于区域的小型函数库已经准备就绪,我们可以像下面这样重构 canSafelyEngageShip(_:friendly:) 这个复杂的函数:

这段代码定义了两个区域: firingRegion friendlyRegion 。通过计算这两个区域的差集(即在 firingRegion 中且不在 friendlyRegion 中的点的集合),我们可以得到我们感兴趣的区域。将这个区域函数作用在目标船舶的位置上,我们就可以计算所需要的布尔值了。

面对同一个问题,与原来的 canSafelyEngageShip1(_:friendly:) 函数相比,使用 Region 函数重构后的版本是更加 声明式 的解决方案。我们坚信后一个版本会更容易理解,因为我们的解决方案是 装配式 的。你可以探究组成它的每个部分,例如 firingRegion friendlyRegion ,看一看它们是如何被装配并解决原来的问题的。另一方面,原来庞大的函数混合了各个组成区域的描述语句和描述它们所需要的算式。通过定义我们之前提出的辅助函数将这些关注点进行分离,显著提高了复杂区域的组合性和易读性。

能做到这样,一等函数是至关重要的。虽然Objective-C也支持一等函数,或者说是 block ,也可以做到类似的事情,但遗憾的是,在Objective-C中使用block十分烦琐。一部分原因是因为语法问题:block的声明和block的类型与Swift的对应部分相比并不是那么简单。在后面的章节中,我们也将看到泛型如何让一等函数更加强大,使其远比在Objective-C中用block实现更加容易。

我们定义 Region 类型的方法有它自身的缺点。我们选择了将 Region 类型定义为简单类型,并作为 Position-> Bool 函数的别名。其实,我们也可以选择将其定义为一个包含单一函数的结构体:

接下来我们可以用extensions的方式为结构体定义一些类似的函数,来代替对原来的 Region 类型进行操作的那些函数。这可以让我们能够通过对区域进行反复的函数调用来变换这个区域,直至得到需要的复杂区域,而不用像以前那样将区域作为参数传递给其他函数:

这种方法有一个优点:它需要的括号更少。再者,在这种方式下Xcode的自动补全在装配复杂的区域时会十分有用。不过,为了便于展示,我们选择了使用简单的类型别名以突出在Swift中使用高阶函数的方法。

此外,值得指出的是,现在我们不能够看到一个区域是如何被构建的:它是由更小的区域组成的吗?还是只是一个以原点为圆心的圆?我们唯一能做的是检验一个给定的点是否在区域内。如果想要形象化一个区域,我们只能对足够多的点进行采样来生成(黑白)位图。在后面的章节中,我们将使用另外一种设计来帮助你解答这些问题。 prNzQYHXGxGMYln4N5FAkgrfivRvHnsS0c5gtj/Pa0AEK8MrN80zzCBLX+EGLLTV

点击中间区域
呼出菜单
上一章
目录
下一章
×