Kotlin learning之Lambda表达式

本文详细介绍了Kotlin中的Lambda表达式,包括其在高阶函数中的应用、如何传递给Java方法、内联函数(inline/noinline)的概念,以及在集合函数式API中的使用。此外,还讨论了`with`、`apply`、`let`和`also`等函数的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Lambda表达式

高阶函数

所谓高阶函数就是把函数当作参数传递或者返回值的函数。

函数类型的声明

我们来看一下实际的例子:

fun main() {
    println(f(1, 2, { a, b -> a + b }))
    println(f(1, 2, { a, b -> a - b }))


}

fun f(i: Int, j: Int, op: (Int, Int) -> Int): Int {
    return op(i, j)
}

// output:3
// output:-2

这里的f函数是一个高阶函数,它有三个参数,一个是分别是i、j、和op,i和j都是Int类型,而op是一个函数类型,我们看下它的类型声明( Int, Int) -> Int,括号里的Int是函数类型声明的参数,-> Int这里的Int指的是函数类型的返回值。那么这个函数参数的意义就是通过两个Int值的某些操作返回一个Int值。而这个所谓的某些操作就是我们要具体传递的方法。例如{ a, b -> a + b }表示将两个Int值相加。{ a, b -> a - b }表示将两个数相减。

所有的函数类型都会有一个括号括起来的参数类型列表和返回类型。比如(A, B) -> C表示一个函数需要传递两个参数,类型分别是A和B,返回值类型是C。参数类型可以省略,但是返回值类型不能省略。如() -> Unit
函数类型可以额外有一个接受者类型。例如这个定义:A.(B) -> C,表示这个函数可以被A类型调用,参数是B类型,返回值是C类型。在这个函数内部可以直接通过this访问A类的属性或者方法,甚至直接省略this

函数类型也有几个特殊的例子:

  • (Int, Int) -> Int?表示参数类型是Int,返回值类型是Int?
  • ((Int, Int) -> Int)?表示参数类型是Int,返回值类型是Int,但是整个函数可以为null
  • (Int) -> ((Int) -> Unit)表示参数类型是Int,返回值类型是另外一个函数类型,而这个函数类型的参数是Int,返回值类型是Unit
  • (Int) -> (Int) -> Unit的箭头优先级的顺序是从右向左,也就是它等价于(Int) -> ((Int) -> Unit)

函数类型的初始化

有以下几种方式获取一个函数类型的实例:

  1. 通过lambda表达式{ a, b -> a + b }
  2. 通过一个匿名函数fun(s: String): Int { return s.toIntOrNull() ?: 0 }
  3. 通过一个引用已经存在的声明String::toInt
  4. 如果类实现了将函数类型接口,可以使用这个类的实例,例如:
class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

函数类型的引用

可以通过invoke方法调用,或者直接调用。举个例子:

    fun main() {
        val intPlus: (Int, Int) -> Int = { a, b -> a + b }
    
        intPlus.invoke(2,3)
        intPlus(4,5)
        
    }

如果是带接受者的函数类型,可以把接受者作为第一个参数调用或者将接受者放在括号外类似于扩展函数的方式调用。例如:

import java.lang.StringBuilder

fun main() {
    // 将string复制n份
    val stringPlus: String.(Int) -> String = { it ->
        var count = it
        val builder = StringBuilder()
        while (count-- > 0) {
            builder.append(this)
        }
        builder.toString()
    }

    println("abs".stringPlus(3))
    println(stringPlus("xyz",5))
    println(stringPlus.invoke("mnp",4))

}
// output:
// absabsabs
// xyzxyzxyzxyzxyz
// mnpmnpmnpmnp

将Lambda表达式传递给java方法

我们知道java中的部分匿名内部类在kotlin中可以使用lambda来表示,那么lambda表达式对应的java code是不是就是一个匿名内部类呢?
我们来看几个例子:

假设我们定义了一个java 接口和设置这个接口的方法:

public interface OnClickListener {
    void onClick();
}
public class HelloJava {
    public  void setListener(OnClickListener onClickListener){

    }
}

现在在Kotlin中调用这个setListener方法:

