RxSwift学习之旅 - 初章

函数式编程

在开始学习RxSwift之前,我们需要了解什么是函数式编程以及函数式编程的思想,便于你在后面的学习中更好的去掌握和使用RxSwift进行函数响应式编程。

如果你已经学习了Swift,你会发现它吸收众多语言的优点,并且在语言层面更好的去保障代码的安全,当然也可以在Swift中看到mapfilterreduce等函数式特性。它们接受一个转换函数对每一个元素进行转换处理然后返回一个新的对象。

所以函数在Swift中是一等值(first-class-values)或者一等公民,你可以把它和一个普通的变量对等起来,它可以作为参数被传递到其它函数,也可以作为其它函数的返回值。

或许讲到这里,你还不是很明白这个一等值以及它的魅力所在,下面来看一个《functional-swift》中提到的一个例子, 相信你在看完这个例子对Swift中的函数有一个全新的概念。

战舰游戏

现在在编写一个战舰类游戏,需要确定当前战舰的攻击范围,有以下几点:

  1. 这个范围在该战舰的攻击范围
  2. 这个范围不能离自己太近
  3. 这个范围不能离友方战舰太近

如下图:

image

现在需要计算的范围就是图中的阴影区域。

常规套路

问题分解:首先假设战舰在原点,计算以原点为中心的一个范围,先定义两种类型,距离和点。(尽量把变量定义成有意义的名字)

1
2
3
4
5
6
typealias Distance = Double
struct Position{
var x: Double
var y: Double
}

Position添加一个函数inRange(_:),检验一个点是否在某个范围区域。

1
2
3
4
5
extension Position{
func inRange(range: Distance) -> Bool{
return sqrt(x * x + y * y) <= range
}
}

问题进化:当战舰不在原点的情况,需要把Position作为战舰Ship的一个属性。

1
2
3
4
5
struct Ship{
var position: Position
var firingRange: Distance //可攻击范围
var unsafeRange: Distance //自身的不安全范围
}

Ship添加一个canSafelyEngageShip(_:),检测另一个战舰是否在可攻击范围内,但是又不能距离自身太近,也就是unsafeRange

1
2
3
4
5
6
7
8
extension Ship{
func canSafelyEngageShip(target: Ship) -> Bool {
let dx = target.position.x - position.x
let dy = target.position.y - position.y
let targetDistance = sqrt(dx * dx + dy * dy)
return targetDistance <= ringRange && targetDistance > unsafeRange
}
}

最后还要避免战绩攻击范围不能距离友方战舰太近,也就是友方战舰不能和敌方战舰太近。所以canSafelyEngageShip进阶为:

1
2
3
4
5
6
7
8
9
10
11
12
13
extension Ship{
func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
let dx = target.position.x - position.x
let dy = target.position.y - position.y
let targetDistance = sqrt(dx * dx + dy * dy)
let friendlyDx = friendly.position.x - target.position.x
let friendlyDy = friendly.position.y - target.position.y
let friendlyDistance = sqrt(friendlyDx * friendlyDx + friendlyDy * friendlyDy)
return targetDistance <= ringRange
&& targetDistance > unsafeRange
&& (friendlyDistance > unsafeRange)
}
}

解决问题:现在发现代码已经变得不是那么好维护了,而且同一个函数中存在处理同样问题的函数(计算距离)。当然可以把这些计算放到Position里面。

1
2
3
4
5
6
7
8
extension Position{
func minus(p: Position) -> Position{
return Position(x: x - p.x, y: y - p.y)
}
var length: Double{
return sqrt(x * x + y * y)
}
}

那么代码可以变成这样:

1
2
3
4
5
6
7
8
extension Ship {
func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
let targetDistance = target.position.minus(position).length
let friendlyDistance = friendly.position.minus(target.position).length
return targetDistance <= ringRange
&& targetDistance > unsafeRange
&& (friendlyDistance > unsafeRange)
} }

函数式思维

虽然上面已经解决了我们的问题,但是通过函数式的思维把上面解决的问题抽象化,前面提到函数在Swift里面是一等值,所以可以把我们要解决的问题抽象化,用函数的方式去声明。

