一个减法的故事:Kotlin 扩展函数 ,Operator 和 性能优化

770 阅读4分钟

前言

在写自定义控件的时候,有时会需要对PointF对象进行一定操作,计算两个点之间的水平间距和垂直间距。

简化需求也就是要算出两个点之间的差值。

用代码实现大概是这样的

fun minusPoint(p1: PointF, p2: PointF): PointF {
    val dx = p1.x - p2.x
    val dy = p1.y - p2.y
    return PointF(dx, dy)
}

//使用
val p = minusPoint(p1,p2)

第一次修改

这样的写法太Java了,因为我们用到的是Kotlin,我们可以改成这样

fun minusPoint(p1: PointF, p2: PointF): PointF = PointF(p1.x - p2.x, p1.y - p2.y)

//使用
val p = minusPoint(p1,p2)

第二次修改

当然,这样也不够好。我们使用Kotlin的扩展函数为PointF这个对象添加上一个扩展函数

fun PointF.minusPoint(p2: PointF): PointF = PointF(this.x - p2.x, this.y - p2.y)

//使用
val p = p1.minusPoint(p2)

这样的调用看起来可读性高了非常多。

第三次修改

因为PointF自带了offset的方法

public final void offset(float dx, float dy) {
  x += dx;
  y += dy;
}

所以我们将可以改成这个样子

fun PointF.minusPoint(p2: PointF): PointF = PointF().apply {
    this.offset(-p2.x, -p2.y)
}

//使用
val p = p1.minusPoint(p2)

第四次修改

有编程经验的小伙伴可能从第一次就发现了这个函数的一个“问题”,就是每次都会创建一个新的PointF对象。所以我们还可以对它进行一次“优化”

fun PointF.minusPoint(p2: PointF): PointF = this.apply {
    this.offset(-p2.x, -p2.y)
}

//使用
val p = p1.minusPoint(p2)

这样每次调用都不会产生新的对象,直接使用原来的对象就可以了。一切都看起来很美妙。

第五次修改

我们再次回到我们一开始的时候,我们一开始需要解决的问题是“计算两个点的差值”,那么从语义上来讲。是不是可以简单的描述成为这样

val p1: Point
val p2: Point
val p = p1 - p2 

了解Kotlin 的operator的同学可能从第一次看到需求的时候就想到了-操作符。

很明显 ktx中就有PointF的扩展操作符。

/**
 * Offsets this point by the negation of the specified point and returns the result
 * as a new point.
 */
inline operator fun PointF.minus(p: PointF): PointF {
    return PointF(x, y).apply {
        offset(-p.x, -p.y)
    }
}

//使用
val p = p1 - p2

再一次被Kotlin 甜到 !

第六次修改

细心的朋友发现,这个扩展操作符每次都返回了一个新的对象。

那是不是ktx这个函数写的不好?

其实不是这样的,现在回到我们的第四次的“优化”。

fun PointF.minusPoint(p2: PointF): PointF = this.apply {
    this.offset(-p2.x, -p2.y)
}

//使用
val p = p1.minusPoint(p2)

现在我们来考虑一个问题,我们使用了p1对象减去p2的获得了一个对象p ,这时p其实就是p1,而它们的属性此时已被改变。如果这时,再去使用p1去做一些其他操作,显然就和预期得到的结果不一样了。

发现问题所在了吗?我们的优化“减法”改变被减数,这显然是不合理的。

所以我们在第五次的修改是不太合理的,但是我又不想用第六次的方案,因为它的确额外的对象,我就是饿死,死在外面,也不会吃这个语法糖的?!。

那么应该怎么办呢?

上面我们说到,一个减法是不应该去改变被减数的,减法得到的值理所当然是一个新的值。

那么是否我们就只能这样了呢?当然不是,我们再次回到我们的需求,“获得两个点之间的差值”,其实这句需求还可以再增加完善一些,“获得两个点之间的差值,为了不产生新的对象可以直接修改其中一个点的值”

那么到这里可以发现,我们有一个非常合适的操作符来描述它,也就是 -=

直接来上代码

inline operator fun PointF.minusAssign(p: PointF) {
    this.apply {
        offset(-p.x, -p.y)
    }
}

//使用,没有返回值
p -= p2

btw,由于传入的参数不是函数类型,这里的inline是多余的。

由于没有返回值,那么我们可以这样调用

val p1 = p.apply {
    this -= center
}

至此,我们的减法就算完成了。

通过这个减法,我得到了什么?

  • 了解了kotlin的operator写法
  • 了解了kotlin的inline的一些规则
  • 函数如果会对传入参数进行修改,需要谨慎是否真的应该这样做。