一文吃透 Kotlin 中眼花缭乱的函数家族...(1)

简介: 一文吃透 Kotlin 中眼花缭乱的函数家族...(1)

1. 简化函数

Kotlin 中定义方法的时候,如果函数体是单个表达式,可以进行函数简化

fun generateAnswerString(count: Int, countThreshold: Int): String {
    return if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }
}

简化的形式是直接赋值:不仅可以省略花括号,还可以省略返回类型以及 return 关键字。

这样子的写法很接近于日常表达习惯,简单明了~

fun generateAnswerString(count: Int, countThreshold: Int) =
    if (count > countThreshold) {
         "I have the answer"
    } else {
         "The answer eludes me"
    }

反编译之后发现其采用的三元运算符的写法。

@NotNull
public final String generateAnswerString(int count, int countThreshold) {
    return count > countThreshold ? "I have the answer." : "The answer eludes me.";
}

2. lambda 表达式和匿名函数

Kotlin 中并不要求函数都拥有名称,只声明其必要的输入类型、输出类型以及表达式即可完成函数的定义。


比如:String 即为输入参数类型,Int 为输出类型,花括号内为 lambda 表达式:


it 是隐式参数名称,也可任意拟定,比如这里用 input 指定。如果不需要参数的话也可以省略

如果有不需要的参数一般用下划线_取代其名称

input.length 为计算逻辑以及表达式返回值

    (String) -> Int = { input ->
        input.length
    }

但没有名称的函数无法直接调用,我们还得为这个匿名函数指定函数引用,使其可以像属性一样被传递、被灵活调用。

val stringLengthFunc: (String) -> Int = { input ->
        input.length
    }

定义了名为 stringLength 的函数引用后,函数没有真正的执行,还需要后续的调用。比如:

val stringLength: Int = stringLengthFunc("Android")

反编译看下:

stringLengthFunc 的函数引用事实上是 Kotlin 中预设的 Function1 接口的实现变量,函数的调用会 invoke 到 Function1 的实现体,即包装了真实表达式的逻辑。

public final class Test {
   @NotNull
   private final Function1 stringLengthFunc;
   ...
   public Test() {
      this.stringLength = ((Number)this.stringLengthFunc.invoke("Android")).intValue();
   }
}

需要留意的是匿名函数当然可以像普通函数一样无参数、无返回值。无参数的时候 () 内留空即可,无返回值的话返回类型写作 Unit

    val printTime: () -> Unit = {
        Log.d("Test", "current:${System.currentTimeMillis()}")
    }

既然匿名函数既然可以像属性一样传递,那么自然可以作为参数传递给其他函数,这就要引出下个话题:高阶函数

在高阶函数调用的场景里还可以见到匿名函数的另一种形式:无需实例化,直接将函数体传入。

3. 高阶函数

高阶函数是将函数用作参数或返回值的函数。支持高阶函数是 Kotlin 函数式编程的一大特性,这在 Kotlin 源码中有大量的使用。

函数作为参数

Kotlin 中函数执行的时候如果需要回调参数继续处理,则无需像 Java 那样定义接口,而是直接将函数作为参数传入。

如下的 stringMapper 即为高阶函数,mapper 即为函数参数的引用名称。

class Temp {
    fun stringMapper(input: String, mapper: (String) -> Int): Int {
        return mapper(input)
    }
}

匿名函数参数

首先这个函数参数的传入可以是匿名函数的引用,比如:

class Temp {
    val stringLengthFunc: (String) -> Int = {
        input -> input.length
    }
    ...
    fun main() {
        stringMapper("Android", stringLengthFunc)
    }
}

当然,如果不实例化匿名函数也是可以的,在使用函数的时候将lambda 表达式直接传入,等待调用。

class Temp {
    ...
    fun main() {
    stringMapper("Android", { input ->
            input.length }
        )
    }
}

如果传入的 lambda 表达式是最后一个参数的话,可单独拎出,更加简洁。

class Temp {
    ...
    fun main() {
        stringMapper("Android"
        ) { input ->
            input.length
        }
    }
}

如果高阶函数只有一个函数参数的话,调用的时候可直接省略圆括号

