kotlin:引子约定plus约定invoke约定 & in约定 & range约定

引子


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

对业务进行抽象

  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//性别
)

  1. 定义筛选函数

fun filterMan(
    man: List, // 候选男生
    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  Iterable.filter(predicate: (T) -> Boolean): List {
    //构建空列表
    return filterTo(ArrayList(), predicate)
}

public inline fun > Iterable.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 : Function {
    public operator fun invoke(): R
}
public interface Function1 : Function {
    public operator fun invoke(p1: P1): R
}
public interface Function2 : Function {
    public operator fun invoke(p1: P1, p2: P2): R
}
public interface Function3 : Function {
    public operator fun invoke(p1: P1, p2: P2, p3: P3): R
}
public interface Function4 : Function {
    public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4): R
}
public interface Function5 : Function {
    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{
    operator fun invoke(p1: T): Boolean
}

回到刚才的业务函数:

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

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

 

fun filterMan(
    man: List,
    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 出现了两次。

使用下面两个约定就能简化它。

  1. in约定:如果用operator声明了contains()函数,则可以使用elment in 集合来简化集合.contains(elment)。IntRange 的基类 ClosedRange 就默认实现了 in 约定:

public interface ClosedRange> {
    public operator fun contains(value: T): Boolean = value >= start && value <= endInclusive
}

  1. 区间约定:如果用operator声明了rangeTo()函数,则可以使用a .. b来表达一个闭区间。

Int 类就实现了区间约定:

public class Int private constructor() : Number(), Comparable {
    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,
    predicate: (Human) -> Boolean
) {
    man.filter (predicate).forEach {
        Log.v("test","man = $it")
    }
}

// 进行筛选
filterMan(man, HandsomeOrSuccessMan(women))

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

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

fun filterMan(man: List, 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)

文章来源于互联网:你的代码太啰嗦了 | 这么多方法调用?引子约定plus约定invoke约定 & in约定 & range约定总结参考推荐阅读

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/14366,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?