比如在上面遇到的一个问题是判断一个点是不是在某个区域内。问题分解:

输入: 某个点 Position
输出: 这个点是不是在该区域
怎么描述这个区域:暂时还不知道,它可能是一个圆,也可能是一个矩形或者其它的形状。

写成函数是这样:

1
2
3
func pointInRange(point: Position) -> Bool{
//怎么描述这个区域,暂时还不知道
}

因为函数就是一等公民,所以可以定义成这样:

1
typealias Region = Position -> Bool

怎么描述一个区域,你到时跟我说吧,反正你给一个点,我就能告诉你这个点在不在你描述的这个区域,其实就是定义的一个区域范围。
假设战舰还是在原点,定义一个在圆心在圆点的一个圆表示它的攻击范围:

1
2
3
func circle(radius: Distance) -> Region{
return {point in point.length <= radius}
}

然后把圆心的位置加进去:

1
2
3
func circle(radius: Distance, center: Position) -> Region{
return {point in point.minus(center).length <= radius}
}

这样每次在创建这个圆的时候都要指定圆点,是不是可以通过对一个在圆点的圆做一个变换之后来得到一个新的圆呢? 比如这样:

1
2
3
func shift(region: Region, offset: Position) -> Region {
return { point in region(point.minus(offset)) }
}

传一个区域和移动的偏移来生成一个新的圆。代码可以从:

1
Region circle = circle(10,Position(x:5, y:5))

变成:

1
2
Region circle = circle(10)
Region newCircle = shift(circle, Position(x:5, y:5))

尽量把一些可变的东西抽象出来!

然后可以基于这个Region操作,比如反转区域:

1
2
3
func invert(region: Region) -> Region {
return { point in !region(point) }
}

取交集:

1
2
3
func intersection(region1: Region, _ region2: Region) -> Region {
return { point in region1(point) && region2(point) }
}

取并集:

1
2
3
func union(region1: Region, _ region2: Region) -> Region {
return { point in region1(point) || region2(point) }
}

在第一个区域不在第二个区域:

1
2
3
func difference(region: Region, minus: Region) -> Region {
return intersection(region, invert(minus))
}

针对一开始的函数canSafelyEngageShip可以改写成:

1
2
3
4
5
6
7
8
extension Ship {
func canSafelyEngageShip(target: Ship, friendly: Ship) -> Bool {
let rangeRegion = difference(circle( ringRange), minus: circle(unsafeRange))
let ringRegion = shift(rangeRegion, offset: position)
let friendlyRegion = shift(circle(unsafeRange), offset: friendly.position)
let resultRegion = difference( ringRegion, minus: friendlyRegion)
return resultRegion(target.position)
} }

在上面的例子中,还可以把Region作为一个struct,从而写出:

1
rangeRegion.shift(ownPosition).difference(friendlyRegion)

响应式编程

那么什么是响应式编程,来看个简单的例子:

1
2
3
4
5
6
7
int a = 3;
int b = 4;
int c = a + b;
NSLog(@"c is %d", c); // => 7
a = 5;
b = 7;
NSLog(@"c is %d", c); // 仍然是7

在这里把a,b,c当成某个状态,ca,b两个状态的组合,正常的编程中,在计算了c的状态之后,再去改变a,b的状态是不会影响到c的状态的。所以在正常编程中我们要去记录很多状态并及时更新状态,比如网络请求的状态,下拉刷新的状态。各种各样的事件响应方式,无形中增加了编码的复杂度。而在响应式编程中,每一个状态的改变都会发出一个信号,更新与之关联的状态。

比如上面a,b的状态改变之后能够及时更新c的状态,而不用重新通过a+b计算c的状态了,在响应式编程中,我们可以创建很多被观察者对象,当这些对象的状态发生改变时,我们能够链式的去更新和处理各个状态的变化和数据。

后面我们会讲到RxSwift中的被观察者和订阅者。

总结

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

AloneMonkey wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!