fun main() {
    var s = "sss"
    // 1
    HelloJava().setListener { println("hi") }
    // 2
    HelloJava().setListener { println("hi") }
    // 3
    HelloJava().setListener { print("$s") }
    // 4
    HelloJava().setListener(object : OnClickListener {
        override fun onClick() {
            println("hi")
        }

    })
}

1、2、3都是使用lambda的方式,而4是使用匿名内部类的方法。
来看下这四种方法对应的java code:

 public static final void main() {
      final ObjectRef s = new ObjectRef();
      s.element = "sss";
      // 1
      (new HelloJava()).setListener((OnClickListener)null.INSTANCE);
      // 2
      (new HelloJava()).setListener((OnClickListener)null.INSTANCE);
      // 3
      (new HelloJava()).setListener((OnClickListener)(new OnClickListener() {
         public final void onClick() {
            String var1 = String.valueOf((String)s.element);
            System.out.print(var1);
         }
      }));
      // 4
      (new HelloJava()).setListener((OnClickListener)(new OnClickListener() {
         public void onClick() {
            String var1 = "hi";
            System.out.println(var1);
         }
      }));
   }

我们可以看到kotlin对应的java code中1、2两个是生成了两个INSTANCE实例。而3、4生成的是匿名内部类。
现在有两个问题:
1.INSTANCE实例对应的类是什么呢?
2.第一个lambda和第二个lambda表达式相同,那么它们生成的INSTANCE对应的是同一个类甚至是同一个实例吗?
3.第三个lambda表达式为什么没有生成INSTANCE实例呢?

首先我们来看下第一个lambda中的相关kotlin bytecode代码:

   L1
    LINENUMBER 7 L1
    NEW HelloJava
    DUP
    INVOKESPECIAL HelloJava.<init> ()V
    GETSTATIC HelloKt$main$1.INSTANCE : LHelloKt$main$1;   // 这里
    CHECKCAST OnClickListener
    INVOKEVIRTUAL HelloJava.setListener (LOnClickListener;)V

我们能看到INSTANCE其实是HelloKt$main$1的一个静态字段。
HelloKt$main$1是一个单例,它的类名生成规则是类名$方法名$后缀
在阅读《Kotlin in Action》时,书中有这么一段话:

When you explicitly declare an object, a new instance is created on each invocation. With a lambda, the situation is different: if the lambda doesn’t access any variables from the function where it’s defined, the corresponding anonymous class instance is reused between calls.

当显式声明匿名内部类时,每次调用都会创建一个新实例。使用lambda表达式的时候是分情况的:如果lambda表达式没有引用范围外的变量,那么它创建的相应的匿名类会在不同调用之间重用。

最开始看这段话的时候,可能理解有些偏差。最开始的理解是:如果两次lambda内容相同,并且都没有引用外面的变量,那么这两个lambda会使用同一个类。
但是后来验证了以下,其实是错误的。
每次创建一个lambda都会创建一个新的类。类名的规则就是类名$方法名$后缀,后缀是递增的。那么之前那段话里的reused该怎么去理解呢?
这里的reused其实是指的是使用同一个lambda表达式:

例如:

    val function = { println("hi") }
    HelloJava().setListener(function)
    HelloJava().setListener(function)

这个代码确实会重用一个单例类。

如果lambda引用了范围外的变量,那么那还是会正常生成一个匿名内部类。引用的变量会通过一个包装类封装,因此在lambda中更改外部变量的值也是有效的。

inline

在上一小节可以看到lambda表达式会生成一个新的类,这样相比于直接执行相应的代码就会有性能上的不足。那么有没有办法既能高效执行代码,又可以将函数抽取出来呢?使用inline就可以。
当一个函数声明为inline的时候,那么函数体会直接替换到函数调用的地方。
来看个例子:


fun main() {
    action(1, 2) { a, b -> a + b }

}

inline fun action(a: Int, b: Int, op: (Int, Int) -> Int): Int {
    return op.invoke(a, b)
}



我们吧action函数声明为inline,来看下它的java code:

@Metadata(
   mv = {1, 1, 15},
   bv = {1, 0, 3},
   k = 2,
   d1 = {"\u0000\u0016\n\u0000\n\u0002\u0010\b\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a3\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u00012\u0006\u0010\u0003\u001a\u00020\u00012\u0018\u0010\u0004\u001a\u0014\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u00010\u0005H\u0086\b\u001a\u0006\u0010\u0006\u001a\u00020\u0007¨\u0006\b"},
   d2 = {"action", "", "a", "b", "op", "Lkotlin/Function2;", "main", "", "LearnKotlin"}
)
public final class HelloKt {
   public static final void main() {
      byte a$iv = 1;
      int b$iv = 2;
      int $i$f$action = false;
      int var5 = false;
      int var10000 = a$iv + b$iv;
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final int action(int a, int b, @NotNull Function2 op) {
      int $i$f$action = 0;
      Intrinsics.checkParameterIsNotNull(op, "op");
      return ((Number)op.invoke(a, b)).intValue();
   }
}

可以看到在main()方法中,没有直接调用action方法,而是把action里的逻辑直接替换处理,这样代码执行效率更高。
作为对比,可以再来看下action不声明为inline时的java code:

@Metadata(
   mv = {1, 1, 15},
   bv = {1, 0, 3},
   k = 2,
   d1 = {"\u0000\u0016\n\u0000\n\u0002\u0010\b\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a0\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u00012\u0006\u0010\u0003\u001a\u00020\u00012\u0018\u0010\u0004\u001a\u0014\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u00010\u0005\u001a\u0006\u0010\u0006\u001a\u00020\u0007¨\u0006\b"},
   d2 = {"action", "", "a", "b", "op", "Lkotlin/Function2;", "main", "", "LearnKotlin"}
)
public final class HelloKt {
   public static final void main() {
      action(1, 2, (Function2)null.INSTANCE);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final int action(int a, int b, @NotNull Function2 op) {
      Intrinsics.checkParameterIsNotNull(op, "op");
      return ((Number)op.invoke(a, b)).intValue();
   }
}

noinline

但是不是所有使用lambda的函数都可以被声明为inline,如果我们的lambda表达式在函数中被保存了下来,那么是不能声明为inline的。举个例子:


fun main() {

    action(1, 2, { i, j -> i + j })
}

inline fun action(a: Int, b: Int, op: (Int, Int) -> Int): Int {
    val lambda = op
    return lambda.invoke(a, b)
}



这段代码是无法被编译通过的,回报如下错误:

Error:(10, 18) Kotlin: Illegal usage of inline-parameter ‘op’ in ‘public inline fun action(a: Int, b: Int, op: (Int, Int) -> Int): Int defined in root package in file Hello.kt’. Add ‘noinline’ modifier to the parameter declaration

假设我们一个函数参数中不只有一个lambda,而有的参数不应该被声明成inline,那么可以将这个参数用noinline修饰。如:

inline fun action(a: Int, b: Int,
                  op1: (Int, Int) -> Int, noinline op2: (Int, Int) -> Int) {
}

集合函数式API

filter:从现有集合中选出满足条件的元素
map:将每一个元素转化为新的元素
all:所有元素是否全部满足条件
any:是否有一个元素满足条件
count:满足条件的元素的个数
find:找到一个满足条件的元素
groupBy:按条件分组,结果是map
flatMap:先根据作为实参给定的函数对集合中的每个元素做映射,然后把多个列表合并平铺成一个列表。
flatten:多个列表合并平铺成一个列表

举个例子:

fun main() {
    val list = listOf<Int>(1, 2, 3, 4, 5, 6, 7)
    val i = list.map { it * it }.filter { it > 20 }.find { it > 0 }
}

首先我们创建了一个list,使用map将每一个元素平方,然后筛选出大于20的元素,再从剩下的集合中找到一个大于0的元素。我们看下对应的java code:

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import kotlin.Metadata;
import kotlin.collections.CollectionsKt;

@Metadata(
   mv = {1, 1, 13},
   bv = {1, 0, 3},
   k = 2,
   d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
   d2 = {"main", "", "LearnKotlin"}
)
public final class HelloKt {
   public static final void main() {
      List list = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5, 6, 7});
      Iterable $receiver$iv = (Iterable)list;
      Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
      Iterator var5 = $receiver$iv.iterator();

      Object element$iv$iv;
      int it;
      boolean var8;
      while(var5.hasNext()) {
         element$iv$iv = var5.next();
         it = ((Number)element$iv$iv).intValue();
         var8 = false;
         Integer var12 = it * it;
         destination$iv$iv.add(var12);
      }

