Kotlin基础:用Kotlin约定简化相亲

it2022-05-05  112

写在前面:一名有三年Android开发经验的女程序员(欢迎大家关注我 ~期待和大家一起交流和学习Android的相关知识) 这篇是@唐子玄写的~嘻嘻嘻

如果用代码实现择偶标准的判断逻辑,会很容易写出又臭又长的代码。本文通过 Kotin 独有的语法特性“约定”来增加代码的可读性、复用性。

业务场景

假设女生的择偶标准如下:未婚且岁数比我大,如果对方是本地帅哥则对收入降低标准(年薪>10万),如果对方非本地则要求岁数不能超过40岁,且年薪在40万以上。(BMI 在 20 到 25 之间的定义为帅哥)

业务分析

将候选人组织成列表,在候选人列表对象上调用filter()将筛选标准传入即可。

1 将候选人抽象成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//性别 )

- 2 定义筛选函数

fun filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) { man.filter { predicate.invoke(it, women) }.forEach { Log.v(“ttaylor”, “man = $it”) } }

函数接收三个参数:

1 man表示一组候选人2 women表示客户3 predicate表示该客户的筛选标准。

其中第三个参数的类型是函数类型,用一个 lambda (Human, Human) -> Boolean来描述,它表示该函数接收两个 Human 类型的输入并输出 Boolean。 函数体中调用了系统预定义的filter(),它的定义如下:

/** * Returns a list containing only elements matching the given [predicate]. */ public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> { //'构建空列表实例' return filterTo(ArrayList<T>(), predicate) } /** * Appends all elements matching the given [predicate] to the given [destination]. */ 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。

这个 case 中的约定可以描述成:通过operator关键词的声明,将plus()函数和+建立了一一对应的关系。Kotlin 中定了很多这样的对应关系,比如times()对应*,equals()对应==。

约定将函数调用转换成运算符调用,以让代码更简洁的同时也更具表现力。

invoke约定

在这些约定中有一个叫 invoke约定 :如果类使用operator声明了invoke(),则该类的对象就可以当做函数一样调用,即在变量后加上()。 Kotlin 中 lambda 都会被编译成实现了FunctionN接口的类,比如filter()中的predicate被定义成(T) -> Boolean,编译时,它会变成这样:

interface Function1<in T, out Boolean>{ operator fun invoke(p1: T): Boolean }

Kotin 为所有的 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)//输出1

回到刚才的业务函数filterTo():

fun filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) { man.filter { predicate.invoke(it, women) }.forEach { Log.v(“ttaylor”, “man = $it”) } }

其实可以使用invoke约定来简化代码如下:

fun filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) { man.filter { predicate(it, women) }.forEach { Log.v(“ttaylor”, “man = $it”) } }

业务实现

来看下我们真正要简化的东西:女生的筛选条件,即实现一个(Human, Human) -> Boolean)类型的 lambda :

{ 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 >= 40 }

通过合理换行和缩进,已经为这一长串逻辑表达式增加了些许可读性,但一眼望去,脑袋还是晕的。而且运用了in约定来简化代码:如果用operator声明了contains()函数,则可以使用elment in list来简化list.contains(elment)。所以在 Java 中,逻辑表达式会更加冗长。

其中有一些长且晦涩的表达式,增加了整体的理解难度。那就把它抽象成一个方法,然后取一个好名字,来降低一点理解难度,在所处的界面类(比如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 >= 40 }

仔细一想女生的筛选标准其实可以概括成两类男生:本地帅哥 或者 外地成功男士。 所以可进一步抽象出两个函数:

//'是否是本地帅哥' 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 isRemoteSuccessful(man :Human, women: Human): Boolean{ return ( !man.married && man.age in women.age..40 && !isLocal(women, man) && man.annualSalary >= 40 ) }

于是乎,代码简化如下:

{ man, women -> isLocalHandsome(man, women) || isRemoteSuccessful(man, women) }

为简化代码付出的代价是在界面类中增加了 4 个私有函数。理论上界面中应该只包含View及对它的操作才对,这 4 个私有函数显得格格不入。而且如果另一个女生还需要找本地帅哥,这段写在界面中的逻辑如何复用?

那就把这四个方法都写到Human类中,这其实是个不错的办法,但如果各式各样的需求不断增多,那Human类中的方法将膨胀。 其实更好的做法是用invoke约定来统筹筛选条件:

//'定义筛选标准类继承自函数类型(Human)->Boolean' class HandsomeOrSuccessfulPredicate(val women: Human) : (Human) -> Boolean { //'定义invoke约定' override fun invoke(human: Human): Boolean = human.isLocalHandsome(women) || human.isRemoteSuccessful(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.isRemoteSuccessful(human: Human): Boolean = ( !married && age in human.age..40 && !isLocal(human) && annualSalary >= 40 ) }

当定义类继承自函数类型时,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 filterCandidate(man: List<Human>, predicate: (Human) -> Boolean) { man.filter (predicate).forEach { Log.v("ttaylor","man = $it") } } //'进行筛选' filterCandidate(man,HandsomeOrSuccessfulPredicate(women))

修改了下filterCandidate(),这次它变得更加简洁了,只需要两个参数。

将它和最开始的版本做一下对比:

fun filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) { man.filter { predicate(it, women) }.forEach { Log.v("ttaylor", "man = $it") } } filterCandidate(man, women) { 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 >= 40 }

你更喜欢哪个版本呢?欢迎评论~


最新回复(0)