引子
假设女生的择偶标准如下:未婚且岁数比我大,如果对方是本地帅哥则要求年薪>10万,如果对方非本地则要求岁数不能超过40岁,且年薪在60万以上。(BMI 在 20 到 25 之间的定义为帅哥)
对业务进行抽象
- 将候选人抽象成
data
类:
data class Human( val age:Int, //年龄 val annualSalary:Int,//年薪 val nativePlace:String, //祖籍 val married:Boolean, //婚否 val height:Int,//身高 val weight:Int, //体重 val gender:String//性别 )
- 定义筛选函数
fun filterMan( man: List<Human>, // 候选男生 women: Human, // 女生 predicate: (Human, Human) -> Boolean // 筛选条件 ) { man.filter { predicate.invoke(it, women) }.forEach { // 打印成功匹配男生 Log.v(“good match”, “man = $it”) } }
函数接收三个参数:man 表示一组男生,women 表示女生,predicate 表示女生筛选标准。
其中第三个参数的类型是函数类型
,用一个 lambda (Human, Human) -> Boolean
来描述,它表示该函数接收两个 Human 类型的输入并输出 Boolean。这是 Kotlin 中独有的语法高阶函数
,即函数的参数可以是另一个函数。
约定
filterMan() 函数体中调用了系统预定义的filter()
,它的定义如下:
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> { //构建空列表 return filterTo(ArrayList<T>(), predicate) } public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C { //遍历集合向列表中添加符合条件的元素 for (element in this) if (predicate(element)) destination.add(element) return destination }
filter() 接收一个函数类型参数predicate
,即筛选标准,该类型用 lambda 描述为(T) -> Boolean
,即函数接收一个列表对象并返回一个 Boolean。
filter() 遍历原列表并将满足条件的元素添加到新列表来完成筛选。在应用条件的时候用到了如下这种语法:
if (predicate(element))
这种语法在 Java 中没有,即变量(参数)
,就好像调用函数一样调用变量,这是一个特殊的变量,里面存放着一个函数,所以这种语法的效果就是将参数传递给变量中的函数并执行它。在 Kotlin 中,称为叫约定(运算符重载)。
plus约定
先看一个更简单的约定:
data class Point( val x: Int, val y: Int){ //声明plus函数 operator fun plus(other: Point): Point{ return Point(x + other.x, y + other.y) } } val p1 = Point(1, 0) val p2 = Point(2, 1) //将Point对象相加 println(p1 + p2)
上述代码的输出是 Point(x=3, y=1)
Point 类使用operator
关键词声明了 plus() 函数,并在其中定义了相加算法,这使得 Point 对象之间可以使用+
来做加法运算,即原本的p1.plus(p2)
可以简写成p1+p2
。
plus 约定可以描述成:通过operator
关键词的声明,将plus()
函数和+
建立了一一对应的关系。Kotlin 中定了很多这样的对应关系,比如times()
对应*
,equals()
对应==
。
约定将函数调用转换成运算符调用,以让代码更简洁的同时也更具表现力。
invoke约定 & in约定 & range约定
在这些约定中有一个叫 invoke约定
:如果类使用operator
声明了invoke()
方法,则该类的对象就可以当做函数一样调用,即在对象后加上()
。
Kotlin 中 lambda 都会被编译成实现了FunctionN
接口的类,FunctionN 中的 N 表示参数的个数:
public interface Function0<out R> : Function<R> { public operator fun invoke(): R } public interface Function1<in P1, out R> : Function<R> { public operator fun invoke(p1: P1): R } public interface Function2<in P1, in P2, out R> : Function<R> { public operator fun invoke(p1: P1, p2: P2): R } public interface Function3<in P1, in P2, in P3, out R> : Function<R> { public operator fun invoke(p1: P1, p2: P2, p3: P3): R } public interface Function4<in P1, in P2, in P3, in P4, out R> : Function<R> { public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4): R } public interface Function5<in P1, in P2, in P3, in P4, in P5, out R> : Function<R> { public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5): R }
上述代码是 kotlin.jvm.functions 文件中的部分内容,整个文件一共定义了 24 个 FunctionN 接口。
Kotlin 中的 lambda 在被编译成 Java 代码时就会根据输入参数的个数匹配为对应的 FunctionN 接口。
并且所有的 FunctionN 接口都默认实现了 invoke() 方法。也就是说,所以的 lambda 都默认可以使用 invoke() 约定。所以执行 lambda 有以下几种方法:
//将 lambda 存储在函数类型的变量中 val printx = { x: Int -> println(x) } //1. 使用invoke约定执行 lambda printx(1) //2. 调用invoke()函数执行 lambda printx.invoke(1) //3. 还有一种极端的方式:定义 lambda 的同时传递参数给它并执行 { x: Int -> println(x) }(1)
比如 filter() 中的predicate
被定义成(T) -> Boolean
,编译时,它会变成这样:
interface Function1<in T, out Boolean>{ operator fun invoke(p1: T): Boolean }
回到刚才的业务函数:
fun filterMan( man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean ) { man.filter { predicate.invoke(it, women) }.forEach { Log.v(“test”, “man = $it”) } }
其实可以使用invoke约定
来简化代码如下:
fun filterMan( man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean ) { man.filter { predicate(it, women) }.forEach { Log.v(“test”, “man = $it”) } }
就这?别急,下面还有三波简化。
来看下我们真正要简化的东西:女生的筛选条件,即实现一个(Human, Human) -> Boolean)
类型的 lambda :
{ man, women -> !man.married && man.age >= women.age && man.age <= 30 && man.nativePlace == woman.nativePlace && man.annualSalary >= 10 && (man.weight / ((man.height.toDouble() / 100)).pow(2)).toInt() in 20..25 || !man.married && man.age >= women.age && man.age <= 40 && man.nativePlace != woman.nativePlace && man.annualSalary >= 60 }
通过合理换行和缩进,已经为这一长串逻辑表达式增加了些许可读性,但一眼望去,脑袋还是晕的。
其中判定年龄区间的逻辑就显得很啰嗦:
man.age >= women.age && man.age <= 30
被判断的对象 man.age 出现了两次。
使用下面两个约定就能简化它。
in约定
:如果用operator
声明了contains()
函数,则可以使用elment in 集合
来简化集合.contains(elment)
。IntRange 的基类 ClosedRange 就默认实现了 in 约定:
public interface ClosedRange<T: Comparable<T>> { public operator fun contains(value: T): Boolean = value >= start && value <= endInclusive }
区间约定
:如果用operator
声明了rangeTo()
函数,则可以使用a .. b
来表达一个闭区间。
Int 类就实现了区间约定:
public class Int private constructor() : Number(), Comparable<Int> { public operator fun rangeTo(other: Int): IntRange }
简化后的代码如下:
{ man, women -> !man.married && // 简化的区间判定 man.age in women.age..30 && man.nativePlace == woman.nativePlace && man.annualSalary >= 10 && (man.weight / ((man.height.toDouble() / 100)).pow(2)).toInt() in 20..25 || !man.married && // 简化的区间判定 man.age in women.age..40 && man.nativePlace != woman.nativePlace && man.annualSalary >= 60 }
其中仍然有一些长且晦涩的表达式,增加了整体的理解难度。那就把它抽象成一个方法,然后取一个好名字,来降低一点理解难度,在所处的界面类(比如Activity)中定义两个私有方法:
//是否具有相同祖籍 private fun isLocal(man1: Human, man2: Human): Boolean { return man1.nativePlace == man2.nativePlace } //BMI 计算公式 private fun bmi(man: Human): Int { return (man.weight / ((man.height.toDouble() / 100)).pow(2)).toInt() }
经过简化之后代码如下:
{ man, women -> !man.married && man.age in women.age..30 && isLocal(women, man) && man.annualSalary >= 10 && bmi(man) in 20..25 || !man.married && man.age in women.age..40 && !isLocal(women, man) && man.annualSalary >= 60 }
仔细一想女生的筛选标准其实可以概括成两类男生:本地帅哥 或者 外地成功男士。所以可进一步抽象出两个函数:
// 是否是本地帅哥 private fun isLocalHandsome(man :Human, women: Human): Boolean{ return ( !man.married && man.age in women.age..30 && isLocal(women, man) && man.annualSalary >= 10 && bmi(man) in 20..25 ) } // 是否是外地成功男士 private fun isRemoteSuccess(man :Human, women: Human): Boolean{ return ( !man.married && man.age in women.age..40 && !isLocal(women, man) && man.annualSalary >= 60 ) }
于是乎,代码简化如下:
{ man, women -> isLocalHandsome(man, women) || isRemoteSuccess(man, women) }
为简化代码付出的代价是在界面类中增加了 4 个私有函数。理论上界面中应该只包含 View 及对它的操作才对,这 4 个私有函数显得格格不入。而且如果另一个女生还需要找本地帅哥,这段写在界面中的逻辑如何复用?
那就把这四个方法都写到 Human 类中,这其实是个不错的办法,但如果各式各样的需求不断增多,那 Human 类中的方法将膨胀。
更好的做法是用invoke约定来统筹筛选条件:
// 定义筛选标准类继承自函数类型(Human)->Boolean class HandsomeOrSuccessMan(val women: Human) : (Human) -> Boolean { // 定义invoke约定 override fun invoke(human: Human): Boolean = human.isLocalHandsome(women) || human.isRemoteSuccess(women) // 为Human定义扩展函数计算BMI private fun Human.bmi(): Int = (weight / ((height.toDouble() / 100)).pow(2)).toInt() // 为Human定义扩展函数判断是否同一祖籍 private fun Human.isLocal(human: Human): Boolean = nativePlace == human.nativePlace // 为Human定义扩展函数判断是否是本地帅哥 private fun Human.isLocalHandsome(human: Human): Boolean = ( !married && age in human.age..30 && isLocal(human) && annualSalary >= 10 && bmi() in 20..25 ) // 为Human定义扩展函数判断是否是外地成功人士 private fun Human.isRemoteSuccess(human: Human): Boolean = ( !married && age in human.age..40 && !isLocal(human) && annualSalary >= 60 ) }
Kotlin 中函数类型也是一种数据类型,它可以被继承。这个语法糖的好处是不用新增一个接口。
当定义类继承自函数类型时,IDE 会提示你重写invoke()
方法,将女生筛选标准的完整逻辑写在invoke()
方法体内,将和筛选标准有关的细分逻辑都作为Human
的扩展函数写在类体内。
虽然新增了一个类,但是,它将复杂的判定条件拆分成多个语义更清晰的片段,使代码更容易理解和修改,并且将片段归总在一个类中,这样筛选标准就可以以一个类的身份到处使用。
为筛选准备一组候选人:
private val man = listOf( Human(age = 30, annualSalary = 40, nativePlace = "山东", married = false, height = 170, weight = 80, gender = "male"), Human(age = 22, annualSalary = 23, nativePlace = "浙江", married = true, height = 189, weight = 90, gender = "male"), Human(age = 40, annualSalary = 13, nativePlace = "上海", married = true, height = 181, weight = 70, gender = "male"), Human(age = 25, annualSalary = 70, nativePlace = "江苏", married = false, height = 167, weight = 66, gender = "male") )
然后开始筛选:
fun filterMan( man: List<Human>, predicate: (Human) -> Boolean ) { man.filter (predicate).forEach { Log.v("test","man = $it") } } // 进行筛选 filterMan(man, HandsomeOrSuccessMan(women))
修改了下filterMan()
,这次它变得更加简洁了,只需要两个参数。
将它和最开始的版本做一下对比:
fun filterMan(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) { man.filter { predicate(it, women) }.forEach { Log.v("ttaylor", "man = $it") } } filterMan(man, women) { man, women -> !man.married && man.age >= women.age && man.age <= 30 && man.nativePlace == woman.nativePlace && man.annualSalary >= 10 && (man.weight / ((man.height.toDouble() / 100)).pow(2)).toInt() in 20..25 || !man.married && man.age >= women.age && man.age <= 40 && man.nativePlace != woman.nativePlace && man.annualSalary >= 40 }
你更喜欢哪个版本?
总结
“约定”(“运算符重载”)是 Kotlin 独有的语法糖,它是用于简化方法调用,通过更简洁的符号来表达更清晰的语义,最终实现简化代码的效果。
现将 Kotlin 支持的所有约定罗列如下:
操作符 | 等价于 |
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a-- | a.dec() |
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b) |
a .. b | a.rangeTo(b) |
a in b | b.contains(a) |
a !in b | !b.contains(a) |
a[i] | a.get() |
a[i, j] | a.get(i, j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ..., i_n] = b | a.set(i_1, ..., i_n, b) |
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |
a += b | a.plusAssign(b) |
a -= b | a.minusAssign(b) |
a *= b | a.timesAssign(b) |
a /= b | a.divAssign(b) |
a %= b | a.remAssign(b) |
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |
参考
Operator overloading | Kotlin (kotlinlang.org)