class Temp {
    val temp = "ddd"
    fun stringMapperNew(mapper: (String) -> Int): Int {
        return mapper(temp)
    }
    fun main() {
        stringMapperNew { input ->
            input.length
        }
    }
}

反编译后可以看到高阶函数的函数参数实际传入的 Function1 接口的实例,函数参数的执行也是接口的回调。

   public final int stringMapper(@NotNull String input, @NotNull Function1 mapper) {
      ...
      return ((Number)mapper.invoke(input)).intValue();
   }

具名函数参数

除了传入匿名函数的方法体或引用,还可以传入普通函数的名称作为参数。

写法稍稍不同,::functionName 的形式。

class Temp {
    private fun stringLengthInner(input: String) = input.length
    fun main() {
        stringMapper("Android", ::stringLengthInner)
    }
}

函数作为返回值

如果函数并非想要知道处理结果,只想获得处理方法的话,可以将返回值定义成匿名函数的规格,并在 return 里写上匿名函数的实现。

    fun stringMapperFunction(input: String): (String) -> Int {
        return {
            val newString = input.substring(
                input.indexOf("start")
            )
            newString.length
        }
    }

同样的看下实现,即返回的类型是 Function1 接口,结果是实现该接口的匿名内部类。

   @NotNull
   public final Function1 stringMapperFunction(@NotNull final String input) {
      Intrinsics.checkNotNullParameter(input, "input");
      return (Function1)(new Function1() {
         public Object invoke(Object var1) {
            return this.invoke((String)var1);
         }
         public final int invoke(@NotNull String it) {
            Intrinsics.checkNotNullParameter(it, "it");
            String var3 = input;
            int var4 = StringsKt.indexOf$default((CharSequence)input, "start", 0, false, 6, (Object)null);
            String var10000 = var3.substring(var4);
            Intrinsics.checkNotNullExpressionValue(var10000, "this as java.lang.String).substring(startIndex)");
            String newString = var10000;
            return newString.length();
         }
      });
   }

与 let 等函数的关系

let 等函数是结合了 inline 函数、扩展函数的高阶函数,以 let 为例看下源码:

@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

可以看到:


其实是 T 类型的扩展函数,接受引用名称为 block 、参数类型为 T、返回类型为 R 的匿名函数

函数本身又返回和匿名函数参数相同的 R 类型

方法体内构建完 contract 之后即调用 block 函数并返回

这样的话,任意对象调用该函数传入的方法体,将拥有等同于对象本身的参数。正如前面所说,lambda 表达式默认用 it 作为其默认引用(当然也可以自定义参数名称),并且 let 方法最终返回的就是方法体的返回类型。

    fun main() {
        val lastResult = stringMapper( ... ).let {
                it -> "$it-done"
        }
    }

既然 let 能接受匿名函数体,自然也可以接受具名函数传入。

class Test {
    private fun stringLengthInner2(input: Int) = "$input-done"
    fun main() {
        val lastResult2 = stringMapper( ... ).let(::stringLengthInner2)
    }
}

除了 let,再瞅一眼 also 函数的源码,进行些对比以加深理解:

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

通过观察其定义,不难理解,also 函数将拥有如下特性:


方法体将拥有和 let 一样的 it 参数

但方法体不存在返回值,即和 let 不同,最后一个表达式的结果无法被 also 直接沿用

而且 also 函数返回的是执行函数的对象本身

inline 函数和扩展函数的原理将会在后面阐述,作为高级、匿名函数部分的原理比较简单,就是按照 let 等源码的顺序调用匿名函数并按约定返回相应类型即可。

4. 扩展函数和扩展属性

扩展函数

Kotlin 可以实现扩展一个类的新功能而无需继承该类。 比如可以为一个不能修改的第三方库中的类编写一个新的函数, 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。 这种机制称为扩展函数

来看一个典型的扩展函数写法:

fun String.lastChar(): Char = this[length - 1]
  • String. 表示扩展的目标类
  • lastChar 即函数名
  • Char 即函数返回类型
  • this 代表当前类的实例,并非必须、可省略
  • […] 即函数体

在 Kotlin 和 Java 中不同的调用方法

