若是用代码实现择偶标准的判断逻辑,会很容易写出又臭又长的代码。本文经过 Kotin 独有的语法特性“约定”来增长代码的可读性、复用性。算法
这是 Kotlin 系列的第七篇,目录详见本文末尾。bash
假设女生的择偶标准以下:未婚且岁数比我大,若是对方是本地帅哥则对收入下降标准(年薪>10万),若是对方非本地则要求岁数不能超过40岁,且年薪在40万以上。(BMI 在 20 到 25 之间的定义为帅哥)ide
将候选人组织成列表,在候选人列表对象上调用filter()
将筛选标准传入便可。函数
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 filterCandidate(man: List<Human>, women: Human, predicate: (Human, Human) -> Boolean) {
man.filter { predicate.invoke(it, women) }.forEach {
Log.v(“ttaylor”, “man = $it”)
}
}
复制代码
函数接收三个参数:post
man
表示一组候选人women
表示客户predicate
表示该客户的筛选标准。其中第三个参数的类型是函数类型
,用一个 lambda (Human, Human) -> Boolean
来描述,它表示该函数接收两个 Human 类型的输入并输出 Boolean。动画
函数体中调用了系统预约义的filter()
,它的定义以下:ui
/**
* 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。this
filter()
遍历原列表并将知足条件的元素添加到新列表来完成筛选。在应用条件的时候用到了以下这种语法:spa
if (predicate(element))
复制代码
这种语法在 Java 中没有,即变量(参数)
,就好像调用函数同样调用变量,这是一个特殊的变量,里面存放着一个函数,因此这种语法的效果就是将参数传递给变量中的函数并执行它。在 Kotlin 中,称为叫约定。code
先看一个更简单的约定:
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约定
:若是类使用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
}
复制代码
你更喜欢哪一个版本?