聊一聊Kotlin函数

可以到掘金,效果更好
聊一聊Kotlin函数

一直想学习下Kotlin,毕竟已经被Android官方宣布kotln first的存在。

有些小伙伴可能会担心毕竟是一门新的语言,难免存在一些Bug,其实我觉得这个担心有点多余, Kotlin语言项目开始于2010年,到现在已经9年时间了,而且还是由全球最严谨的一批程序员开发的产品,有什么理由不相信它呢?另外也有很多大公司在生产环境使用了,比如百度,Jetbrain(没错就是开发Kotlin的公司),facebook等。

再去翻Android 官方文档的时候,发现提供的示例代码已经变成了 Kotlin

逛全球最大同性交友网站(GitHub)也发现越来越多的开源库都是基于 Kotlin 语言。

今天先看下 Kotlin中的函数。

1.表达式函数体

通过下面这个简单的例子看下函数声明相关的概念,函数声明的关键字是fun,嗯,比JSfunction还简单。

Kotlin中参数类型是放在变量:后面,函数返回类型也是。

fun max(a: Int, b: Int) : Int {
    if (a > b) {
        return a 
    } else {
        return b
    }
}

当然, Kotlin是有类型推导功能,如果可以根据函数表达式推导出类型,也可以不写返回类型。

但是上面的还是有点繁琐,还能再简单,在 Kotlin中if是表达式,也就是有返回值的,因此可以直接return,另外判断式中只有一行一句也可以省略掉大括号:

fun max(a: Int, b: Int)  {
    return if (a > b) a else b
}

还能在简单点吗?可以,if是表达式,那么就可以通过表达式函数体返回:

fun max(a: Int, b: Int)  = if(a > b)  a else b

最终只需要一行代码。

Example

再看下面这个例子,后面会基于这个例子进行修改。这个函数把集合以某种格式输出,而不是默认的toString().

<T>是泛型,在这里形参集合中的元素都是T类型。返回String类型。

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        postfix: String
): String {
    val sb = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) sb.append(separator)
        sb.append(element)
    }

    sb.append(postfix)
    return sb.toString()
}

2.命名参数调用

先来看下函数调用,相比Java, Kotlin中可以类似于JavaScript中带命名参数进行调用,而且可以不用按函数声明中的顺序进行调用,可以打乱顺序,比如下面:

joinToString(separator = " ", collection = list, postfix = "}", prefix = "{")

// example
val list = arrayListOf("10", "11", "1001")
println(joinToString(separator = " ", collection = list, postfix = "}", prefix = "{"))

>>> {10 11 1001}

3.默认参数值

Java里面有重载这一说,或者JavaScript有默认参数值这一说,Kotlin采用了默认参数值。调用的时候就不需要给有默认参数值的形参传实参。上面的函数改成如下:

fun <T> joinToString(
        collection: Collection<T>,
        separator: String = " ",
        prefix: String = "[",
        postfix: String = "]"
): String {
    ...
}

// 
joinToString(list)

那么调用的时候如果默认参数值自己的满足要求,就可以只传入集合list即可。

4.顶层函数

不同于Java中函数只能定义在每个类里面,Kotlin采用了JavaScript种的做法,可以在文件任意位置处定义函数,这种函数称为顶层函数。

编译后顶层函数会成为文件类下的静态函数,比如在文件名是join.kt下定义的joinToString函数可以通过JoinKt.joinToSting调用,其中JoinKt是编译后的类名。

// 编译成静态函数
// 文件名 join.kt
package strings
fun joinToString() : String {...}

/* Java */
import strings.JoinKt;
JoinKt.joinToSting(....)

看下上面函数编译后的效果:

// 编译成class文件后反编译结果
@NotNull
public static final String joinToString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix, @NotNull String postfix) {
      Intrinsics.checkParameterIsNotNull(collection, "collection");
      Intrinsics.checkParameterIsNotNull(separator, "separator");
      Intrinsics.checkParameterIsNotNull(prefix, "prefix");
      Intrinsics.checkParameterIsNotNull(postfix, "postfix");
      StringBuilder sb = new StringBuilder(prefix);
      int index = 0;

      for(Iterator var7 = ((Iterable)collection).iterator(); var7.hasNext(); ++index) {
         Object element = var7.next();
         if (index > 0) {
            sb.append(separator);
         }

         sb.append(element);
      }

      sb.append(postfix);
      String var10000 = sb.toString();
      Intrinsics.checkExpressionValueIsNotNull(var10000, "sb.toString()");
      return var10000;
   }

// 默认函数值
public static String joinToString$default(Collection var0, String var1, String var2, String var3, int var4, Object var5) {
      if ((var4 & 2) != 0) {
         var1 = " ";
      }

      if ((var4 & 4) != 0) {
         var2 = "[";
      }

      if ((var4 & 8) != 0) {
         var3 = "]";
      }

      return joinToString(var0, var1, var2, var3);
}

接下来看下Kotlin中很重要的一个特性,扩展函数。

5.扩展函数

扩展函数是类的一个成员函数,不过定义在类的外面

扩展函数不能访问私有的或者受保护的成员