Kotlin 中直接调用:

class Test {
    ...
    fun main() {
        ...
        Log.d("test", "last char:${"Ellison".lastChar()}")
    }
}
fun String.lastChar(): Char = this[length - 1]

Java 中则是像静态类一样调用该扩展方法,要注意两点:

  1. Java 中当镜头方法调用它,类名为扩展函数所存在的 Kt 文件名 + Kt。此处即为 TestKt
  2. 调用函数传入的第一个参数为实例,其后为函数参数
public class TestJava {
    static void main(String[] args) {
        Log.d("test", "last char" + TestKt.lastChar("Ellison"));
    }
}

泛型扩展函数

对于泛型类的也可以拥有扩展函数,只不过需要在声明的函数前指定泛型,否则无法扩展成功。

比如给 MutableMap 添加新的函数:

fun <K, V>  MutableMap<K, V> .putLast(): V? {
    ...
}

与 apply 等函数的关系

apply 函数,了解下源码中扩展函数的应用:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

apply 和 let 等函数一样是扩展自任意类型对象的函数,因为泛型的缘故在函数前添加了 <T> 的声明、block 参数的 T.()->Unit 指的是带有指向 T 实例的 this 的参数并无返回值。


T.()->Unit 这种带有接收者的参数形式被称为函数字面值,其和扩展函数的形式有点像,但并不是。通过反编译之后会发现它仍然属于匿名函数的范畴,通过 Function2 接口实现,只不过传入的参数是 T 其本身。


apply 函数返回 T 类型,内部则是调用 block() 传入 T 对象,进行处理之后,返回对象本身。


从如下的 run 函数的源码可以看出与 apply 之间的区别:其函数参数和返回值均是 R 类型,这将导致像 let 和 also 一样的不同点:


apply 总是返回的是对象本身

run 返回的是函数结果

public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

扩展属性

扩展属性提供了一种方法能通过属性语法进行访问的 API 来扩展。尽管它们被叫做属性,但是它们不能拥有任何状态,它不能添加额外的字段到现有的 Java 对象实例。


比如下面的为 List 添加一个 last 属性用于获取列表的最后一个元素,this 可以省略。注意:泛型仍要声明在扩展属性前。

val <T> List<T>.last: T get() = get(size - 1)
val listString = listOf("Android Q", "Android N", "Android M") 
fun main() {        
    println("listString.last${listString.last}")
}

与 KTX 的关系

KTX 是专门为 Android 库设计的 Kotlin 扩展程序,以提供简洁易用的 Kotlin 代码,其中部分 KTX 采用了扩展属性的写法,比如 viewModelScope。


它向 ViewModel 类扩展了 viewModelScope 属性,供 ViewModel 中便捷地使用 CoroutineScope:绑定至 Dispatchers.Main,并且会在清除 ViewModel 后自动取消。

public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

伴生对象扩展函数和属性

如果一个类定义了伴生对象,那么我们也可以为伴生对象定义扩展函数与属性,并且就可以和伴生对象一样使用类名直接访问:

class Job {
    companion object {}
}
class Test {
    fun main() {
        Job.print("Extension for Companion object.")
    }
}
fun Job.Companion.print(summary: String) {
    Log.d("Test", "Job:$summary")
}

原理

看下上述 String.lastChar() 扩展函数反编译后的代码:

即在 TestKt Class 内生成了同名的静态函数,接收的参数即为目标 Class 即 String 实例,内部将调用扩展函数的函数体。

public final class TestKt {
   public static final char lastChar(@NotNull String $this$lastChar) {
      Intrinsics.checkNotNullParameter($this$lastChar, "$this$lastChar");
      return $this$lastChar.charAt($this$lastChar.length() - 1);
   }
}

再看下 viewModelScope KTX 的反编译来研究下扩展属性的原理:

同样在 XXXKt 的 ViewModelKt Class 内生成了静态方法,不过名称为 getXXX 形式,其接收的参数为 ViewModel 实例,内部执行 get() 的逻辑并返回。