      $receiver$iv = (Iterable)((List)destination$iv$iv);
      destination$iv$iv = (Collection)(new ArrayList());
      var5 = $receiver$iv.iterator();

      while(var5.hasNext()) {
         element$iv$iv = var5.next();
         it = ((Number)element$iv$iv).intValue();
         var8 = false;
         if (it > 20) {
            destination$iv$iv.add(element$iv$iv);
         }
      }

      $receiver$iv = (Iterable)((List)destination$iv$iv);
      Iterator var13 = $receiver$iv.iterator();

      Object var10000;
      while(true) {
         if (var13.hasNext()) {
            Object var14 = var13.next();
            int it = ((Number)var14).intValue();
            int var16 = false;
            if (it <= 0) {
               continue;
            }

            var10000 = var14;
            break;
         }

         var10000 = null;
         break;
      }

      Integer i = (Integer)var10000;
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

通过java code我们可以看到在集合的操作过程中产生了一个临时集合destination$iv$iv,这样对性能是有一定影响的。其实这些函数式API每进行一次操作,就会将所有元素赋值到临时数组中,实际上我们有更好的方式来减少性能损耗。就是使用asSequence将集合转化为序列。

asSequence

将集合转换成序列之后,操作符分成了两类,一种是中间操作符,一种是末端操作符。中间操作符例如map、filter,末端操作符如asList,中间操作符的实现是懒加载,也就是说在遇到末端操作符之前是不会实际运行的。

还有一点,就是计算执行顺序不同。正常集合的操作是每进行一次函数式操作,都会产生一个中间集合。而序列的的顺序是,针对每一个元素都进行完整的所有操作。这样的好处在于在某些情况下可以提升程序执行效率。

举例来说:

对于集合来说:

    val list = listOf<Int>(1, 2, 3, 4, 5, 6, 7)
    val i = list.map { it * it }.find { it > 20 }

它的执行顺序如图:

map将集合中所有元素映射成新元素
find从新集合中从头开始寻找满足条件的元素
1___ 2___3___4___5___6___7
1___4___9___16___25___36___49
1不满足___4不满足___9不满足___16不满足___25满足
结束

而对于序列来说:

    val list = listOf<Int>(1, 2, 3, 4, 5, 6, 7)
    val i = list.asSequence().map { it * it }.find { it > 20 }

它的执行顺序如图:

map
判断是否满足find条件
map
判断是否满足find条件
map
判断是否满足find条件
map
判断是否满足find条件
1___ 2___3___4___5___6___7
1
1不满足
4
4不满足
16
16不满足
25
25满足
结束

很明显,序列的操作省去了元素6和7的map操作。执行效率更高。

集合的map等函数都是inline的,所以它们执行少量数据时效率很高,因为它们不会把lambda转换为对应的类。而序列的map等函数是非inline的,它们在执行大量数据时效率较高,因为不用产生临时对象。

with/apply/let/also/run

我们看下这几个函数的定义:


/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

/**
 * 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 {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

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

/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@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
}

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}


可以看到这几个函数都是inline的。

分别看一下每个方法是如何使用的:

run函数(直接调用)

/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

run函数里直接传递一个lambda表达式,lambda的返回值就是run函数的返回值。

run函数(通过类调用)

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

通过T类来调用run函数,lambda表达式可以通过this引用类中的方法或属性,this可以省略。lambda的返回值就是run函数的返回值。

with函数

/**
 * 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 {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

使用T作为接受者,lambda表达式中可以使用this引用类中的方法或属性,this可以省略。with的返回值是lambda的最后一行。

apply函数

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

通过T类调用run方法,lambda表达式中可以使用this引用类中的方法或属性,this可以省略。apply的返回值是T。

also函数

/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@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方法从1.1版本开始,通过T类调用also方法,lambda表达式中需要传递T类作为参数,lambda表达式中使用it引用类中方法或者属性。also方法返回值是T。

let函数

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

let方法通过T类调用,lambda表达式中需要传递T类作为参数,lambda表达式中使用it引用类中方法或者属性。also方法返回值是lambda的返回值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值