扩展函数也是编译成静态函数

所以可以在Java库的基础上通过扩展函数进行封装,假装好像都是在调用Kotlin自己的库一样,在KotlinCollection就是这么干的。

再对上面的joinToString来一个改造,终结版:

fun <T> Collection<T>.joinToString(
        separator: String = " ",
        prefix: String = "[",
        postfix: String = "]"
): String {
    val sb = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {
        if (index > 0) sb.append(separator)
        sb.append(element)
    }

    sb.append(postfix)
    return sb.toString()
}

在这里声明成了Collection接口类的扩展函数,这样就可以直接通过list进行调用, 在扩展函数里面照常可以使用this,这里的this就是指向接收者对象,在这里就是list。

val list = arrayListOf("10", "11", "1001")
println(list.joinToString())

>>> [10 11 1001]

经常我们需要对代码进行重构,其中一个重要的措施就是减少重复代码,在java中可以抽取出独立的函数,但这样有时候对整体结构并不太好,Kotlin提供了局部函数来解决这个问题。

6.局部函数

顾名思义,局部函数就是可以在函数内部定义函数。先看下没有使用局部函数的一个例子,这个例子先对传进来的用户名和地址进行校验,只有都不为空的情况下才存进数据库:

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Name")
    }

    if (user.address.isEmpty()) {
        throw IllegalArgumentException(
            "Can't save user ${user.id}: empty Address")
    }

    // Save user to the database
}

上面有重复的代码,就是对name和address的校验重复了,只是入参的不同,因此可以抽出一个校验函数,使用局部函数重写:

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                    "Can't save user ${user.id}: empty $fieldName")
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
}

布局函数可以访问所在函数中的所有参数和变量。

如果不支持Lambda都不好意思称自己是一门现代语言,来看看Kotlin中的表演。

7.Lambda表达式

lambda本质上是可以传递给其他函数的一小段代码,可以当成值到处传递

Lambda表达式以左大括号开始,以右大括号结束,箭头->分割成两边,左边是入参,右边是函数体。

val sum = {x : Int, y : Int -> x + y}
println(sum(1, 2))

// 可以直接run
run { println(42)}

如果lambda表达式是函数调用的最后一个实参,可以放到括号外边;

当lambda是函数唯一实参时,可以去掉调用代码中的空括号

和局部变量一样,如果lambda参数的类型可以被推导出来,就不需要显示的指定

val people = listOf(User(1, "A", "B"), User(2, "C", "D"))
people.maxBy { it.id }

如果在函数内部使用lambda,可以访问这个函数的参数,还有在lambda之前定义的局部变量

fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}

考虑这么一种情况,如果一个函数A接收一个函数类型参数,但是这个参数功能已经在其它地方定义成函数B了,有一种办法就是传入一个lambda表达式给A,在这个表达式中调用B,但是这样就有点繁琐了,有没有可以直接拿到B的方式呢?

我都说了这么多了,肯定是有了。。。那就是成员引用

8.成员引用

  • 如果lambda刚好是函数或者属性的委托,可以用成员引用替换
people.maxBy(User::id)

不管引用的是函数还是属性,都不要在成员引用的名称后面加括号

  • 引用顶层函数
fun salute() = println("Salute!")
run(::salute)
  • 如果lambda要委托给一个接收多个参数的函数,提供成员引用代替会非常方便:
fun sendEmail(person: Person, message: String) {
    println("message: $message")
}

val action = { person: Person, message: String ->
        sendEmail(person, message)
}
// action可以简化如下
val action = ::sendEmail
// 
action(p, "HaHa")
  • 可以用 构造方法引用 存储或者延期执行创建类实例的动作,构造方法的引用的形式是在双冒号后指定类名称:
data class Person(val name: String, val age: Int)
val createPerson = ::Person
val p = createPerson("Alice", 29)
  • 还可以用同样的方式引用扩展函数
fun Person.isAdult() = age>= 21
val predicate = Person::isAdult

不看点稍微底层的,就显得不够专业,逼格不够,接下来稍微探究下lambda的原理。

9.Lambda表达式原理

Kotlin 1.0起,每个lambda表达式都会被编译成一个匿名类,除非它是一个内联lambda。后续版本计划支持生成Java 8字节码,一旦实现,编译器就可以避免为每一个lambda表达式都生成一个独立的.class文件。

如果lambda捕捉了变量,每个被捕捉的变量会在匿名类中有对应的字段,而且每次调用都会创建一个这个匿名类的新实例。否则,一个单例就会被创建。类的名称由lambda声明所在的函数名称加上后缀衍生出来,这个例子中就是TestLambdaKt$main$1.class

// TestLambda.kt
package ch05

fun salute(callback: () -> Unit) = callback()

fun main(args: Array<String>) {
    salute { println(3) }
}

编译后,生成两个文件

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        2019/7/24     14:33           1239 TestLambdaKt$main$1.class
-a----        2019/7/24     14:35           1237 TestLambdaKt.class

先看下TestLambdaKt$main$1.class, 构造一个静态实例ch05.TestLambdaKt$main$1 INSTANCE,在类加载的时候进行赋值,同时继承接口Function0,实现invoke方法:

final class ch05.TestLambdaKt$main$1 extends kotlin.jvm.internal.Lambda implements kotlin.jvm.functions.Function0<kotlin.Unit>
  minor version: 0
  major version: 50
  flags: ACC_FINAL, ACC_SUPER
  Constant pool:...
{
  public static final ch05.TestLambdaKt$main$1 INSTANCE;
    descriptor: Lch05/TestLambdaKt$main$1;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

  public java.lang.Object invoke();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #12                 // Method invoke:()V
         4: getstatic     #18                 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
         7: areturn

  public final void invoke();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_3
         1: istore_1
         2: getstatic     #24                 // Field java/lang/System.out:Ljava/io/PrintStream;
         5: iload_1
         6: invokevirtual #30                 // Method java/io/PrintStream.println:(I)V
         9: return
      LineNumberTable:
        line 6: 0
        line 6: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lch05/TestLambdaKt$main$1;

  ch05.TestLambdaKt$main$1();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: iconst_0
         2: invokespecial #35                 // Method kotlin/jvm/internal/Lambda."<init>":(I)V
         5: return

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #2                  // class ch05/TestLambdaKt$main$1
         3: dup
         4: invokespecial #56                 // Method "<init>":()V
         7: putstatic     #58                 // Field INSTANCE:Lch05/TestLambdaKt$main$1;
        10: return
}

再看下另外一个类TestLambdaKt.class, 在main方法中传入TestLambdaKt$main$1.INSTANCE给方法salute,在方法salute中调用接口方法invoke,见上面。

public final class ch05.TestLambdaKt
  minor version: 0
  major version: 50
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER
Constant pool:
  ...
{
  public static final void salute(kotlin.jvm.functions.Function0<kotlin.Unit>);
    descriptor: (Lkotlin/jvm/functions/Function0;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #10                 // String callback
         3: invokestatic  #16                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
         6: aload_0
         7: invokeinterface #22,  1           // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object;
        12: pop
        13: return
      LineNumberTable:
        line 3: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0 callback   Lkotlin/jvm/functions/Function0;
    Signature: #7                           // (Lkotlin/jvm/functions/Function0<Lkotlin/Unit;>;)V
    RuntimeInvisibleParameterAnnotations:
      0:
        0: #8()

  public static final void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #27                 // String args
         3: invokestatic  #16                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
         6: getstatic     #33                 // Field ch05/TestLambdaKt$main$1.INSTANCE:Lch05/TestLambdaKt$main$1;
         9: checkcast     #18                 // class kotlin/jvm/functions/Function0
        12: invokestatic  #35                 // Method salute:(Lkotlin/jvm/functions/Function0;)V
        15: return
      LineNumberTable:
        line 6: 6
        line 7: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  args   [Ljava/lang/String;
    RuntimeInvisibleParameterAnnotations:
      0:
        0: #8()
}

lambda内部没有匿名对象那样的的this:没有办法引用到lambda转换成的匿名类实例。从编译器角度看,lambda是一个代码块不是一个对象,不能把它当成对象引用。Lambda中的this引用指向的是包围它的类。

如果在lambda中要用到常规意义上this呢?这个就需要带接收者的函数。看下比较常用的两个函数withapply.

10.with函数

直接上Kotlin的源码,with在这里声明成内联函数(后面找机会说), 接收两个参数,在函数体里面对接收者调用lambda表达式。在lambda表达式里面可以通过this引用到这个receiver对象。

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()

看个例子:

fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
         result.append(letter)
    }
    result.append("\nNow I know the alphabet!")
    return result.toString()
}

with改造, 在with里面就不用显示通过StringBuilder进行append调用。

fun alphabet(): String {
    val result = StringBuilder()
    return with(result) {
        for (letter in  'A'..'Z') {
            append(letter)
        }
        append("\nNow I know the alphabet!")
        this.toString()
    }
}

// 再进一步
fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
    toString()
}

with返回的值是执行lambda代码的结果,该结果是lambda中的最后一个表达式的值。如果想返回的是接收者对象,而不是执行lambda的结果,需要用apply函数。

11.apply函数

apply函数几乎和with函数一模一样,唯一的区别就是apply始终返回作为实参传递给它的对象,也就是接收者对象

/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

apply被声明称一个扩展函数,它的接收者变成了作为实参传入的lambda的接收者

fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}.toString()

可以调用库函数再简化:

fun alphabet() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}

//
/**
 * Builds new string by populating newly created [StringBuilder] using provided [builderAction]
 * and then converting it to [String].
 */
@kotlin.internal.InlineOnly
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
        StringBuilder().apply(builderAction).toString()

不能再往下写了,要不太长会被人打,做个总结。

12.总结

今天只是说了Kotlin中关于函数的一点特性,当然也没讲全,比如内联函数,高阶函数等,因为再写下去太长了,所以后面再补充。从上面几个例子也能大概感受到Kotlin的务实作风,提供了很多特性帮助开发者减少冗余代码的编写,可以提高效率,也能减少异常,让程序猿早点小班,永葆头发乌黑靓丽。

参考:

  • Kotlin Action
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350