public final class ViewModelKt {
   @NotNull
   public static final CoroutineScope getViewModelScope(@NotNull ViewModel $this$viewModelScope) {
      Intrinsics.checkNotNullParameter($this$viewModelScope, "$this$viewModelScope");
      CoroutineScope scope = (CoroutineScope)$this$viewModelScope.getTag("androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY");
      if (scope != null) {
         return scope;
      } else {
         Object var10000 = $this$viewModelScope.setTagIfAbsent("androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY", new CloseableCoroutineScope(SupervisorKt.SupervisorJob$default((Job)null, 1, (Object)null).plus((CoroutineContext)Dispatchers.getMain().getImmediate())));
         Intrinsics.checkNotNullExpressionValue(var10000, "setTagIfAbsent(\n        …Main.immediate)\n        )");
         return (CoroutineScope)var10000;
      }
   }
}

不要滥用

扩展函数、扩展属性虽好但不要滥用,因为会造成一些弊端:


扩展函数无法像普通函数那样进行函数引用

多个接受者隐式的访问可能会令人困惑

当修改引用接受者的时候,不清楚是修改的是扩展接受者还是调度接受者

对于经验较少的开发人员来说,看到成员扩展可能是违反直觉,可读性很差

具体的可以参考避免滥用 Kotlin 扩展函数文章的描述。


相关文章
|
9月前
|
API Kotlin
Kotlin中扩展函数、infix关键字、apply函数和DSL的详解
Kotlin中扩展函数、infix关键字、apply函数和DSL的详解
78 0
|
9月前
|
安全 Java Kotlin
Kotlin中空安全操作符,异常处理和自定义异常,以及先决条件函数详解
Kotlin中空安全操作符,异常处理和自定义异常,以及先决条件函数详解
86 0
|
9月前
|
Java 编译器 Kotlin
Kotlin 中变量,类型,表达式,函数详解
Kotlin 中变量,类型,表达式,函数详解
68 0
|
7月前
|
存储 Java 编译器
Kotlin 学习笔记(四)—— 作用域函数、inline 关键字、反引号等 Kotlin 基本用法(下)
Kotlin 学习笔记(四)—— 作用域函数、inline 关键字、反引号等 Kotlin 基本用法(下)
33 0
|
7月前
|
Java Android开发 开发者
Kotlin 学习笔记(四)—— 作用域函数、inline 关键字、反引号等 Kotlin 基本用法(上)
Kotlin 学习笔记(四)—— 作用域函数、inline 关键字、反引号等 Kotlin 基本用法(上)
46 0
|
9月前
|
安全 Java 编译器
Kotlin 学习笔记(一)—— 基本类型、函数、lambda、类与对象的写法
Kotlin 作为 Android 开发的首选语言,其基础语法和特性需要重点学习。本文概括总结了 Kotlin 的基本类型、关键字、函数、闭包、高阶函数、类与对象等核心知识点,并给出示例代码进行讲解。
152 0
Kotlin 学习笔记(一)—— 基本类型、函数、lambda、类与对象的写法
|
9月前
|
Java Kotlin
Kotlin中与Java互操作与可空性、类型映射、属性访问、@JvmOverloads、@JvmField、@JvmStatic、@Throws和函数类型操作详解
Kotlin中与Java互操作与可空性、类型映射、属性访问、@JvmOverloads、@JvmField、@JvmStatic、@Throws和函数类型操作详解
70 0
|
9月前
|
Kotlin
Kotlin中标准库函数(apply、let、run、with、also、takeIf、takeUnless)的使用详解
Kotlin中标准库函数(apply、let、run、with、also、takeIf、takeUnless)的使用详解
58 0
|
10月前
|
XML Java Android开发
Kotlin作用域函数let、with、run、apply、also
Kotlin作用域函数let、with、run、apply、also
65 0
|
12月前
|
安全 IDE JavaScript
From Java To Kotlin:空安全、扩展、函数、Lambda很详细,这次终于懂了
Kotlin 是什么? 可以做什么? Android 官方开发语言从Java变为Kotlin,Java 有哪些问题? Kotlin的优点 Kotlin 特性(Features)
11542 1
From Java To Kotlin:空安全、扩展、函数、Lambda很详细,这次终于懂了